mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-20 18:25:21 -04:00
refactor: replace TypeORM with knex.js
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
6e151c8a1b
commit
c0ce00b3f9
242 changed files with 4601 additions and 6871 deletions
|
@ -33,12 +33,12 @@ exports[`NotesService toNoteDto works 1`] = `
|
|||
},
|
||||
],
|
||||
},
|
||||
"primaryAddress": "testAlias",
|
||||
"primaryAlias": "testAlias",
|
||||
"tags": [
|
||||
"tag1",
|
||||
],
|
||||
"title": "mockTitle",
|
||||
"updateUsername": "hardcoded",
|
||||
"lastUpdatedBy": "hardcoded",
|
||||
"updatedAt": "2019-02-04T20:34:12.000Z",
|
||||
"version": undefined,
|
||||
"viewCount": 1337,
|
||||
|
@ -76,12 +76,12 @@ exports[`NotesService toNoteMetadataDto works 1`] = `
|
|||
},
|
||||
],
|
||||
},
|
||||
"primaryAddress": "testAlias",
|
||||
"primaryAlias": "testAlias",
|
||||
"tags": [
|
||||
"tag1",
|
||||
],
|
||||
"title": "mockTitle",
|
||||
"updateUsername": "hardcoded",
|
||||
"lastUpdatedBy": "hardcoded",
|
||||
"updatedAt": "2019-02-04T20:34:12.000Z",
|
||||
"version": undefined,
|
||||
"viewCount": 1337,
|
||||
|
|
|
@ -1,313 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import { ApiToken } from '../api-token/api-token.entity';
|
||||
import { Identity } from '../auth/identity.entity';
|
||||
import { Author } from '../authors/author.entity';
|
||||
import appConfigMock from '../config/mock/app.config.mock';
|
||||
import authConfigMock from '../config/mock/auth.config.mock';
|
||||
import databaseConfigMock from '../config/mock/database.config.mock';
|
||||
import noteConfigMock from '../config/mock/note.config.mock';
|
||||
import { User } from '../database/user.entity';
|
||||
import {
|
||||
AlreadyInDBError,
|
||||
ForbiddenIdError,
|
||||
NotInDBError,
|
||||
PrimaryAliasDeletionForbiddenError,
|
||||
} from '../errors/errors';
|
||||
import { eventModuleConfig } from '../events';
|
||||
import { Group } from '../groups/group.entity';
|
||||
import { GroupsModule } from '../groups/groups.module';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
|
||||
import { Edit } from '../revisions/edit.entity';
|
||||
import { Revision } from '../revisions/revision.entity';
|
||||
import { RevisionsModule } from '../revisions/revisions.module';
|
||||
import { Session } from '../sessions/session.entity';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { mockSelectQueryBuilderInRepo } from '../utils/test-utils/mockSelectQueryBuilder';
|
||||
import { Alias } from './alias.entity';
|
||||
import { AliasService } from './alias.service';
|
||||
import { Note } from './note.entity';
|
||||
import { NotesModule } from './notes.module';
|
||||
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 () => {
|
||||
noteRepo = new Repository<Note>(
|
||||
'',
|
||||
new EntityManager(
|
||||
new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
}),
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
aliasRepo = new Repository<Alias>(
|
||||
'',
|
||||
new EntityManager(
|
||||
new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
}),
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AliasService,
|
||||
NotesService,
|
||||
{
|
||||
provide: getRepositoryToken(Note),
|
||||
useValue: noteRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Alias),
|
||||
useValue: aliasRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Tag),
|
||||
useClass: Repository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useClass: Repository,
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [
|
||||
appConfigMock,
|
||||
databaseConfigMock,
|
||||
authConfigMock,
|
||||
noteConfigMock,
|
||||
],
|
||||
}),
|
||||
LoggerModule,
|
||||
UsersModule,
|
||||
GroupsModule,
|
||||
RevisionsModule,
|
||||
NotesModule,
|
||||
RealtimeNoteModule,
|
||||
EventEmitterModule.forRoot(eventModuleConfig),
|
||||
],
|
||||
})
|
||||
.overrideProvider(getRepositoryToken(Note))
|
||||
.useValue(noteRepo)
|
||||
.overrideProvider(getRepositoryToken(Tag))
|
||||
.useClass(Repository)
|
||||
.overrideProvider(getRepositoryToken(Alias))
|
||||
.useValue(aliasRepo)
|
||||
.overrideProvider(getRepositoryToken(User))
|
||||
.useClass(Repository)
|
||||
.overrideProvider(getRepositoryToken(ApiToken))
|
||||
.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('noteConfig').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) as Note;
|
||||
jest
|
||||
.spyOn(noteRepo, 'save')
|
||||
.mockImplementationOnce(async (note: Note): Promise<Note> => note);
|
||||
jest.spyOn(noteRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
jest.spyOn(aliasRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
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) as Note;
|
||||
jest
|
||||
.spyOn(noteRepo, 'save')
|
||||
.mockImplementationOnce(async (note: Note): Promise<Note> => note);
|
||||
jest.spyOn(noteRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
jest.spyOn(aliasRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
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) as Note;
|
||||
it('with an already used name', async () => {
|
||||
jest.spyOn(noteRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
jest.spyOn(aliasRepo, 'existsBy').mockResolvedValueOnce(true);
|
||||
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', () => {
|
||||
let note: Note;
|
||||
beforeAll(async () => {
|
||||
note = Note.create(user, alias) as Note;
|
||||
(await note.aliases).push(Alias.create(alias2, note, false) as Alias);
|
||||
});
|
||||
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);
|
||||
const aliases = await savedNote.aliases;
|
||||
expect(aliases).toHaveLength(1);
|
||||
expect(aliases[0].name).toEqual(alias);
|
||||
expect(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(await savedNote.aliases).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
describe('does not remove one alias', () => {
|
||||
let note: Note;
|
||||
beforeEach(async () => {
|
||||
note = Note.create(user, alias) as Note;
|
||||
(await note.aliases).push(Alias.create(alias2, note, false) as Alias);
|
||||
});
|
||||
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 user = User.create('hardcoded', 'Testy') as User;
|
||||
const aliasName = 'testAlias';
|
||||
let note: Note;
|
||||
let alias: Alias;
|
||||
let alias2: Alias;
|
||||
beforeEach(async () => {
|
||||
note = Note.create(user, aliasName) as Note;
|
||||
alias = Alias.create(aliasName, note, true) as Alias;
|
||||
alias2 = Alias.create('testAlias2', note, false) as Alias;
|
||||
(await note.aliases).push(
|
||||
Alias.create('testAlias2', note, false) as Alias,
|
||||
);
|
||||
});
|
||||
|
||||
it('mark the alias as primary', async () => {
|
||||
jest
|
||||
.spyOn(aliasRepo, 'findOneByOrFail')
|
||||
.mockResolvedValueOnce(alias)
|
||||
.mockResolvedValueOnce(alias2);
|
||||
jest
|
||||
.spyOn(aliasRepo, 'save')
|
||||
.mockImplementationOnce(async (alias: Alias): Promise<Alias> => alias)
|
||||
.mockImplementationOnce(async (alias: Alias): Promise<Alias> => alias);
|
||||
|
||||
mockSelectQueryBuilderInRepo(
|
||||
noteRepo,
|
||||
Mock.of<Note>({
|
||||
...note,
|
||||
aliases: Promise.resolve(
|
||||
(await note.aliases).map((anAlias) => {
|
||||
if (anAlias.primary) {
|
||||
anAlias.primary = false;
|
||||
}
|
||||
if (anAlias.name === alias2.name) {
|
||||
anAlias.primary = true;
|
||||
}
|
||||
return anAlias;
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
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 user = User.create('hardcoded', 'Testy') as User;
|
||||
const note = Note.create(user, aliasName) as Note;
|
||||
const alias = Alias.create(aliasName, note, true) as Alias;
|
||||
const aliasDto = service.toAliasDto(alias, note);
|
||||
expect(aliasDto.name).toEqual(aliasName);
|
||||
expect(aliasDto.primaryAlias).toBeTruthy();
|
||||
expect(aliasDto.noteId).toEqual(note.publicId);
|
||||
});
|
||||
});
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { AliasDto } from '@hedgedoc/commons';
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
NotInDBError,
|
||||
PrimaryAliasDeletionForbiddenError,
|
||||
} from '../errors/errors';
|
||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||
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(forwardRef(() => 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.
|
||||
* @throws {ForbiddenIdError} the requested id or alias is forbidden
|
||||
* @return {Alias} the new alias
|
||||
*/
|
||||
async addAlias(note: Note, alias: string): Promise<Alias> {
|
||||
await this.notesService.ensureNoteIdOrAliasIsAvailable(alias);
|
||||
|
||||
let newAlias;
|
||||
if ((await note.aliases).length === 0) {
|
||||
// the first alias is automatically made the primary alias
|
||||
newAlias = Alias.create(alias, note, true);
|
||||
} else {
|
||||
newAlias = Alias.create(alias, note, false);
|
||||
}
|
||||
(await note.aliases).push(newAlias as Alias);
|
||||
|
||||
await this.noteRepository.save(note);
|
||||
return newAlias as Alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 {ForbiddenIdError} the requested id or alias is forbidden
|
||||
* @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 = 0;
|
||||
let newPrimaryId = 0;
|
||||
|
||||
for (const anAlias of await 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.findOneByOrFail({
|
||||
id: oldPrimaryId,
|
||||
});
|
||||
const newPrimary = await this.aliasRepository.findOneByOrFail({
|
||||
id: newPrimaryId,
|
||||
});
|
||||
|
||||
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 {ForbiddenIdError} the requested id or alias is forbidden
|
||||
* @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 = await getPrimaryAlias(note);
|
||||
const noteAliases = await note.aliases;
|
||||
|
||||
if (primaryAlias === alias && noteAliases.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: Alias[] = [];
|
||||
let aliasToDelete: Alias | null = null;
|
||||
let aliasFound = false;
|
||||
|
||||
for (const anAlias of noteAliases) {
|
||||
if (anAlias.name === alias) {
|
||||
aliasFound = true;
|
||||
aliasToDelete = anAlias;
|
||||
} else {
|
||||
filteredAliases.push(anAlias);
|
||||
}
|
||||
}
|
||||
|
||||
if (!aliasFound) {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (aliasToDelete !== null) {
|
||||
await this.aliasRepository.remove(aliasToDelete);
|
||||
}
|
||||
|
||||
note.aliases = Promise.resolve(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,
|
||||
};
|
||||
}
|
||||
}
|
35
backend/src/notes/note.module.ts
Normal file
35
backend/src/notes/note.module.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { KnexModule } from 'nest-knexjs';
|
||||
|
||||
import { AliasModule } from '../alias/alias.module';
|
||||
import { GroupsModule } from '../groups/groups.module';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { PermissionsModule } from '../permissions/permissions.module';
|
||||
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
|
||||
import { RevisionsModule } from '../revisions/revisions.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { NoteService } from './note.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AliasModule,
|
||||
RevisionsModule,
|
||||
UsersModule,
|
||||
GroupsModule,
|
||||
LoggerModule,
|
||||
forwardRef(() => PermissionsModule),
|
||||
ConfigModule,
|
||||
RealtimeNoteModule,
|
||||
KnexModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [NoteService],
|
||||
exports: [NoteService],
|
||||
})
|
||||
export class NoteModule {}
|
|
@ -15,6 +15,7 @@ import {
|
|||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { AliasService } from '../alias/alias.service';
|
||||
import { ApiToken } from '../api-token/api-token.entity';
|
||||
import { Identity } from '../auth/identity.entity';
|
||||
import { Author } from '../authors/author.entity';
|
||||
|
@ -49,16 +50,15 @@ import { RevisionsService } from '../revisions/revisions.service';
|
|||
import { Session } from '../sessions/session.entity';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { mockSelectQueryBuilderInRepo } from '../utils/test-utils/mockSelectQueryBuilder';
|
||||
import { Alias } from './alias.entity';
|
||||
import { AliasService } from './alias.service';
|
||||
import { Alias } from './aliases.entity';
|
||||
import { Note } from './note.entity';
|
||||
import { NotesService } from './notes.service';
|
||||
import { NoteService } from './note.service';
|
||||
import { Tag } from './tag.entity';
|
||||
|
||||
jest.mock('../revisions/revisions.service');
|
||||
|
||||
describe('NotesService', () => {
|
||||
let service: NotesService;
|
||||
let service: NoteService;
|
||||
let revisionsService: RevisionsService;
|
||||
const noteMockConfig: NoteConfig = createDefaultMockNoteConfig();
|
||||
let noteRepo: Repository<Note>;
|
||||
|
@ -137,7 +137,7 @@ describe('NotesService', () => {
|
|||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
NotesService,
|
||||
NoteService,
|
||||
{
|
||||
provide: RevisionsService,
|
||||
useValue: revisionsService,
|
||||
|
@ -219,7 +219,7 @@ describe('NotesService', () => {
|
|||
forbiddenNoteId = noteConfig.forbiddenNoteIds[0];
|
||||
everyoneDefaultAccessPermission = noteConfig.permissions.default.everyone;
|
||||
loggedinDefaultAccessPermission = noteConfig.permissions.default.loggedIn;
|
||||
service = module.get<NotesService>(NotesService);
|
||||
service = module.get<NoteService>(NoteService);
|
||||
noteRepo = module.get<Repository<Note>>(getRepositoryToken(Note));
|
||||
aliasRepo = module.get<Repository<Alias>>(getRepositoryToken(Alias));
|
||||
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
|
||||
|
@ -335,19 +335,19 @@ describe('NotesService', () => {
|
|||
|
||||
it('with no note', async () => {
|
||||
mockSelectQueryBuilderInRepo(noteRepo, null);
|
||||
const notes = await service.getUserNotes(user);
|
||||
const notes = await service.getUserNoteIds(user);
|
||||
expect(notes).toEqual([]);
|
||||
});
|
||||
|
||||
it('with one note', async () => {
|
||||
mockSelectQueryBuilderInRepo(noteRepo, note);
|
||||
const notes = await service.getUserNotes(user);
|
||||
const notes = await service.getUserNoteIds(user);
|
||||
expect(notes).toEqual([note]);
|
||||
});
|
||||
|
||||
it('with multiple note', async () => {
|
||||
mockSelectQueryBuilderInRepo(noteRepo, [note, note]);
|
||||
const notes = await service.getUserNotes(user);
|
||||
const notes = await service.getUserNoteIds(user);
|
||||
expect(notes).toEqual([note, note]);
|
||||
});
|
||||
});
|
||||
|
@ -377,7 +377,7 @@ describe('NotesService', () => {
|
|||
|
||||
mockSelectQueryBuilderInRepo(noteRepo, null);
|
||||
});
|
||||
it('without alias, without owner', async () => {
|
||||
it('without aliases, without owner', async () => {
|
||||
const newNote = await service.createNote(content, null);
|
||||
|
||||
expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content);
|
||||
|
@ -403,7 +403,7 @@ describe('NotesService', () => {
|
|||
expect(await newNote.owner).toBeNull();
|
||||
expect(await newNote.aliases).toHaveLength(0);
|
||||
});
|
||||
it('without alias, with owner', async () => {
|
||||
it('without aliases, with owner', async () => {
|
||||
const newNote = await service.createNote(content, user);
|
||||
expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content);
|
||||
expect(await newNote.revisions).toStrictEqual([newRevision]);
|
||||
|
@ -429,7 +429,7 @@ describe('NotesService', () => {
|
|||
expect(await newNote.owner).toEqual(user);
|
||||
expect(await newNote.aliases).toHaveLength(0);
|
||||
});
|
||||
it('with alias, without owner', async () => {
|
||||
it('with aliases, without owner', async () => {
|
||||
const newNote = await service.createNote(content, null, alias);
|
||||
expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content);
|
||||
expect(await newNote.revisions).toStrictEqual([newRevision]);
|
||||
|
@ -454,7 +454,7 @@ describe('NotesService', () => {
|
|||
expect(await newNote.owner).toBeNull();
|
||||
expect(await newNote.aliases).toHaveLength(1);
|
||||
});
|
||||
it('with alias, with owner', async () => {
|
||||
it('with aliases, with owner', async () => {
|
||||
const newNote = await service.createNote(content, user, alias);
|
||||
|
||||
expect(createRevisionSpy).toHaveBeenCalledWith(newNote, content);
|
||||
|
@ -549,7 +549,7 @@ describe('NotesService', () => {
|
|||
mockSelectQueryBuilderInRepo(noteRepo, null);
|
||||
});
|
||||
|
||||
it('alias is forbidden', async () => {
|
||||
it('aliases is forbidden', async () => {
|
||||
jest.spyOn(noteRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
jest.spyOn(aliasRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
await expect(
|
||||
|
@ -557,7 +557,7 @@ describe('NotesService', () => {
|
|||
).rejects.toThrow(ForbiddenIdError);
|
||||
});
|
||||
|
||||
it('alias is already used (as another alias)', async () => {
|
||||
it('aliases is already used (as another aliases)', async () => {
|
||||
mockGroupRepo();
|
||||
jest.spyOn(noteRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
jest.spyOn(aliasRepo, 'existsBy').mockResolvedValueOnce(true);
|
||||
|
@ -569,7 +569,7 @@ describe('NotesService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('alias is already used (as publicId)', async () => {
|
||||
it('aliases is already used (as publicId)', async () => {
|
||||
mockGroupRepo();
|
||||
jest.spyOn(noteRepo, 'existsBy').mockResolvedValueOnce(true);
|
||||
jest.spyOn(aliasRepo, 'existsBy').mockResolvedValueOnce(false);
|
||||
|
@ -614,20 +614,20 @@ describe('NotesService', () => {
|
|||
const user = User.create('hardcoded', 'Testy') as User;
|
||||
const note = Note.create(user) as Note;
|
||||
mockSelectQueryBuilderInRepo(noteRepo, note);
|
||||
const foundNote = await service.getNoteByIdOrAlias('noteThatExists');
|
||||
const foundNote = await service.getNoteIdByAlias('noteThatExists');
|
||||
expect(foundNote).toEqual(note);
|
||||
});
|
||||
describe('fails:', () => {
|
||||
it('no note found', async () => {
|
||||
mockSelectQueryBuilderInRepo(noteRepo, null);
|
||||
await expect(
|
||||
service.getNoteByIdOrAlias('noteThatDoesNoteExist'),
|
||||
service.getNoteIdByAlias('noteThatDoesNoteExist'),
|
||||
).rejects.toThrow(NotInDBError);
|
||||
});
|
||||
it('id is forbidden', async () => {
|
||||
await expect(
|
||||
service.getNoteByIdOrAlias(forbiddenNoteId),
|
||||
).rejects.toThrow(ForbiddenIdError);
|
||||
await expect(service.getNoteIdByAlias(forbiddenNoteId)).rejects.toThrow(
|
||||
ForbiddenIdError,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -704,10 +704,10 @@ describe('NotesService', () => {
|
|||
expect(metadataDto).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('returns publicId if no alias exists', async () => {
|
||||
it('returns publicId if no aliases exists', async () => {
|
||||
const [note, ,] = await getMockData();
|
||||
const metadataDto = await service.toNoteMetadataDto(note);
|
||||
expect(metadataDto.primaryAddress).toEqual(note.publicId);
|
||||
expect(metadataDto.primaryAlias).toEqual(note.publicId);
|
||||
});
|
||||
});
|
||||
|
466
backend/src/notes/note.service.ts
Normal file
466
backend/src/notes/note.service.ts
Normal file
|
@ -0,0 +1,466 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import {
|
||||
NoteDto,
|
||||
NoteMetadataDto,
|
||||
NotePermissionsDto,
|
||||
SpecialGroup,
|
||||
} from '@hedgedoc/commons';
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Knex } from 'knex';
|
||||
import { InjectConnection } from 'nest-knexjs';
|
||||
|
||||
import { AliasService } from '../alias/alias.service';
|
||||
import { DefaultAccessLevel } from '../config/default-access-level.enum';
|
||||
import noteConfiguration, { NoteConfig } from '../config/note.config';
|
||||
import {
|
||||
FieldNameAlias,
|
||||
FieldNameGroup,
|
||||
FieldNameNote,
|
||||
FieldNameNoteGroupPermission,
|
||||
FieldNameNoteUserPermission,
|
||||
FieldNameRevision,
|
||||
FieldNameUser,
|
||||
Group,
|
||||
Note,
|
||||
NoteGroupPermission,
|
||||
NoteUserPermission,
|
||||
TableAlias,
|
||||
TableGroup,
|
||||
TableNote,
|
||||
TableNoteGroupPermission,
|
||||
TableNoteUserPermission,
|
||||
TableUser,
|
||||
User,
|
||||
} from '../database/types';
|
||||
import {
|
||||
ForbiddenIdError,
|
||||
GenericDBError,
|
||||
MaximumDocumentLengthExceededError,
|
||||
NotInDBError,
|
||||
} from '../errors/errors';
|
||||
import { NoteEventMap } from '../events';
|
||||
import { GroupsService } from '../groups/groups.service';
|
||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||
import { PermissionService } from '../permissions/permission.service';
|
||||
import { RealtimeNoteStore } from '../realtime/realtime-note/realtime-note-store';
|
||||
import { RealtimeNoteService } from '../realtime/realtime-note/realtime-note.service';
|
||||
import { RevisionsService } from '../revisions/revisions.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
|
||||
@Injectable()
|
||||
export class NoteService {
|
||||
constructor(
|
||||
@InjectConnection()
|
||||
private readonly knex: Knex,
|
||||
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@Inject(UsersService) private usersService: UsersService,
|
||||
@Inject(GroupsService) private groupsService: GroupsService,
|
||||
private revisionsService: RevisionsService,
|
||||
@Inject(noteConfiguration.KEY)
|
||||
private noteConfig: NoteConfig,
|
||||
@Inject(AliasService)
|
||||
private aliasService: AliasService,
|
||||
@Inject(forwardRef(() => PermissionService))
|
||||
private permissionService: PermissionService,
|
||||
private realtimeNoteService: RealtimeNoteService,
|
||||
private realtimeNoteStore: RealtimeNoteStore,
|
||||
private eventEmitter: EventEmitter2<NoteEventMap>,
|
||||
) {
|
||||
this.logger.setContext(NoteService.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notes owned by a user
|
||||
*
|
||||
* @param userId The id of the user who owns the notes
|
||||
* @return Array of notes owned by the user
|
||||
*/
|
||||
async getUserNoteIds(userId: number): Promise<number[]> {
|
||||
const result = await this.knex(TableNote)
|
||||
.select(FieldNameNote.id)
|
||||
.where(FieldNameNote.ownerId, userId);
|
||||
return result.map((row) => row[FieldNameNote.id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note
|
||||
*
|
||||
* @param noteContent The content of the new note, in most cases an empty string
|
||||
* @param givenAlias An optional alias the note should have
|
||||
* @param ownerUserId The owner of the note
|
||||
* @return The newly created note
|
||||
* @throws {AlreadyInDBError} a note with the requested id or aliases already exists
|
||||
* @throws {ForbiddenIdError} the requested id or aliases is forbidden
|
||||
* @throws {MaximumDocumentLengthExceededError} the noteContent is longer than the maxDocumentLength
|
||||
* @thorws {GenericDBError} the database returned a non-expected value
|
||||
*/
|
||||
async createNote(
|
||||
noteContent: string,
|
||||
ownerUserId: number,
|
||||
givenAlias?: string,
|
||||
): Promise<number> {
|
||||
// Check if new note doesn't violate application constraints
|
||||
if (noteContent.length > this.noteConfig.maxDocumentLength) {
|
||||
throw new MaximumDocumentLengthExceededError();
|
||||
}
|
||||
return await this.knex.transaction(async (transaction) => {
|
||||
// Create note itself in the database
|
||||
const createdNotes = await transaction(TableNote).insert(
|
||||
{
|
||||
[FieldNameNote.ownerId]: ownerUserId,
|
||||
[FieldNameNote.version]: 2,
|
||||
},
|
||||
[FieldNameNote.id],
|
||||
);
|
||||
|
||||
if (createdNotes.length !== 1) {
|
||||
throw new GenericDBError(
|
||||
'The note could not be created in the database',
|
||||
this.logger.getContext(),
|
||||
'createNote',
|
||||
);
|
||||
}
|
||||
|
||||
const noteId = createdNotes[0][FieldNameNote.id];
|
||||
|
||||
if (givenAlias !== undefined) {
|
||||
await this.aliasService.ensureAliasIsAvailable(givenAlias, transaction);
|
||||
}
|
||||
const newAlias =
|
||||
givenAlias === undefined
|
||||
? this.aliasService.generateRandomAlias()
|
||||
: givenAlias;
|
||||
await this.aliasService.addAlias(noteId, newAlias, transaction);
|
||||
|
||||
await this.revisionsService.createRevision(
|
||||
noteId,
|
||||
noteContent,
|
||||
transaction,
|
||||
);
|
||||
|
||||
const isUserRegistered = await this.usersService.isRegisteredUser(
|
||||
ownerUserId,
|
||||
transaction,
|
||||
);
|
||||
|
||||
const everyoneAccessLevel = isUserRegistered
|
||||
? // Use the default access level from the config for registered users
|
||||
this.noteConfig.permissions.default.everyone
|
||||
: // If the owner is a guest, this is an anonymous note
|
||||
// Anonymous notes are always writeable by everyone
|
||||
DefaultAccessLevel.WRITE;
|
||||
|
||||
const loggedInUsersAccessLevel =
|
||||
this.noteConfig.permissions.default.loggedIn;
|
||||
|
||||
if (everyoneAccessLevel !== DefaultAccessLevel.NONE) {
|
||||
const everyoneAccessGroupId = await this.groupsService.getGroupIdByName(
|
||||
SpecialGroup.EVERYONE,
|
||||
);
|
||||
await this.permissionService.setGroupPermission(
|
||||
noteId,
|
||||
everyoneAccessGroupId,
|
||||
everyoneAccessLevel === DefaultAccessLevel.WRITE,
|
||||
transaction,
|
||||
);
|
||||
}
|
||||
|
||||
if (loggedInUsersAccessLevel !== DefaultAccessLevel.NONE) {
|
||||
const loggedInUsersAccessGroupId =
|
||||
await this.groupsService.getGroupIdByName(SpecialGroup.LOGGED_IN);
|
||||
await this.permissionService.setGroupPermission(
|
||||
noteId,
|
||||
loggedInUsersAccessGroupId,
|
||||
loggedInUsersAccessLevel === DefaultAccessLevel.WRITE,
|
||||
transaction,
|
||||
);
|
||||
}
|
||||
|
||||
return noteId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current content of the note
|
||||
*
|
||||
* @param noteId the note to use
|
||||
* @param transaction The optional database transaction to use
|
||||
* @throws {NotInDBError} the note is not in the DB
|
||||
* @return {string} the content of the note
|
||||
*/
|
||||
async getNoteContent(noteId: number, transaction?: Knex): Promise<string> {
|
||||
const realtimeContent = this.realtimeNoteStore
|
||||
.find(noteId)
|
||||
?.getRealtimeDoc()
|
||||
.getCurrentContent();
|
||||
if (realtimeContent) {
|
||||
return realtimeContent;
|
||||
}
|
||||
|
||||
const latestRevision = await this.revisionsService.getLatestRevision(
|
||||
noteId,
|
||||
transaction,
|
||||
);
|
||||
return latestRevision.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a note by either their id or aliases
|
||||
*
|
||||
* @param alias the notes id or aliases
|
||||
* @param transaction The optional database transaction to use
|
||||
* @return the note id
|
||||
* @throws {NotInDBError} there is no note with this id or aliases
|
||||
* @throws {ForbiddenIdError} the requested id or aliases is forbidden
|
||||
*/
|
||||
async getNoteIdByAlias(alias: string, transaction?: Knex): Promise<number> {
|
||||
const dbActor = transaction ?? this.knex;
|
||||
const isForbidden = this.aliasService.isAliasForbidden(alias);
|
||||
if (isForbidden) {
|
||||
throw new ForbiddenIdError(
|
||||
`The note id or alias '${alias}' is forbidden by the administrator.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.debug(`Trying to find note '${alias}'`, 'getNoteIdByAlias');
|
||||
|
||||
/*
|
||||
* This query gets the note's aliases, owner, groupPermissions (and the groups), userPermissions (and the users) and tags and
|
||||
* then only selects the note, that has a alias with this name.
|
||||
*/
|
||||
const note = await dbActor(TableAlias)
|
||||
.select<Pick<Note, FieldNameNote.id>>(`${TableNote}.${FieldNameNote.id}`)
|
||||
.where(FieldNameAlias.alias, alias)
|
||||
.join(
|
||||
TableNote,
|
||||
`${TableAlias}.${FieldNameAlias.noteId}`,
|
||||
`${TableNote}.${FieldNameNote.id}`,
|
||||
)
|
||||
.first();
|
||||
|
||||
if (note === undefined) {
|
||||
const message = `Could not find note '${alias}'`;
|
||||
this.logger.debug(message, 'getNoteIdByAlias');
|
||||
throw new NotInDBError(message);
|
||||
}
|
||||
this.logger.debug(`Found note '${alias}'`, 'getNoteIdByAlias');
|
||||
return note[FieldNameNote.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a note
|
||||
*
|
||||
* @param noteId If of the note to delete
|
||||
* @throws {NotInDBError} if there is no note with this id
|
||||
*/
|
||||
async deleteNote(noteId: Note[FieldNameNote.id]): Promise<void> {
|
||||
// TODO Disconnect realtime clients first
|
||||
const numberOfDeletedNotes = await this.knex(TableNote)
|
||||
.where(FieldNameNote.id, noteId)
|
||||
.delete();
|
||||
if (numberOfDeletedNotes === 0) {
|
||||
throw new NotInDBError(`There is no note with the to delete.`);
|
||||
}
|
||||
// TODO Message realtime clients
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Update the content of a note
|
||||
*
|
||||
* @param noteId - the note
|
||||
* @param noteContent - the new content
|
||||
* @return the note with a new revision and new content
|
||||
* @throws {NotInDBError} there is no note with this id or aliases
|
||||
*/
|
||||
async updateNote(noteId: number, noteContent: string): Promise<void> {
|
||||
// TODO Disconnect realtime clients first
|
||||
await this.revisionsService.createRevision(noteId, noteContent);
|
||||
// TODO Reload realtime note
|
||||
}
|
||||
|
||||
/**
|
||||
* Build NotePermissionsDto from a note.
|
||||
* @param noteId The id of the ntoe to get the permissions for
|
||||
* @param transaction The optional database transaction to use
|
||||
* @return The built NotePermissionDto
|
||||
*/
|
||||
async toNotePermissionsDto(
|
||||
noteId: number,
|
||||
transaction?: Knex,
|
||||
): Promise<NotePermissionsDto> {
|
||||
if (transaction === undefined) {
|
||||
return await this.knex.transaction(async (newTransaction) => {
|
||||
return await this.innerToNotePermissionsDto(noteId, newTransaction);
|
||||
});
|
||||
}
|
||||
return await this.innerToNotePermissionsDto(noteId, transaction);
|
||||
}
|
||||
|
||||
async innerToNotePermissionsDto(
|
||||
noteId: number,
|
||||
transaction: Knex,
|
||||
): Promise<NotePermissionsDto> {
|
||||
const ownerUsername = await transaction(TableNote)
|
||||
.join(
|
||||
TableUser,
|
||||
`${TableNote}.${FieldNameNote.ownerId}`,
|
||||
`${TableUser}.${FieldNameUser.id}`,
|
||||
)
|
||||
.select<
|
||||
Pick<User, FieldNameUser.username>
|
||||
>(`${TableUser}.${FieldNameUser.username}`)
|
||||
.where(`${TableNote}.${FieldNameNote.id}`, noteId)
|
||||
.first();
|
||||
if (ownerUsername === undefined) {
|
||||
throw new NotInDBError(
|
||||
`The note does not exist.`,
|
||||
this.logger.getContext(),
|
||||
'toNotePermissionsDto',
|
||||
);
|
||||
}
|
||||
const userPermissions = await transaction(TableNoteUserPermission)
|
||||
.join(
|
||||
TableUser,
|
||||
`${TableNoteUserPermission}.${FieldNameNoteUserPermission.userId}`,
|
||||
`${TableUser}.${FieldNameUser.id}`,
|
||||
)
|
||||
.select<
|
||||
({ [FieldNameUser.username]: string } & Pick<
|
||||
NoteUserPermission,
|
||||
FieldNameNoteUserPermission.canEdit
|
||||
>)[]
|
||||
>(`${TableUser}.${FieldNameUser.username}`, `${TableNoteUserPermission}.${FieldNameNoteUserPermission.canEdit}`)
|
||||
.whereNotNull(`${TableUser}.${FieldNameUser.username}`)
|
||||
.andWhere(
|
||||
`${TableNoteUserPermission}.${FieldNameNoteUserPermission.noteId}`,
|
||||
noteId,
|
||||
);
|
||||
const groupPermissions = await transaction(TableNoteGroupPermission)
|
||||
.join(
|
||||
TableGroup,
|
||||
`${TableNoteGroupPermission}.${FieldNameNoteGroupPermission.groupId}`,
|
||||
`${TableGroup}.${FieldNameGroup.id}`,
|
||||
)
|
||||
.select<
|
||||
(Pick<Group, FieldNameGroup.name> &
|
||||
Pick<NoteGroupPermission, FieldNameNoteGroupPermission.canEdit>)[]
|
||||
>(`${TableGroup}.${FieldNameGroup.name}`, `${TableNoteGroupPermission}.${FieldNameNoteGroupPermission.canEdit}`)
|
||||
.where(
|
||||
`${TableNoteGroupPermission}.${FieldNameNoteGroupPermission.noteId}`,
|
||||
noteId,
|
||||
);
|
||||
|
||||
return {
|
||||
owner: ownerUsername[FieldNameUser.username],
|
||||
sharedToUsers: userPermissions.map((noteUserPermission) => ({
|
||||
username: noteUserPermission[FieldNameUser.username],
|
||||
canEdit: noteUserPermission[FieldNameNoteUserPermission.canEdit],
|
||||
})),
|
||||
sharedToGroups: groupPermissions.map((noteGroupPermission) => ({
|
||||
groupName: noteGroupPermission[FieldNameGroup.name],
|
||||
canEdit: noteGroupPermission[FieldNameNoteGroupPermission.canEdit],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Build NoteMetadataDto from a note.
|
||||
* @param noteId The if of the note to get the metadata for
|
||||
* @param transaction The optional database transaction to use
|
||||
* @return The built NoteMetadataDto
|
||||
*/
|
||||
async toNoteMetadataDto(
|
||||
noteId: number,
|
||||
transaction?: Knex,
|
||||
): Promise<NoteMetadataDto> {
|
||||
if (transaction === undefined) {
|
||||
return await this.knex.transaction(async (newTransaction) => {
|
||||
return await this.innerToNoteMetadataDto(noteId, newTransaction);
|
||||
});
|
||||
}
|
||||
return await this.innerToNoteMetadataDto(noteId, transaction);
|
||||
}
|
||||
|
||||
private async innerToNoteMetadataDto(
|
||||
noteId: number,
|
||||
transaction: Knex,
|
||||
): Promise<NoteMetadataDto> {
|
||||
const aliases = await this.aliasService.getAllAliases(noteId, transaction);
|
||||
const primaryAlias = aliases.find(
|
||||
(alias) => alias[FieldNameAlias.isPrimary],
|
||||
);
|
||||
if (primaryAlias === undefined) {
|
||||
throw new NotInDBError(
|
||||
'The note has no primary alias.',
|
||||
this.logger.getContext(),
|
||||
'toNoteMetadataDto',
|
||||
);
|
||||
}
|
||||
const note = await transaction(TableNote)
|
||||
.select(FieldNameNote.createdAt, FieldNameNote.version)
|
||||
.where(FieldNameNote.id, noteId)
|
||||
.first();
|
||||
if (note === undefined) {
|
||||
throw new NotInDBError(
|
||||
`The note '${primaryAlias[FieldNameAlias.alias]}' does not exist.`,
|
||||
this.logger.getContext(),
|
||||
'toNoteMetadataDto',
|
||||
);
|
||||
}
|
||||
const latestRevision = await this.revisionsService.getLatestRevision(
|
||||
noteId,
|
||||
transaction,
|
||||
);
|
||||
const tags = await this.revisionsService.getTagsByRevisionUuid(
|
||||
latestRevision[FieldNameRevision.uuid],
|
||||
transaction,
|
||||
);
|
||||
const updateUsers = await this.revisionsService.getRevisionUserInfo(
|
||||
latestRevision[FieldNameRevision.uuid],
|
||||
);
|
||||
updateUsers.users.sort(
|
||||
(userA, userB) => userB.createdAt.getTime() - userA.createdAt.getTime(),
|
||||
);
|
||||
const lastEdit = updateUsers.users[0];
|
||||
const editedBy = updateUsers.users.map((user) => user.username);
|
||||
const permissions = await this.toNotePermissionsDto(noteId, transaction);
|
||||
|
||||
return {
|
||||
aliases: aliases.map((alias) => alias[FieldNameAlias.alias]),
|
||||
primaryAlias: primaryAlias[FieldNameAlias.alias],
|
||||
title: latestRevision.title,
|
||||
description: latestRevision.description,
|
||||
tags: tags,
|
||||
createdAt: note[FieldNameNote.createdAt].toISOString(),
|
||||
editedBy: editedBy,
|
||||
permissions: permissions,
|
||||
version: note[FieldNameNote.version],
|
||||
updatedAt: lastEdit.createdAt.toISOString(),
|
||||
lastUpdatedBy: lastEdit.username,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the note data for the note DTO
|
||||
*
|
||||
* @param noteId The id of the note to transform
|
||||
* @return {NoteDto} the built NoteDto
|
||||
*/
|
||||
async toNoteDto(noteId: number): Promise<NoteDto> {
|
||||
return await this.knex.transaction(async (transaction) => {
|
||||
return {
|
||||
content: await this.getNoteContent(noteId, transaction),
|
||||
metadata: await this.toNoteMetadataDto(noteId, transaction),
|
||||
editedByAtPosition: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { User } from '../database/user.entity';
|
||||
import { GroupsModule } from '../groups/groups.module';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
|
||||
import { RevisionsModule } from '../revisions/revisions.module';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Note,
|
||||
Tag,
|
||||
NoteGroupPermission,
|
||||
NoteUserPermission,
|
||||
User,
|
||||
Alias,
|
||||
]),
|
||||
RevisionsModule,
|
||||
UsersModule,
|
||||
GroupsModule,
|
||||
LoggerModule,
|
||||
ConfigModule,
|
||||
RealtimeNoteModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [NotesService, AliasService],
|
||||
exports: [NotesService, AliasService],
|
||||
})
|
||||
export class NotesModule {}
|
|
@ -1,455 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import {
|
||||
NoteDto,
|
||||
NoteMetadataDto,
|
||||
NotePermissionsDto,
|
||||
} from '@hedgedoc/commons';
|
||||
import { Optional } from '@mrdrogdrog/optional';
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { DefaultAccessLevel } from '../config/default-access-level.enum';
|
||||
import noteConfiguration, { NoteConfig } from '../config/note.config';
|
||||
import { User } from '../database/user.entity';
|
||||
import {
|
||||
AlreadyInDBError,
|
||||
ForbiddenIdError,
|
||||
MaximumDocumentLengthExceededError,
|
||||
NotInDBError,
|
||||
} from '../errors/errors';
|
||||
import { NoteEvent, NoteEventMap } from '../events';
|
||||
import { Group } from '../groups/group.entity';
|
||||
import { GroupsService } from '../groups/groups.service';
|
||||
import { HistoryEntry } from '../history/history-entry.entity';
|
||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { RealtimeNoteStore } from '../realtime/realtime-note/realtime-note-store';
|
||||
import { RealtimeNoteService } from '../realtime/realtime-note/realtime-note.service';
|
||||
import { RevisionsService } from '../revisions/revisions.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { Alias } from './alias.entity';
|
||||
import { AliasService } from './alias.service';
|
||||
import { Note } from './note.entity';
|
||||
import { Tag } from './tag.entity';
|
||||
import { getPrimaryAlias } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class NotesService {
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@InjectRepository(Note) private noteRepository: Repository<Note>,
|
||||
@InjectRepository(Tag) private tagRepository: Repository<Tag>,
|
||||
@InjectRepository(Alias) private aliasRepository: Repository<Alias>,
|
||||
@InjectRepository(User) private userRepository: Repository<User>,
|
||||
@Inject(UsersService) private usersService: UsersService,
|
||||
@Inject(GroupsService) private groupsService: GroupsService,
|
||||
private revisionsService: RevisionsService,
|
||||
@Inject(noteConfiguration.KEY)
|
||||
private noteConfig: NoteConfig,
|
||||
@Inject(forwardRef(() => AliasService)) private aliasService: AliasService,
|
||||
private realtimeNoteService: RealtimeNoteService,
|
||||
private realtimeNoteStore: RealtimeNoteStore,
|
||||
private eventEmitter: EventEmitter2<NoteEventMap>,
|
||||
) {
|
||||
this.logger.setContext(NotesService.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Get all notes owned by a user.
|
||||
* @param {User} user - the user who owns the notes
|
||||
* @return {Note[]} arary of notes owned by the user
|
||||
*/
|
||||
async getUserNotes(user: User): Promise<Note[]> {
|
||||
const notes = await this.noteRepository
|
||||
.createQueryBuilder('note')
|
||||
.where('note.owner = :user', { user: user.id })
|
||||
.getMany();
|
||||
if (notes === null) {
|
||||
return [];
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Create a new note.
|
||||
* @param {string} noteContent - the content the new note should have
|
||||
* @param {string=} alias - an optional alias the note should have
|
||||
* @param {User=} owner - the owner of the note
|
||||
* @return {Note} the newly created note
|
||||
* @throws {AlreadyInDBError} a note with the requested id or alias already exists
|
||||
* @throws {ForbiddenIdError} the requested id or alias is forbidden
|
||||
* @throws {MaximumDocumentLengthExceededError} the noteContent is longer than the maxDocumentLength
|
||||
*/
|
||||
async createNote(
|
||||
noteContent: string,
|
||||
owner: User | null,
|
||||
alias?: string,
|
||||
): Promise<Note> {
|
||||
// Check if new note doesn't violate application constraints
|
||||
if (alias) {
|
||||
await this.ensureNoteIdOrAliasIsAvailable(alias);
|
||||
}
|
||||
if (noteContent.length > this.noteConfig.maxDocumentLength) {
|
||||
throw new MaximumDocumentLengthExceededError();
|
||||
}
|
||||
|
||||
// We cast to a note early to keep the later code clean
|
||||
const newNote = Note.create(owner, alias) as Note;
|
||||
const newRevision = await this.revisionsService.createRevision(
|
||||
newNote,
|
||||
noteContent,
|
||||
);
|
||||
newNote.revisions = Promise.resolve(
|
||||
newRevision === undefined ? [] : [newRevision],
|
||||
);
|
||||
|
||||
let everyoneAccessLevel;
|
||||
|
||||
if (owner) {
|
||||
// When we know an owner, an initial history entry is created
|
||||
newNote.historyEntries = Promise.resolve([
|
||||
HistoryEntry.create(owner, newNote) as HistoryEntry,
|
||||
]);
|
||||
// Use the default access level from the config
|
||||
everyoneAccessLevel = this.noteConfig.permissions.default.everyone;
|
||||
} else {
|
||||
// If there is no owner, this is an anonymous note
|
||||
// Anonymous notes are always writeable by everyone
|
||||
everyoneAccessLevel = DefaultAccessLevel.WRITE;
|
||||
}
|
||||
|
||||
// Create permission object for the 'everyone' group
|
||||
const everyonePermission = this.createGroupPermission(
|
||||
newNote,
|
||||
await this.groupsService.getEveryoneGroup(),
|
||||
everyoneAccessLevel,
|
||||
);
|
||||
|
||||
// Create permission object for logged-in users
|
||||
const loggedInPermission = this.createGroupPermission(
|
||||
newNote,
|
||||
await this.groupsService.getLoggedInGroup(),
|
||||
this.noteConfig.permissions.default.loggedIn,
|
||||
);
|
||||
|
||||
// Merge into permissions array
|
||||
newNote.groupPermissions = Promise.resolve([
|
||||
...Optional.ofNullable(everyonePermission).wrapInArray(),
|
||||
...Optional.ofNullable(loggedInPermission).wrapInArray(),
|
||||
]);
|
||||
|
||||
try {
|
||||
return await this.noteRepository.save(newNote);
|
||||
} catch (e) {
|
||||
if (alias) {
|
||||
this.logger.debug(
|
||||
`A note with the alias '${alias}' already exists.`,
|
||||
'createNote',
|
||||
);
|
||||
throw new AlreadyInDBError(
|
||||
`A note with the alias '${alias}' already exists.`,
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createGroupPermission(
|
||||
note: Note,
|
||||
group: Group,
|
||||
accessLevel: DefaultAccessLevel,
|
||||
): NoteGroupPermission | null {
|
||||
if (accessLevel === DefaultAccessLevel.NONE) {
|
||||
return null;
|
||||
}
|
||||
return NoteGroupPermission.create(
|
||||
group,
|
||||
note,
|
||||
accessLevel === DefaultAccessLevel.WRITE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Get the current content of the note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {string} the content of the note
|
||||
*/
|
||||
async getNoteContent(note: Note): Promise<string> {
|
||||
return (
|
||||
this.realtimeNoteStore
|
||||
.find(note.id)
|
||||
?.getRealtimeDoc()
|
||||
.getCurrentContent() ??
|
||||
(await this.revisionsService.getLatestRevision(note)).content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Get a note by either their id or alias.
|
||||
* @param {string} noteIdOrAlias - the notes id or alias
|
||||
* @return {Note} the note
|
||||
* @throws {NotInDBError} there is no note with this id or alias
|
||||
* @throws {ForbiddenIdError} the requested id or alias is forbidden
|
||||
*/
|
||||
async getNoteByIdOrAlias(noteIdOrAlias: string): Promise<Note> {
|
||||
const isForbidden = this.noteIdOrAliasIsForbidden(noteIdOrAlias);
|
||||
if (isForbidden) {
|
||||
throw new ForbiddenIdError(
|
||||
`The note id or alias '${noteIdOrAlias}' is forbidden by the administrator.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Trying to find note '${noteIdOrAlias}'`,
|
||||
'getNoteByIdOrAlias',
|
||||
);
|
||||
|
||||
/**
|
||||
* This query gets the note's aliases, owner, groupPermissions (and the groups), userPermissions (and the users) and tags and
|
||||
* then only gets the note, that either has a publicId :noteIdOrAlias or has any alias with this name.
|
||||
**/
|
||||
const note = await this.noteRepository
|
||||
.createQueryBuilder('note')
|
||||
.leftJoinAndSelect('note.aliases', 'alias')
|
||||
.leftJoinAndSelect('note.owner', 'owner')
|
||||
.leftJoinAndSelect('note.groupPermissions', 'group_permission')
|
||||
.leftJoinAndSelect('group_permission.group', 'group')
|
||||
.leftJoinAndSelect('note.userPermissions', 'user_permission')
|
||||
.leftJoinAndSelect('user_permission.user', 'user')
|
||||
.where('note.publicId = :noteIdOrAlias')
|
||||
.orWhere((queryBuilder) => {
|
||||
const subQuery = queryBuilder
|
||||
.subQuery()
|
||||
.select('alias.noteId')
|
||||
.from(Alias, 'alias')
|
||||
.where('alias.name = :noteIdOrAlias')
|
||||
.getQuery();
|
||||
return 'note.id IN ' + subQuery;
|
||||
})
|
||||
.setParameter('noteIdOrAlias', noteIdOrAlias)
|
||||
.getOne();
|
||||
|
||||
if (note === null) {
|
||||
this.logger.debug(
|
||||
`Could not find note '${noteIdOrAlias}'`,
|
||||
'getNoteByIdOrAlias',
|
||||
);
|
||||
throw new NotInDBError(
|
||||
`Note with id/alias '${noteIdOrAlias}' not found.`,
|
||||
);
|
||||
}
|
||||
this.logger.debug(`Found note '${noteIdOrAlias}'`, 'getNoteByIdOrAlias');
|
||||
return note;
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Get all users that ever appeared as an author for the given note
|
||||
* @param note The note to search authors for
|
||||
*/
|
||||
async getAuthorUsers(note: Note): Promise<User[]> {
|
||||
return await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.innerJoin('user.authors', 'author')
|
||||
.innerJoin('author.edits', 'edit')
|
||||
.innerJoin('edit.revisions', 'revision')
|
||||
.innerJoin('revision.note', 'note')
|
||||
.where('note.id = :id', { id: note.id })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the provided note id or alias is available for notes
|
||||
* @param noteIdOrAlias - the alias or id in question
|
||||
* @throws {ForbiddenIdError} the requested id or alias is not available
|
||||
*/
|
||||
async ensureNoteIdOrAliasIsAvailable(noteIdOrAlias: string): Promise<void> {
|
||||
const isForbidden = this.noteIdOrAliasIsForbidden(noteIdOrAlias);
|
||||
if (isForbidden) {
|
||||
throw new ForbiddenIdError(
|
||||
`The note id or alias '${noteIdOrAlias}' is forbidden by the administrator.`,
|
||||
);
|
||||
}
|
||||
const isUsed = await this.noteIdOrAliasIsUsed(noteIdOrAlias);
|
||||
if (isUsed) {
|
||||
throw new AlreadyInDBError(
|
||||
`A note with the id or alias '${noteIdOrAlias}' already exists.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the provided note id or alias is forbidden
|
||||
* @param noteIdOrAlias - the alias or id in question
|
||||
* @return {boolean} true if the id or alias is forbidden, false otherwise
|
||||
*/
|
||||
noteIdOrAliasIsForbidden(noteIdOrAlias: string): boolean {
|
||||
const forbidden = this.noteConfig.forbiddenNoteIds.includes(noteIdOrAlias);
|
||||
if (forbidden) {
|
||||
this.logger.debug(
|
||||
`A note with the alias '${noteIdOrAlias}' is forbidden by the administrator.`,
|
||||
'noteIdOrAliasIsForbidden',
|
||||
);
|
||||
}
|
||||
return forbidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Check if the provided note id or alias is already used
|
||||
* @param noteIdOrAlias - the alias or id in question
|
||||
* @return {boolean} true if the id or alias is already used, false otherwise
|
||||
*/
|
||||
async noteIdOrAliasIsUsed(noteIdOrAlias: string): Promise<boolean> {
|
||||
const noteWithPublicIdExists = await this.noteRepository.existsBy({
|
||||
publicId: noteIdOrAlias,
|
||||
});
|
||||
const noteWithAliasExists = await this.aliasRepository.existsBy({
|
||||
name: noteIdOrAlias,
|
||||
});
|
||||
if (noteWithPublicIdExists || noteWithAliasExists) {
|
||||
this.logger.debug(
|
||||
`A note with the id or alias '${noteIdOrAlias}' already exists.`,
|
||||
'noteIdOrAliasIsUsed',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Delete a note
|
||||
* @param {Note} note - the note to delete
|
||||
* @return {Note} the note, that was deleted
|
||||
* @throws {NotInDBError} there is no note with this id or alias
|
||||
*/
|
||||
async deleteNote(note: Note): Promise<Note> {
|
||||
this.eventEmitter.emit(NoteEvent.DELETION, note.id);
|
||||
return await this.noteRepository.remove(note);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Update a notes content.
|
||||
* @param {Note} note - the note
|
||||
* @param {string} noteContent - the new content
|
||||
* @return {Note} the note with a new revision and new content
|
||||
* @throws {NotInDBError} there is no note with this id or alias
|
||||
*/
|
||||
async updateNote(note: Note, noteContent: string): Promise<Note> {
|
||||
const revisions = await note.revisions;
|
||||
const newRevision = await this.revisionsService.createRevision(
|
||||
note,
|
||||
noteContent,
|
||||
);
|
||||
if (newRevision !== undefined) {
|
||||
revisions.push(newRevision);
|
||||
note.revisions = Promise.resolve(revisions);
|
||||
}
|
||||
return await this.noteRepository.save(note);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Calculate the updateUser (for the NoteDto) for a Note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {User} user to be used as updateUser in the NoteDto
|
||||
*/
|
||||
async calculateUpdateUser(note: Note): Promise<User | null> {
|
||||
const lastRevision = await this.revisionsService.getLatestRevision(note);
|
||||
const edits = await lastRevision.edits;
|
||||
if (edits.length > 0) {
|
||||
// Sort the last Revisions Edits by their updatedAt Date to get the latest one
|
||||
// the user of that Edit is the updateUser
|
||||
return await (
|
||||
await edits.sort(
|
||||
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
|
||||
)[0].author
|
||||
).user;
|
||||
}
|
||||
// If there are no Edits, the owner is the updateUser
|
||||
return await note.owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build NotePermissionsDto from a note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {NotePermissionsDto} the built NotePermissionDto
|
||||
*/
|
||||
async toNotePermissionsDto(note: Note): Promise<NotePermissionsDto> {
|
||||
const owner = await note.owner;
|
||||
const userPermissions = await note.userPermissions;
|
||||
const groupPermissions = await note.groupPermissions;
|
||||
return {
|
||||
owner: owner ? owner.username : null,
|
||||
sharedToUsers: await Promise.all(
|
||||
userPermissions.map(async (noteUserPermission) => ({
|
||||
username: (await noteUserPermission.user).username,
|
||||
canEdit: noteUserPermission.canEdit,
|
||||
})),
|
||||
),
|
||||
sharedToGroups: await Promise.all(
|
||||
groupPermissions.map(async (noteGroupPermission) => ({
|
||||
groupName: (await noteGroupPermission.group).name,
|
||||
canEdit: noteGroupPermission.canEdit,
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Build NoteMetadataDto from a note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {NoteMetadataDto} the built NoteMetadataDto
|
||||
*/
|
||||
async toNoteMetadataDto(note: Note): Promise<NoteMetadataDto> {
|
||||
const updateUser = await this.calculateUpdateUser(note);
|
||||
const latestRevision = await this.revisionsService.getLatestRevision(note);
|
||||
return {
|
||||
id: note.publicId,
|
||||
aliases: await Promise.all(
|
||||
(await note.aliases).map((alias) =>
|
||||
this.aliasService.toAliasDto(alias, note),
|
||||
),
|
||||
),
|
||||
primaryAddress: (await getPrimaryAlias(note)) ?? note.publicId,
|
||||
title: latestRevision.title,
|
||||
description: latestRevision.description,
|
||||
tags: (await latestRevision.tags).map((tag) => tag.name),
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
editedBy: (await this.getAuthorUsers(note)).map((user) => user.username),
|
||||
permissions: await this.toNotePermissionsDto(note),
|
||||
version: note.version,
|
||||
updatedAt: latestRevision.createdAt.toISOString(),
|
||||
updateUsername: updateUser ? updateUser.username : null,
|
||||
viewCount: note.viewCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Build NoteDto from a note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {NoteDto} the built NoteDto
|
||||
*/
|
||||
async toNoteDto(note: Note): Promise<NoteDto> {
|
||||
return {
|
||||
content: await this.getNoteContent(note),
|
||||
metadata: await this.toNoteMetadataDto(note),
|
||||
editedByAtPosition: [],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ValueTransformer } from 'typeorm';
|
||||
|
||||
export class PrimaryValueTransformer implements ValueTransformer {
|
||||
from(value: boolean | null): boolean {
|
||||
if (value === null) {
|
||||
return false;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
to(value: boolean): boolean | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { User } from '../database/user.entity';
|
||||
import { Alias } from './alias.entity';
|
||||
import { Note } from './note.entity';
|
||||
import { generatePublicId, getPrimaryAlias } from './utils';
|
||||
|
||||
jest.mock('crypto');
|
||||
const random128bitBuffer = Buffer.from([
|
||||
0xe1, 0x75, 0x86, 0xb7, 0xc3, 0xfb, 0x03, 0xa9, 0x26, 0x9f, 0xc9, 0xd6, 0x8c,
|
||||
0x2d, 0x7b, 0x7b,
|
||||
]);
|
||||
const mockRandomBytes = randomBytes as jest.MockedFunction<typeof randomBytes>;
|
||||
mockRandomBytes.mockImplementation((_) => random128bitBuffer);
|
||||
|
||||
it('generatePublicId', () => {
|
||||
expect(generatePublicId()).toEqual('w5trddy3zc1tj9mzs7b8rbbvfc');
|
||||
});
|
||||
|
||||
describe('getPrimaryAlias', () => {
|
||||
const alias = 'alias';
|
||||
let note: Note;
|
||||
beforeEach(() => {
|
||||
const user = User.create('hardcoded', 'Testy') as User;
|
||||
note = Note.create(user, alias) as Note;
|
||||
});
|
||||
it('finds correct primary alias', async () => {
|
||||
(await note.aliases).push(Alias.create('annother', note, false) as Alias);
|
||||
expect(await getPrimaryAlias(note)).toEqual(alias);
|
||||
});
|
||||
it('returns undefined if there is no alias', async () => {
|
||||
(await note.aliases)[0].primary = false;
|
||||
expect(await getPrimaryAlias(note)).toEqual(undefined);
|
||||
});
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import base32Encode from 'base32-encode';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { Alias } from './alias.entity';
|
||||
import { Note } from './note.entity';
|
||||
|
||||
/**
|
||||
* Generate publicId for a note.
|
||||
* This is a randomly generated 128-bit value encoded with base32-encode using the crockford variant and converted to lowercase.
|
||||
*/
|
||||
export function generatePublicId(): string {
|
||||
const randomId = randomBytes(16);
|
||||
return base32Encode(randomId, 'Crockford').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the primary alias from a aliases of a note
|
||||
* @param {Note} note - the note from which the primary alias should be extracted
|
||||
*/
|
||||
export async function getPrimaryAlias(note: Note): Promise<string | undefined> {
|
||||
const listWithPrimaryAlias = (await note.aliases).filter(
|
||||
(alias: Alias) => alias.primary,
|
||||
);
|
||||
if (listWithPrimaryAlias.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
return listWithPrimaryAlias[0].name;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue