From b4b91acddb25300b41411025888ec2be02f37f71 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 14:51:12 +0200 Subject: [PATCH 01/40] NotesController: Use custom logic to access raw markdown NestJS does not support content-types other than application/json. Therefore we need to directly access the request object to get the raw body content. Signed-off-by: David Mehren --- package.json | 1 + src/api/public/notes/notes.controller.ts | 27 ++++++++++++++++++++++-- yarn.lock | 12 ++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 41bb7c760..8f7006537 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "class-transformer": "^0.2.3", "class-validator": "^0.12.2", "connect-typeorm": "^1.1.4", + "raw-body": "^2.4.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.5.4", diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 7d64834ff..33d23bfd3 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -1,27 +1,50 @@ import { + BadRequestException, Body, Controller, Delete, Get, Header, + Logger, Param, Post, Put, + Req, } from '@nestjs/common'; +import { Request } from 'express'; +import * as getRawBody from 'raw-body'; import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto'; import { NotesService } from '../../../notes/notes.service'; import { RevisionsService } from '../../../revisions/revisions.service'; @Controller('notes') export class NotesController { + private readonly logger = new Logger(NotesController.name); + constructor( private noteService: NotesService, private revisionsService: RevisionsService, ) {} + /** + * Extract the raw markdown from the request body and create a new note with it + * + * Implementation inspired by https://stackoverflow.com/questions/52283713/how-do-i-pass-plain-text-as-my-request-body-using-nestjs + */ @Post() - createNote(@Body() noteContent: string) { - return this.noteService.createNote(noteContent); + @Header('content-type', 'text/markdown') + async createNote(@Req() req: Request) { + // we have to check req.readable because of raw-body issue #57 + // https://github.com/stream-utils/raw-body/issues/57 + if (req.readable) { + let bodyText: string = await getRawBody(req, 'utf-8'); + bodyText = bodyText.trim(); + this.logger.debug('Got raw markdown:\n' + bodyText); + return this.noteService.createNote(bodyText); + } else { + // TODO: Better error message + throw new BadRequestException('Invalid body'); + } } @Get(':noteIdOrAlias') diff --git a/yarn.lock b/yarn.lock index 1dd3417ae..bfe54b370 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3622,7 +3622,7 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@~1.7.2: +http-errors@1.7.3, http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== @@ -5944,6 +5944,16 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" + integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== + dependencies: + bytes "3.1.0" + http-errors "1.7.3" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" From b17da345c719cdf01d5e7a467802de2f0c8bb7eb Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 14:54:08 +0200 Subject: [PATCH 02/40] NoteDto: Rename attribute `metdata` to `metadata` Signed-off-by: David Mehren --- src/notes/note.dto.ts | 2 +- src/notes/notes.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notes/note.dto.ts b/src/notes/note.dto.ts index 40b209644..58917606d 100644 --- a/src/notes/note.dto.ts +++ b/src/notes/note.dto.ts @@ -7,7 +7,7 @@ export class NoteDto { content: string; @ValidateNested() - metdata: NoteMetadataDto; + metadata: NoteMetadataDto; @IsArray() @ValidateNested({ each: true }) diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index aebd48c5e..bc57472d5 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -82,7 +82,7 @@ export class NotesService { this.logger.warn('Using hardcoded data!'); return { content: 'noteContent', - metdata: { + metadata: { alias: null, createTime: new Date(), description: 'Very descriptive text.', From 99f44f255113e4932eb2c158784a946164b25710 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 15:48:00 +0200 Subject: [PATCH 03/40] Reverse cardinality of owner relationship Signed-off-by: David Mehren --- docs/dev/db-schema.plantuml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/db-schema.plantuml b/docs/dev/db-schema.plantuml index 130bcb63f..9527984f5 100644 --- a/docs/dev/db-schema.plantuml +++ b/docs/dev/db-schema.plantuml @@ -115,11 +115,11 @@ entity "Group" { *canEdit : boolean } -Note "1" -- "1..*" Revision -Revision "0..*" -- "0..*" Authorship +Note "1" - "1..*" Revision +Revision "0..*" - "0..*" Authorship (Revision, Authorship) .. RevisionAuthorship Authorship "0..*" -- "1" User -Note "1" -- "0..*" User : owner +Note "0..*" -- "1" User : owner Note "1" -- "0..*" NoteUserPermission NoteUserPermission "1" -- "1" User Note "1" -- "0..*" NoteGroupPermission From 2c3a75187e8567c778f14efbab120a44a206da25 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 15:50:58 +0200 Subject: [PATCH 04/40] NoteController: Do not use text/markdown as response content-type for createNote Signed-off-by: David Mehren --- src/api/public/notes/notes.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 33d23bfd3..8405b1007 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -32,7 +32,6 @@ export class NotesController { * Implementation inspired by https://stackoverflow.com/questions/52283713/how-do-i-pass-plain-text-as-my-request-body-using-nestjs */ @Post() - @Header('content-type', 'text/markdown') async createNote(@Req() req: Request) { // we have to check req.readable because of raw-body issue #57 // https://github.com/stream-utils/raw-body/issues/57 From 4ff60b162e63ecc6341f8d0fe97de5b762f349c3 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 15:57:34 +0200 Subject: [PATCH 05/40] NoteEntity: Enable CASCADE for revision column This makes creating new Notes easier, as the first Revision is automatically created in the database. Signed-off-by: David Mehren --- src/notes/note.entity.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/notes/note.entity.ts b/src/notes/note.entity.ts index 151379dde..ef8936bb5 100644 --- a/src/notes/note.entity.ts +++ b/src/notes/note.entity.ts @@ -57,6 +57,7 @@ export class Note { @OneToMany( _ => Revision, revision => revision.note, + { cascade: true }, ) revisions: Revision[]; From 8c050b3c2fc3e1459251c45823661fccf20da9d6 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 15:57:57 +0200 Subject: [PATCH 06/40] NoteEntity: Formatting fixes Signed-off-by: David Mehren --- src/notes/note.entity.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/notes/note.entity.ts b/src/notes/note.entity.ts index ef8936bb5..670460797 100644 --- a/src/notes/note.entity.ts +++ b/src/notes/note.entity.ts @@ -16,51 +16,43 @@ import { AuthorColor } from './author-color.entity'; export class Note { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ nullable: false, unique: true, }) shortid: string; - @Column({ unique: true, nullable: true, }) alias: string; - @OneToMany( _ => NoteGroupPermission, groupPermission => groupPermission.note, ) groupPermissions: NoteGroupPermission[]; - @OneToMany( _ => NoteUserPermission, userPermission => userPermission.note, ) userPermissions: NoteUserPermission[]; - @Column({ nullable: false, default: 0, }) viewcount: number; - @ManyToOne( _ => User, user => user.ownedNotes, { onDelete: 'CASCADE' }, ) owner: User; - @OneToMany( _ => Revision, revision => revision.note, { cascade: true }, ) revisions: Revision[]; - @OneToMany( _ => AuthorColor, authorColor => authorColor.note, From 39410ab9c88a442da893b22ce279b0252d7287a2 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 16:00:29 +0200 Subject: [PATCH 07/40] NoteEntity: Move constructor-code to create() method TypeORM does not like having application code in the constructor (https://github.com/typeorm/typeorm/issues/1772#issuecomment-514787854), therefore that is moved into a new `create() static method. Additionally, the constructor is now `private`, which enforces the use of the new method. Signed-off-by: David Mehren --- src/notes/note.entity.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/notes/note.entity.ts b/src/notes/note.entity.ts index 670460797..70e828738 100644 --- a/src/notes/note.entity.ts +++ b/src/notes/note.entity.ts @@ -59,14 +59,18 @@ export class Note { ) authorColors: AuthorColor[]; - constructor(shortid: string, alias: string, owner: User) { - if (shortid) { - this.shortid = shortid; - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - this.shortid = shortIdGenerate() as string; + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + public static create(owner?: User, alias?: string, shortid?: string) { + if (!shortid) { + shortid = shortIdGenerate(); } - this.alias = alias; - this.owner = owner; + const newNote = new Note(); + newNote.shortid = shortid; + newNote.alias = alias; + newNote.viewcount = 0; + newNote.owner = owner; + return newNote; } } From 2ab90917bc888146380b439fcf96965e2908cf99 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 16:01:32 +0200 Subject: [PATCH 08/40] NotesService: `createNote()` now saves new notes to the database Signed-off-by: David Mehren --- src/notes/notes.service.ts | 78 ++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index bc57472d5..daa4f6280 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -1,15 +1,24 @@ import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Revision } from '../revisions/revision.entity'; +import { User } from '../users/user.entity'; import { NoteMetadataDto } from './note-metadata.dto'; import { NotePermissionsDto, NotePermissionsUpdateDto, } from './note-permissions.dto'; import { NoteDto } from './note.dto'; +import { Note } from './note.entity'; @Injectable() export class NotesService { private readonly logger = new Logger(NotesService.name); + constructor( + @InjectRepository(Note) private noteRepository: Repository, + ) {} + getUserNotes(username: string): NoteMetadataDto[] { this.logger.warn('Using hardcoded data!'); return [ @@ -43,38 +52,59 @@ export class NotesService { ]; } - createNote(noteContent: string, alias?: NoteMetadataDto['alias']): NoteDto { + async createNote( + noteContent: string, + alias?: NoteMetadataDto['alias'], + owner?: User, + ): Promise { this.logger.warn('Using hardcoded data!'); + const newNote = Note.create(); + newNote.revisions = [Revision.create(noteContent, noteContent)]; + if (alias) { + newNote.alias = alias; + } + if (owner) { + newNote.owner = owner; + } + const savedNote = await this.noteRepository.save(newNote); return { - content: noteContent, - metdata: { - alias: alias, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], - id: 'foobar-barfoo', - permission: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], - }, - tags: [], - title: 'Title!', - updateTime: new Date(), - updateUser: { + content: this.getCurrentContent(savedNote), + metadata: this.getMetadata(savedNote), + editedByAtPosition: [], + }; + } + + getCurrentContent(note: Note) { + return note.revisions[note.revisions.length - 1].content; + } + + getMetadata(note: Note) { + return { + alias: note.alias, + createTime: new Date(), + description: 'Very descriptive text.', + editedBy: [], + id: note.id, + permission: { + owner: { displayName: 'foo', userName: 'fooUser', email: 'foo@example.com', photo: '', }, - viewCount: 42, + sharedToUsers: [], + sharedToGroups: [], }, - editedByAtPosition: [], + tags: [], + title: 'Title!', + updateTime: new Date(), + updateUser: { + displayName: 'foo', + userName: 'fooUser', + email: 'foo@example.com', + photo: '', + }, + viewCount: 42, }; } From 5d07481387ff4ef7932cffa2ef8f19127a402efe Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 16:04:30 +0200 Subject: [PATCH 09/40] RevisionEntity: Add `create()` method Signed-off-by: David Mehren --- src/revisions/revision.entity.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/revisions/revision.entity.ts b/src/revisions/revision.entity.ts index 4427454e2..2c15515af 100644 --- a/src/revisions/revision.entity.ts +++ b/src/revisions/revision.entity.ts @@ -65,4 +65,15 @@ export class Revision { ) @JoinTable() authorships: Authorship[]; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + static create(content: string, patch: string): Revision { + const newRevision = new Revision(); + newRevision.patch = patch; + newRevision.content = content; + newRevision.length = content.length; + return newRevision; + } } From f5e043b8b1e434c4683b291aea9eb57cc2e1b945 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 17:30:58 +0200 Subject: [PATCH 10/40] NoteEntity: Always initialize arrays The `create()` function did not initialize all arrays, which caused them to be `undefined` instead of empty. Signed-off-by: David Mehren --- src/notes/note.entity.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/notes/note.entity.ts b/src/notes/note.entity.ts index 70e828738..b89539e17 100644 --- a/src/notes/note.entity.ts +++ b/src/notes/note.entity.ts @@ -71,6 +71,9 @@ export class Note { newNote.alias = alias; newNote.viewcount = 0; newNote.owner = owner; + newNote.authorColors = []; + newNote.userPermissions = []; + newNote.groupPermissions = []; return newNote; } } From f9370424392caa46d31848b50f670f2ae0928a96 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 17:32:08 +0200 Subject: [PATCH 11/40] NoteUtils: Add methods to parse note metadata These methods are intended to parse metadata details from YAML tags, but not implemented for now. Signed-off-by: David Mehren --- src/notes/note.utils.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/notes/note.utils.ts diff --git a/src/notes/note.utils.ts b/src/notes/note.utils.ts new file mode 100644 index 000000000..d84c4b182 --- /dev/null +++ b/src/notes/note.utils.ts @@ -0,0 +1,18 @@ +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']; + } +} From e1e0e4543498b12b320dada8bc3fc22c2cbb6a59 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 17:33:29 +0200 Subject: [PATCH 12/40] UsersService: Add `toUserDto()` converter This conversion function makes sure that a photo URL exists. Signed-off-by: David Mehren --- src/users/users.service.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/users/users.service.ts b/src/users/users.service.ts index c558775ac..9e8cb79fc 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { UserInfoDto } from './user-info.dto'; +import { User } from './user.entity'; @Injectable() export class UsersService { @@ -15,4 +16,26 @@ export class UsersService { photo: '', }; } + + getPhotoUrl(user: User) { + if (user.photo) { + return user.photo; + } else { + // TODO: Create new photo, see old code + return ''; + } + } + + toUserDto(user: User): UserInfoDto { + if (user === undefined) { + this.logger.warn('toUserDto recieved undefined argument!'); + return null; + } + return { + userName: user.userName, + displayName: user.displayName, + photo: this.getPhotoUrl(user), + email: user.email, + }; + } } From 6805c2a41e1ce26e0498727f4904e806b5417a52 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 19 Sep 2020 17:40:50 +0200 Subject: [PATCH 13/40] NotesService: Get more note metadata from the database Some previously hardcoded metadata-values are now retrieved from the database. Signed-off-by: David Mehren --- src/notes/notes.module.ts | 8 +++++- src/notes/notes.service.ts | 52 +++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/notes/notes.module.ts b/src/notes/notes.module.ts index 1054935d3..a06575138 100644 --- a/src/notes/notes.module.ts +++ b/src/notes/notes.module.ts @@ -1,11 +1,17 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { RevisionsModule } from '../revisions/revisions.module'; +import { UsersModule } from '../users/users.module'; import { AuthorColor } from './author-color.entity'; import { Note } from './note.entity'; import { NotesService } from './notes.service'; @Module({ - imports: [TypeOrmModule.forFeature([Note, AuthorColor])], + imports: [ + TypeOrmModule.forFeature([Note, AuthorColor]), + RevisionsModule, + UsersModule, + ], controllers: [], providers: [NotesService], exports: [NotesService], diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index daa4f6280..ab283137c 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -1,8 +1,9 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Revision } from '../revisions/revision.entity'; import { User } from '../users/user.entity'; +import { UsersService } from '../users/users.service'; import { NoteMetadataDto } from './note-metadata.dto'; import { NotePermissionsDto, @@ -10,6 +11,7 @@ import { } from './note-permissions.dto'; import { NoteDto } from './note.dto'; import { Note } from './note.entity'; +import { NoteUtils } from './note.utils'; @Injectable() export class NotesService { @@ -17,6 +19,7 @@ export class NotesService { constructor( @InjectRepository(Note) private noteRepository: Repository, + @Inject(UsersService) private usersService: UsersService, ) {} getUserNotes(username: string): NoteMetadataDto[] { @@ -75,32 +78,41 @@ export class NotesService { } getCurrentContent(note: Note) { - return note.revisions[note.revisions.length - 1].content; + return this.getLastRevision(note).content; } - getMetadata(note: Note) { + getLastRevision(note: Note) { + return note.revisions[note.revisions.length - 1]; + } + + getMetadata(note: Note): NoteMetadataDto { return { - alias: note.alias, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], + // TODO: Convert DB UUID to base64 id: note.id, + alias: note.alias, + title: NoteUtils.parseTitle(note), + // TODO: Get actual createTime + createTime: new Date(), + description: NoteUtils.parseDescription(note), + editedBy: note.authorColors.map(authorColor => authorColor.user.userName), + // TODO: Extract into method permission: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], + owner: this.usersService.toUserDto(note.owner), + sharedToUsers: note.userPermissions.map(noteUserPermission => ({ + user: this.usersService.toUserDto(noteUserPermission.user), + canEdit: noteUserPermission.canEdit, + })), + sharedToGroups: note.groupPermissions.map(noteGroupPermission => ({ + group: noteGroupPermission.group, + canEdit: noteGroupPermission.canEdit, + })), }, - tags: [], - title: 'Title!', - updateTime: new Date(), + tags: NoteUtils.parseTags(note), + updateTime: this.getLastRevision(note).createdAt, + // TODO: Get actual updateUser updateUser: { - displayName: 'foo', - userName: 'fooUser', + displayName: 'Hardcoded User', + userName: 'hardcoded', email: 'foo@example.com', photo: '', }, From fae8c679a9de3210e175ef8cea9d96bfa3669ec5 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 17:28:57 +0200 Subject: [PATCH 14/40] UsersService: Add `null` check to `toUserDto()` converter Signed-off-by: David Mehren --- src/users/users.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 9e8cb79fc..ccbcea032 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -31,6 +31,10 @@ export class UsersService { this.logger.warn('toUserDto recieved undefined argument!'); return null; } + if (user === null) { + this.logger.warn('toUserDto recieved null argument!'); + return null; + } return { userName: user.userName, displayName: user.displayName, From 74b03fc1fd5d4b6c5b4cc48308223ca2c7c3dee6 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 17:29:59 +0200 Subject: [PATCH 15/40] RevisionsService: Implement `getLatestRevision` and `createRevision` methods Signed-off-by: David Mehren --- src/revisions/revisions.service.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index f93e17eb2..dad768872 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -1,9 +1,16 @@ import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { RevisionMetadataDto } from './revision-metadata.dto'; import { RevisionDto } from './revision.dto'; +import { Revision } from './revision.entity'; @Injectable() export class RevisionsService { + constructor( + @InjectRepository(Revision) + private revisionRepository: Repository, + ) {} private readonly logger = new Logger(RevisionsService.name); getNoteRevisionMetadatas(noteIdOrAlias: string): RevisionMetadataDto[] { this.logger.warn('Using hardcoded data!'); @@ -24,4 +31,25 @@ export class RevisionsService { patch: 'barfoo', }; } + + getLatestRevision(noteId: string): Promise { + return this.revisionRepository.findOne({ + where: { + note: noteId, + }, + order: { + createdAt: 'DESC', + }, + }); + } + + createRevision(content: string) { + // TODO: Add previous revision + // TODO: Calculate patch + return this.revisionRepository.create({ + content: content, + length: content.length, + patch: '', + }); + } } From e97f9fe17438acd73433e139618e064a45231296 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 17:32:35 +0200 Subject: [PATCH 16/40] NoteEntity: Lazy-load `revisions` relation Using a `Promise` type in a TypeORM entity automatically enables lazy-loading of that relation. See https://typeorm.io/#/eager-and-lazy-relations/lazy-relations Signed-off-by: David Mehren --- src/notes/note.entity.ts | 2 +- src/notes/notes.service.ts | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/notes/note.entity.ts b/src/notes/note.entity.ts index b89539e17..815e7c643 100644 --- a/src/notes/note.entity.ts +++ b/src/notes/note.entity.ts @@ -52,7 +52,7 @@ export class Note { revision => revision.note, { cascade: true }, ) - revisions: Revision[]; + revisions: Promise; @OneToMany( _ => AuthorColor, authorColor => authorColor.note, diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index ab283137c..f3d08f836 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -60,9 +60,10 @@ export class NotesService { alias?: NoteMetadataDto['alias'], owner?: User, ): Promise { - this.logger.warn('Using hardcoded data!'); const newNote = Note.create(); - newNote.revisions = [Revision.create(noteContent, noteContent)]; + newNote.revisions = Promise.resolve([ + Revision.create(noteContent, noteContent), + ]); if (alias) { newNote.alias = alias; } @@ -71,21 +72,21 @@ export class NotesService { } const savedNote = await this.noteRepository.save(newNote); return { - content: this.getCurrentContent(savedNote), - metadata: this.getMetadata(savedNote), + content: await this.getCurrentContent(savedNote), + metadata: await this.getMetadata(savedNote), editedByAtPosition: [], }; } - getCurrentContent(note: Note) { - return this.getLastRevision(note).content; + async getCurrentContent(note: Note) { + return (await this.getLastRevision(note)).content; } - getLastRevision(note: Note) { - return note.revisions[note.revisions.length - 1]; + async getLastRevision(note: Note): Promise { + return this.revisionsService.getLatestRevision(note.id); } - getMetadata(note: Note): NoteMetadataDto { + async getMetadata(note: Note): Promise { return { // TODO: Convert DB UUID to base64 id: note.id, @@ -108,7 +109,7 @@ export class NotesService { })), }, tags: NoteUtils.parseTags(note), - updateTime: this.getLastRevision(note).createdAt, + updateTime: (await this.getLastRevision(note)).createdAt, // TODO: Get actual updateUser updateUser: { displayName: 'Hardcoded User', From a2a9ad224f5bc588e80400342cd8f93c470dc317 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 17:34:33 +0200 Subject: [PATCH 17/40] NotesService: Find note by ID or alias in database This commit also introduces the `getNoteDtoByIdOrAlias` method, that converts a `Note` entity to a `NoteDto` Signed-off-by: David Mehren --- src/api/public/notes/notes.controller.ts | 2 +- src/notes/notes.service.ts | 63 +++++++++++------------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 8405b1007..1df997ab4 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -48,7 +48,7 @@ export class NotesController { @Get(':noteIdOrAlias') getNote(@Param('noteIdOrAlias') noteIdOrAlias: string) { - return this.noteService.getNoteByIdOrAlias(noteIdOrAlias); + return this.noteService.getNoteDtoByIdOrAlias(noteIdOrAlias); } @Post(':noteAlias') diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index f3d08f836..11d4f1394 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; 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'; @@ -20,6 +21,7 @@ export class NotesService { constructor( @InjectRepository(Note) private noteRepository: Repository, @Inject(UsersService) private usersService: UsersService, + @Inject(RevisionsService) private revisionsService: RevisionsService, ) {} getUserNotes(username: string): NoteMetadataDto[] { @@ -121,39 +123,26 @@ export class NotesService { }; } - getNoteByIdOrAlias(noteIdOrAlias: string) { - this.logger.warn('Using hardcoded data!'); - return { - content: 'noteContent', - metadata: { - alias: null, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], - id: noteIdOrAlias, - permission: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], - }, - tags: [], - title: 'Title!', - updateTime: new Date(), - updateUser: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - viewCount: 42, - }, - editedByAtPosition: [], - }; + async getNoteByIdOrAlias(noteIdOrAlias: string): Promise { + const note = await this.noteRepository.findOne({ + where: [{ id: noteIdOrAlias }, { alias: noteIdOrAlias }], + relations: [ + 'authorColors', + 'owner', + 'groupPermissions', + 'userPermissions', + ], + }); + if (note === undefined) { + //TODO: Improve error handling + throw new Error('Note not found'); + } + return note; + } + + async getNoteDtoByIdOrAlias(noteIdOrAlias: string): Promise { + const note = await this.getNoteByIdOrAlias(noteIdOrAlias); + return this.toNoteDto(note); } deleteNoteByIdOrAlias(noteIdOrAlias: string) { @@ -248,4 +237,12 @@ export class NotesService { this.logger.warn('Using hardcoded data!'); return '# Markdown'; } + + async toNoteDto(note: Note): Promise { + return { + content: await this.getCurrentContent(note), + metadata: await this.getMetadata(note), + editedByAtPosition: [], + }; + } } From eb4278dd737f89663c6476ea48fc0dfc55891488 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 17:53:17 +0200 Subject: [PATCH 18/40] Update Note E2E tests to use new `getNoteDtoByIdOrAlias` method Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index 3446c0f44..1bc920410 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -27,7 +27,8 @@ describe('Notes', () => { .expect(201); expect(response.body.metadata?.id).toBeDefined(); expect( - notesService.getNoteByIdOrAlias(response.body.metadata.id).content, + (await notesService.getNoteDtoByIdOrAlias(response.body.metadata.id)) + .content, ).toEqual(newNote); }); @@ -49,7 +50,8 @@ describe('Notes', () => { .expect(201); expect(response.body.metadata?.id).toBeDefined(); return expect( - notesService.getNoteByIdOrAlias(response.body.metadata.id).content, + (await notesService.getNoteDtoByIdOrAlias(response.body.metadata.id)) + .content, ).toEqual(newNote); }); @@ -67,9 +69,9 @@ describe('Notes', () => { .put('/notes/test4') .send('New note text') .expect(200); - return expect(notesService.getNoteByIdOrAlias('test4').content).toEqual( - 'New note text', - ); + return expect( + (await notesService.getNoteDtoByIdOrAlias('test4')).content, + ).toEqual('New note text'); }); it.skip(`PUT /notes/{note}/metadata`, () => { From 3436990ac6bb1f22262cf8afe4286af6598a3e88 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 18:27:35 +0200 Subject: [PATCH 19/40] Restructure test setup in Note E2E tests to not load the whole application Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index 1bc920410..a75843863 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -1,8 +1,12 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; +import { TypeOrmModule } from '@nestjs/typeorm'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { PublicApiModule } from '../../src/api/public/public-api.module'; +import { GroupsModule } from '../../src/groups/groups.module'; +import { NotesModule } from '../../src/notes/notes.module'; import { NotesService } from '../../src/notes/notes.service'; +import { PermissionsModule } from '../../src/permissions/permissions.module'; describe('Notes', () => { let app: INestApplication; @@ -10,12 +14,23 @@ describe('Notes', () => { beforeAll(async () => { const moduleRef = await Test.createTestingModule({ - imports: [AppModule], + imports: [ + PublicApiModule, + NotesModule, + PermissionsModule, + GroupsModule, + TypeOrmModule.forRoot({ + type: 'sqlite', + database: ':memory:', + autoLoadEntities: true, + synchronize: true, + }), + ], }).compile(); app = moduleRef.createNestApplication(); - notesService = moduleRef.get(NotesService); await app.init(); + notesService = moduleRef.get(NotesService); }); it(`POST /notes`, async () => { From 9da1a88e7409143dfe8cb03aec869f3c5aac7214 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 18:30:22 +0200 Subject: [PATCH 20/40] Note E2E tests: Set a non-JSON content-type to avoid Nest trying to parse markdown to JSON. Nest automatically tries to parse incoming requests with application/json as content-type and responds with HTTP 400 if the parsing fails. As our test-note-content is not valid JSON, we need to set another content-type. Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index a75843863..fad8db385 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -37,6 +37,7 @@ describe('Notes', () => { const newNote = 'This is a test note.'; const response = await request(app.getHttpServer()) .post('/notes') + .set('Content-Type', 'text/markdown') .send(newNote) .expect('Content-Type', /json/) .expect(201); @@ -60,6 +61,7 @@ describe('Notes', () => { const newNote = 'This is a test note.'; const response = await request(app.getHttpServer()) .post('/notes/test2') + .set('Content-Type', 'text/markdown') .send(newNote) .expect('Content-Type', /json/) .expect(201); @@ -82,6 +84,7 @@ describe('Notes', () => { notesService.createNote('This is a test note.', 'test4'); await request(app.getHttpServer()) .put('/notes/test4') + .set('Content-Type', 'text/markdown') .send('New note text') .expect(200); return expect( @@ -93,6 +96,7 @@ describe('Notes', () => { // TODO return request(app.getHttpServer()) .post('/notes/test5/metadata') + .set('Content-Type', 'text/markdown') .expect(200); }); From 1a22f749bebcda65bcdefc25bf9519dd178a6a96 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 18:31:20 +0200 Subject: [PATCH 21/40] NotesController: Get text from request body when creating a named note. Signed-off-by: David Mehren --- src/api/public/notes/notes.controller.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 1df997ab4..800e2e734 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -52,11 +52,21 @@ export class NotesController { } @Post(':noteAlias') - createNamedNote( + async createNamedNote( @Param('noteAlias') noteAlias: string, - @Body() noteContent: string, + @Req() req: Request, ) { - return this.noteService.createNote(noteContent, noteAlias); + // we have to check req.readable because of raw-body issue #57 + // https://github.com/stream-utils/raw-body/issues/57 + if (req.readable) { + let bodyText: string = await getRawBody(req, 'utf-8'); + bodyText = bodyText.trim(); + this.logger.debug('Got raw markdown:\n' + bodyText); + return this.noteService.createNote(bodyText, noteAlias); + } else { + // TODO: Better error message + throw new BadRequestException('Invalid body'); + } } @Delete(':noteIdOrAlias') From 99dccc056717b6af68c55affcb8761a38a813073 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 20:06:16 +0200 Subject: [PATCH 22/40] RevisionEntity: Change primary key type from UUID to number The precision of sqlites datetime() timestamp is only one second (see https://www.sqlite.org/lang_datefunc.html). Therefore we could not order revisions of one note that were created in the same second. To remedy this, the primary key was changed to a monotonically increasing number, which solves the ordering problem. Signed-off-by: David Mehren --- docs/dev/db-schema.plantuml | 4 ++-- src/revisions/revision-metadata.dto.ts | 2 +- src/revisions/revision.dto.ts | 4 ++-- src/revisions/revision.entity.ts | 4 ++-- src/revisions/revisions.service.ts | 5 +++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/dev/db-schema.plantuml b/docs/dev/db-schema.plantuml index 9527984f5..56063af26 100644 --- a/docs/dev/db-schema.plantuml +++ b/docs/dev/db-schema.plantuml @@ -58,7 +58,7 @@ entity "Session" as seesion { entity "Revision" { - *id : uuid <> + *id : number <> -- *noteId : uuid <> *content : text @@ -78,7 +78,7 @@ entity "Authorship" { } entity "RevisionAuthorship" { - *revisionId : uuid <> + *revisionId : number <> *authorshipId : uuid <> } diff --git a/src/revisions/revision-metadata.dto.ts b/src/revisions/revision-metadata.dto.ts index 897e64501..68c2cda65 100644 --- a/src/revisions/revision-metadata.dto.ts +++ b/src/revisions/revision-metadata.dto.ts @@ -2,7 +2,7 @@ import { IsDate, IsNumber, IsString } from 'class-validator'; import { Revision } from './revision.entity'; export class RevisionMetadataDto { - @IsString() + @IsNumber() id: Revision['id']; @IsDate() diff --git a/src/revisions/revision.dto.ts b/src/revisions/revision.dto.ts index c917f8990..faff5e6ba 100644 --- a/src/revisions/revision.dto.ts +++ b/src/revisions/revision.dto.ts @@ -1,8 +1,8 @@ -import { IsString } from 'class-validator'; +import { IsNumber, IsString } from 'class-validator'; import { Revision } from './revision.entity'; export class RevisionDto { - @IsString() + @IsNumber() id: Revision['id']; @IsString() content: string; diff --git a/src/revisions/revision.entity.ts b/src/revisions/revision.entity.ts index 2c15515af..4cf7a93ba 100644 --- a/src/revisions/revision.entity.ts +++ b/src/revisions/revision.entity.ts @@ -16,8 +16,8 @@ import { Authorship } from './authorship.entity'; */ @Entity() export class Revision { - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryGeneratedColumn() + id: number; /** * The patch from the previous revision to this one. diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index dad768872..167274fc4 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -16,14 +16,14 @@ export class RevisionsService { this.logger.warn('Using hardcoded data!'); return [ { - id: 'some-uuid', + id: 42, updatedAt: new Date(), length: 42, }, ]; } - getNoteRevision(noteIdOrAlias: string, revisionId: string): RevisionDto { + getNoteRevision(noteIdOrAlias: string, revisionId: number): RevisionDto { this.logger.warn('Using hardcoded data!'); return { id: revisionId, @@ -39,6 +39,7 @@ export class RevisionsService { }, order: { createdAt: 'DESC', + id: 'DESC', }, }); } From e43008c627603952bd39449a19ff8a342856833c Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 20:06:56 +0200 Subject: [PATCH 23/40] NotesController: Get text from request body when updating and deleting a note. Signed-off-by: David Mehren --- src/api/public/notes/notes.controller.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 800e2e734..7a59d4a0f 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -70,16 +70,29 @@ export class NotesController { } @Delete(':noteIdOrAlias') - deleteNote(@Param('noteIdOrAlias') noteIdOrAlias: string) { - return this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias); + async deleteNote(@Param('noteIdOrAlias') noteIdOrAlias: string) { + this.logger.debug('Deleting note: ' + noteIdOrAlias); + await this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias); + this.logger.debug('Successfully deleted ' + noteIdOrAlias); + return; } @Put(':noteIdOrAlias') - updateNote( + async updateNote( @Param('noteIdOrAlias') noteIdOrAlias: string, - @Body() noteContent: string, + @Req() req: Request, ) { - return this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, noteContent); + // we have to check req.readable because of raw-body issue #57 + // https://github.com/stream-utils/raw-body/issues/57 + if (req.readable) { + let bodyText: string = await getRawBody(req, 'utf-8'); + bodyText = bodyText.trim(); + this.logger.debug('Got raw markdown:\n' + bodyText); + return this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, bodyText); + } else { + // TODO: Better error message + throw new BadRequestException('Invalid body'); + } } @Get(':noteIdOrAlias/content') From 47bf8c9c17c58831a560dbbdbc74141d5bce9950 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 20:07:36 +0200 Subject: [PATCH 24/40] NotesService: Use the database for delete and update actions. Signed-off-by: David Mehren --- src/notes/notes.service.ts | 46 +++++++++----------------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index 11d4f1394..c3b622cdf 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -145,44 +145,18 @@ export class NotesService { return this.toNoteDto(note); } - deleteNoteByIdOrAlias(noteIdOrAlias: string) { - this.logger.warn('Using hardcoded data!'); - return; + async deleteNoteByIdOrAlias(noteIdOrAlias: string) { + const note = await this.getNoteByIdOrAlias(noteIdOrAlias); + return await this.noteRepository.remove(note); } - updateNoteByIdOrAlias(noteIdOrAlias: string, noteContent: string) { - this.logger.warn('Using hardcoded data!'); - return { - content: noteContent, - metdata: { - alias: null, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], - id: noteIdOrAlias, - permission: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], - }, - tags: [], - title: 'Title!', - updateTime: new Date(), - updateUser: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - viewCount: 42, - }, - editedByAtPosition: [], - }; + async updateNoteByIdOrAlias(noteIdOrAlias: string, noteContent: string) { + const note = await this.getNoteByIdOrAlias(noteIdOrAlias); + const revisions = await note.revisions; + //TODO: Calculate patch + revisions.push(Revision.create(noteContent, noteContent)); + note.revisions = Promise.resolve(revisions); + await this.noteRepository.save(note); } getNoteMetadata(noteIdOrAlias: string): NoteMetadataDto { From 53cea4cb1d4dec5888de8b275595c59bf8ec600e Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 20:09:36 +0200 Subject: [PATCH 25/40] Note E2E tests: Use on-disk sqlite to aid debugging It was helpful to inspect database contents while the code was stopped by the debugger. Therefore the E2E test database is now persisted on disk and cleared before every test-run. Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index fad8db385..ba978d2ba 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -21,7 +21,7 @@ describe('Notes', () => { GroupsModule, TypeOrmModule.forRoot({ type: 'sqlite', - database: ':memory:', + database: './hedgedoc-e2e.sqlite', autoLoadEntities: true, synchronize: true, }), @@ -31,6 +31,8 @@ describe('Notes', () => { app = moduleRef.createNestApplication(); await app.init(); notesService = moduleRef.get(NotesService); + const noteRepository = moduleRef.get('NoteRepository'); + noteRepository.clear(); }); it(`POST /notes`, async () => { From 7c0e069cbf66c468ac0d2ec1fca02f8736a210d1 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:04:55 +0200 Subject: [PATCH 26/40] RevisionMetadataDto: Rename attribute `updatedAt` to `createdAt` Signed-off-by: David Mehren --- src/revisions/revision-metadata.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/revisions/revision-metadata.dto.ts b/src/revisions/revision-metadata.dto.ts index 68c2cda65..213e598de 100644 --- a/src/revisions/revision-metadata.dto.ts +++ b/src/revisions/revision-metadata.dto.ts @@ -6,7 +6,7 @@ export class RevisionMetadataDto { id: Revision['id']; @IsDate() - updatedAt: Date; + createdAt: Date; @IsNumber() length: number; From 4f5bb75766515d68dfc12b26fdbc4a76da6bcc66 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:07:41 +0200 Subject: [PATCH 27/40] Public API spec: Update NoteRevisionsMetadata and timestamp definition NoteRevisionsMetadata is an array containing revision data and not an object with a single property containing an array. Revision timestamps are ISO strings, not UNIX timestamps. Signed-off-by: David Mehren --- docs/dev/public_api.yml | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/dev/public_api.yml b/docs/dev/public_api.yml index 0fc6813bc..f7314686c 100644 --- a/docs/dev/public_api.yml +++ b/docs/dev/public_api.yml @@ -368,7 +368,7 @@ paths: - note summary: Returns a list of the available note revisions operationId: getAllRevisionsOfNote - description: The list is returned as a JSON object with an array of revision-id and length associations. The revision-id equals to the timestamp when the revision was saved. + description: The list contains the revision-id, the length and a ISO-timestamp of the creation date. responses: '200': description: Revisions of the note. @@ -399,7 +399,7 @@ paths: description: The revision is returned as a JSON object with the content of the note and the authorship. responses: '200': - description: Revision of the note for the given timestamp. + description: Revision of the note for the given id. content: application/json: schema: @@ -421,7 +421,7 @@ paths: - name: revision-id in: path required: true - description: The id (timestamp) of the revision to fetch. + description: The id of the revision to fetch. content: text/plain: example: 1570921051959 @@ -579,7 +579,7 @@ components: description: A tag updateTime: type: integer - description: UNIX-timestamp of when the note was last changed. + description: ISO-timestamp of when the note was last changed. updateUser: $ref: "#/components/schemas/UserInfo" viewCount: @@ -588,7 +588,7 @@ components: description: How often the published version of the note was viewed. createTime: type: string - description: The timestamp when the note was created in ISO 8601 format. + description: The ISO-timestamp when the note was created in ISO 8601 format. editedBy: type: array description: List of usernames who edited the note. @@ -614,20 +614,19 @@ components: type: boolean NoteRevisionsMetadata: - type: object - properties: - revision: - type: array - description: Array that holds all revision-info objects. - items: - type: object - properties: - time: - type: integer - description: UNIX-timestamp of when the revision was saved. Is also the revision-id. - length: - type: integer - description: Length of the document to the timepoint the revision was saved. + type: array + items: + type: object + properties: + id: + type: integer + description: The id of the revision + createdAt: + type: integer + description: ISO-timestamp of when the revision was saved. Is also the revision-id. + length: + type: integer + description: Length of the document to the timepoint the revision was saved. NoteRevision: type: object properties: From e1079947e1d4b7c6b8f62f02dec23dcae7b437a1 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:08:14 +0200 Subject: [PATCH 28/40] NotesController: `revisionId` is a `number` Signed-off-by: David Mehren --- src/api/public/notes/notes.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 7a59d4a0f..00c6c4ee9 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -122,7 +122,7 @@ export class NotesController { @Get(':noteIdOrAlias/revisions/:revisionId') getNoteRevision( @Param('noteIdOrAlias') noteIdOrAlias: string, - @Param('revisionId') revisionId: string, + @Param('revisionId') revisionId: number, ) { return this.revisionsService.getNoteRevision(noteIdOrAlias, revisionId); } From 51aec1ea5467a83cc2f76b6b75c1d4e609a3a2ea Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:09:14 +0200 Subject: [PATCH 29/40] RevisionService: Implement `getNoteRevisionMetadatas` Signed-off-by: David Mehren --- src/notes/notes.module.ts | 4 ++-- src/notes/notes.service.ts | 5 +++-- src/revisions/revisions.module.ts | 8 ++++++-- src/revisions/revisions.service.ts | 29 ++++++++++++++++++++--------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/notes/notes.module.ts b/src/notes/notes.module.ts index a06575138..0d2470ba3 100644 --- a/src/notes/notes.module.ts +++ b/src/notes/notes.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RevisionsModule } from '../revisions/revisions.module'; import { UsersModule } from '../users/users.module'; @@ -9,7 +9,7 @@ import { NotesService } from './notes.service'; @Module({ imports: [ TypeOrmModule.forFeature([Note, AuthorColor]), - RevisionsModule, + forwardRef(() => RevisionsModule), UsersModule, ], controllers: [], diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index c3b622cdf..c82ecf471 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Revision } from '../revisions/revision.entity'; @@ -21,7 +21,8 @@ export class NotesService { constructor( @InjectRepository(Note) private noteRepository: Repository, @Inject(UsersService) private usersService: UsersService, - @Inject(RevisionsService) private revisionsService: RevisionsService, + @Inject(forwardRef(() => RevisionsService)) + private revisionsService: RevisionsService, ) {} getUserNotes(username: string): NoteMetadataDto[] { diff --git a/src/revisions/revisions.module.ts b/src/revisions/revisions.module.ts index 6b1bc365f..a959f703a 100644 --- a/src/revisions/revisions.module.ts +++ b/src/revisions/revisions.module.ts @@ -1,11 +1,15 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotesModule } from '../notes/notes.module'; import { Authorship } from './authorship.entity'; import { Revision } from './revision.entity'; import { RevisionsService } from './revisions.service'; @Module({ - imports: [TypeOrmModule.forFeature([Revision, Authorship])], + imports: [ + TypeOrmModule.forFeature([Revision, Authorship]), + forwardRef(() => NotesModule), + ], providers: [RevisionsService], exports: [RevisionsService], }) diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index 167274fc4..26beaa01e 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -1,6 +1,7 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { NotesService } from '../notes/notes.service'; import { RevisionMetadataDto } from './revision-metadata.dto'; import { RevisionDto } from './revision.dto'; import { Revision } from './revision.entity'; @@ -10,17 +11,19 @@ export class RevisionsService { constructor( @InjectRepository(Revision) private revisionRepository: Repository, + @Inject(NotesService) private notesService: NotesService, ) {} private readonly logger = new Logger(RevisionsService.name); - getNoteRevisionMetadatas(noteIdOrAlias: string): RevisionMetadataDto[] { - this.logger.warn('Using hardcoded data!'); - return [ - { - id: 42, - updatedAt: new Date(), - length: 42, + async getNoteRevisionMetadatas( + noteIdOrAlias: string, + ): Promise { + const note = await this.notesService.getNoteByIdOrAlias(noteIdOrAlias); + const revisions = await this.revisionRepository.find({ + where: { + note: note.id, }, - ]; + }); + return revisions.map(revision => this.toMetadataDto(revision)); } getNoteRevision(noteIdOrAlias: string, revisionId: number): RevisionDto { @@ -44,6 +47,14 @@ export class RevisionsService { }); } + toMetadataDto(revision: Revision): RevisionMetadataDto { + return { + id: revision.id, + length: revision.length, + createdAt: revision.createdAt, + }; + } + createRevision(content: string) { // TODO: Add previous revision // TODO: Calculate patch From a98c4fbb1bd96ec9f6be149eec960b696b318f22 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:10:09 +0200 Subject: [PATCH 30/40] Note E2E tests: Await all note-creations and fix test for note-deletion. Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index ba978d2ba..b349d56f9 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -51,7 +51,7 @@ describe('Notes', () => { }); it(`GET /notes/{note}`, async () => { - notesService.createNote('This is a test note.', 'test1'); + await notesService.createNote('This is a test note.', 'test1'); const response = await request(app.getHttpServer()) .get('/notes/test1') .expect('Content-Type', /json/) @@ -75,15 +75,17 @@ describe('Notes', () => { }); it(`DELETE /notes/{note}`, async () => { - notesService.createNote('This is a test note.', 'test3'); + await notesService.createNote('This is a test note.', 'test3'); await request(app.getHttpServer()) .delete('/notes/test3') .expect(200); - return expect(notesService.getNoteByIdOrAlias('test3')).toBeNull(); + return expect(notesService.getNoteByIdOrAlias('test3')).rejects.toEqual( + Error('Note not found'), + ); }); it(`PUT /notes/{note}`, async () => { - notesService.createNote('This is a test note.', 'test4'); + await notesService.createNote('This is a test note.', 'test4'); await request(app.getHttpServer()) .put('/notes/test4') .set('Content-Type', 'text/markdown') @@ -111,7 +113,7 @@ describe('Notes', () => { }); it(`GET /notes/{note}/revisions`, async () => { - notesService.createNote('This is a test note.', 'test7'); + await notesService.createNote('This is a test note.', 'test7'); const response = await request(app.getHttpServer()) .get('/notes/test7/revisions') .expect('Content-Type', /json/) @@ -120,7 +122,7 @@ describe('Notes', () => { }); it(`GET /notes/{note}/revisions/{revision-id}`, async () => { - notesService.createNote('This is a test note.', 'test8'); + await notesService.createNote('This is a test note.', 'test8'); const response = await request(app.getHttpServer()) .get('/notes/test8/revisions/1') .expect('Content-Type', /json/) @@ -129,7 +131,7 @@ describe('Notes', () => { }); it(`GET /notes/{note}/content`, async () => { - notesService.createNote('This is a test note.', 'test9'); + await notesService.createNote('This is a test note.', 'test9'); const response = await request(app.getHttpServer()) .get('/notes/test9/content') .expect(200); From a3ba5bccf7cb70374b3b6125dd04771c7fb7c1d6 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:11:56 +0200 Subject: [PATCH 31/40] Note E2E tests: The response for the /notes//revision route does not contain a `revisions` property Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index b349d56f9..d2185777b 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -118,7 +118,7 @@ describe('Notes', () => { .get('/notes/test7/revisions') .expect('Content-Type', /json/) .expect(200); - expect(response.body.revisions).toHaveLength(1); + expect(response.body).toHaveLength(1); }); it(`GET /notes/{note}/revisions/{revision-id}`, async () => { From 881263f2a428a68fe5224559d104a265436226c5 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:42:17 +0200 Subject: [PATCH 32/40] RevisionsService: Get note revision from database Signed-off-by: David Mehren --- src/revisions/revision.dto.ts | 4 +++- src/revisions/revisions.service.ts | 32 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/revisions/revision.dto.ts b/src/revisions/revision.dto.ts index faff5e6ba..3bebce22a 100644 --- a/src/revisions/revision.dto.ts +++ b/src/revisions/revision.dto.ts @@ -1,4 +1,4 @@ -import { IsNumber, IsString } from 'class-validator'; +import { IsDate, IsNumber, IsString } from 'class-validator'; import { Revision } from './revision.entity'; export class RevisionDto { @@ -8,4 +8,6 @@ export class RevisionDto { content: string; @IsString() patch: string; + @IsDate() + createdAt: Date; } diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index 26beaa01e..1a64a89ea 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -8,12 +8,14 @@ import { Revision } from './revision.entity'; @Injectable() export class RevisionsService { + private readonly logger = new Logger(RevisionsService.name); + constructor( @InjectRepository(Revision) private revisionRepository: Repository, @Inject(NotesService) private notesService: NotesService, ) {} - private readonly logger = new Logger(RevisionsService.name); + async getNoteRevisionMetadatas( noteIdOrAlias: string, ): Promise { @@ -26,13 +28,18 @@ export class RevisionsService { return revisions.map(revision => this.toMetadataDto(revision)); } - getNoteRevision(noteIdOrAlias: string, revisionId: number): RevisionDto { - this.logger.warn('Using hardcoded data!'); - return { - id: revisionId, - content: 'Foobar', - patch: 'barfoo', - }; + async getNoteRevision( + noteIdOrAlias: string, + revisionId: number, + ): Promise { + const note = await this.notesService.getNoteByIdOrAlias(noteIdOrAlias); + const revision = await this.revisionRepository.findOne({ + where: { + id: revisionId, + note: note, + }, + }); + return this.toDto(revision); } getLatestRevision(noteId: string): Promise { @@ -55,6 +62,15 @@ export class RevisionsService { }; } + toDto(revision: Revision): RevisionDto { + return { + id: revision.id, + content: revision.content, + createdAt: revision.createdAt, + patch: revision.patch, + }; + } + createRevision(content: string) { // TODO: Add previous revision // TODO: Calculate patch From 8b9a45b738e08c411e3422914c0c5d30aa5eb11c Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:44:53 +0200 Subject: [PATCH 33/40] NotesService: Let `createNote` create an actual `Note` and introduce `createNoteDto` to create & convert in one step. It might be handy to have access to the original `Note` after creating one, so the creation and conversion to a `NoteDto` is now split. Signed-off-by: David Mehren --- src/api/public/notes/notes.controller.ts | 4 ++-- src/notes/notes.service.ts | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 00c6c4ee9..e0f15d551 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -39,7 +39,7 @@ export class NotesController { let bodyText: string = await getRawBody(req, 'utf-8'); bodyText = bodyText.trim(); this.logger.debug('Got raw markdown:\n' + bodyText); - return this.noteService.createNote(bodyText); + return this.noteService.createNoteDto(bodyText); } else { // TODO: Better error message throw new BadRequestException('Invalid body'); @@ -62,7 +62,7 @@ export class NotesController { let bodyText: string = await getRawBody(req, 'utf-8'); bodyText = bodyText.trim(); this.logger.debug('Got raw markdown:\n' + bodyText); - return this.noteService.createNote(bodyText, noteAlias); + return this.noteService.createNoteDto(bodyText, noteAlias); } else { // TODO: Better error message throw new BadRequestException('Invalid body'); diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index c82ecf471..ec76150ec 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -58,11 +58,20 @@ export class NotesService { ]; } - async createNote( + async createNoteDto( noteContent: string, alias?: NoteMetadataDto['alias'], owner?: User, ): Promise { + const note = await this.createNote(noteContent, alias, owner); + return this.toNoteDto(note); + } + + async createNote( + noteContent: string, + alias?: NoteMetadataDto['alias'], + owner?: User, + ): Promise { const newNote = Note.create(); newNote.revisions = Promise.resolve([ Revision.create(noteContent, noteContent), @@ -73,12 +82,7 @@ export class NotesService { if (owner) { newNote.owner = owner; } - const savedNote = await this.noteRepository.save(newNote); - return { - content: await this.getCurrentContent(savedNote), - metadata: await this.getMetadata(savedNote), - editedByAtPosition: [], - }; + return this.noteRepository.save(newNote); } async getCurrentContent(note: Note) { From 6e4893d17990966b8d73aeac62d651f095dfd9cc Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:45:20 +0200 Subject: [PATCH 34/40] NotesService: Implement `getNoteContent` and `getNoteMetdata` Signed-off-by: David Mehren --- src/notes/notes.service.ts | 38 ++++++-------------------------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index ec76150ec..af7f4253a 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -164,35 +164,9 @@ export class NotesService { await this.noteRepository.save(note); } - getNoteMetadata(noteIdOrAlias: string): NoteMetadataDto { - this.logger.warn('Using hardcoded data!'); - return { - alias: null, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], - id: noteIdOrAlias, - permission: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], - }, - tags: [], - title: 'Title!', - updateTime: new Date(), - updateUser: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - viewCount: 42, - }; + async getNoteMetadata(noteIdOrAlias: string): Promise { + const note = await this.getNoteByIdOrAlias(noteIdOrAlias); + return this.getMetadata(note); } updateNotePermissions( @@ -212,9 +186,9 @@ export class NotesService { }; } - getNoteContent(noteIdOrAlias: string) { - this.logger.warn('Using hardcoded data!'); - return '# Markdown'; + async getNoteContent(noteIdOrAlias: string): Promise { + const note = await this.getNoteByIdOrAlias(noteIdOrAlias); + return this.getCurrentContent(note); } async toNoteDto(note: Note): Promise { From c5842d69e124bcff03c72133c0fd73b3030ade18 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:45:56 +0200 Subject: [PATCH 35/40] Note E2E tests: Use the correct revision-id when checking `GET /notes/{note}/revisions/{revision-id}` Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index d2185777b..fd8d2700f 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -122,9 +122,10 @@ describe('Notes', () => { }); it(`GET /notes/{note}/revisions/{revision-id}`, async () => { - await notesService.createNote('This is a test note.', 'test8'); + const note = await notesService.createNote('This is a test note.', 'test8'); + const revision = await notesService.getLastRevision(note); const response = await request(app.getHttpServer()) - .get('/notes/test8/revisions/1') + .get('/notes/test8/revisions/' + revision.id) .expect('Content-Type', /json/) .expect(200); expect(response.body.content).toEqual('This is a test note.'); From b06dc5f9679a0aa252e91e07d6c20fd1b2b6d8c0 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:46:32 +0200 Subject: [PATCH 36/40] Note E2E tests: Use `response.body` to get the note content Signed-off-by: David Mehren --- test/public-api/notes.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index fd8d2700f..a68349be1 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -136,7 +136,7 @@ describe('Notes', () => { const response = await request(app.getHttpServer()) .get('/notes/test9/content') .expect(200); - expect(response.body).toEqual('This is a test note.'); + expect(response.text).toEqual('This is a test note.'); }); afterAll(async () => { From 2ce87f3d645c5914b8e225e4b2d5f814a0836166 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Tue, 22 Sep 2020 21:59:09 +0200 Subject: [PATCH 37/40] Add various missing imports and provider overrides to fix unit tests. Signed-off-by: David Mehren --- src/api/public/me/me.controller.spec.ts | 6 +++ src/api/public/notes/notes.controller.spec.ts | 25 ++++++++++++- src/notes/notes.service.spec.ts | 37 +++++++++++++++++-- src/revisions/revisions.service.spec.ts | 35 +++++++++++++++++- 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/api/public/me/me.controller.spec.ts b/src/api/public/me/me.controller.spec.ts index 66a5b33ed..1aef2f688 100644 --- a/src/api/public/me/me.controller.spec.ts +++ b/src/api/public/me/me.controller.spec.ts @@ -4,6 +4,8 @@ import { HistoryModule } from '../../../history/history.module'; import { AuthorColor } from '../../../notes/author-color.entity'; import { Note } from '../../../notes/note.entity'; import { NotesModule } from '../../../notes/notes.module'; +import { Authorship } from '../../../revisions/authorship.entity'; +import { Revision } from '../../../revisions/revision.entity'; import { AuthToken } from '../../../users/auth-token.entity'; import { Identity } from '../../../users/identity.entity'; import { User } from '../../../users/user.entity'; @@ -28,6 +30,10 @@ describe('Me Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(AuthorColor)) .useValue({}) + .overrideProvider(getRepositoryToken(Authorship)) + .useValue({}) + .overrideProvider(getRepositoryToken(Revision)) + .useValue({}) .compile(); controller = module.get(MeController); diff --git a/src/api/public/notes/notes.controller.spec.ts b/src/api/public/notes/notes.controller.spec.ts index e6992c48d..863461266 100644 --- a/src/api/public/notes/notes.controller.spec.ts +++ b/src/api/public/notes/notes.controller.spec.ts @@ -1,10 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { AuthorColor } from '../../../notes/author-color.entity'; import { Note } from '../../../notes/note.entity'; import { NotesService } from '../../../notes/notes.service'; import { Authorship } from '../../../revisions/authorship.entity'; import { Revision } from '../../../revisions/revision.entity'; import { RevisionsModule } from '../../../revisions/revisions.module'; +import { AuthToken } from '../../../users/auth-token.entity'; +import { Identity } from '../../../users/identity.entity'; +import { User } from '../../../users/user.entity'; +import { UsersModule } from '../../../users/users.module'; import { NotesController } from './notes.controller'; describe('Notes Controller', () => { @@ -13,8 +18,14 @@ describe('Notes Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [NotesController], - providers: [NotesService], - imports: [RevisionsModule], + providers: [ + NotesService, + { + provide: getRepositoryToken(Note), + useValue: {}, + }, + ], + imports: [RevisionsModule, UsersModule], }) .overrideProvider(getRepositoryToken(Note)) .useValue({}) @@ -22,6 +33,16 @@ describe('Notes Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(Authorship)) .useValue({}) + .overrideProvider(getRepositoryToken(AuthorColor)) + .useValue({}) + .overrideProvider(getRepositoryToken(User)) + .useValue({}) + .overrideProvider(getRepositoryToken(AuthToken)) + .useValue({}) + .overrideProvider(getRepositoryToken(Identity)) + .useValue({}) + .overrideProvider(getRepositoryToken(Note)) + .useValue({}) .compile(); controller = module.get(NotesController); diff --git a/src/notes/notes.service.spec.ts b/src/notes/notes.service.spec.ts index 18f1c22c6..53d44e3d2 100644 --- a/src/notes/notes.service.spec.ts +++ b/src/notes/notes.service.spec.ts @@ -1,4 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Authorship } from '../revisions/authorship.entity'; +import { Revision } from '../revisions/revision.entity'; +import { RevisionsModule } from '../revisions/revisions.module'; +import { AuthToken } from '../users/auth-token.entity'; +import { Identity } from '../users/identity.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'; describe('NotesService', () => { @@ -6,9 +16,30 @@ describe('NotesService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [NotesService], - }).compile(); - + providers: [ + NotesService, + { + provide: getRepositoryToken(Note), + useValue: {}, + }, + ], + imports: [UsersModule, RevisionsModule], + }) + .overrideProvider(getRepositoryToken(User)) + .useValue({}) + .overrideProvider(getRepositoryToken(AuthToken)) + .useValue({}) + .overrideProvider(getRepositoryToken(Identity)) + .useValue({}) + .overrideProvider(getRepositoryToken(Authorship)) + .useValue({}) + .overrideProvider(getRepositoryToken(AuthorColor)) + .useValue({}) + .overrideProvider(getRepositoryToken(Revision)) + .useValue({}) + .overrideProvider(getRepositoryToken(Note)) + .useValue({}) + .compile(); service = module.get(NotesService); }); diff --git a/src/revisions/revisions.service.spec.ts b/src/revisions/revisions.service.spec.ts index b9685df92..658f8e5e9 100644 --- a/src/revisions/revisions.service.spec.ts +++ b/src/revisions/revisions.service.spec.ts @@ -1,4 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { AuthorColor } from '../notes/author-color.entity'; +import { Note } from '../notes/note.entity'; +import { NotesModule } from '../notes/notes.module'; +import { AuthToken } from '../users/auth-token.entity'; +import { Identity } from '../users/identity.entity'; +import { User } from '../users/user.entity'; +import { Authorship } from './authorship.entity'; +import { Revision } from './revision.entity'; import { RevisionsService } from './revisions.service'; describe('RevisionsService', () => { @@ -6,8 +15,30 @@ describe('RevisionsService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [RevisionsService], - }).compile(); + providers: [ + RevisionsService, + { + provide: getRepositoryToken(Revision), + useValue: {}, + }, + ], + imports: [NotesModule], + }) + .overrideProvider(getRepositoryToken(Authorship)) + .useValue({}) + .overrideProvider(getRepositoryToken(AuthorColor)) + .useValue({}) + .overrideProvider(getRepositoryToken(User)) + .useValue({}) + .overrideProvider(getRepositoryToken(AuthToken)) + .useValue({}) + .overrideProvider(getRepositoryToken(Identity)) + .useValue({}) + .overrideProvider(getRepositoryToken(Note)) + .useValue({}) + .overrideProvider(getRepositoryToken(Revision)) + .useValue({}) + .compile(); service = module.get(RevisionsService); }); From a028dac448012d6969dfd6ec67e189eb4e219a47 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Fri, 25 Sep 2020 20:42:35 +0200 Subject: [PATCH 38/40] RevisionsService: Asynchronously inject NotesService to resolve circular dependency while testing Signed-off-by: David Mehren --- src/revisions/revisions.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index 1a64a89ea..8b4742b6f 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { NotesService } from '../notes/notes.service'; @@ -13,7 +13,7 @@ export class RevisionsService { constructor( @InjectRepository(Revision) private revisionRepository: Repository, - @Inject(NotesService) private notesService: NotesService, + @Inject(forwardRef(() => NotesService)) private notesService: NotesService, ) {} async getNoteRevisionMetadatas( From 8fada8809ccd21af85396dae178f1d53d4b926e4 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 26 Sep 2020 16:00:17 +0200 Subject: [PATCH 39/40] UsersService: Merge if-statements and add `null` to return type in `toUserDto` Signed-off-by: David Mehren --- src/users/users.service.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/users/users.service.ts b/src/users/users.service.ts index ccbcea032..88e5fbbf9 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -26,13 +26,9 @@ export class UsersService { } } - toUserDto(user: User): UserInfoDto { - if (user === undefined) { - this.logger.warn('toUserDto recieved undefined argument!'); - return null; - } - if (user === null) { - this.logger.warn('toUserDto recieved null argument!'); + toUserDto(user: User | null | undefined): UserInfoDto | null { + if (!user) { + this.logger.warn(`toUserDto recieved ${user} argument!`); return null; } return { From a0740ffdf7dbaab5cc0fedad4fa0c9b7f4e9aaa5 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 26 Sep 2020 16:01:01 +0200 Subject: [PATCH 40/40] NotesService: Add TODO that `createNote` still needs to calculate a proper patch Signed-off-by: David Mehren --- src/notes/notes.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index af7f4253a..9557cc069 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -74,6 +74,7 @@ export class NotesService { ): Promise { const newNote = Note.create(); newNote.revisions = Promise.resolve([ + //TODO: Calculate patch Revision.create(noteContent, noteContent), ]); if (alias) {