mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 19:26:31 -05:00
feat: add alias service
Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
17c55195c3
commit
d1b7c2a2db
3 changed files with 456 additions and 2 deletions
267
src/notes/alias.service.spec.ts
Normal file
267
src/notes/alias.service.spec.ts
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { AuthToken } from '../auth/auth-token.entity';
|
||||||
|
import { Author } from '../authors/author.entity';
|
||||||
|
import appConfigMock from '../config/mock/app.config.mock';
|
||||||
|
import {
|
||||||
|
AlreadyInDBError,
|
||||||
|
ForbiddenIdError,
|
||||||
|
NotInDBError,
|
||||||
|
PrimaryAliasDeletionForbiddenError,
|
||||||
|
} from '../errors/errors';
|
||||||
|
import { Group } from '../groups/group.entity';
|
||||||
|
import { GroupsModule } from '../groups/groups.module';
|
||||||
|
import { Identity } from '../identity/identity.entity';
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
|
import { Edit } from '../revisions/edit.entity';
|
||||||
|
import { Revision } from '../revisions/revision.entity';
|
||||||
|
import { RevisionsModule } from '../revisions/revisions.module';
|
||||||
|
import { Session } from '../users/session.entity';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { Alias } from './alias.entity';
|
||||||
|
import { AliasService } from './alias.service';
|
||||||
|
import { Note } from './note.entity';
|
||||||
|
import { NotesService } from './notes.service';
|
||||||
|
import { Tag } from './tag.entity';
|
||||||
|
|
||||||
|
describe('AliasService', () => {
|
||||||
|
let service: AliasService;
|
||||||
|
let noteRepo: Repository<Note>;
|
||||||
|
let aliasRepo: Repository<Alias>;
|
||||||
|
let forbiddenNoteId: string;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AliasService,
|
||||||
|
NotesService,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(Note),
|
||||||
|
useClass: Repository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(Alias),
|
||||||
|
useClass: Repository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(Tag),
|
||||||
|
useClass: Repository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(User),
|
||||||
|
useClass: Repository,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfigMock],
|
||||||
|
}),
|
||||||
|
LoggerModule,
|
||||||
|
UsersModule,
|
||||||
|
GroupsModule,
|
||||||
|
RevisionsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.overrideProvider(getRepositoryToken(Note))
|
||||||
|
.useClass(Repository)
|
||||||
|
.overrideProvider(getRepositoryToken(Tag))
|
||||||
|
.useClass(Repository)
|
||||||
|
.overrideProvider(getRepositoryToken(Alias))
|
||||||
|
.useClass(Repository)
|
||||||
|
.overrideProvider(getRepositoryToken(User))
|
||||||
|
.useClass(Repository)
|
||||||
|
.overrideProvider(getRepositoryToken(AuthToken))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Edit))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Revision))
|
||||||
|
.useClass(Repository)
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Group))
|
||||||
|
.useClass(Repository)
|
||||||
|
.overrideProvider(getRepositoryToken(Session))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Author))
|
||||||
|
.useValue({})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
const config = module.get<ConfigService>(ConfigService);
|
||||||
|
forbiddenNoteId = config.get('appConfig').forbiddenNoteIds[0];
|
||||||
|
service = module.get<AliasService>(AliasService);
|
||||||
|
noteRepo = module.get<Repository<Note>>(getRepositoryToken(Note));
|
||||||
|
aliasRepo = module.get<Repository<Alias>>(getRepositoryToken(Alias));
|
||||||
|
});
|
||||||
|
describe('addAlias', () => {
|
||||||
|
const alias = 'testAlias';
|
||||||
|
const alias2 = 'testAlias2';
|
||||||
|
const user = User.create('hardcoded', 'Testy') as User;
|
||||||
|
describe('creates', () => {
|
||||||
|
it('an primary alias if no alias is already present', async () => {
|
||||||
|
const note = Note.create(user);
|
||||||
|
jest
|
||||||
|
.spyOn(noteRepo, 'save')
|
||||||
|
.mockImplementationOnce(async (note: Note): Promise<Note> => note);
|
||||||
|
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||||
|
jest.spyOn(aliasRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||||
|
const savedAlias = await service.addAlias(note, alias);
|
||||||
|
expect(savedAlias.name).toEqual(alias);
|
||||||
|
expect(savedAlias.primary).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('an non-primary alias if an primary alias is already present', async () => {
|
||||||
|
const note = Note.create(user, alias);
|
||||||
|
jest
|
||||||
|
.spyOn(noteRepo, 'save')
|
||||||
|
.mockImplementationOnce(async (note: Note): Promise<Note> => note);
|
||||||
|
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||||
|
jest.spyOn(aliasRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||||
|
const savedAlias = await service.addAlias(note, alias2);
|
||||||
|
expect(savedAlias.name).toEqual(alias2);
|
||||||
|
expect(savedAlias.primary).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('does not create an alias', () => {
|
||||||
|
const note = Note.create(user, alias2);
|
||||||
|
it('with an already used name', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(aliasRepo, 'findOne')
|
||||||
|
.mockResolvedValueOnce(Alias.create(alias2));
|
||||||
|
await expect(service.addAlias(note, alias2)).rejects.toThrow(
|
||||||
|
AlreadyInDBError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('with a forbidden name', async () => {
|
||||||
|
await expect(service.addAlias(note, forbiddenNoteId)).rejects.toThrow(
|
||||||
|
ForbiddenIdError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeAlias', () => {
|
||||||
|
const alias = 'testAlias';
|
||||||
|
const alias2 = 'testAlias2';
|
||||||
|
const user = User.create('hardcoded', 'Testy') as User;
|
||||||
|
describe('removes one alias correctly', () => {
|
||||||
|
const note = Note.create(user, alias);
|
||||||
|
note.aliases.push(Alias.create(alias2));
|
||||||
|
it('with two aliases', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(noteRepo, 'save')
|
||||||
|
.mockImplementationOnce(async (note: Note): Promise<Note> => note);
|
||||||
|
jest
|
||||||
|
.spyOn(aliasRepo, 'remove')
|
||||||
|
.mockImplementationOnce(
|
||||||
|
async (alias: Alias): Promise<Alias> => alias,
|
||||||
|
);
|
||||||
|
const savedNote = await service.removeAlias(note, alias2);
|
||||||
|
expect(savedNote.aliases).toHaveLength(1);
|
||||||
|
expect(savedNote.aliases[0].name).toEqual(alias);
|
||||||
|
expect(savedNote.aliases[0].primary).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('with one alias, that is primary', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(noteRepo, 'save')
|
||||||
|
.mockImplementationOnce(async (note: Note): Promise<Note> => note);
|
||||||
|
jest
|
||||||
|
.spyOn(aliasRepo, 'remove')
|
||||||
|
.mockImplementationOnce(
|
||||||
|
async (alias: Alias): Promise<Alias> => alias,
|
||||||
|
);
|
||||||
|
const savedNote = await service.removeAlias(note, alias);
|
||||||
|
expect(savedNote.aliases).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('does not remove one alias', () => {
|
||||||
|
const note = Note.create(user, alias);
|
||||||
|
note.aliases.push(Alias.create(alias2));
|
||||||
|
it('if the alias is unknown', async () => {
|
||||||
|
await expect(service.removeAlias(note, 'non existent')).rejects.toThrow(
|
||||||
|
NotInDBError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('if it is primary and not the last one', async () => {
|
||||||
|
await expect(service.removeAlias(note, alias)).rejects.toThrow(
|
||||||
|
PrimaryAliasDeletionForbiddenError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeAliasPrimary', () => {
|
||||||
|
const alias = Alias.create('testAlias', true);
|
||||||
|
const alias2 = Alias.create('testAlias2');
|
||||||
|
const user = User.create('hardcoded', 'Testy') as User;
|
||||||
|
const note = Note.create(user, alias.name);
|
||||||
|
note.aliases.push(alias2);
|
||||||
|
it('mark the alias as primary', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(aliasRepo, 'findOne')
|
||||||
|
.mockResolvedValueOnce(alias)
|
||||||
|
.mockResolvedValueOnce(alias2);
|
||||||
|
jest
|
||||||
|
.spyOn(aliasRepo, 'save')
|
||||||
|
.mockImplementationOnce(async (alias: Alias): Promise<Alias> => alias)
|
||||||
|
.mockImplementationOnce(async (alias: Alias): Promise<Alias> => alias);
|
||||||
|
const createQueryBuilder = {
|
||||||
|
leftJoinAndSelect: () => createQueryBuilder,
|
||||||
|
where: () => createQueryBuilder,
|
||||||
|
orWhere: () => createQueryBuilder,
|
||||||
|
setParameter: () => createQueryBuilder,
|
||||||
|
getOne: () => {
|
||||||
|
return {
|
||||||
|
...note,
|
||||||
|
aliases: note.aliases.map((anAlias) => {
|
||||||
|
if (anAlias.primary) {
|
||||||
|
anAlias.primary = false;
|
||||||
|
}
|
||||||
|
if (anAlias.name === alias2.name) {
|
||||||
|
anAlias.primary = true;
|
||||||
|
}
|
||||||
|
return anAlias;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
jest
|
||||||
|
.spyOn(noteRepo, 'createQueryBuilder')
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
.mockImplementation(() => createQueryBuilder);
|
||||||
|
const savedAlias = await service.makeAliasPrimary(note, alias2.name);
|
||||||
|
expect(savedAlias.name).toEqual(alias2.name);
|
||||||
|
expect(savedAlias.primary).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('does not mark the alias as primary, if the alias does not exist', async () => {
|
||||||
|
await expect(
|
||||||
|
service.makeAliasPrimary(note, 'i_dont_exist'),
|
||||||
|
).rejects.toThrow(NotInDBError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toAliasDto correctly creates an AliasDto', () => {
|
||||||
|
const aliasName = 'testAlias';
|
||||||
|
const alias = Alias.create(aliasName, true);
|
||||||
|
const user = User.create('hardcoded', 'Testy') as User;
|
||||||
|
const note = Note.create(user, alias.name);
|
||||||
|
const aliasDto = service.toAliasDto(alias, note);
|
||||||
|
expect(aliasDto.name).toEqual(aliasName);
|
||||||
|
expect(aliasDto.primaryAlias).toBeTruthy();
|
||||||
|
expect(aliasDto.noteId).toEqual(note.publicId);
|
||||||
|
});
|
||||||
|
});
|
186
src/notes/alias.service.ts
Normal file
186
src/notes/alias.service.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlreadyInDBError,
|
||||||
|
NotInDBError,
|
||||||
|
PrimaryAliasDeletionForbiddenError,
|
||||||
|
} from '../errors/errors';
|
||||||
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
|
import { AliasDto } from './alias.dto';
|
||||||
|
import { Alias } from './alias.entity';
|
||||||
|
import { Note } from './note.entity';
|
||||||
|
import { NotesService } from './notes.service';
|
||||||
|
import { getPrimaryAlias } from './utils';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AliasService {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@InjectRepository(Note) private noteRepository: Repository<Note>,
|
||||||
|
@InjectRepository(Alias) private aliasRepository: Repository<Alias>,
|
||||||
|
@Inject(NotesService) private notesService: NotesService,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(AliasService.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Add the specified alias to the note.
|
||||||
|
* @param {Note} note - the note to add the alias to
|
||||||
|
* @param {string} alias - the alias to add to the note
|
||||||
|
* @throws {AlreadyInDBError} the alias is already in use.
|
||||||
|
* @return {Alias} the new alias
|
||||||
|
*/
|
||||||
|
async addAlias(note: Note, alias: string): Promise<Alias> {
|
||||||
|
this.notesService.checkNoteIdOrAlias(alias);
|
||||||
|
|
||||||
|
const foundAlias = await this.aliasRepository.findOne({
|
||||||
|
where: { name: alias },
|
||||||
|
});
|
||||||
|
if (foundAlias !== undefined) {
|
||||||
|
this.logger.debug(`The alias '${alias}' is already used.`, 'addAlias');
|
||||||
|
throw new AlreadyInDBError(`The alias '${alias}' is already used.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundNote = await this.noteRepository.findOne({
|
||||||
|
where: { publicId: alias },
|
||||||
|
});
|
||||||
|
if (foundNote !== undefined) {
|
||||||
|
this.logger.debug(
|
||||||
|
`The alias '${alias}' is already a public id.`,
|
||||||
|
'addAlias',
|
||||||
|
);
|
||||||
|
throw new AlreadyInDBError(
|
||||||
|
`The alias '${alias}' is already a public id.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let newAlias: Alias;
|
||||||
|
if (note.aliases.length === 0) {
|
||||||
|
// the first alias is automatically made the primary alias
|
||||||
|
newAlias = Alias.create(alias, true);
|
||||||
|
} else {
|
||||||
|
newAlias = Alias.create(alias);
|
||||||
|
}
|
||||||
|
note.aliases.push(newAlias);
|
||||||
|
|
||||||
|
await this.noteRepository.save(note);
|
||||||
|
return newAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Set the specified alias as the primary alias of the note.
|
||||||
|
* @param {Note} note - the note to change the primary alias
|
||||||
|
* @param {string} alias - the alias to be the new primary alias of the note
|
||||||
|
* @throws {NotInDBError} the alias is not part of this note.
|
||||||
|
* @return {Alias} the new primary alias
|
||||||
|
*/
|
||||||
|
async makeAliasPrimary(note: Note, alias: string): Promise<Alias> {
|
||||||
|
let newPrimaryFound = false;
|
||||||
|
let oldPrimaryId = '';
|
||||||
|
let newPrimaryId = '';
|
||||||
|
|
||||||
|
for (const anAlias of note.aliases) {
|
||||||
|
// found old primary
|
||||||
|
if (anAlias.primary) {
|
||||||
|
oldPrimaryId = anAlias.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// found new primary
|
||||||
|
if (anAlias.name === alias) {
|
||||||
|
newPrimaryFound = true;
|
||||||
|
newPrimaryId = anAlias.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPrimaryFound) {
|
||||||
|
// the provided alias is not already an alias of this note
|
||||||
|
this.logger.debug(
|
||||||
|
`The alias '${alias}' is not used by this note.`,
|
||||||
|
'makeAliasPrimary',
|
||||||
|
);
|
||||||
|
throw new NotInDBError(`The alias '${alias}' is not used by this note.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPrimary = await this.aliasRepository.findOne(oldPrimaryId);
|
||||||
|
const newPrimary = await this.aliasRepository.findOne(newPrimaryId);
|
||||||
|
|
||||||
|
if (!oldPrimary || !newPrimary) {
|
||||||
|
throw new Error('This should not happen!');
|
||||||
|
}
|
||||||
|
|
||||||
|
oldPrimary.primary = false;
|
||||||
|
newPrimary.primary = true;
|
||||||
|
|
||||||
|
await this.aliasRepository.save(oldPrimary);
|
||||||
|
await this.aliasRepository.save(newPrimary);
|
||||||
|
|
||||||
|
return newPrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Remove the specified alias from the note.
|
||||||
|
* @param {Note} note - the note to remove the alias from
|
||||||
|
* @param {string} alias - the alias to remove from the note
|
||||||
|
* @throws {NotInDBError} the alias is not part of this note.
|
||||||
|
* @throws {PrimaryAliasDeletionForbiddenError} the primary alias can only be deleted if it's the only alias
|
||||||
|
*/
|
||||||
|
async removeAlias(note: Note, alias: string): Promise<Note> {
|
||||||
|
const primaryAlias = getPrimaryAlias(note);
|
||||||
|
|
||||||
|
if (primaryAlias === alias && note.aliases.length !== 1) {
|
||||||
|
this.logger.debug(
|
||||||
|
`The alias '${alias}' is the primary alias, which can only be removed if it's the only alias.`,
|
||||||
|
'removeAlias',
|
||||||
|
);
|
||||||
|
throw new PrimaryAliasDeletionForbiddenError(
|
||||||
|
`The alias '${alias}' is the primary alias, which can only be removed if it's the only alias.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredAliases = note.aliases.filter(
|
||||||
|
(anAlias) => anAlias.name !== alias,
|
||||||
|
);
|
||||||
|
if (note.aliases.length === filteredAliases.length) {
|
||||||
|
this.logger.debug(
|
||||||
|
`The alias '${alias}' is not used by this note or is the primary alias, which can't be removed.`,
|
||||||
|
'removeAlias',
|
||||||
|
);
|
||||||
|
throw new NotInDBError(
|
||||||
|
`The alias '${alias}' is not used by this note or is the primary alias, which can't be removed.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const aliasToDelete = note.aliases.find(
|
||||||
|
(anAlias) => anAlias.name === alias,
|
||||||
|
);
|
||||||
|
if (aliasToDelete !== undefined) {
|
||||||
|
await this.aliasRepository.remove(aliasToDelete);
|
||||||
|
}
|
||||||
|
note.aliases = filteredAliases;
|
||||||
|
return await this.noteRepository.save(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* Build AliasDto from a note.
|
||||||
|
* @param {Alias} alias - the alias to use
|
||||||
|
* @param {Note} note - the note to use
|
||||||
|
* @return {AliasDto} the built AliasDto
|
||||||
|
* @throws {NotInDBError} the specified alias does not exist
|
||||||
|
*/
|
||||||
|
toAliasDto(alias: Alias, note: Note): AliasDto {
|
||||||
|
return {
|
||||||
|
name: alias.name,
|
||||||
|
primaryAlias: alias.primary,
|
||||||
|
noteId: note.publicId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import { RevisionsModule } from '../revisions/revisions.module';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
import { Alias } from './alias.entity';
|
import { Alias } from './alias.entity';
|
||||||
|
import { AliasService } from './alias.service';
|
||||||
import { Note } from './note.entity';
|
import { Note } from './note.entity';
|
||||||
import { NotesService } from './notes.service';
|
import { NotesService } from './notes.service';
|
||||||
import { Tag } from './tag.entity';
|
import { Tag } from './tag.entity';
|
||||||
|
@ -36,7 +37,7 @@ import { Tag } from './tag.entity';
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [NotesService],
|
providers: [NotesService, AliasService],
|
||||||
exports: [NotesService],
|
exports: [NotesService, AliasService],
|
||||||
})
|
})
|
||||||
export class NotesModule {}
|
export class NotesModule {}
|
||||||
|
|
Loading…
Reference in a new issue