diff --git a/docs/dev/db-schema.plantuml b/docs/dev/db-schema.plantuml index 3ec0fb4a7..789eac254 100644 --- a/docs/dev/db-schema.plantuml +++ b/docs/dev/db-schema.plantuml @@ -1,17 +1,16 @@ @startuml -' hide the spot hide circle - -' avoid problems with angled crows feet -skinparam linetype ortho +skinparam nodesep 60 entity "Note" { *id : uuid <> -- *shortid : text - *alias : text + alias : text *viewcount : number *ownerId : uuid <> + description: text + title: text } entity "User" { @@ -49,7 +48,7 @@ entity "Identity" { passwordHash : text } -entity "Session" as seesion { +entity "Session" { *id : text -- *expiredAt : number @@ -108,13 +107,18 @@ entity "Group" { *special : boolean } - entity "NoteGroupPermission" { +entity "NoteGroupPermission" { *groupId : number <> *noteId : uuid <> -- *canEdit : boolean } +entity "Tag" { + *id: number <> + *name: text +} + entity "MediaUpload" { *id : text <> -- @@ -125,20 +129,26 @@ entity "MediaUpload" { *createdAt : date } -Note "1" - "1..*" Revision +User "1" -- "0..*" Note: owner +User "1" -u- "1..*" Identity +User "1" - "1..*" authToken +User "1" -l- "1..*" Session +User "1" - "0..*" MediaUpload +User "0..*" -- "0..*" Note +User "1" - "0..*" Authorship + +(User, Note) . AuthorColors + Revision "0..*" - "0..*" Authorship (Revision, Authorship) .. RevisionAuthorship -Authorship "0..*" -- "1" User -Note "0..*" -- "1" User : owner -Note "1" -- "0..*" NoteUserPermission -NoteUserPermission "1" -- "1" User -Note "1" -- "0..*" NoteGroupPermission -NoteGroupPermission "0..*" -- "1" Group -Identity "1..*" -- "1" User -authToken "1..*" -- "1" User -seesion "1..*" -- "1" User -Note "0..*" -- "0..*" User : color -(Note, User) .. AuthorColors -MediaUpload "0..*" -- "1" Note -MediaUpload "0..*" -- "1" User + +MediaUpload "0..*" -- "1" Note +Note "1" - "1..*" Revision +Note "0..*" -l- "0..*" Tag +Note "0..*" -- "0..*" Group + +User "0..*" -- "0..*" Note +(User, Note) . NoteUserPermission +(Note, Group) . NoteGroupPermission + @enduml diff --git a/src/api/public/me/me.controller.spec.ts b/src/api/public/me/me.controller.spec.ts index 7ea008b21..9831c6ef0 100644 --- a/src/api/public/me/me.controller.spec.ts +++ b/src/api/public/me/me.controller.spec.ts @@ -5,6 +5,7 @@ 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 { AuthToken } from '../../../users/auth-token.entity'; @@ -35,6 +36,8 @@ describe('Me Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(Revision)) .useValue({}) + .overrideProvider(getRepositoryToken(Tag)) + .useValue({}) .compile(); controller = module.get(MeController); diff --git a/src/api/public/media/media.controller.spec.ts b/src/api/public/media/media.controller.spec.ts index c5cf4914e..330cdeba4 100644 --- a/src/api/public/media/media.controller.spec.ts +++ b/src/api/public/media/media.controller.spec.ts @@ -6,6 +6,7 @@ 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'; import { Authorship } from '../../../revisions/authorship.entity'; import { Revision } from '../../../revisions/revision.entity'; import { AuthToken } from '../../../users/auth-token.entity'; @@ -37,6 +38,8 @@ describe('Media Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(User)) .useValue({}) + .overrideProvider(getRepositoryToken(Tag)) + .useValue({}) .compile(); controller = module.get(MediaController); diff --git a/src/api/public/notes/notes.controller.spec.ts b/src/api/public/notes/notes.controller.spec.ts index 6cbf98960..806f2f40f 100644 --- a/src/api/public/notes/notes.controller.spec.ts +++ b/src/api/public/notes/notes.controller.spec.ts @@ -4,6 +4,7 @@ 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'; import { Authorship } from '../../../revisions/authorship.entity'; import { Revision } from '../../../revisions/revision.entity'; import { RevisionsModule } from '../../../revisions/revisions.module'; @@ -25,6 +26,10 @@ describe('Notes Controller', () => { provide: getRepositoryToken(Note), useValue: {}, }, + { + provide: getRepositoryToken(Tag), + useValue: {}, + }, ], imports: [RevisionsModule, UsersModule, LoggerModule], }) @@ -44,6 +49,8 @@ describe('Notes Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(Note)) .useValue({}) + .overrideProvider(getRepositoryToken(Tag)) + .useValue({}) .compile(); controller = module.get(NotesController); diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index fc3c7690e..9000d63cb 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -9,6 +9,7 @@ import { Put, } from '@nestjs/common'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; +import { NoteMetadataUpdateDto } from '../../../notes/note-metadata.dto'; import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto'; import { NotesService } from '../../../notes/notes.service'; import { RevisionsService } from '../../../revisions/revisions.service'; @@ -21,7 +22,7 @@ export class NotesController { private noteService: NotesService, private revisionsService: RevisionsService, ) { - this.logger.setContext(NotesController.name); + this.logger.setContext(NotesController.name); } @Post() diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts index 19369f72d..2e7d44977 100644 --- a/src/media/media.service.spec.ts +++ b/src/media/media.service.spec.ts @@ -4,6 +4,7 @@ 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 { AuthToken } from '../users/auth-token.entity'; @@ -43,6 +44,8 @@ describe('MediaService', () => { .useValue({}) .overrideProvider(getRepositoryToken(User)) .useValue({}) + .overrideProvider(getRepositoryToken(Tag)) + .useValue({}) .compile(); service = module.get(MediaService); diff --git a/src/notes/note-metadata.dto.ts b/src/notes/note-metadata.dto.ts index 2b26a37bb..fea43be93 100644 --- a/src/notes/note-metadata.dto.ts +++ b/src/notes/note-metadata.dto.ts @@ -34,3 +34,13 @@ export class NoteMetadataDto { @ValidateNested() permissions: NotePermissionsDto; } + +export class NoteMetadataUpdateDto { + @IsString() + title: string; + @IsString() + description: string; + @IsArray() + @IsString({ each: true }) + tags: string[]; +} diff --git a/src/notes/note.entity.ts b/src/notes/note.entity.ts index 815e7c643..817c857f9 100644 --- a/src/notes/note.entity.ts +++ b/src/notes/note.entity.ts @@ -2,6 +2,8 @@ import { generate as shortIdGenerate } from 'shortid'; import { Column, Entity, + JoinTable, + ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, @@ -11,6 +13,7 @@ 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'; @Entity('Notes') export class Note { @@ -25,7 +28,7 @@ export class Note { unique: true, nullable: true, }) - alias: string; + alias?: string; @OneToMany( _ => NoteGroupPermission, groupPermission => groupPermission.note, @@ -59,10 +62,27 @@ export class Note { ) authorColors: AuthorColor[]; + @Column({ + nullable: true, + }) + description?: string; + @Column({ + nullable: true, + }) + title?: string; + + @ManyToMany( + _ => Tag, + tag => tag.notes, + { eager: true, cascade: true }, + ) + @JoinTable() + tags: Tag[]; + // eslint-disable-next-line @typescript-eslint/no-empty-function private constructor() {} - public static create(owner?: User, alias?: string, shortid?: string) { + public static create(owner?: User, alias?: string, shortid?: string): Note { if (!shortid) { shortid = shortIdGenerate(); } @@ -74,6 +94,10 @@ export class Note { newNote.authorColors = []; newNote.userPermissions = []; newNote.groupPermissions = []; + newNote.revisions = Promise.resolve([]); + newNote.description = null; + newNote.title = null; + newNote.tags = []; return newNote; } } diff --git a/src/notes/note.utils.ts b/src/notes/note.utils.ts deleted file mode 100644 index d84c4b182..000000000 --- a/src/notes/note.utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Note } from './note.entity'; - -export class NoteUtils { - public static parseTitle(note: Note): string { - // TODO: Implement method - return 'Hardcoded note title'; - } - - public static parseDescription(note: Note): string { - // TODO: Implement method - return 'Hardcoded note description'; - } - - public static parseTags(note: Note): string[] { - // TODO: Implement method - return ['Hardcoded note tag']; - } -} diff --git a/src/notes/notes.module.ts b/src/notes/notes.module.ts index f5a93a721..b35c300d3 100644 --- a/src/notes/notes.module.ts +++ b/src/notes/notes.module.ts @@ -6,10 +6,11 @@ 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'; @Module({ imports: [ - TypeOrmModule.forFeature([Note, AuthorColor]), + TypeOrmModule.forFeature([Note, AuthorColor, Tag]), forwardRef(() => RevisionsModule), UsersModule, LoggerModule, diff --git a/src/notes/notes.service.spec.ts b/src/notes/notes.service.spec.ts index 4d18ca55f..a3383572d 100644 --- a/src/notes/notes.service.spec.ts +++ b/src/notes/notes.service.spec.ts @@ -11,6 +11,7 @@ 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'; describe('NotesService', () => { let service: NotesService; @@ -23,6 +24,10 @@ describe('NotesService', () => { provide: getRepositoryToken(Note), useValue: {}, }, + { + provide: getRepositoryToken(Tag), + useValue: {}, + }, ], imports: [UsersModule, RevisionsModule, LoggerModule], }) @@ -40,6 +45,8 @@ describe('NotesService', () => { .useValue({}) .overrideProvider(getRepositoryToken(Note)) .useValue({}) + .overrideProvider(getRepositoryToken(Tag)) + .useValue({}) .compile(); service = module.get(NotesService); }); diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index 8290bc04e..c077f7d35 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -7,20 +7,21 @@ import { Revision } from '../revisions/revision.entity'; import { RevisionsService } from '../revisions/revisions.service'; import { User } from '../users/user.entity'; import { UsersService } from '../users/users.service'; -import { NoteMetadataDto } from './note-metadata.dto'; +import { NoteMetadataDto, NoteMetadataUpdateDto } from './note-metadata.dto'; import { NotePermissionsDto, NotePermissionsUpdateDto, } from './note-permissions.dto'; import { NoteDto } from './note.dto'; import { Note } from './note.entity'; -import { NoteUtils } from './note.utils'; +import { Tag } from './tag.entity'; @Injectable() export class NotesService { constructor( private readonly logger: ConsoleLoggerService, @InjectRepository(Note) private noteRepository: Repository, + @InjectRepository(Tag) private tagRepository: Repository, @Inject(UsersService) private usersService: UsersService, @Inject(forwardRef(() => RevisionsService)) private revisionsService: RevisionsService, @@ -102,10 +103,10 @@ export class NotesService { // TODO: Convert DB UUID to base64 id: note.id, alias: note.alias, - title: NoteUtils.parseTitle(note), + title: note.title, // TODO: Get actual createTime createTime: new Date(), - description: NoteUtils.parseDescription(note), + description: note.description, editedBy: note.authorColors.map(authorColor => authorColor.user.userName), // TODO: Extract into method permissions: { @@ -119,7 +120,7 @@ export class NotesService { canEdit: noteGroupPermission.canEdit, })), }, - tags: NoteUtils.parseTags(note), + tags: note.tags.map(tag => tag.name), updateTime: (await this.getLastRevision(note)).createdAt, // TODO: Get actual updateUser updateUser: { diff --git a/src/notes/tag.entity.ts b/src/notes/tag.entity.ts new file mode 100644 index 000000000..fb3a1ad98 --- /dev/null +++ b/src/notes/tag.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { Note } from './note.entity'; + +@Entity() +export class Tag { + @PrimaryGeneratedColumn() + id: number; + + @Column({ + nullable: false, + }) + name: string; + + @ManyToMany( + _ => Note, + note => note.tags, + ) + notes: Note[]; +} diff --git a/src/revisions/revisions.service.spec.ts b/src/revisions/revisions.service.spec.ts index 04e929973..4eb976257 100644 --- a/src/revisions/revisions.service.spec.ts +++ b/src/revisions/revisions.service.spec.ts @@ -10,6 +10,7 @@ import { User } from '../users/user.entity'; import { Authorship } from './authorship.entity'; import { Revision } from './revision.entity'; import { RevisionsService } from './revisions.service'; +import { Tag } from '../notes/tag.entity'; describe('RevisionsService', () => { let service: RevisionsService; @@ -39,6 +40,8 @@ describe('RevisionsService', () => { .useValue({}) .overrideProvider(getRepositoryToken(Revision)) .useValue({}) + .overrideProvider(getRepositoryToken(Tag)) + .useValue({}) .compile(); service = module.get(RevisionsService); diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index af0b5ce7a..cb58a1ded 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -76,6 +76,7 @@ export class RevisionsService { createRevision(content: string) { // TODO: Add previous revision // TODO: Calculate patch + // TODO: Save metadata return this.revisionRepository.create({ content: content, length: content.length, diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index ce2190947..1941dbd4b 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -98,20 +98,28 @@ describe('Notes', () => { ).toEqual('New note text'); }); - it.skip(`PUT /notes/{note}/metadata`, () => { - // TODO - return request(app.getHttpServer()) - .post('/notes/test5/metadata') - .set('Content-Type', 'text/markdown') - .expect(200); - }); - - it.skip(`GET /notes/{note}/metadata`, () => { - notesService.createNote('This is a test note.', 'test6'); - return request(app.getHttpServer()) + it(`GET /notes/{note}/metadata`, async () => { + await notesService.createNote('This is a test note.', 'test6'); + const metadata = await request(app.getHttpServer()) .get('/notes/test6/metadata') .expect(200); - // TODO: Find out how to check the structure of the returned JSON + expect(typeof metadata.body.id).toEqual('string'); + expect(metadata.body.alias).toEqual('test6'); + expect(metadata.body.title).toBeNull(); + expect(metadata.body.description).toBeNull(); + expect(typeof metadata.body.createTime).toEqual('string'); + expect(metadata.body.editedBy).toEqual([]); + expect(metadata.body.permissions.owner).toBeNull(); + expect(metadata.body.permissions.sharedToUsers).toEqual([]); + expect(metadata.body.permissions.sharedToUsers).toEqual([]); + expect(metadata.body.tags).toEqual([]); + expect(typeof metadata.body.updateTime).toEqual('string'); + expect(typeof metadata.body.updateUser.displayName).toEqual('string'); + expect(typeof metadata.body.updateUser.userName).toEqual('string'); + expect(typeof metadata.body.updateUser.email).toEqual('string'); + expect(typeof metadata.body.updateUser.photo).toEqual('string'); + expect(typeof metadata.body.viewCount).toEqual('number'); + expect(metadata.body.editedBy).toEqual([]); }); it(`GET /notes/{note}/revisions`, async () => {