Merge branch 'develop' into public-api-uploads

This commit is contained in:
Yannick Bungers 2020-10-30 22:46:08 +01:00 committed by GitHub
commit 8a6e81e1c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 919 additions and 683 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
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

@ -47,6 +47,8 @@ paths:
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/History"
'401':
"$ref": "#/components/responses/UnauthorizedError"
@ -60,11 +62,11 @@ paths:
description: JSON Object which contains id, title, tags, last visit time and pinned status
responses:
'200':
description: The list of recently viewed notes and pinned notes.
description: Information about the history entry
content:
application/json:
schema:
"$ref": "#/components/schemas/HistoryObject"
"$ref": "#/components/schemas/History"
'401':
"$ref": "#/components/responses/UnauthorizedError"
'404':
@ -88,14 +90,14 @@ paths:
content:
application/json:
schema:
"$ref": "#/components/schemas/HistoryObject"
"$ref": "#/components/schemas/HistoryUpdate"
responses:
'200':
description: The new history.
description: The new history object.
content:
application/json:
schema:
"$ref": "#/components/schemas/HistoryObject"
"$ref": "#/components/schemas/History"
'401':
"$ref": "#/components/responses/UnauthorizedError"
'404':
@ -183,7 +185,7 @@ paths:
markdownExample:
"$ref": '#/components/examples/markdownExample'
responses:
'200':
'201':
description: Get information about the newly created note.
content:
application/json:
@ -226,7 +228,7 @@ paths:
- note
summary: Imports some markdown data into a new note with a given alias
operationId: createNoteWithAlias
description: This endpoint equals to the above one except that the alias from the url will be assigned to the note if [FreeURL-mode](https://github.com/codimd/server/tree/master/docs/configuration-env-vars.md#users-and-privileges) is enabled.
description: This endpoint creates a new note with the content of the HTTP request body and the alias from the URL parameter.
requestBody:
required: true
description: The content of the note to be imported as markdown.
@ -238,7 +240,7 @@ paths:
markdownExample:
"$ref": '#/components/examples/markdownExample'
responses:
'200':
'201':
description: Get information about the newly created note.
content:
application/json:
@ -285,7 +287,7 @@ paths:
- note
summary: Imports some markdown data into an existing note, creating a new revision
operationId: createNewRevisionForNote
description: This endpoint equals to the above one except that the alias from the url will be assigned to the note if [FreeURL-mode](https://github.com/codimd/server/tree/master/docs/configuration-env-vars.md#users-and-privileges) is enabled.
description: This endpoint updates the note content of an existing note. The old content is completely replaced and a new revision is created.
requestBody:
required: true
description: The content of the note to be imported as markdown.
@ -318,20 +320,14 @@ paths:
text/plain:
example: my-note
/notes/{note}/metadata:
put:
get:
tags:
- note
summary: Set the permissions of a note
operationId: updateNoteMetadata
requestBody:
required: true
content:
application/json:
schema:
"$ref": "#/components/schemas/NoteMetadata"
summary: Get the metadata of a note
operationId: getNoteMetadata
responses:
'200':
description: The updated permissions of the note.
description: The metadata of the note.
content:
application/json:
schema:
@ -350,18 +346,24 @@ paths:
content:
text/plain:
example: my-note
get:
tags:
- note
summary: Get the permissions of a note
operationId: getNoteMetadata
responses:
'200':
description: The permissions of the note.
/notes/{note}/permissions:
put:
tags: [ note ]
summary: Set permissions of a note
operationId: updateNotePermissions
requestBody:
required: true
content:
application/json:
schema:
"$ref": "#/components/schemas/NoteMetadata"
"$ref": "#/components/schemas/NotePermissionsUpdate"
responses:
'200':
description: The updated permissions of the note.
content:
application/json:
schema:
"$ref": "#/components/schemas/NotePermissions"
'401':
"$ref": "#/components/responses/UnauthorizedError"
'403':
@ -389,6 +391,8 @@ paths:
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/NoteRevisionsMetadata"
'401':
"$ref": "#/components/responses/UnauthorizedError"
@ -552,7 +556,7 @@ paths:
required: true
description: ID or alias of the parent note
responses:
'200':
'201':
description: The file was uploaded successfully.
content:
application/json:
@ -642,6 +646,15 @@ components:
properties:
password:
type: string
GroupInfo:
type: object
properties:
name:
type: string
displayName:
type: string
special:
type: boolean
ImageProxyRequest:
type: object
properties:
@ -713,9 +726,32 @@ components:
type: object
properties:
owner:
type: string
description: Username of the owner of the note
sharedTo:
$ref: "#/components/schemas/UserInfo"
sharedToUsers:
type: array
description: Contains all users that can read the note and a boolean that denotes if they can also edit.
items:
type: object
properties:
user:
$ref: "#/components/schemas/UserInfo"
canEdit:
type: boolean
sharedToGroups:
type: array
description: Contains all groups that can read the note and a boolean that denotes if they can also edit.
items:
type: object
properties:
group:
$ref: "#/components/schemas/GroupInfo"
canEdit:
type: boolean
NotePermissionsUpdate:
type: object
description: Contains only title, description and tags of a note.
properties:
sharedToUsers:
type: array
description: Contains all usernames that can read the note and a boolean that denotes if they can also edit.
items:
@ -725,10 +761,17 @@ components:
type: string
canEdit:
type: boolean
NoteRevisionsMetadata:
sharedToGroups:
type: array
description: Contains all groups that can read the note and a boolean that denotes if they can also edit.
items:
type: object
properties:
groupname:
type: string
canEdit:
type: boolean
NoteRevisionsMetadata:
type: object
properties:
id:
@ -840,7 +883,7 @@ components:
type: boolean
disconnectSocketQueueLength:
type: integer
HistoryObject:
History:
type: object
properties:
metadata:
@ -848,14 +891,12 @@ components:
pinned:
type: boolean
description: Whether the user has pinned this note.
History:
HistoryUpdate:
type: object
properties:
history:
type: array
description: The array that contains history objects.
items:
"$ref": "#/components/schemas/HistoryObject"
pinned:
type: boolean
description: Whether the user has pinned this note.
MediaUpload:
type: object
properties:

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';

View file

@ -20,7 +20,7 @@ export class HistoryService {
description: 'Very descriptive text.',
editedBy: [],
id: 'foobar-barfoo',
permission: {
permissions: {
owner: {
displayName: 'foo',
userName: 'fooUser',
@ -59,7 +59,7 @@ export class HistoryService {
description: 'Very descriptive text.',
editedBy: [],
id: 'foobar-barfoo',
permission: {
permissions: {
owner: {
displayName: 'foo',
userName: 'fooUser',

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

@ -32,5 +32,15 @@ export class NoteMetadataDto {
@ValidateNested()
editedBy: UserInfoDto['userName'][];
@ValidateNested()
permission: NotePermissionsDto;
permissions: NotePermissionsDto;
}
export class NoteMetadataUpdateDto {
@IsString()
title: string;
@IsString()
description: string;
@IsArray()
@IsString({ each: true })
tags: string[];
}

View file

@ -8,7 +8,7 @@ export class NoteUserPermissionEntryDto {
canEdit: boolean;
}
export class NotePermissionEntryUpdateDto {
export class NoteUserPermissionUpdateDto {
@IsString()
username: string;
@IsBoolean()
@ -31,6 +31,13 @@ export class NoteGroupPermissionEntryDto {
canEdit: boolean;
}
export class NoteGroupPermissionUpdateDto {
@IsString()
groupname: string;
@IsBoolean()
canEdit: boolean;
}
export class NotePermissionsDto {
@ValidateNested()
owner: UserInfoDto;
@ -45,5 +52,8 @@ export class NotePermissionsDto {
export class NotePermissionsUpdateDto {
@IsArray()
@ValidateNested()
sharedTo: NotePermissionEntryUpdateDto[];
sharedToUsers: NoteUserPermissionUpdateDto[];
@IsArray()
@ValidateNested()
sharedToGroups: NoteGroupPermissionUpdateDto[];
}

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,
@ -37,7 +38,7 @@ export class NotesService {
description: 'Very descriptive text.',
editedBy: [],
id: 'foobar-barfoo',
permission: {
permissions: {
owner: {
displayName: 'foo',
userName: 'fooUser',
@ -102,13 +103,13 @@ 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
permission: {
permissions: {
owner: this.usersService.toUserDto(note.owner),
sharedToUsers: note.userPermissions.map(noteUserPermission => ({
user: this.usersService.toUserDto(noteUserPermission.user),
@ -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 () => {

1228
yarn.lock

File diff suppressed because it is too large Load diff