Merge pull request #500 from codimd/routes/notes/services

This commit is contained in:
David Mehren 2020-09-26 18:03:42 +02:00 committed by GitHub
commit d07d8fe278
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 513 additions and 229 deletions

View file

@ -58,7 +58,7 @@ entity "Session" as seesion {
entity "Revision" {
*id : uuid <<generated>>
*id : number <<generated>>
--
*noteId : uuid <<FK Note>>
*content : text
@ -78,7 +78,7 @@ entity "Authorship" {
}
entity "RevisionAuthorship" {
*revisionId : uuid <<FK Revision>>
*revisionId : number <<FK Revision>>
*authorshipId : uuid <<FK Authorship>>
}
@ -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

View file

@ -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:

View file

@ -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",

View file

@ -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>(MeController);

View file

@ -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>(NotesController);

View file

@ -1,53 +1,98 @@
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);
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.createNoteDto(bodyText);
} else {
// TODO: Better error message
throw new BadRequestException('Invalid body');
}
}
@Get(':noteIdOrAlias')
getNote(@Param('noteIdOrAlias') noteIdOrAlias: string) {
return this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
return this.noteService.getNoteDtoByIdOrAlias(noteIdOrAlias);
}
@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.createNoteDto(bodyText, noteAlias);
} else {
// TODO: Better error message
throw new BadRequestException('Invalid body');
}
}
@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')
@ -77,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);
}

View file

@ -7,7 +7,7 @@ export class NoteDto {
content: string;
@ValidateNested()
metdata: NoteMetadataDto;
metadata: NoteMetadataDto;
@IsArray()
@ValidateNested({ each: true })

View file

@ -16,64 +16,64 @@ 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[];
revisions: Promise<Revision[]>;
@OneToMany(
_ => AuthorColor,
authorColor => authorColor.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;
newNote.authorColors = [];
newNote.userPermissions = [];
newNote.groupPermissions = [];
return newNote;
}
}

18
src/notes/note.utils.ts Normal file
View file

@ -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'];
}
}

View file

@ -1,11 +1,17 @@
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';
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]),
forwardRef(() => RevisionsModule),
UsersModule,
],
controllers: [],
providers: [NotesService],
exports: [NotesService],

View file

@ -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>(NotesService);
});

View file

@ -1,15 +1,30 @@
import { 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';
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 {
NotePermissionsDto,
NotePermissionsUpdateDto,
} from './note-permissions.dto';
import { NoteDto } from './note.dto';
import { Note } from './note.entity';
import { NoteUtils } from './note.utils';
@Injectable()
export class NotesService {
private readonly logger = new Logger(NotesService.name);
constructor(
@InjectRepository(Note) private noteRepository: Repository<Note>,
@Inject(UsersService) private usersService: UsersService,
@Inject(forwardRef(() => RevisionsService))
private revisionsService: RevisionsService,
) {}
getUserNotes(username: string): NoteMetadataDto[] {
this.logger.warn('Using hardcoded data!');
return [
@ -43,140 +58,70 @@ export class NotesService {
];
}
createNote(noteContent: string, alias?: NoteMetadataDto['alias']): NoteDto {
this.logger.warn('Using hardcoded data!');
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: {
displayName: 'foo',
userName: 'fooUser',
email: 'foo@example.com',
photo: '',
},
viewCount: 42,
},
editedByAtPosition: [],
};
async createNoteDto(
noteContent: string,
alias?: NoteMetadataDto['alias'],
owner?: User,
): Promise<NoteDto> {
const note = await this.createNote(noteContent, alias, owner);
return this.toNoteDto(note);
}
getNoteByIdOrAlias(noteIdOrAlias: 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 createNote(
noteContent: string,
alias?: NoteMetadataDto['alias'],
owner?: User,
): Promise<Note> {
const newNote = Note.create();
newNote.revisions = Promise.resolve([
//TODO: Calculate patch
Revision.create(noteContent, noteContent),
]);
if (alias) {
newNote.alias = alias;
}
if (owner) {
newNote.owner = owner;
}
return this.noteRepository.save(newNote);
}
deleteNoteByIdOrAlias(noteIdOrAlias: string) {
this.logger.warn('Using hardcoded data!');
return;
async getCurrentContent(note: Note) {
return (await this.getLastRevision(note)).content;
}
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 getLastRevision(note: Note): Promise<Revision> {
return this.revisionsService.getLatestRevision(note.id);
}
getNoteMetadata(noteIdOrAlias: string): NoteMetadataDto {
this.logger.warn('Using hardcoded data!');
async getMetadata(note: Note): Promise<NoteMetadataDto> {
return {
alias: null,
// TODO: Convert DB UUID to base64
id: note.id,
alias: note.alias,
title: NoteUtils.parseTitle(note),
// TODO: Get actual createTime
createTime: new Date(),
description: 'Very descriptive text.',
editedBy: [],
id: noteIdOrAlias,
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: (await this.getLastRevision(note)).createdAt,
// TODO: Get actual updateUser
updateUser: {
displayName: 'foo',
userName: 'fooUser',
displayName: 'Hardcoded User',
userName: 'hardcoded',
email: 'foo@example.com',
photo: '',
},
@ -184,6 +129,47 @@ export class NotesService {
};
}
async getNoteByIdOrAlias(noteIdOrAlias: string): Promise<Note> {
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<NoteDto> {
const note = await this.getNoteByIdOrAlias(noteIdOrAlias);
return this.toNoteDto(note);
}
async deleteNoteByIdOrAlias(noteIdOrAlias: string) {
const note = await this.getNoteByIdOrAlias(noteIdOrAlias);
return await this.noteRepository.remove(note);
}
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);
}
async getNoteMetadata(noteIdOrAlias: string): Promise<NoteMetadataDto> {
const note = await this.getNoteByIdOrAlias(noteIdOrAlias);
return this.getMetadata(note);
}
updateNotePermissions(
noteIdOrAlias: string,
newPermissions: NotePermissionsUpdateDto,
@ -201,8 +187,16 @@ export class NotesService {
};
}
getNoteContent(noteIdOrAlias: string) {
this.logger.warn('Using hardcoded data!');
return '# Markdown';
async getNoteContent(noteIdOrAlias: string): Promise<string> {
const note = await this.getNoteByIdOrAlias(noteIdOrAlias);
return this.getCurrentContent(note);
}
async toNoteDto(note: Note): Promise<NoteDto> {
return {
content: await this.getCurrentContent(note),
metadata: await this.getMetadata(note),
editedByAtPosition: [],
};
}
}

View file

@ -2,11 +2,11 @@ import { IsDate, IsNumber, IsString } from 'class-validator';
import { Revision } from './revision.entity';
export class RevisionMetadataDto {
@IsString()
@IsNumber()
id: Revision['id'];
@IsDate()
updatedAt: Date;
createdAt: Date;
@IsNumber()
length: number;

View file

@ -1,11 +1,13 @@
import { IsString } from 'class-validator';
import { IsDate, IsNumber, IsString } from 'class-validator';
import { Revision } from './revision.entity';
export class RevisionDto {
@IsString()
@IsNumber()
id: Revision['id'];
@IsString()
content: string;
@IsString()
patch: string;
@IsDate()
createdAt: Date;
}

View file

@ -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.
@ -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;
}
}

View file

@ -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],
})

View file

@ -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>(RevisionsService);
});

View file

@ -1,27 +1,83 @@
import { 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';
import { RevisionMetadataDto } from './revision-metadata.dto';
import { RevisionDto } from './revision.dto';
import { Revision } from './revision.entity';
@Injectable()
export class RevisionsService {
private readonly logger = new Logger(RevisionsService.name);
getNoteRevisionMetadatas(noteIdOrAlias: string): RevisionMetadataDto[] {
this.logger.warn('Using hardcoded data!');
return [
{
id: 'some-uuid',
updatedAt: new Date(),
length: 42,
constructor(
@InjectRepository(Revision)
private revisionRepository: Repository<Revision>,
@Inject(forwardRef(() => NotesService)) private notesService: NotesService,
) {}
async getNoteRevisionMetadatas(
noteIdOrAlias: string,
): Promise<RevisionMetadataDto[]> {
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: string): RevisionDto {
this.logger.warn('Using hardcoded data!');
async getNoteRevision(
noteIdOrAlias: string,
revisionId: number,
): Promise<RevisionDto> {
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<Revision> {
return this.revisionRepository.findOne({
where: {
note: noteId,
},
order: {
createdAt: 'DESC',
id: 'DESC',
},
});
}
toMetadataDto(revision: Revision): RevisionMetadataDto {
return {
id: revisionId,
content: 'Foobar',
patch: 'barfoo',
id: revision.id,
length: revision.length,
createdAt: revision.createdAt,
};
}
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
return this.revisionRepository.create({
content: content,
length: content.length,
patch: '',
});
}
}

View file

@ -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 | null | undefined): UserInfoDto | null {
if (!user) {
this.logger.warn(`toUserDto recieved ${user} argument!`);
return null;
}
return {
userName: user.userName,
displayName: user.displayName,
photo: this.getPhotoUrl(user),
email: user.email,
};
}
}

View file

@ -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,29 +14,44 @@ describe('Notes', () => {
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
imports: [
PublicApiModule,
NotesModule,
PermissionsModule,
GroupsModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: './hedgedoc-e2e.sqlite',
autoLoadEntities: true,
synchronize: true,
}),
],
}).compile();
app = moduleRef.createNestApplication();
notesService = moduleRef.get(NotesService);
await app.init();
notesService = moduleRef.get(NotesService);
const noteRepository = moduleRef.get('NoteRepository');
noteRepository.clear();
});
it(`POST /notes`, async () => {
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);
expect(response.body.metadata?.id).toBeDefined();
expect(
notesService.getNoteByIdOrAlias(response.body.metadata.id).content,
(await notesService.getNoteDtoByIdOrAlias(response.body.metadata.id))
.content,
).toEqual(newNote);
});
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/)
@ -44,38 +63,44 @@ 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);
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);
});
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')
.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`, () => {
// TODO
return request(app.getHttpServer())
.post('/notes/test5/metadata')
.set('Content-Type', 'text/markdown')
.expect(200);
});
@ -88,29 +113,30 @@ 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/)
.expect(200);
expect(response.body.revisions).toHaveLength(1);
expect(response.body).toHaveLength(1);
});
it(`GET /notes/{note}/revisions/{revision-id}`, async () => {
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.');
});
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);
expect(response.body).toEqual('This is a test note.');
expect(response.text).toEqual('This is a test note.');
});
afterAll(async () => {

View file

@ -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"