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

View file

@ -5,6 +5,7 @@ import { LoggerModule } from '../../../logger/logger.module';
import { AuthorColor } from '../../../notes/author-color.entity'; import { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity'; import { Note } from '../../../notes/note.entity';
import { NotesModule } from '../../../notes/notes.module'; import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity'; import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity'; import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../users/auth-token.entity'; import { AuthToken } from '../../../users/auth-token.entity';
@ -35,6 +36,8 @@ describe('Me Controller', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile(); .compile();
controller = module.get<MeController>(MeController); 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 { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity'; import { Note } from '../../../notes/note.entity';
import { NotesModule } from '../../../notes/notes.module'; import { NotesModule } from '../../../notes/notes.module';
import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity'; import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity'; import { Revision } from '../../../revisions/revision.entity';
import { AuthToken } from '../../../users/auth-token.entity'; import { AuthToken } from '../../../users/auth-token.entity';
@ -37,6 +38,8 @@ describe('Media Controller', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(User)) .overrideProvider(getRepositoryToken(User))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile(); .compile();
controller = module.get<MediaController>(MediaController); 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 { AuthorColor } from '../../../notes/author-color.entity';
import { Note } from '../../../notes/note.entity'; import { Note } from '../../../notes/note.entity';
import { NotesService } from '../../../notes/notes.service'; import { NotesService } from '../../../notes/notes.service';
import { Tag } from '../../../notes/tag.entity';
import { Authorship } from '../../../revisions/authorship.entity'; import { Authorship } from '../../../revisions/authorship.entity';
import { Revision } from '../../../revisions/revision.entity'; import { Revision } from '../../../revisions/revision.entity';
import { RevisionsModule } from '../../../revisions/revisions.module'; import { RevisionsModule } from '../../../revisions/revisions.module';
@ -25,6 +26,10 @@ describe('Notes Controller', () => {
provide: getRepositoryToken(Note), provide: getRepositoryToken(Note),
useValue: {}, useValue: {},
}, },
{
provide: getRepositoryToken(Tag),
useValue: {},
},
], ],
imports: [RevisionsModule, UsersModule, LoggerModule], imports: [RevisionsModule, UsersModule, LoggerModule],
}) })
@ -44,6 +49,8 @@ describe('Notes Controller', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Note)) .overrideProvider(getRepositoryToken(Note))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.compile(); .compile();
controller = module.get<NotesController>(NotesController); controller = module.get<NotesController>(NotesController);

View file

@ -9,6 +9,7 @@ import {
Put, Put,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { NoteMetadataUpdateDto } from '../../../notes/note-metadata.dto';
import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto'; import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto';
import { NotesService } from '../../../notes/notes.service'; import { NotesService } from '../../../notes/notes.service';
import { RevisionsService } from '../../../revisions/revisions.service'; import { RevisionsService } from '../../../revisions/revisions.service';

View file

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

View file

@ -34,3 +34,13 @@ export class NoteMetadataDto {
@ValidateNested() @ValidateNested()
permissions: NotePermissionsDto; 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 { import {
Column, Column,
Entity, Entity,
JoinTable,
ManyToMany,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
@ -11,6 +13,7 @@ import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { AuthorColor } from './author-color.entity'; import { AuthorColor } from './author-color.entity';
import { Tag } from './tag.entity';
@Entity('Notes') @Entity('Notes')
export class Note { export class Note {
@ -25,7 +28,7 @@ export class Note {
unique: true, unique: true,
nullable: true, nullable: true,
}) })
alias: string; alias?: string;
@OneToMany( @OneToMany(
_ => NoteGroupPermission, _ => NoteGroupPermission,
groupPermission => groupPermission.note, groupPermission => groupPermission.note,
@ -59,10 +62,27 @@ export class Note {
) )
authorColors: AuthorColor[]; 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 // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
public static create(owner?: User, alias?: string, shortid?: string) { public static create(owner?: User, alias?: string, shortid?: string): Note {
if (!shortid) { if (!shortid) {
shortid = shortIdGenerate(); shortid = shortIdGenerate();
} }
@ -74,6 +94,10 @@ export class Note {
newNote.authorColors = []; newNote.authorColors = [];
newNote.userPermissions = []; newNote.userPermissions = [];
newNote.groupPermissions = []; newNote.groupPermissions = [];
newNote.revisions = Promise.resolve([]);
newNote.description = null;
newNote.title = null;
newNote.tags = [];
return newNote; 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 { AuthorColor } from './author-color.entity';
import { Note } from './note.entity'; import { Note } from './note.entity';
import { NotesService } from './notes.service'; import { NotesService } from './notes.service';
import { Tag } from './tag.entity';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Note, AuthorColor]), TypeOrmModule.forFeature([Note, AuthorColor, Tag]),
forwardRef(() => RevisionsModule), forwardRef(() => RevisionsModule),
UsersModule, UsersModule,
LoggerModule, LoggerModule,

View file

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

View file

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

View file

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

View file

@ -98,20 +98,28 @@ describe('Notes', () => {
).toEqual('New note text'); ).toEqual('New note text');
}); });
it.skip(`PUT /notes/{note}/metadata`, () => { it(`GET /notes/{note}/metadata`, async () => {
// TODO await notesService.createNote('This is a test note.', 'test6');
return request(app.getHttpServer()) const metadata = await 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())
.get('/notes/test6/metadata') .get('/notes/test6/metadata')
.expect(200); .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 () => { it(`GET /notes/{note}/revisions`, async () => {