Merge pull request #1266 from hedgedoc/feature/anonymous_user_colors

This commit is contained in:
David Mehren 2021-05-31 21:31:35 +02:00 committed by GitHub
commit 2b0fa17d03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 288 additions and 136 deletions

View file

@ -74,7 +74,7 @@ entity "revision" {
entity "authorship" {
*id : uuid <<generated>>
--
*userId : uuid <FK user>>
*authorId : uuid <FK user>>
*startPos : number
*endPos : number
*createdAt : date
@ -86,11 +86,12 @@ entity "revision_authorship" {
*authorshipId : uuid <<FK authorship>>
}
entity "author_colors" {
*noteId : uuid <<FK note>>
*userId : uuid <<FK user>>
entity "author" {
*id : number <<generated>>
--
*color : text
sessionID : text <<FK session>>
userId : uuid <<FK user>>
}
@ -148,22 +149,23 @@ entity "history_entry" {
*updatedAt: date
}
user "1" -- "0..*" note: owner
user "0..1" -- "0..*" note: owner
user "1" -u- "1..*" identity
user "1" -l- "1..*" auth_token: authTokens
user "1" -r- "1..*" session
user "1" -- "0..*" media_upload
user "1" - "0..*" history_entry
user "1" -- "0..*" history_entry
user "0..*" -- "0..*" note
user "1" -- "0..*" authorship
user "0..1" -- "0..*" author
(user, note) . author_colors
author "1" -- "0..*" authorship
author "1" -u- "0..*" session
revision "0..*" -- "0..*" authorship
(revision, authorship) .. revision_authorship
media_upload "0..*" -- "1" note
note "1" - "1..*" revision
note "1" -d- "1..*" revision
note "1" - "0..*" history_entry
note "0..*" -l- "0..*" tag
note "0..*" -- "0..*" group

View file

@ -5,6 +5,8 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Author } from '../../../../authors/author.entity';
import { Session } from '../../../../users/session.entity';
import { HistoryController } from './history.controller';
import { LoggerModule } from '../../../../logger/logger.module';
import { UsersModule } from '../../../../users/users.module';
@ -19,7 +21,6 @@ import { User } from '../../../../users/user.entity';
import { Note } from '../../../../notes/note.entity';
import { AuthToken } from '../../../../auth/auth-token.entity';
import { Identity } from '../../../../users/identity.entity';
import { AuthorColor } from '../../../../notes/author-color.entity';
import { Authorship } from '../../../../revisions/authorship.entity';
import { Revision } from '../../../../revisions/revision.entity';
import { Tag } from '../../../../notes/tag.entity';
@ -58,8 +59,6 @@ describe('HistoryController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Identity))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
@ -74,6 +73,10 @@ describe('HistoryController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.compile();
controller = module.get<HistoryController>(HistoryController);

View file

@ -5,6 +5,8 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Author } from '../../../authors/author.entity';
import { Session } from '../../../users/session.entity';
import { MeController } from './me.controller';
import { UsersModule } from '../../../users/users.module';
import { LoggerModule } from '../../../logger/logger.module';
@ -12,7 +14,6 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from '../../../users/user.entity';
import { Identity } from '../../../users/identity.entity';
import { MediaModule } from '../../../media/media.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
import { Authorship } from '../../../revisions/authorship.entity';
@ -62,8 +63,6 @@ describe('MeController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(NoteGroupPermission))
.useValue({})
.overrideProvider(getRepositoryToken(NoteUserPermission))
@ -72,6 +71,10 @@ describe('MeController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(MediaUpload))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
controller = module.get<MeController>(MeController);

View file

@ -5,6 +5,9 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Author } from '../../../authors/author.entity';
import { Session } from '../../../users/session.entity';
import { UsersModule } from '../../../users/users.module';
import { MediaController } from './media.controller';
import { LoggerModule } from '../../../logger/logger.module';
import { ConfigModule } from '@nestjs/config';
@ -16,7 +19,6 @@ import externalConfigMock from '../../../config/mock/external-services.config.mo
import { MediaModule } from '../../../media/media.module';
import { NotesModule } from '../../../notes/notes.module';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Authorship } from '../../../revisions/authorship.entity';
import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity';
@ -48,11 +50,10 @@ describe('MediaController', () => {
externalConfigMock,
],
}),
UsersModule,
],
controllers: [MediaController],
})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
@ -75,6 +76,10 @@ describe('MediaController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
controller = module.get<MediaController>(MediaController);

View file

@ -5,6 +5,8 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Author } from '../../../authors/author.entity';
import { Session } from '../../../users/session.entity';
import { NotesController } from './notes.controller';
import { NotesService } from '../../../notes/notes.service';
import {
@ -26,7 +28,6 @@ import appConfigMock from '../../../config/mock/app.config.mock';
import mediaConfigMock from '../../../config/mock/media.config.mock';
import { Revision } from '../../../revisions/revision.entity';
import { Authorship } from '../../../revisions/authorship.entity';
import { AuthorColor } from '../../../notes/author-color.entity';
import { User } from '../../../users/user.entity';
import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity';
@ -52,6 +53,10 @@ describe('NotesController', () => {
provide: getRepositoryToken(Tag),
useValue: {},
},
{
provide: getRepositoryToken(User),
useValue: {},
},
],
imports: [
RevisionsModule,
@ -74,8 +79,6 @@ describe('NotesController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
@ -96,6 +99,10 @@ describe('NotesController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(MediaUpload))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
controller = module.get<NotesController>(NotesController);

View file

@ -5,6 +5,7 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Session } from '../../../users/session.entity';
import { TokensController } from './tokens.controller';
import { LoggerModule } from '../../../logger/logger.module';
import { getRepositoryToken } from '@nestjs/typeorm';
@ -36,6 +37,8 @@ describe('TokensController', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Identity))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.compile();
controller = module.get<TokensController>(TokensController);

View file

@ -10,9 +10,9 @@ import {
getRepositoryToken,
TypeOrmModule,
} from '@nestjs/typeorm';
import { Author } from '../../../authors/author.entity';
import { HistoryModule } from '../../../history/history.module';
import { LoggerModule } from '../../../logger/logger.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity';
import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity';
@ -20,6 +20,7 @@ import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity';
import { Session } from '../../../users/session.entity';
import { User } from '../../../users/user.entity';
import { UsersModule } from '../../../users/users.module';
import { MeController } from './me.controller';
@ -62,8 +63,6 @@ describe('Me Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Identity))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
@ -80,6 +79,10 @@ describe('Me Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(MediaUpload))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
controller = module.get<MeController>(MeController);

View file

@ -7,12 +7,12 @@
import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Author } from '../../../authors/author.entity';
import appConfigMock from '../../../config/mock/app.config.mock';
import mediaConfigMock from '../../../config/mock/media.config.mock';
import { LoggerModule } from '../../../logger/logger.module';
import { MediaUpload } from '../../../media/media-upload.entity';
import { MediaModule } from '../../../media/media.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity';
import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity';
@ -20,6 +20,7 @@ import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity';
import { Session } from '../../../users/session.entity';
import { User } from '../../../users/user.entity';
import { MediaController } from './media.controller';
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
@ -42,8 +43,6 @@ describe('Media Controller', () => {
NotesModule,
],
})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
@ -66,6 +65,10 @@ describe('Media Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
controller = module.get<MediaController>(MediaController);

View file

@ -10,8 +10,8 @@ import {
getRepositoryToken,
TypeOrmModule,
} from '@nestjs/typeorm';
import { Author } from '../../../authors/author.entity';
import { LoggerModule } from '../../../logger/logger.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity';
import { NotesService } from '../../../notes/notes.service';
import { Tag } from '../../../notes/tag.entity';
@ -20,6 +20,7 @@ import { Revision } from '../../../revisions/revision.entity';
import { RevisionsModule } from '../../../revisions/revisions.module';
import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity';
import { Session } from '../../../users/session.entity';
import { User } from '../../../users/user.entity';
import { UsersModule } from '../../../users/users.module';
import { NotesController } from './notes.controller';
@ -52,6 +53,10 @@ describe('Notes Controller', () => {
provide: getRepositoryToken(Tag),
useValue: {},
},
{
provide: getRepositoryToken(User),
useValue: {},
},
],
imports: [
RevisionsModule,
@ -76,8 +81,6 @@ describe('Notes Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
@ -98,6 +101,10 @@ describe('Notes Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(MediaUpload))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
controller = module.get<NotesController>(NotesController);

View file

@ -5,6 +5,7 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Session } from '../users/session.entity';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { getRepositoryToken } from '@nestjs/typeorm';
@ -49,6 +50,8 @@ describe('AuthService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(User))
.useClass(Repository)
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.compile();
service = module.get<AuthService>(AuthService);

View file

@ -4,14 +4,68 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Note } from '../notes/note.entity';
import {
Column,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Authorship } from '../revisions/authorship.entity';
import { Session } from '../users/session.entity';
import { User } from '../users/user.entity';
export type AuthorColor = number;
/**
* The author represents a single user editing a note.
* A 'user' can either be a registered and logged-in user or a browser session identified by its cookie.
* All edits (aka authorships) of one user in a note must belong to the same author, so that the same color can be displayed.
*/
@Entity()
export class Author {
//TODO: Still missing many properties
@PrimaryGeneratedColumn()
id: number;
note: Note;
/**
* The id of the color of this author
* The application maps the id to an actual color
*/
@Column({ type: 'int' })
color: AuthorColor;
/**
* A list of (browser) sessions this author has
* Only contains sessions for anonymous users, which don't have a user set
*/
@OneToMany(() => Session, (session) => session.author)
sessions: Session[];
/**
* User that this author corresponds to
* Only set when the user was identified (by a browser session) as a registered user at edit-time
*/
@ManyToOne(() => User, (user) => user.authors, { nullable: true })
user: User | null;
/**
* List of authorships that this author created
* All authorships must belong to the same note
*/
@OneToMany(() => Authorship, (authorship) => authorship.author)
authorships: Authorship[];
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static create(
color: number,
): Pick<Author, 'color' | 'sessions' | 'user' | 'authorships'> {
const newAuthor = new Author();
newAuthor.color = color;
newAuthor.sessions = [];
newAuthor.user = null;
newAuthor.authorships = [];
return newAuthor;
}
}

View file

@ -5,14 +5,15 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Author } from '../authors/author.entity';
import { LoggerModule } from '../logger/logger.module';
import { Session } from '../users/session.entity';
import { HistoryService } from './history.service';
import { UsersModule } from '../users/users.module';
import { NotesModule } from '../notes/notes.module';
import { getConnectionToken, getRepositoryToken } from '@nestjs/typeorm';
import { Identity } from '../users/identity.entity';
import { User } from '../users/user.entity';
import { AuthorColor } from '../notes/author-color.entity';
import { Authorship } from '../revisions/authorship.entity';
import { HistoryEntry } from './history-entry.entity';
import { Note } from '../notes/note.entity';
@ -75,8 +76,6 @@ describe('HistoryService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useValue({})
.overrideProvider(getRepositoryToken(Note))
@ -89,6 +88,10 @@ describe('HistoryService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
service = module.get<HistoryService>(HistoryService);

View file

@ -7,9 +7,9 @@
import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Author } from '../authors/author.entity';
import mediaConfigMock from '../config/mock/media.config.mock';
import { LoggerModule } from '../logger/logger.module';
import { AuthorColor } from '../notes/author-color.entity';
import { Note } from '../notes/note.entity';
import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity';
@ -17,6 +17,7 @@ import { Authorship } from '../revisions/authorship.entity';
import { Revision } from '../revisions/revision.entity';
import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity';
import { Session } from '../users/session.entity';
import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module';
import { FilesystemBackend } from './backends/filesystem-backend';
@ -56,8 +57,6 @@ describe('MediaService', () => {
UsersModule,
],
})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
@ -80,6 +79,10 @@ describe('MediaService', () => {
.useClass(Repository)
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
service = module.get<MediaService>(MediaService);

View file

@ -1,25 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Column, Entity, ManyToOne } from 'typeorm';
import { User } from '../users/user.entity';
import { Note } from './note.entity';
@Entity()
export class AuthorColor {
@ManyToOne((_) => Note, (note) => note.authorColors, {
primary: true,
})
note: Note;
@ManyToOne((_) => User, {
primary: true,
})
user: User;
@Column()
color: string;
}

View file

@ -17,7 +17,6 @@ import { NoteGroupPermission } from '../permissions/note-group-permission.entity
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Revision } from '../revisions/revision.entity';
import { User } from '../users/user.entity';
import { AuthorColor } from './author-color.entity';
import { Tag } from './tag.entity';
import { HistoryEntry } from '../history/history-entry.entity';
import { MediaUpload } from '../media/media-upload.entity';
@ -59,8 +58,6 @@ export class Note {
owner: User | null;
@OneToMany((_) => Revision, (revision) => revision.note, { cascade: true })
revisions: Promise<Revision[]>;
@OneToMany((_) => AuthorColor, (authorColor) => authorColor.note)
authorColors: AuthorColor[];
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
historyEntries: HistoryEntry[];
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note)
@ -90,7 +87,6 @@ export class Note {
newNote.alias = alias ?? null;
newNote.viewCount = 0;
newNote.owner = owner ?? null;
newNote.authorColors = [];
newNote.userPermissions = [];
newNote.groupPermissions = [];
newNote.revisions = Promise.resolve([]) as Promise<Revision[]>;

View file

@ -5,27 +5,27 @@
*/
import { forwardRef, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
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 { RevisionsModule } from '../revisions/revisions.module';
import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module';
import { AuthorColor } from './author-color.entity';
import { Note } from './note.entity';
import { NotesService } from './notes.service';
import { Tag } from './tag.entity';
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { GroupsModule } from '../groups/groups.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forFeature([
Note,
AuthorColor,
Tag,
NoteGroupPermission,
NoteUserPermission,
User,
]),
forwardRef(() => RevisionsModule),
UsersModule,

View file

@ -6,15 +6,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Author } from '../authors/author.entity';
import { LoggerModule } from '../logger/logger.module';
import { Authorship } from '../revisions/authorship.entity';
import { Revision } from '../revisions/revision.entity';
import { RevisionsModule } from '../revisions/revisions.module';
import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity';
import { Session } from '../users/session.entity';
import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module';
import { AuthorColor } from './author-color.entity';
import { Note } from './note.entity';
import { NotesService } from './notes.service';
import { Repository } from 'typeorm';
@ -45,6 +46,12 @@ describe('NotesService', () => {
let forbiddenNoteId: string;
beforeEach(async () => {
/**
* We need to have *one* userRepo for both the providers array and
* the overrideProvider call, as otherwise we have two instances
* and the mock of createQueryBuilder replaces the wrong one
* **/
userRepo = new Repository<User>();
const module: TestingModule = await Test.createTestingModule({
providers: [
NotesService,
@ -56,6 +63,10 @@ describe('NotesService', () => {
provide: getRepositoryToken(Tag),
useClass: Repository,
},
{
provide: getRepositoryToken(User),
useValue: userRepo,
},
],
imports: [
ConfigModule.forRoot({
@ -73,15 +84,13 @@ describe('NotesService', () => {
.overrideProvider(getRepositoryToken(Tag))
.useClass(Repository)
.overrideProvider(getRepositoryToken(User))
.useClass(Repository)
.useValue(userRepo)
.overrideProvider(getRepositoryToken(AuthToken))
.useValue({})
.overrideProvider(getRepositoryToken(Identity))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useClass(Repository)
.overrideProvider(getRepositoryToken(NoteGroupPermission))
@ -90,6 +99,10 @@ describe('NotesService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useClass(Repository)
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
const config = module.get<ConfigService>(ConfigService);
@ -658,7 +671,8 @@ describe('NotesService', () => {
describe('toNoteMetadataDto', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const otherUser = User.create('other hardcoded', 'Testy2') as User;
const author = Author.create(1);
author.user = user;
const group = Group.create('testGroup', 'testGroup');
const content = 'testContent';
jest
@ -668,33 +682,36 @@ describe('NotesService', () => {
const revisions = await note.revisions;
revisions[0].authorships = [
{
user: otherUser,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452000),
author: author,
} as Authorship,
{
user: user,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452001),
author: author,
} as Authorship,
];
revisions[0].createdAt = new Date(1549312452000);
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(revisions[0]);
const createQueryBuilder = {
innerJoin: () => createQueryBuilder,
where: () => createQueryBuilder,
getMany: () => [user],
};
jest
.spyOn(userRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
note.publicId = 'testId';
note.alias = 'testAlias';
note.title = 'testTitle';
note.description = 'testDescription';
note.authorColors = [
{
note: note,
user: user,
color: 'red',
} as AuthorColor,
];
note.owner = user;
note.userPermissions = [
{
@ -748,6 +765,8 @@ describe('NotesService', () => {
describe('toNoteDto', () => {
it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User;
const author = Author.create(1);
author.user = user;
const otherUser = User.create('other hardcoded', 'Testy2') as User;
otherUser.userName = 'other hardcoded user';
const group = Group.create('testGroup', 'testGroup');
@ -759,18 +778,18 @@ describe('NotesService', () => {
const revisions = await note.revisions;
revisions[0].authorships = [
{
user: otherUser,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452000),
author: author,
} as Authorship,
{
user: user,
revisions: revisions,
startPos: 0,
endPos: 1,
updatedAt: new Date(1549312452001),
author: author,
} as Authorship,
];
revisions[0].createdAt = new Date(1549312452000);
@ -778,17 +797,20 @@ describe('NotesService', () => {
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(revisions[0])
.mockResolvedValue(revisions[0]);
const createQueryBuilder = {
innerJoin: () => createQueryBuilder,
where: () => createQueryBuilder,
getMany: () => [user],
};
jest
.spyOn(userRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
note.publicId = 'testId';
note.alias = 'testAlias';
note.title = 'testTitle';
note.description = 'testDescription';
note.authorColors = [
{
note: note,
user: user,
color: 'red',
} as AuthorColor,
];
note.owner = user;
note.userPermissions = [
{

View file

@ -41,6 +41,7 @@ export class NotesService {
private readonly logger: ConsoleLoggerService,
@InjectRepository(Note) private noteRepository: Repository<Note>,
@InjectRepository(Tag) private tagRepository: Repository<Tag>,
@InjectRepository(User) private userRepository: Repository<User>,
@Inject(UsersService) private usersService: UsersService,
@Inject(GroupsService) private groupsService: GroupsService,
@Inject(forwardRef(() => RevisionsService))
@ -60,13 +61,7 @@ export class NotesService {
async getUserNotes(user: User): Promise<Note[]> {
const notes = await this.noteRepository.find({
where: { owner: user },
relations: [
'owner',
'userPermissions',
'groupPermissions',
'authorColors',
'tags',
],
relations: ['owner', 'userPermissions', 'groupPermissions', 'tags'],
});
if (notes === undefined) {
return [];
@ -173,7 +168,6 @@ export class NotesService {
},
],
relations: [
'authorColors',
'owner',
'groupPermissions',
'groupPermissions.group',
@ -195,6 +189,22 @@ export class NotesService {
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.authorships', 'authorship')
.innerJoin('authorship.revisions', 'revision')
.innerJoin('revision.note', 'note')
.where('note.id = :id', { id: note.id })
.getMany();
}
/**
* Check if the provided note id or alias is not forbidden
* @param noteIdOrAlias - the alias or id in question
@ -317,7 +327,7 @@ export class NotesService {
// the user of that Authorship is the updateUser
return lastRevision.authorships.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
)[0].user;
)[0].author.user;
}
// If there are no Authorships, the owner is the updateUser
return note.owner;
@ -365,9 +375,7 @@ export class NotesService {
title: note.title ?? '',
createTime: (await this.getFirstRevision(note)).createdAt,
description: note.description ?? '',
editedBy: note.authorColors.map(
(authorColor) => authorColor.user.userName,
),
editedBy: (await this.getAuthorUsers(note)).map((user) => user.userName),
permissions: this.toNotePermissionsDto(note),
tags: this.toTagList(note),
updateTime: (await this.getLatestRevision(note)).createdAt,

View file

@ -7,15 +7,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AuthToken } from '../auth/auth-token.entity';
import { Author } from '../authors/author.entity';
import { Group } from '../groups/group.entity';
import { LoggerModule } from '../logger/logger.module';
import { AuthorColor } from '../notes/author-color.entity';
import { Note } from '../notes/note.entity';
import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity';
import { Authorship } from '../revisions/authorship.entity';
import { Revision } from '../revisions/revision.entity';
import { Identity } from '../users/identity.entity';
import { Session } from '../users/session.entity';
import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module';
import { NoteGroupPermission } from './note-group-permission.entity';
@ -50,8 +51,6 @@ describe('PermissionsService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useValue({})
.overrideProvider(getRepositoryToken(Note))
@ -64,6 +63,10 @@ describe('PermissionsService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
permissionsService = module.get<PermissionsService>(PermissionsService);
});

View file

@ -13,11 +13,11 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../users/user.entity';
import { Author } from '../authors/author.entity';
import { Revision } from './revision.entity';
/**
* This class stores which parts of a revision were edited by a particular user.
* The Authorship represents a change in the content of a note by a particular {@link Author}
*/
@Entity()
export class Authorship {
@ -31,10 +31,10 @@ export class Authorship {
revisions: Revision[];
/**
* User this authorship represents
* Author that created the change
*/
@ManyToOne((_) => User)
user: User;
@ManyToOne(() => Author, (author) => author.authorships)
author: Author;
@Column()
startPos: number;
@ -47,4 +47,15 @@ export class Authorship {
@UpdateDateColumn()
updatedAt: Date;
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static create(author: Author, startPos: number, endPos: number) {
const newAuthorship = new Authorship();
newAuthorship.author = author;
newAuthorship.startPos = startPos;
newAuthorship.endPos = endPos;
return newAuthorship;
}
}

View file

@ -6,6 +6,7 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthorsModule } from '../authors/authors.module';
import { LoggerModule } from '../logger/logger.module';
import { NotesModule } from '../notes/notes.module';
import { Authorship } from './authorship.entity';
@ -19,6 +20,7 @@ import { ConfigModule } from '@nestjs/config';
forwardRef(() => NotesModule),
LoggerModule,
ConfigModule,
AuthorsModule,
],
providers: [RevisionsService],
exports: [RevisionsService],

View file

@ -7,13 +7,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Author } from '../authors/author.entity';
import { AuthorsModule } from '../authors/authors.module';
import { NotInDBError } from '../errors/errors';
import { LoggerModule } from '../logger/logger.module';
import { AuthorColor } from '../notes/author-color.entity';
import { Note } from '../notes/note.entity';
import { NotesModule } from '../notes/notes.module';
import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity';
import { Session } from '../users/session.entity';
import { User } from '../users/user.entity';
import { Authorship } from './authorship.entity';
import { Revision } from './revision.entity';
@ -49,8 +51,6 @@ describe('RevisionsService', () => {
})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
@ -69,6 +69,10 @@ describe('RevisionsService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile();
service = module.get<RevisionsService>(RevisionsService);

View file

@ -5,6 +5,8 @@
*/
import { createConnection } from 'typeorm';
import { Author } from './authors/author.entity';
import { Session } from './users/session.entity';
import { User } from './users/user.entity';
import { Note } from './notes/note.entity';
import { Revision } from './revisions/revision.entity';
@ -12,7 +14,6 @@ import { Authorship } from './revisions/authorship.entity';
import { NoteGroupPermission } from './permissions/note-group-permission.entity';
import { NoteUserPermission } from './permissions/note-user-permission.entity';
import { Group } from './groups/group.entity';
import { AuthorColor } from './notes/author-color.entity';
import { HistoryEntry } from './history/history-entry.entity';
import { MediaUpload } from './media/media-upload.entity';
import { Tag } from './notes/tag.entity';
@ -33,28 +34,50 @@ createConnection({
NoteGroupPermission,
NoteUserPermission,
Group,
AuthorColor,
HistoryEntry,
MediaUpload,
Tag,
AuthToken,
Identity,
Author,
Session,
],
synchronize: true,
logging: false,
dropSchema: true,
})
.then(async (connection) => {
const user = User.create('hardcoded', 'Test User');
const note = Note.create(undefined, 'test');
const revision = Revision.create(
'This is a test note',
'This is a test note',
);
note.revisions = Promise.all([revision]);
note.userPermissions = [];
note.groupPermissions = [];
user.ownedNotes = [note];
await connection.manager.save([user, note, revision]);
const users = [];
users.push(User.create('hardcoded', 'Test User 1'));
users.push(User.create('hardcoded_2', 'Test User 2'));
users.push(User.create('hardcoded_3', 'Test User 3'));
const notes: Note[] = [];
notes.push(Note.create(undefined, 'test'));
notes.push(Note.create(undefined, 'test2'));
notes.push(Note.create(undefined, 'test3'));
for (let i = 0; i < 3; i++) {
const author = connection.manager.create(Author, Author.create(1));
const user = connection.manager.create(User, users[i]);
author.user = user;
const revision = Revision.create(
'This is a test note',
'This is a test note',
);
const authorship = Authorship.create(author, 1, 42);
revision.authorships = [authorship];
notes[i].revisions = Promise.all([revision]);
notes[i].userPermissions = [];
notes[i].groupPermissions = [];
user.ownedNotes = [notes[i]];
await connection.manager.save([
notes[i],
user,
revision,
authorship,
author,
]);
}
const foundUser = await connection.manager.findOne(User);
if (!foundUser) {
throw new Error('Could not find freshly seeded user. Aborting.');

View file

@ -5,7 +5,8 @@
*/
import { ISession } from 'connect-typeorm';
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm';
import { Author } from '../authors/author.entity';
@Entity()
export class Session implements ISession {
@ -18,4 +19,7 @@ export class Session implements ISession {
@Column('text')
public json = '';
@ManyToOne(() => Author, (author) => author.sessions)
author: Author;
}

View file

@ -12,6 +12,7 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { Column, OneToMany } from 'typeorm';
import { Author } from '../authors/author.entity';
import { Note } from '../notes/note.entity';
import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from './identity.entity';
@ -68,6 +69,9 @@ export class User {
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.user)
mediaUploads: MediaUpload[];
@OneToMany(() => Author, (author) => author.user)
authors: Author[];
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}

View file

@ -8,11 +8,12 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerModule } from '../logger/logger.module';
import { Identity } from './identity.entity';
import { Session } from './session.entity';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([User, Identity]), LoggerModule],
imports: [TypeOrmModule.forFeature([User, Identity, Session]), LoggerModule],
providers: [UsersService],
exports: [UsersService],
})