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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,14 +4,68 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Entity, PrimaryGeneratedColumn } from 'typeorm'; import {
import { Note } from '../notes/note.entity'; 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() @Entity()
export class Author { export class Author {
//TODO: Still missing many properties
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; 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 { Test, TestingModule } from '@nestjs/testing';
import { Author } from '../authors/author.entity';
import { LoggerModule } from '../logger/logger.module'; import { LoggerModule } from '../logger/logger.module';
import { Session } from '../users/session.entity';
import { HistoryService } from './history.service'; import { HistoryService } from './history.service';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { NotesModule } from '../notes/notes.module'; import { NotesModule } from '../notes/notes.module';
import { getConnectionToken, getRepositoryToken } from '@nestjs/typeorm'; import { getConnectionToken, getRepositoryToken } from '@nestjs/typeorm';
import { Identity } from '../users/identity.entity'; import { Identity } from '../users/identity.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { AuthorColor } from '../notes/author-color.entity';
import { Authorship } from '../revisions/authorship.entity'; import { Authorship } from '../revisions/authorship.entity';
import { HistoryEntry } from './history-entry.entity'; import { HistoryEntry } from './history-entry.entity';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
@ -75,8 +76,6 @@ describe('HistoryService', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Authorship)) .overrideProvider(getRepositoryToken(Authorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Note)) .overrideProvider(getRepositoryToken(Note))
@ -89,6 +88,10 @@ describe('HistoryService', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Group)) .overrideProvider(getRepositoryToken(Group))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile(); .compile();
service = module.get<HistoryService>(HistoryService); service = module.get<HistoryService>(HistoryService);

View file

@ -7,9 +7,9 @@
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { Author } from '../authors/author.entity';
import mediaConfigMock from '../config/mock/media.config.mock'; import mediaConfigMock from '../config/mock/media.config.mock';
import { LoggerModule } from '../logger/logger.module'; import { LoggerModule } from '../logger/logger.module';
import { AuthorColor } from '../notes/author-color.entity';
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';
@ -17,6 +17,7 @@ import { Authorship } from '../revisions/authorship.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { AuthToken } from '../auth/auth-token.entity'; import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity'; import { Identity } from '../users/identity.entity';
import { Session } from '../users/session.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { FilesystemBackend } from './backends/filesystem-backend'; import { FilesystemBackend } from './backends/filesystem-backend';
@ -56,8 +57,6 @@ describe('MediaService', () => {
UsersModule, UsersModule,
], ],
}) })
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship)) .overrideProvider(getRepositoryToken(Authorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(AuthToken)) .overrideProvider(getRepositoryToken(AuthToken))
@ -80,6 +79,10 @@ describe('MediaService', () => {
.useClass(Repository) .useClass(Repository)
.overrideProvider(getRepositoryToken(Group)) .overrideProvider(getRepositoryToken(Group))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile(); .compile();
service = module.get<MediaService>(MediaService); 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 { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { AuthorColor } from './author-color.entity';
import { Tag } from './tag.entity'; import { Tag } from './tag.entity';
import { HistoryEntry } from '../history/history-entry.entity'; import { HistoryEntry } from '../history/history-entry.entity';
import { MediaUpload } from '../media/media-upload.entity'; import { MediaUpload } from '../media/media-upload.entity';
@ -59,8 +58,6 @@ export class Note {
owner: User | null; owner: User | null;
@OneToMany((_) => Revision, (revision) => revision.note, { cascade: true }) @OneToMany((_) => Revision, (revision) => revision.note, { cascade: true })
revisions: Promise<Revision[]>; revisions: Promise<Revision[]>;
@OneToMany((_) => AuthorColor, (authorColor) => authorColor.note)
authorColors: AuthorColor[];
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user) @OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
historyEntries: HistoryEntry[]; historyEntries: HistoryEntry[];
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note) @OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note)
@ -90,7 +87,6 @@ export class Note {
newNote.alias = alias ?? null; newNote.alias = alias ?? null;
newNote.viewCount = 0; newNote.viewCount = 0;
newNote.owner = owner ?? null; newNote.owner = owner ?? null;
newNote.authorColors = [];
newNote.userPermissions = []; newNote.userPermissions = [];
newNote.groupPermissions = []; newNote.groupPermissions = [];
newNote.revisions = Promise.resolve([]) as Promise<Revision[]>; newNote.revisions = Promise.resolve([]) as Promise<Revision[]>;

View file

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

View file

@ -6,15 +6,16 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { Author } from '../authors/author.entity';
import { LoggerModule } from '../logger/logger.module'; import { LoggerModule } from '../logger/logger.module';
import { Authorship } from '../revisions/authorship.entity'; import { Authorship } from '../revisions/authorship.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 { AuthToken } from '../auth/auth-token.entity'; import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from '../users/identity.entity'; import { Identity } from '../users/identity.entity';
import { Session } from '../users/session.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { AuthorColor } from './author-color.entity';
import { Note } from './note.entity'; import { Note } from './note.entity';
import { NotesService } from './notes.service'; import { NotesService } from './notes.service';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -45,6 +46,12 @@ describe('NotesService', () => {
let forbiddenNoteId: string; let forbiddenNoteId: string;
beforeEach(async () => { 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({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
NotesService, NotesService,
@ -56,6 +63,10 @@ describe('NotesService', () => {
provide: getRepositoryToken(Tag), provide: getRepositoryToken(Tag),
useClass: Repository, useClass: Repository,
}, },
{
provide: getRepositoryToken(User),
useValue: userRepo,
},
], ],
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
@ -73,15 +84,13 @@ describe('NotesService', () => {
.overrideProvider(getRepositoryToken(Tag)) .overrideProvider(getRepositoryToken(Tag))
.useClass(Repository) .useClass(Repository)
.overrideProvider(getRepositoryToken(User)) .overrideProvider(getRepositoryToken(User))
.useClass(Repository) .useValue(userRepo)
.overrideProvider(getRepositoryToken(AuthToken)) .overrideProvider(getRepositoryToken(AuthToken))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Identity)) .overrideProvider(getRepositoryToken(Identity))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Authorship)) .overrideProvider(getRepositoryToken(Authorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useClass(Repository) .useClass(Repository)
.overrideProvider(getRepositoryToken(NoteGroupPermission)) .overrideProvider(getRepositoryToken(NoteGroupPermission))
@ -90,6 +99,10 @@ describe('NotesService', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Group)) .overrideProvider(getRepositoryToken(Group))
.useClass(Repository) .useClass(Repository)
.overrideProvider(getRepositoryToken(Session))
.useValue({})
.overrideProvider(getRepositoryToken(Author))
.useValue({})
.compile(); .compile();
const config = module.get<ConfigService>(ConfigService); const config = module.get<ConfigService>(ConfigService);
@ -658,7 +671,8 @@ describe('NotesService', () => {
describe('toNoteMetadataDto', () => { describe('toNoteMetadataDto', () => {
it('works', async () => { it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User; 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 group = Group.create('testGroup', 'testGroup');
const content = 'testContent'; const content = 'testContent';
jest jest
@ -668,33 +682,36 @@ describe('NotesService', () => {
const revisions = await note.revisions; const revisions = await note.revisions;
revisions[0].authorships = [ revisions[0].authorships = [
{ {
user: otherUser,
revisions: revisions, revisions: revisions,
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452000), updatedAt: new Date(1549312452000),
author: author,
} as Authorship, } as Authorship,
{ {
user: user,
revisions: revisions, revisions: revisions,
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452001), updatedAt: new Date(1549312452001),
author: author,
} as Authorship, } as Authorship,
]; ];
revisions[0].createdAt = new Date(1549312452000); revisions[0].createdAt = new Date(1549312452000);
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(revisions[0]); 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.publicId = 'testId';
note.alias = 'testAlias'; note.alias = 'testAlias';
note.title = 'testTitle'; note.title = 'testTitle';
note.description = 'testDescription'; note.description = 'testDescription';
note.authorColors = [
{
note: note,
user: user,
color: 'red',
} as AuthorColor,
];
note.owner = user; note.owner = user;
note.userPermissions = [ note.userPermissions = [
{ {
@ -748,6 +765,8 @@ describe('NotesService', () => {
describe('toNoteDto', () => { describe('toNoteDto', () => {
it('works', async () => { it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User; 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; const otherUser = User.create('other hardcoded', 'Testy2') as User;
otherUser.userName = 'other hardcoded user'; otherUser.userName = 'other hardcoded user';
const group = Group.create('testGroup', 'testGroup'); const group = Group.create('testGroup', 'testGroup');
@ -759,18 +778,18 @@ describe('NotesService', () => {
const revisions = await note.revisions; const revisions = await note.revisions;
revisions[0].authorships = [ revisions[0].authorships = [
{ {
user: otherUser,
revisions: revisions, revisions: revisions,
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452000), updatedAt: new Date(1549312452000),
author: author,
} as Authorship, } as Authorship,
{ {
user: user,
revisions: revisions, revisions: revisions,
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452001), updatedAt: new Date(1549312452001),
author: author,
} as Authorship, } as Authorship,
]; ];
revisions[0].createdAt = new Date(1549312452000); revisions[0].createdAt = new Date(1549312452000);
@ -778,17 +797,20 @@ describe('NotesService', () => {
.spyOn(revisionRepo, 'findOne') .spyOn(revisionRepo, 'findOne')
.mockResolvedValue(revisions[0]) .mockResolvedValue(revisions[0])
.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.publicId = 'testId';
note.alias = 'testAlias'; note.alias = 'testAlias';
note.title = 'testTitle'; note.title = 'testTitle';
note.description = 'testDescription'; note.description = 'testDescription';
note.authorColors = [
{
note: note,
user: user,
color: 'red',
} as AuthorColor,
];
note.owner = user; note.owner = user;
note.userPermissions = [ note.userPermissions = [
{ {

View file

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

View file

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

View file

@ -13,11 +13,11 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { User } from '../users/user.entity'; import { Author } from '../authors/author.entity';
import { Revision } from './revision.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() @Entity()
export class Authorship { export class Authorship {
@ -31,10 +31,10 @@ export class Authorship {
revisions: Revision[]; revisions: Revision[];
/** /**
* User this authorship represents * Author that created the change
*/ */
@ManyToOne((_) => User) @ManyToOne(() => Author, (author) => author.authorships)
user: User; author: Author;
@Column() @Column()
startPos: number; startPos: number;
@ -47,4 +47,15 @@ export class Authorship {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; 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 { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthorsModule } from '../authors/authors.module';
import { LoggerModule } from '../logger/logger.module'; import { LoggerModule } from '../logger/logger.module';
import { NotesModule } from '../notes/notes.module'; import { NotesModule } from '../notes/notes.module';
import { Authorship } from './authorship.entity'; import { Authorship } from './authorship.entity';
@ -19,6 +20,7 @@ import { ConfigModule } from '@nestjs/config';
forwardRef(() => NotesModule), forwardRef(() => NotesModule),
LoggerModule, LoggerModule,
ConfigModule, ConfigModule,
AuthorsModule,
], ],
providers: [RevisionsService], providers: [RevisionsService],
exports: [RevisionsService], exports: [RevisionsService],

View file

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

View file

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

View file

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

View file

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

View file

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