mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
wip: range authorships backend storage
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
c5dc671398
commit
68780f54e1
29 changed files with 316 additions and 133 deletions
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -76,7 +76,8 @@ const routes: Routes = [
|
||||||
detectTsNode() ? 'ts' : 'js'
|
detectTsNode() ? 'ts' : 'js'
|
||||||
}`,
|
}`,
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
migrationsRun: false,
|
||||||
|
synchronize: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -11,7 +11,7 @@ import {
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { RangeAuthorship } from '../revisions/range-authorship.entity';
|
||||||
import { Session } from '../sessions/session.entity';
|
import { Session } from '../sessions/session.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
|
|
||||||
|
@ -52,8 +52,8 @@ export class Author {
|
||||||
* List of edits that this author created
|
* List of edits that this author created
|
||||||
* All edits must belong to the same note
|
* All edits must belong to the same note
|
||||||
*/
|
*/
|
||||||
@OneToMany(() => Edit, (edit) => edit.author)
|
@OneToMany(() => RangeAuthorship, (edit) => edit.author)
|
||||||
edits: Promise<Edit[]>;
|
edits: Promise<RangeAuthorship[]>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { Author } from './author.entity';
|
import { Author } from './author.entity';
|
||||||
|
import { AuthorsService } from './authors.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Author])],
|
imports: [TypeOrmModule.forFeature([Author]), LoggerModule],
|
||||||
|
providers: [AuthorsService],
|
||||||
|
exports: [AuthorsService],
|
||||||
})
|
})
|
||||||
export class AuthorsModule {}
|
export class AuthorsModule {}
|
||||||
|
|
42
backend/src/authors/authors.service.ts
Normal file
42
backend/src/authors/authors.service.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { ShortRealtimeUser } from '@hedgedoc/commons';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
|
import { Author } from './author.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthorsService {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@InjectRepository(Author) private authorsRepository: Repository<Author>,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(AuthorsService.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @async
|
||||||
|
// * Get or create the author specified by the short realtime user
|
||||||
|
// * @param {Username} username the username by which the user is specified
|
||||||
|
// * @param {UserRelationEnum[]} [withRelations=[]] if the returned user object should contain certain relations
|
||||||
|
// * @return {User} the specified user
|
||||||
|
// */
|
||||||
|
// async findOrCreateAuthor(
|
||||||
|
// shortRealtimeUser: ShortRealtimeUser,
|
||||||
|
// ): Promise<Author> {
|
||||||
|
// const author = await this.authorsRepository.findOne({
|
||||||
|
// where: { username: username },
|
||||||
|
// relations: withRelations,
|
||||||
|
// });
|
||||||
|
// if (user === null) {
|
||||||
|
// throw new NotInDBError(`User with username '${username}' not found`);
|
||||||
|
// }
|
||||||
|
// return user;
|
||||||
|
// }
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -28,7 +28,7 @@ import { NotesModule } from '../notes/notes.module';
|
||||||
import { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { RangeAuthorship } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { RevisionsModule } from '../revisions/revisions.module';
|
import { RevisionsModule } from '../revisions/revisions.module';
|
||||||
import { RevisionsService } from '../revisions/revisions.service';
|
import { RevisionsService } from '../revisions/revisions.service';
|
||||||
|
@ -116,7 +116,7 @@ describe('HistoryService', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Identity))
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
.overrideProvider(getRepositoryToken(RangeAuthorship))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Revision))
|
.overrideProvider(getRepositoryToken(Revision))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -28,7 +28,7 @@ import { NotesModule } from '../notes/notes.module';
|
||||||
import { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { RangeAuthorship } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { Session } from '../sessions/session.entity';
|
import { Session } from '../sessions/session.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
|
@ -82,7 +82,7 @@ describe('MediaService', () => {
|
||||||
EventEmitterModule.forRoot(eventModuleConfig),
|
EventEmitterModule.forRoot(eventModuleConfig),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
.overrideProvider(getRepositoryToken(RangeAuthorship))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(AuthToken))
|
.overrideProvider(getRepositoryToken(AuthToken))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { LoggerModule } from '../logger/logger.module';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
|
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { RangeAuthorship } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { RevisionsModule } from '../revisions/revisions.module';
|
import { RevisionsModule } from '../revisions/revisions.module';
|
||||||
import { Session } from '../sessions/session.entity';
|
import { Session } from '../sessions/session.entity';
|
||||||
|
@ -122,7 +122,7 @@ describe('AliasService', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Identity))
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
.overrideProvider(getRepositoryToken(RangeAuthorship))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Revision))
|
.overrideProvider(getRepositoryToken(Revision))
|
||||||
.useClass(Repository)
|
.useClass(Repository)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsString, ValidateNested } from 'class-validator';
|
import { IsArray, IsString, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
import { EditDto } from '../revisions/edit.dto';
|
import { RangeAuthorshipDto } from '../revisions/range-authorship.dto';
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto.';
|
||||||
import { NoteMetadataDto } from './note-metadata.dto';
|
import { NoteMetadataDto } from './note-metadata.dto';
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export class NoteDto extends BaseDto {
|
||||||
*/
|
*/
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => EditDto)
|
@Type(() => RangeAuthorshipDto)
|
||||||
@ApiProperty({ isArray: true, type: EditDto })
|
@ApiProperty({ isArray: true, type: RangeAuthorshipDto })
|
||||||
editedByAtPosition: EditDto[];
|
editedByAtPosition: RangeAuthorshipDto[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ import { LoggerModule } from '../logger/logger.module';
|
||||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
|
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { RangeAuthorship } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { RevisionsModule } from '../revisions/revisions.module';
|
import { RevisionsModule } from '../revisions/revisions.module';
|
||||||
import { RevisionsService } from '../revisions/revisions.service';
|
import { RevisionsService } from '../revisions/revisions.service';
|
||||||
|
@ -198,7 +198,7 @@ describe('NotesService', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Identity))
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
.overrideProvider(getRepositoryToken(RangeAuthorship))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Revision))
|
.overrideProvider(getRepositoryToken(Revision))
|
||||||
.useClass(Repository)
|
.useClass(Repository)
|
||||||
|
@ -248,13 +248,13 @@ describe('NotesService', () => {
|
||||||
endPos: 1,
|
endPos: 1,
|
||||||
updatedAt: new Date(1549312452000),
|
updatedAt: new Date(1549312452000),
|
||||||
author: Promise.resolve(author),
|
author: Promise.resolve(author),
|
||||||
} as Edit,
|
} as RangeAuthorship,
|
||||||
{
|
{
|
||||||
startPos: 0,
|
startPos: 0,
|
||||||
endPos: 1,
|
endPos: 1,
|
||||||
updatedAt: new Date(1549312452001),
|
updatedAt: new Date(1549312452001),
|
||||||
author: Promise.resolve(author),
|
author: Promise.resolve(author),
|
||||||
} as Edit,
|
} as RangeAuthorship,
|
||||||
]),
|
]),
|
||||||
createdAt: new Date(1549312452000),
|
createdAt: new Date(1549312452000),
|
||||||
tags: Promise.resolve([
|
tags: Promise.resolve([
|
||||||
|
|
|
@ -104,6 +104,7 @@ export class NotesService {
|
||||||
const newRevision = await this.revisionsService.createRevision(
|
const newRevision = await this.revisionsService.createRevision(
|
||||||
newNote,
|
newNote,
|
||||||
noteContent,
|
noteContent,
|
||||||
|
[], // TODO Use the correct rangeAuthorships
|
||||||
);
|
);
|
||||||
newNote.revisions = Promise.resolve(
|
newNote.revisions = Promise.resolve(
|
||||||
newRevision === undefined ? [] : [newRevision],
|
newRevision === undefined ? [] : [newRevision],
|
||||||
|
@ -261,7 +262,7 @@ export class NotesService {
|
||||||
.createQueryBuilder('user')
|
.createQueryBuilder('user')
|
||||||
.innerJoin('user.authors', 'author')
|
.innerJoin('user.authors', 'author')
|
||||||
.innerJoin('author.edits', 'edit')
|
.innerJoin('author.edits', 'edit')
|
||||||
.innerJoin('edit.revisions', 'revision')
|
.innerJoin('edit.revision', 'revision')
|
||||||
.innerJoin('revision.note', 'note')
|
.innerJoin('revision.note', 'note')
|
||||||
.where('note.id = :id', { id: note.id })
|
.where('note.id = :id', { id: note.id })
|
||||||
.getMany();
|
.getMany();
|
||||||
|
@ -351,6 +352,7 @@ export class NotesService {
|
||||||
const newRevision = await this.revisionsService.createRevision(
|
const newRevision = await this.revisionsService.createRevision(
|
||||||
note,
|
note,
|
||||||
noteContent,
|
noteContent,
|
||||||
|
[], // TODO Use the correct rangeAuthorships
|
||||||
);
|
);
|
||||||
if (newRevision !== undefined) {
|
if (newRevision !== undefined) {
|
||||||
revisions.push(newRevision);
|
revisions.push(newRevision);
|
||||||
|
@ -367,12 +369,12 @@ export class NotesService {
|
||||||
*/
|
*/
|
||||||
async calculateUpdateUser(note: Note): Promise<User | null> {
|
async calculateUpdateUser(note: Note): Promise<User | null> {
|
||||||
const lastRevision = await this.revisionsService.getLatestRevision(note);
|
const lastRevision = await this.revisionsService.getLatestRevision(note);
|
||||||
const edits = await lastRevision.edits;
|
const rangeAuthorships = await lastRevision.rangeAuthorships;
|
||||||
if (edits.length > 0) {
|
if (rangeAuthorships.length > 0) {
|
||||||
// Sort the last Revisions Edits by their updatedAt Date to get the latest one
|
// Sort the last Revisions Edits by their updatedAt Date to get the latest one
|
||||||
// the user of that Edit is the updateUser
|
// the user of that Edit is the updateUser
|
||||||
return await (
|
return await (
|
||||||
await edits.sort(
|
await rangeAuthorships.sort(
|
||||||
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
|
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
|
||||||
)[0].author
|
)[0].author
|
||||||
).user;
|
).user;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -38,7 +38,7 @@ import {
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { NotesModule } from '../notes/notes.module';
|
import { NotesModule } from '../notes/notes.module';
|
||||||
import { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { Edit } from '../revisions/edit.entity';
|
import { RangeAuthorship } from '../revisions/edit.entity';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { Session } from '../sessions/session.entity';
|
import { Session } from '../sessions/session.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
|
@ -166,7 +166,7 @@ describe('PermissionsService', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Identity))
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
.overrideProvider(getRepositoryToken(RangeAuthorship))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Revision))
|
.overrideProvider(getRepositoryToken(Revision))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -47,6 +47,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
||||||
.createAndSaveRevision(
|
.createAndSaveRevision(
|
||||||
realtimeNote.getNote(),
|
realtimeNote.getNote(),
|
||||||
realtimeNote.getRealtimeDoc().getCurrentContent(),
|
realtimeNote.getRealtimeDoc().getCurrentContent(),
|
||||||
|
realtimeNote.getRealtimeDoc().getAbsolutePositionAuthorships(),
|
||||||
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
|
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
|
||||||
)
|
)
|
||||||
.catch((reason) => this.logger.error(reason));
|
.catch((reason) => this.logger.error(reason));
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -125,6 +125,7 @@ export class RealtimeUserStatusAdapter {
|
||||||
ownUser: {
|
ownUser: {
|
||||||
displayName: this.realtimeUser.displayName,
|
displayName: this.realtimeUser.displayName,
|
||||||
styleIndex: this.realtimeUser.styleIndex,
|
styleIndex: this.realtimeUser.styleIndex,
|
||||||
|
username: this.realtimeUser.username,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -33,7 +33,7 @@ import { NotePermission } from '../../permissions/note-permission.enum';
|
||||||
import { NoteUserPermission } from '../../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../../permissions/note-user-permission.entity';
|
||||||
import { PermissionsModule } from '../../permissions/permissions.module';
|
import { PermissionsModule } from '../../permissions/permissions.module';
|
||||||
import { PermissionsService } from '../../permissions/permissions.service';
|
import { PermissionsService } from '../../permissions/permissions.service';
|
||||||
import { Edit } from '../../revisions/edit.entity';
|
import { RangeAuthorship } from '../../revisions/range-authorship.entity';
|
||||||
import { Revision } from '../../revisions/revision.entity';
|
import { Revision } from '../../revisions/revision.entity';
|
||||||
import { Session } from '../../sessions/session.entity';
|
import { Session } from '../../sessions/session.entity';
|
||||||
import { SessionModule } from '../../sessions/session.module';
|
import { SessionModule } from '../../sessions/session.module';
|
||||||
|
@ -126,7 +126,7 @@ describe('Websocket gateway', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Identity))
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
.overrideProvider(getRepositoryToken(RangeAuthorship))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Revision))
|
.overrideProvider(getRepositoryToken(Revision))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { EditDto } from './edit.dto';
|
|
||||||
import { Edit } from './edit.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class EditService {
|
|
||||||
async toEditDto(edit: Edit): Promise<EditDto> {
|
|
||||||
const authorUser = await (await edit.author).user;
|
|
||||||
|
|
||||||
return {
|
|
||||||
username: authorUser ? authorUser.username : null,
|
|
||||||
startPos: edit.startPos,
|
|
||||||
endPos: edit.endPos,
|
|
||||||
createdAt: edit.createdAt,
|
|
||||||
updatedAt: edit.updatedAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -10,7 +10,7 @@ import { IsDate, IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
||||||
import { UserInfoDto } from '../users/user-info.dto';
|
import { UserInfoDto } from '../users/user-info.dto';
|
||||||
import { BaseDto } from '../utils/base.dto.';
|
import { BaseDto } from '../utils/base.dto.';
|
||||||
|
|
||||||
export class EditDto extends BaseDto {
|
export class RangeAuthorshipDto extends BaseDto {
|
||||||
/**
|
/**
|
||||||
* Username of the user who authored this section
|
* Username of the user who authored this section
|
||||||
* Is `null` if the user is anonymous
|
* Is `null` if the user is anonymous
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -7,7 +7,6 @@ import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
ManyToMany,
|
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
@ -17,18 +16,18 @@ import { Author } from '../authors/author.entity';
|
||||||
import { Revision } from './revision.entity';
|
import { Revision } from './revision.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Edit represents a change in the content of a note by a particular {@link Author}
|
* The RangeAuthorship represents a change in the content of a note by a particular {@link Author}
|
||||||
*/
|
*/
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Edit {
|
export class RangeAuthorship {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revisions this edit appears in
|
* Revisions this edit appears in
|
||||||
*/
|
*/
|
||||||
@ManyToMany((_) => Revision, (revision) => revision.edits)
|
@ManyToOne((_) => Revision, (revision) => revision.rangeAuthorships)
|
||||||
revisions: Promise<Revision[]>;
|
revision: Promise<Revision>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Author that created the change
|
* Author that created the change
|
||||||
|
@ -55,9 +54,8 @@ export class Edit {
|
||||||
author: Author,
|
author: Author,
|
||||||
startPos: number,
|
startPos: number,
|
||||||
endPos: number,
|
endPos: number,
|
||||||
): Omit<Edit, 'id' | 'createdAt' | 'updatedAt'> {
|
): Omit<RangeAuthorship, 'id' | 'createdAt' | 'updatedAt' | 'revision'> {
|
||||||
const newEdit = new Edit();
|
const newEdit = new RangeAuthorship();
|
||||||
newEdit.revisions = Promise.resolve([]);
|
|
||||||
newEdit.author = Promise.resolve(author);
|
newEdit.author = Promise.resolve(author);
|
||||||
newEdit.startPos = startPos;
|
newEdit.startPos = startPos;
|
||||||
newEdit.endPos = endPos;
|
newEdit.endPos = endPos;
|
50
backend/src/revisions/range-authorship.service.ts
Normal file
50
backend/src/revisions/range-authorship.service.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { AbsolutePositionAuthorship } from '@hedgedoc/commons';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Author } from '../authors/author.entity';
|
||||||
|
import { RangeAuthorshipDto } from './range-authorship.dto';
|
||||||
|
import { RangeAuthorship } from './range-authorship.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RangeAuthorshipService {
|
||||||
|
createRangeAuthorshipsFromAbsolutePositions(
|
||||||
|
absolutePositions: AbsolutePositionAuthorship[],
|
||||||
|
noteLength: number,
|
||||||
|
): RangeAuthorship[] {
|
||||||
|
return absolutePositions
|
||||||
|
.sort(([positionA], [positionB]) => {
|
||||||
|
return positionB - positionA;
|
||||||
|
})
|
||||||
|
.reduce<RangeAuthorship[]>((authorships, [startPosition, _]) => {
|
||||||
|
const author = Author.create(1) as Author; // ToDo: use the real author here
|
||||||
|
const endPosition = authorships[0]?.startPos - 1 ?? noteLength;
|
||||||
|
authorships.unshift(
|
||||||
|
RangeAuthorship.create(
|
||||||
|
author,
|
||||||
|
startPosition,
|
||||||
|
endPosition,
|
||||||
|
) as RangeAuthorship,
|
||||||
|
);
|
||||||
|
return authorships;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toRangeAuthorshipDto(
|
||||||
|
rangeAuthorship: RangeAuthorship,
|
||||||
|
): Promise<RangeAuthorshipDto> {
|
||||||
|
const authorUser = await (await rangeAuthorship.author).user;
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: authorUser ? authorUser.username : null,
|
||||||
|
startPos: rangeAuthorship.startPos,
|
||||||
|
endPos: rangeAuthorship.endPos,
|
||||||
|
createdAt: rangeAuthorship.createdAt,
|
||||||
|
updatedAt: rangeAuthorship.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsString, ValidateNested } from 'class-validator';
|
import { IsString, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
import { EditDto } from './edit.dto';
|
import { RangeAuthorshipDto } from './range-authorship.dto';
|
||||||
import { RevisionMetadataDto } from './revision-metadata.dto';
|
import { RevisionMetadataDto } from './revision-metadata.dto';
|
||||||
|
|
||||||
export class RevisionDto extends RevisionMetadataDto {
|
export class RevisionDto extends RevisionMetadataDto {
|
||||||
|
@ -27,10 +27,10 @@ export class RevisionDto extends RevisionMetadataDto {
|
||||||
patch: string;
|
patch: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All edit objects which are used in the revision.
|
* All range authorship objects which are used in the revision.
|
||||||
*/
|
*/
|
||||||
@Type(() => EditDto)
|
@Type(() => RangeAuthorshipDto)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@ApiProperty({ isArray: true, type: EditDto })
|
@ApiProperty({ isArray: true, type: RangeAuthorshipDto })
|
||||||
edits: EditDto[];
|
rangeAuthorships: RangeAuthorshipDto[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -10,12 +10,13 @@ import {
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { Edit } from './edit.entity';
|
import { RangeAuthorship } from './range-authorship.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state of a note at a particular point in time,
|
* The state of a note at a particular point in time,
|
||||||
|
@ -84,9 +85,12 @@ export class Revision {
|
||||||
/**
|
/**
|
||||||
* All edit objects which are used in the revision.
|
* All edit objects which are used in the revision.
|
||||||
*/
|
*/
|
||||||
@ManyToMany((_) => Edit, (edit) => edit.revisions)
|
@OneToMany(
|
||||||
@JoinTable()
|
(_) => RangeAuthorship,
|
||||||
edits: Promise<Edit[]>;
|
(rangeAuthorship) => rangeAuthorship.revision,
|
||||||
|
{ onDelete: 'CASCADE' },
|
||||||
|
)
|
||||||
|
rangeAuthorships: Promise<RangeAuthorship[]>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
@ -99,6 +103,7 @@ export class Revision {
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
tags: Tag[],
|
tags: Tag[],
|
||||||
|
rangeAuthorships: RangeAuthorship[],
|
||||||
): Omit<Revision, 'id' | 'createdAt'> {
|
): Omit<Revision, 'id' | 'createdAt'> {
|
||||||
const newRevision = new Revision();
|
const newRevision = new Revision();
|
||||||
newRevision.patch = patch;
|
newRevision.patch = patch;
|
||||||
|
@ -108,7 +113,7 @@ export class Revision {
|
||||||
newRevision.description = description;
|
newRevision.description = description;
|
||||||
newRevision.tags = Promise.resolve(tags);
|
newRevision.tags = Promise.resolve(tags);
|
||||||
newRevision.note = Promise.resolve(note);
|
newRevision.note = Promise.resolve(note);
|
||||||
newRevision.edits = Promise.resolve([]);
|
newRevision.rangeAuthorships = Promise.resolve(rangeAuthorships);
|
||||||
newRevision.yjsStateVector = yjsStateVector ?? null;
|
newRevision.yjsStateVector = yjsStateVector ?? null;
|
||||||
return newRevision;
|
return newRevision;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -9,19 +9,19 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { AuthorsModule } from '../authors/authors.module';
|
import { AuthorsModule } from '../authors/authors.module';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { Edit } from './edit.entity';
|
import { RangeAuthorship } from './range-authorship.entity';
|
||||||
import { EditService } from './edit.service';
|
import { RangeAuthorshipService } from './range-authorship.service';
|
||||||
import { Revision } from './revision.entity';
|
import { Revision } from './revision.entity';
|
||||||
import { RevisionsService } from './revisions.service';
|
import { RevisionsService } from './revisions.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Revision, Edit]),
|
TypeOrmModule.forFeature([Revision, RangeAuthorship]),
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
AuthorsModule,
|
AuthorsModule,
|
||||||
],
|
],
|
||||||
providers: [RevisionsService, EditService],
|
providers: [RevisionsService, RangeAuthorshipService],
|
||||||
exports: [RevisionsService, EditService],
|
exports: [RevisionsService, RangeAuthorshipService],
|
||||||
})
|
})
|
||||||
export class RevisionsModule {}
|
export class RevisionsModule {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -29,8 +29,8 @@ import { NoteGroupPermission } from '../permissions/note-group-permission.entity
|
||||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
import { Session } from '../sessions/session.entity';
|
import { Session } from '../sessions/session.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { Edit } from './edit.entity';
|
import { RangeAuthorship } from './edit.entity';
|
||||||
import { EditService } from './edit.service';
|
import { RangeAuthorshipService } from './range-authorship.service';
|
||||||
import { Revision } from './revision.entity';
|
import { Revision } from './revision.entity';
|
||||||
import { RevisionsService } from './revisions.service';
|
import { RevisionsService } from './revisions.service';
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ describe('RevisionsService', () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
RevisionsService,
|
RevisionsService,
|
||||||
EditService,
|
RangeAuthorshipService,
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(Revision),
|
provide: getRepositoryToken(Revision),
|
||||||
useClass: Repository,
|
useClass: Repository,
|
||||||
|
@ -63,7 +63,7 @@ describe('RevisionsService', () => {
|
||||||
EventEmitterModule.forRoot(eventModuleConfig),
|
EventEmitterModule.forRoot(eventModuleConfig),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideProvider(getRepositoryToken(Edit))
|
.overrideProvider(getRepositoryToken(RangeAuthorship))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(User))
|
.overrideProvider(getRepositoryToken(User))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
@ -183,12 +183,14 @@ describe('RevisionsService', () => {
|
||||||
author.user = Promise.resolve(user);
|
author.user = Promise.resolve(user);
|
||||||
const anonAuthor = Author.create(123) as Author;
|
const anonAuthor = Author.create(123) as Author;
|
||||||
const anonAuthor2 = Author.create(123) as Author;
|
const anonAuthor2 = Author.create(123) as Author;
|
||||||
const edits = [Edit.create(author, 12, 15) as Edit];
|
const edits = [RangeAuthorship.create(author, 12, 15) as RangeAuthorship];
|
||||||
edits.push(Edit.create(author, 16, 18) as Edit);
|
edits.push(RangeAuthorship.create(author, 16, 18) as RangeAuthorship);
|
||||||
edits.push(Edit.create(author, 29, 20) as Edit);
|
edits.push(RangeAuthorship.create(author, 29, 20) as RangeAuthorship);
|
||||||
edits.push(Edit.create(anonAuthor, 29, 20) as Edit);
|
edits.push(RangeAuthorship.create(anonAuthor, 29, 20) as RangeAuthorship);
|
||||||
edits.push(Edit.create(anonAuthor, 29, 20) as Edit);
|
edits.push(RangeAuthorship.create(anonAuthor, 29, 20) as RangeAuthorship);
|
||||||
edits.push(Edit.create(anonAuthor2, 29, 20) as Edit);
|
edits.push(
|
||||||
|
RangeAuthorship.create(anonAuthor2, 29, 20) as RangeAuthorship,
|
||||||
|
);
|
||||||
const revision = Mock.of<Revision>({});
|
const revision = Mock.of<Revision>({});
|
||||||
revision.edits = Promise.resolve(edits);
|
revision.edits = Promise.resolve(edits);
|
||||||
|
|
||||||
|
@ -210,7 +212,7 @@ describe('RevisionsService', () => {
|
||||||
description: 'mockDescription',
|
description: 'mockDescription',
|
||||||
patch: 'mockPatch',
|
patch: 'mockPatch',
|
||||||
edits: Promise.resolve([
|
edits: Promise.resolve([
|
||||||
Mock.of<Edit>({
|
Mock.of<RangeAuthorship>({
|
||||||
endPos: 93,
|
endPos: 93,
|
||||||
startPos: 34,
|
startPos: 34,
|
||||||
createdAt: new Date('2020-03-04T20:12:00.000Z'),
|
createdAt: new Date('2020-03-04T20:12:00.000Z'),
|
||||||
|
@ -259,7 +261,7 @@ describe('RevisionsService', () => {
|
||||||
description: 'mockDescription',
|
description: 'mockDescription',
|
||||||
patch: 'mockPatch',
|
patch: 'mockPatch',
|
||||||
edits: Promise.resolve([
|
edits: Promise.resolve([
|
||||||
Mock.of<Edit>({
|
Mock.of<RangeAuthorship>({
|
||||||
endPos: 93,
|
endPos: 93,
|
||||||
startPos: 34,
|
startPos: 34,
|
||||||
createdAt: new Date('2020-03-04T22:32:00.000Z'),
|
createdAt: new Date('2020-03-04T22:32:00.000Z'),
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import type { AbsolutePositionAuthorship } from '@hedgedoc/commons';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { createPatch } from 'diff';
|
import { createPatch } from 'diff';
|
||||||
|
@ -12,7 +13,7 @@ import { NotInDBError } from '../errors/errors';
|
||||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
import { EditService } from './edit.service';
|
import { RangeAuthorshipService } from './range-authorship.service';
|
||||||
import { RevisionMetadataDto } from './revision-metadata.dto';
|
import { RevisionMetadataDto } from './revision-metadata.dto';
|
||||||
import { RevisionDto } from './revision.dto';
|
import { RevisionDto } from './revision.dto';
|
||||||
import { Revision } from './revision.entity';
|
import { Revision } from './revision.entity';
|
||||||
|
@ -29,7 +30,7 @@ export class RevisionsService {
|
||||||
private readonly logger: ConsoleLoggerService,
|
private readonly logger: ConsoleLoggerService,
|
||||||
@InjectRepository(Revision)
|
@InjectRepository(Revision)
|
||||||
private revisionRepository: Repository<Revision>,
|
private revisionRepository: Repository<Revision>,
|
||||||
private editService: EditService,
|
private rangeAuthorshipService: RangeAuthorshipService,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(RevisionsService.name);
|
this.logger.setContext(RevisionsService.name);
|
||||||
}
|
}
|
||||||
|
@ -97,7 +98,9 @@ export class RevisionsService {
|
||||||
async getRevisionUserInfo(revision: Revision): Promise<RevisionUserInfo> {
|
async getRevisionUserInfo(revision: Revision): Promise<RevisionUserInfo> {
|
||||||
// get a deduplicated list of all authors
|
// get a deduplicated list of all authors
|
||||||
let authors = await Promise.all(
|
let authors = await Promise.all(
|
||||||
(await revision.edits).map(async (edit) => await edit.author),
|
(await revision.rangeAuthorships).map(
|
||||||
|
async (rangeAuthorship) => await rangeAuthorship.author,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
authors = [...new Set(authors)]; // remove duplicates with Set
|
authors = [...new Set(authors)]; // remove duplicates with Set
|
||||||
|
|
||||||
|
@ -142,9 +145,12 @@ export class RevisionsService {
|
||||||
authorUsernames: revisionUserInfo.usernames,
|
authorUsernames: revisionUserInfo.usernames,
|
||||||
anonymousAuthorCount: revisionUserInfo.anonymousUserCount,
|
anonymousAuthorCount: revisionUserInfo.anonymousUserCount,
|
||||||
patch: revision.patch,
|
patch: revision.patch,
|
||||||
edits: await Promise.all(
|
rangeAuthorships: await Promise.all(
|
||||||
(await revision.edits).map(
|
(await revision.rangeAuthorships).map(
|
||||||
async (edit) => await this.editService.toEditDto(edit),
|
async (rangeAuthorship) =>
|
||||||
|
await this.rangeAuthorshipService.toRangeAuthorshipDto(
|
||||||
|
rangeAuthorship,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -158,12 +164,14 @@ export class RevisionsService {
|
||||||
* @param note The note for which the revision should be created
|
* @param note The note for which the revision should be created
|
||||||
* @param newContent The new note content
|
* @param newContent The new note content
|
||||||
* @param yjsStateVector The yjs state vector that describes the new content
|
* @param yjsStateVector The yjs state vector that describes the new content
|
||||||
|
* @param absolutePositionAuthorships The absolute positions for authorship marks
|
||||||
* @return {Revision} the created revision
|
* @return {Revision} the created revision
|
||||||
* @return {undefined} if the revision couldn't be created because e.g. the content hasn't changed
|
* @return {undefined} if the revision couldn't be created because e.g. the content hasn't changed
|
||||||
*/
|
*/
|
||||||
async createRevision(
|
async createRevision(
|
||||||
note: Note,
|
note: Note,
|
||||||
newContent: string,
|
newContent: string,
|
||||||
|
absolutePositionAuthorships: AbsolutePositionAuthorship[],
|
||||||
yjsStateVector?: number[],
|
yjsStateVector?: number[],
|
||||||
): Promise<Revision | undefined> {
|
): Promise<Revision | undefined> {
|
||||||
const latestRevision =
|
const latestRevision =
|
||||||
|
@ -186,6 +194,12 @@ export class RevisionsService {
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rangeAuthorships =
|
||||||
|
this.rangeAuthorshipService.createRangeAuthorshipsFromAbsolutePositions(
|
||||||
|
absolutePositionAuthorships,
|
||||||
|
newContent.length,
|
||||||
|
);
|
||||||
|
|
||||||
return Revision.create(
|
return Revision.create(
|
||||||
newContent,
|
newContent,
|
||||||
patch,
|
patch,
|
||||||
|
@ -194,6 +208,7 @@ export class RevisionsService {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
tagEntities,
|
tagEntities,
|
||||||
|
rangeAuthorships,
|
||||||
) as Revision;
|
) as Revision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,16 +218,19 @@ export class RevisionsService {
|
||||||
* @async
|
* @async
|
||||||
* @param note The note for which the revision should be created
|
* @param note The note for which the revision should be created
|
||||||
* @param newContent The new note content
|
* @param newContent The new note content
|
||||||
|
* @param absolutePositionAuthorships The absolute positions for authorship marks
|
||||||
* @param yjsStateVector The yjs state vector that describes the new content
|
* @param yjsStateVector The yjs state vector that describes the new content
|
||||||
*/
|
*/
|
||||||
async createAndSaveRevision(
|
async createAndSaveRevision(
|
||||||
note: Note,
|
note: Note,
|
||||||
newContent: string,
|
newContent: string,
|
||||||
|
absolutePositionAuthorships: AbsolutePositionAuthorship[],
|
||||||
yjsStateVector?: number[],
|
yjsStateVector?: number[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const revision = await this.createRevision(
|
const revision = await this.createRevision(
|
||||||
note,
|
note,
|
||||||
newContent,
|
newContent,
|
||||||
|
absolutePositionAuthorships,
|
||||||
yjsStateVector,
|
yjsStateVector,
|
||||||
);
|
);
|
||||||
if (revision) {
|
if (revision) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -17,7 +17,7 @@ import { Note } from './notes/note.entity';
|
||||||
import { Tag } from './notes/tag.entity';
|
import { Tag } from './notes/tag.entity';
|
||||||
import { NoteGroupPermission } from './permissions/note-group-permission.entity';
|
import { NoteGroupPermission } from './permissions/note-group-permission.entity';
|
||||||
import { NoteUserPermission } from './permissions/note-user-permission.entity';
|
import { NoteUserPermission } from './permissions/note-user-permission.entity';
|
||||||
import { Edit } from './revisions/edit.entity';
|
import { RangeAuthorship } from './revisions/range-authorship.entity';
|
||||||
import { Revision } from './revisions/revision.entity';
|
import { Revision } from './revisions/revision.entity';
|
||||||
import { Session } from './sessions/session.entity';
|
import { Session } from './sessions/session.entity';
|
||||||
import { User } from './users/user.entity';
|
import { User } from './users/user.entity';
|
||||||
|
@ -33,7 +33,7 @@ const dataSource = new DataSource({
|
||||||
User,
|
User,
|
||||||
Note,
|
Note,
|
||||||
Revision,
|
Revision,
|
||||||
Edit,
|
RangeAuthorship,
|
||||||
NoteGroupPermission,
|
NoteGroupPermission,
|
||||||
NoteUserPermission,
|
NoteUserPermission,
|
||||||
Group,
|
Group,
|
||||||
|
@ -81,9 +81,14 @@ dataSource
|
||||||
'Test note',
|
'Test note',
|
||||||
'',
|
'',
|
||||||
[],
|
[],
|
||||||
|
[],
|
||||||
) as Revision;
|
) as Revision;
|
||||||
const edit = Edit.create(author, 1, 42) as Edit;
|
const rangeAuthorship = RangeAuthorship.create(
|
||||||
revision.edits = Promise.resolve([edit]);
|
author,
|
||||||
|
1,
|
||||||
|
42,
|
||||||
|
) as RangeAuthorship;
|
||||||
|
revision.rangeAuthorships = Promise.resolve([rangeAuthorship]);
|
||||||
notes[i].revisions = Promise.all([revision]);
|
notes[i].revisions = Promise.all([revision]);
|
||||||
notes[i].userPermissions = Promise.resolve([]);
|
notes[i].userPermissions = Promise.resolve([]);
|
||||||
notes[i].groupPermissions = Promise.resolve([]);
|
notes[i].groupPermissions = Promise.resolve([]);
|
||||||
|
@ -92,7 +97,7 @@ dataSource
|
||||||
notes[i],
|
notes[i],
|
||||||
user,
|
user,
|
||||||
revision,
|
revision,
|
||||||
edit,
|
rangeAuthorship,
|
||||||
author,
|
author,
|
||||||
identity,
|
identity,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { RealtimeUser, RemoteCursor } from './realtime-user.js'
|
import {
|
||||||
|
RealtimeUser,
|
||||||
|
RemoteCursor,
|
||||||
|
ShortRealtimeUser
|
||||||
|
} from './realtime-user.js'
|
||||||
|
|
||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
||||||
|
@ -33,10 +37,7 @@ export interface MessagePayloads {
|
||||||
[MessageType.NOTE_CONTENT_UPDATE]: number[]
|
[MessageType.NOTE_CONTENT_UPDATE]: number[]
|
||||||
[MessageType.REALTIME_USER_STATE_SET]: {
|
[MessageType.REALTIME_USER_STATE_SET]: {
|
||||||
users: RealtimeUser[]
|
users: RealtimeUser[]
|
||||||
ownUser: {
|
ownUser: ShortRealtimeUser
|
||||||
displayName: string
|
|
||||||
styleIndex: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
[MessageType.REALTIME_USER_SINGLE_UPDATE]: RemoteCursor
|
[MessageType.REALTIME_USER_SINGLE_UPDATE]: RemoteCursor
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface RealtimeUser {
|
export interface RealtimeUser extends ShortRealtimeUser {
|
||||||
displayName: string
|
|
||||||
username: string | null
|
|
||||||
active: boolean
|
active: boolean
|
||||||
styleIndex: number
|
|
||||||
cursor: RemoteCursor | null
|
cursor: RemoteCursor | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,3 +13,9 @@ export interface RemoteCursor {
|
||||||
from: number
|
from: number
|
||||||
to?: number
|
to?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShortRealtimeUser {
|
||||||
|
displayName: string
|
||||||
|
styleIndex: number
|
||||||
|
username: string | null
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -7,3 +7,4 @@ export * from './y-doc-sync-client-adapter.js'
|
||||||
export * from './y-doc-sync-server-adapter.js'
|
export * from './y-doc-sync-server-adapter.js'
|
||||||
export * from './y-doc-sync-adapter.js'
|
export * from './y-doc-sync-adapter.js'
|
||||||
export * from './realtime-doc.js'
|
export * from './realtime-doc.js'
|
||||||
|
export * from './position-authorship.js'
|
||||||
|
|
14
commons/src/y-doc-sync/position-authorship.ts
Normal file
14
commons/src/y-doc-sync/position-authorship.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ShortRealtimeUser } from '../message-transporters/index.js'
|
||||||
|
import { AbsolutePosition, RelativePosition } from 'yjs'
|
||||||
|
|
||||||
|
export type RelativePositionAuthorship = [RelativePosition, ShortRealtimeUser]
|
||||||
|
export type OptionalAbsolutePositionAuthorship = [
|
||||||
|
AbsolutePosition | null,
|
||||||
|
ShortRealtimeUser
|
||||||
|
]
|
||||||
|
export type AbsolutePositionAuthorship = [number, ShortRealtimeUser]
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -10,10 +10,21 @@ import {
|
||||||
Doc,
|
Doc,
|
||||||
encodeStateAsUpdate,
|
encodeStateAsUpdate,
|
||||||
encodeStateVector,
|
encodeStateVector,
|
||||||
Text as YText
|
Text as YText,
|
||||||
|
Array as YArray,
|
||||||
|
createAbsolutePositionFromRelativePosition,
|
||||||
|
createRelativePositionFromTypeIndex,
|
||||||
|
AbsolutePosition
|
||||||
} from 'yjs'
|
} from 'yjs'
|
||||||
|
import {
|
||||||
|
AbsolutePositionAuthorship,
|
||||||
|
OptionalAbsolutePositionAuthorship,
|
||||||
|
RelativePositionAuthorship
|
||||||
|
} from './position-authorship.js'
|
||||||
|
import { RealtimeUser } from '../message-transporters/index.js'
|
||||||
|
|
||||||
const MARKDOWN_CONTENT_CHANNEL_NAME = 'markdownContent'
|
const MARKDOWN_CONTENT_CHANNEL_NAME = 'markdownContent'
|
||||||
|
const RELATIVE_POSITION_AUTHORSHIPS_CHANNEL_NAME = 'relativePositionAuthorships'
|
||||||
|
|
||||||
export interface RealtimeDocEvents extends EventMap {
|
export interface RealtimeDocEvents extends EventMap {
|
||||||
update: (update: number[], origin: unknown) => void
|
update: (update: number[], origin: unknown) => void
|
||||||
|
@ -36,15 +47,35 @@ export class RealtimeDoc extends EventEmitter2<RealtimeDocEvents> {
|
||||||
*
|
*
|
||||||
* @param initialTextContent the initial text content of the {@link Doc YDoc}
|
* @param initialTextContent the initial text content of the {@link Doc YDoc}
|
||||||
* @param initialYjsState the initial yjs state. If provided this will be used instead of the text content
|
* @param initialYjsState the initial yjs state. If provided this will be used instead of the text content
|
||||||
|
* @param initialAbsolutePositionAuthorships the initial realtime range authorships
|
||||||
*/
|
*/
|
||||||
constructor(initialTextContent?: string, initialYjsState?: number[]) {
|
constructor(
|
||||||
|
initialTextContent?: string,
|
||||||
|
initialYjsState?: number[],
|
||||||
|
initialAbsolutePositionAuthorships?: AbsolutePositionAuthorship[]
|
||||||
|
) {
|
||||||
super()
|
super()
|
||||||
if (initialYjsState) {
|
if (initialYjsState) {
|
||||||
this.applyUpdate(initialYjsState, this)
|
this.applyUpdate(initialYjsState, this)
|
||||||
} else if (initialTextContent) {
|
} else {
|
||||||
|
if (initialTextContent) {
|
||||||
this.getMarkdownContentChannel().insert(0, initialTextContent)
|
this.getMarkdownContentChannel().insert(0, initialTextContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialAbsolutePositionAuthorships) {
|
||||||
|
this.getRelativePositionAuthorshipsChannel().insert(
|
||||||
|
0,
|
||||||
|
initialAbsolutePositionAuthorships.map(([index, user]) => [
|
||||||
|
createRelativePositionFromTypeIndex(
|
||||||
|
this.getMarkdownContentChannel(),
|
||||||
|
index
|
||||||
|
),
|
||||||
|
user
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.docUpdateListener = (update, origin) => {
|
this.docUpdateListener = (update, origin) => {
|
||||||
this.emit('update', Array.from(update), origin)
|
this.emit('update', Array.from(update), origin)
|
||||||
}
|
}
|
||||||
|
@ -60,6 +91,15 @@ export class RealtimeDoc extends EventEmitter2<RealtimeDocEvents> {
|
||||||
return this.doc.getText(MARKDOWN_CONTENT_CHANNEL_NAME)
|
return this.doc.getText(MARKDOWN_CONTENT_CHANNEL_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the {@link YMap map channel} that contains the realtime range authorships.
|
||||||
|
*
|
||||||
|
* @return The realtime range authorships channel
|
||||||
|
*/
|
||||||
|
public getRelativePositionAuthorshipsChannel(): YArray<RelativePositionAuthorship> {
|
||||||
|
return this.doc.getArray(RELATIVE_POSITION_AUTHORSHIPS_CHANNEL_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current content of the note as it's currently edited in realtime.
|
* Gets the current content of the note as it's currently edited in realtime.
|
||||||
*
|
*
|
||||||
|
@ -72,6 +112,25 @@ export class RealtimeDoc extends EventEmitter2<RealtimeDocEvents> {
|
||||||
return this.getMarkdownContentChannel().toString()
|
return this.getMarkdownContentChannel().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current authorship positions for the realtime.
|
||||||
|
*
|
||||||
|
* Please be aware that the return of this method may be very quickly outdated.
|
||||||
|
*
|
||||||
|
* @return An array of .
|
||||||
|
*/
|
||||||
|
public getAbsolutePositionAuthorships(): AbsolutePositionAuthorship[] {
|
||||||
|
return this.getRelativePositionAuthorshipsChannel()
|
||||||
|
.map<OptionalAbsolutePositionAuthorship>(([relativePosition, user]) => [
|
||||||
|
createAbsolutePositionFromRelativePosition(relativePosition, this.doc),
|
||||||
|
user
|
||||||
|
])
|
||||||
|
.filter(
|
||||||
|
<T, S>(value: [T | null, S]): value is [T, S] => value[0] !== null
|
||||||
|
)
|
||||||
|
.map(([absolutePosition, user]) => [absolutePosition.index, user])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes the current state of the doc as update so it can be applied to other y-docs.
|
* Encodes the current state of the doc as update so it can be applied to other y-docs.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue