Merge pull request #514 from codimd/note-metadata-in-db

Save note metadata in the database
This commit is contained in:
Yannick Bungers 2020-10-30 22:31:37 +01:00 committed by GitHub
commit 135e7ddfc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 143 additions and 60 deletions

View file

@ -1,17 +1,16 @@
@startuml
' hide the spot
hide circle
' avoid problems with angled crows feet
skinparam linetype ortho
skinparam nodesep 60
entity "Note" {
*id : uuid <<generated>>
--
*shortid : text
*alias : text
alias : text
*viewcount : number
*ownerId : uuid <<FK User>>
description: text
title: text
}
entity "User" {
@ -49,7 +48,7 @@ entity "Identity" {
passwordHash : text
}
entity "Session" as seesion {
entity "Session" {
*id : text
--
*expiredAt : number
@ -108,13 +107,18 @@ entity "Group" {
*special : boolean
}
entity "NoteGroupPermission" {
entity "NoteGroupPermission" {
*groupId : number <<FK Group>>
*noteId : uuid <<FK Note>>
--
*canEdit : boolean
}
entity "Tag" {
*id: number <<generated>>
*name: text
}
entity "MediaUpload" {
*id : text <<unique>>
--
@ -125,20 +129,26 @@ entity "MediaUpload" {
*createdAt : date
}
Note "1" - "1..*" Revision
User "1" -- "0..*" Note: owner
User "1" -u- "1..*" Identity
User "1" - "1..*" authToken
User "1" -l- "1..*" Session
User "1" - "0..*" MediaUpload
User "0..*" -- "0..*" Note
User "1" - "0..*" Authorship
(User, Note) . AuthorColors
Revision "0..*" - "0..*" Authorship
(Revision, Authorship) .. RevisionAuthorship
Authorship "0..*" -- "1" User
Note "0..*" -- "1" User : owner
Note "1" -- "0..*" NoteUserPermission
NoteUserPermission "1" -- "1" User
Note "1" -- "0..*" NoteGroupPermission
NoteGroupPermission "0..*" -- "1" Group
Identity "1..*" -- "1" User
authToken "1..*" -- "1" User
seesion "1..*" -- "1" User
Note "0..*" -- "0..*" User : color
(Note, User) .. AuthorColors
MediaUpload "0..*" -- "1" Note
MediaUpload "0..*" -- "1" User
MediaUpload "0..*" -- "1" Note
Note "1" - "1..*" Revision
Note "0..*" -l- "0..*" Tag
Note "0..*" -- "0..*" Group
User "0..*" -- "0..*" Note
(User, Note) . NoteUserPermission
(Note, Group) . NoteGroupPermission
@enduml

View file

@ -5,6 +5,7 @@ import { LoggerModule } from '../../../logger/logger.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity';
import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../users/auth-token.entity';
@ -35,6 +36,8 @@ describe('Me Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile();
controller = module.get<MeController>(MeController);

View file

@ -6,6 +6,7 @@ import { MediaModule } from '../../../media/media.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity';
import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../users/auth-token.entity';
@ -37,6 +38,8 @@ describe('Media Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile();
controller = module.get<MediaController>(MediaController);

View file

@ -4,6 +4,7 @@ import { LoggerModule } from '../../../logger/logger.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity';
import { NotesService } from '../../../notes/notes.service';
import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity';
import { RevisionsModule } from '../../../revisions/revisions.module';
@ -25,6 +26,10 @@ describe('Notes Controller', () => {
provide: getRepositoryToken(Note),
useValue: {},
},
{
provide: getRepositoryToken(Tag),
useValue: {},
},
],
imports: [RevisionsModule, UsersModule, LoggerModule],
})
@ -44,6 +49,8 @@ describe('Notes Controller', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Note))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile();
controller = module.get<NotesController>(NotesController);

View file

@ -9,6 +9,7 @@ import {
Put,
} from '@nestjs/common';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { NoteMetadataUpdateDto } from '../../../notes/note-metadata.dto';
import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto';
import { NotesService } from '../../../notes/notes.service';
import { RevisionsService } from '../../../revisions/revisions.service';
@ -21,7 +22,7 @@ export class NotesController {
private noteService: NotesService,
private revisionsService: RevisionsService,
) {
this.logger.setContext(NotesController.name);
this.logger.setContext(NotesController.name);
}
@Post()

View file

@ -4,6 +4,7 @@ import { LoggerModule } from '../logger/logger.module';
import { AuthorColor } from '../notes/author-color.entity';
import { Note } from '../notes/note.entity';
import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity';
import { Authorship } from '../revisions/authorship.entity';
import { Revision } from '../revisions/revision.entity';
import { AuthToken } from '../users/auth-token.entity';
@ -43,6 +44,8 @@ describe('MediaService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile();
service = module.get<MediaService>(MediaService);

View file

@ -34,3 +34,13 @@ export class NoteMetadataDto {
@ValidateNested()
permissions: NotePermissionsDto;
}
export class NoteMetadataUpdateDto {
@IsString()
title: string;
@IsString()
description: string;
@IsArray()
@IsString({ each: true })
tags: string[];
}

View file

@ -2,6 +2,8 @@ import { generate as shortIdGenerate } from 'shortid';
import {
Column,
Entity,
JoinTable,
ManyToMany,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
@ -11,6 +13,7 @@ import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Revision } from '../revisions/revision.entity';
import { User } from '../users/user.entity';
import { AuthorColor } from './author-color.entity';
import { Tag } from './tag.entity';
@Entity('Notes')
export class Note {
@ -25,7 +28,7 @@ export class Note {
unique: true,
nullable: true,
})
alias: string;
alias?: string;
@OneToMany(
_ => NoteGroupPermission,
groupPermission => groupPermission.note,
@ -59,10 +62,27 @@ export class Note {
)
authorColors: AuthorColor[];
@Column({
nullable: true,
})
description?: string;
@Column({
nullable: true,
})
title?: string;
@ManyToMany(
_ => Tag,
tag => tag.notes,
{ eager: true, cascade: true },
)
@JoinTable()
tags: Tag[];
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static create(owner?: User, alias?: string, shortid?: string) {
public static create(owner?: User, alias?: string, shortid?: string): Note {
if (!shortid) {
shortid = shortIdGenerate();
}
@ -74,6 +94,10 @@ export class Note {
newNote.authorColors = [];
newNote.userPermissions = [];
newNote.groupPermissions = [];
newNote.revisions = Promise.resolve([]);
newNote.description = null;
newNote.title = null;
newNote.tags = [];
return newNote;
}
}

View file

@ -1,18 +0,0 @@
import { Note } from './note.entity';
export class NoteUtils {
public static parseTitle(note: Note): string {
// TODO: Implement method
return 'Hardcoded note title';
}
public static parseDescription(note: Note): string {
// TODO: Implement method
return 'Hardcoded note description';
}
public static parseTags(note: Note): string[] {
// TODO: Implement method
return ['Hardcoded note tag'];
}
}

View file

@ -6,10 +6,11 @@ import { UsersModule } from '../users/users.module';
import { AuthorColor } from './author-color.entity';
import { Note } from './note.entity';
import { NotesService } from './notes.service';
import { Tag } from './tag.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Note, AuthorColor]),
TypeOrmModule.forFeature([Note, AuthorColor, Tag]),
forwardRef(() => RevisionsModule),
UsersModule,
LoggerModule,

View file

@ -11,6 +11,7 @@ import { UsersModule } from '../users/users.module';
import { AuthorColor } from './author-color.entity';
import { Note } from './note.entity';
import { NotesService } from './notes.service';
import { Tag } from './tag.entity';
describe('NotesService', () => {
let service: NotesService;
@ -23,6 +24,10 @@ describe('NotesService', () => {
provide: getRepositoryToken(Note),
useValue: {},
},
{
provide: getRepositoryToken(Tag),
useValue: {},
},
],
imports: [UsersModule, RevisionsModule, LoggerModule],
})
@ -40,6 +45,8 @@ describe('NotesService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Note))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile();
service = module.get<NotesService>(NotesService);
});

View file

@ -7,20 +7,21 @@ import { Revision } from '../revisions/revision.entity';
import { RevisionsService } from '../revisions/revisions.service';
import { User } from '../users/user.entity';
import { UsersService } from '../users/users.service';
import { NoteMetadataDto } from './note-metadata.dto';
import { NoteMetadataDto, NoteMetadataUpdateDto } from './note-metadata.dto';
import {
NotePermissionsDto,
NotePermissionsUpdateDto,
} from './note-permissions.dto';
import { NoteDto } from './note.dto';
import { Note } from './note.entity';
import { NoteUtils } from './note.utils';
import { Tag } from './tag.entity';
@Injectable()
export class NotesService {
constructor(
private readonly logger: ConsoleLoggerService,
@InjectRepository(Note) private noteRepository: Repository<Note>,
@InjectRepository(Tag) private tagRepository: Repository<Tag>,
@Inject(UsersService) private usersService: UsersService,
@Inject(forwardRef(() => RevisionsService))
private revisionsService: RevisionsService,
@ -102,10 +103,10 @@ export class NotesService {
// TODO: Convert DB UUID to base64
id: note.id,
alias: note.alias,
title: NoteUtils.parseTitle(note),
title: note.title,
// TODO: Get actual createTime
createTime: new Date(),
description: NoteUtils.parseDescription(note),
description: note.description,
editedBy: note.authorColors.map(authorColor => authorColor.user.userName),
// TODO: Extract into method
permissions: {
@ -119,7 +120,7 @@ export class NotesService {
canEdit: noteGroupPermission.canEdit,
})),
},
tags: NoteUtils.parseTags(note),
tags: note.tags.map(tag => tag.name),
updateTime: (await this.getLastRevision(note)).createdAt,
// TODO: Get actual updateUser
updateUser: {

19
src/notes/tag.entity.ts Normal file
View file

@ -0,0 +1,19 @@
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Note } from './note.entity';
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column({
nullable: false,
})
name: string;
@ManyToMany(
_ => Note,
note => note.tags,
)
notes: Note[];
}

View file

@ -10,6 +10,7 @@ import { User } from '../users/user.entity';
import { Authorship } from './authorship.entity';
import { Revision } from './revision.entity';
import { RevisionsService } from './revisions.service';
import { Tag } from '../notes/tag.entity';
describe('RevisionsService', () => {
let service: RevisionsService;
@ -39,6 +40,8 @@ describe('RevisionsService', () => {
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile();
service = module.get<RevisionsService>(RevisionsService);

View file

@ -76,6 +76,7 @@ export class RevisionsService {
createRevision(content: string) {
// TODO: Add previous revision
// TODO: Calculate patch
// TODO: Save metadata
return this.revisionRepository.create({
content: content,
length: content.length,

View file

@ -98,20 +98,28 @@ describe('Notes', () => {
).toEqual('New note text');
});
it.skip(`PUT /notes/{note}/metadata`, () => {
// TODO
return request(app.getHttpServer())
.post('/notes/test5/metadata')
.set('Content-Type', 'text/markdown')
.expect(200);
});
it.skip(`GET /notes/{note}/metadata`, () => {
notesService.createNote('This is a test note.', 'test6');
return request(app.getHttpServer())
it(`GET /notes/{note}/metadata`, async () => {
await notesService.createNote('This is a test note.', 'test6');
const metadata = await request(app.getHttpServer())
.get('/notes/test6/metadata')
.expect(200);
// TODO: Find out how to check the structure of the returned JSON
expect(typeof metadata.body.id).toEqual('string');
expect(metadata.body.alias).toEqual('test6');
expect(metadata.body.title).toBeNull();
expect(metadata.body.description).toBeNull();
expect(typeof metadata.body.createTime).toEqual('string');
expect(metadata.body.editedBy).toEqual([]);
expect(metadata.body.permissions.owner).toBeNull();
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
expect(metadata.body.tags).toEqual([]);
expect(typeof metadata.body.updateTime).toEqual('string');
expect(typeof metadata.body.updateUser.displayName).toEqual('string');
expect(typeof metadata.body.updateUser.userName).toEqual('string');
expect(typeof metadata.body.updateUser.email).toEqual('string');
expect(typeof metadata.body.updateUser.photo).toEqual('string');
expect(typeof metadata.body.viewCount).toEqual('number');
expect(metadata.body.editedBy).toEqual([]);
});
it(`GET /notes/{note}/revisions`, async () => {