diff --git a/code/client/src/services/communication/socket/operationEmitter.ts b/code/client/src/services/communication/socket/operationEmitter.ts index 96fb93b2..ce077eec 100644 --- a/code/client/src/services/communication/socket/operationEmitter.ts +++ b/code/client/src/services/communication/socket/operationEmitter.ts @@ -30,7 +30,6 @@ export class OperationEmitter { } private emitOperations() { - console.log('operation'); if (isEmpty(this.operationBuffer)) return; if (this.operationBuffer.length > this.chunkSize) { this.emitChunked(); diff --git a/code/client/src/services/communication/socket/socketCommunication.ts b/code/client/src/services/communication/socket/socketCommunication.ts index 29076c78..4a2ed4bb 100644 --- a/code/client/src/services/communication/socket/socketCommunication.ts +++ b/code/client/src/services/communication/socket/socketCommunication.ts @@ -23,7 +23,6 @@ function emit(str: string, data: any) { const socket = namespaces[`/${namespace}`]; if (!socket) throw new Error('Invalid namespace'); socket.emit(event, data); - console.log(namespace, event, data); } function on(eventHandlers: SocketEventHandlers) { diff --git a/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts b/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts index 2ab78487..906abdcb 100644 --- a/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts +++ b/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts @@ -20,7 +20,6 @@ function useWorkspaces() { } async function createWorkspace(values: { [key: string]: string }) { - console.log('Creating workspace', values); if (!values.name) throw new Error('Workspace name is required'); // ... validate other fields const workspace = await http.post('/workspaces', values); diff --git a/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts b/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts index a8c1468a..a1d6d378 100644 --- a/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts +++ b/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts @@ -13,15 +13,15 @@ function resourcesHandlers(service: ResourcesService, io: Server) { * @param res */ const createResource = async (req: Request, res: Response) => { + const { wid } = req.params; const resource = req.body as ResourceInputModel; if (!resource) throw new InvalidParameterError('Body is required'); - const { workspace, type, name, parent } = resource; - if (!workspace) throw new InvalidParameterError('Workspace id is required'); + const { type, name, parent } = resource; + if (!wid) throw new InvalidParameterError('Workspace id is required'); if (!type) throw new InvalidParameterError('Resource type is required'); - if (!name) throw new InvalidParameterError('Resource name is required'); if (!parent) throw new InvalidParameterError('Resource parent is required'); - const id = await service.createResource(workspace, name, type, parent); - io.of('/workspaces').in(workspace).emit('resources:create', { id, name, type, parent }); + const id = await service.createResource(wid, name, type, parent); + io.of('/workspaces').in(wid).emit('resources:create', { id, name, type, parent }); httpResponse.created(res).json({ id }); }; @@ -45,6 +45,8 @@ function resourcesHandlers(service: ResourcesService, io: Server) { */ const updateResource = async (req: Request, res: Response) => { const { wid, id } = req.params; + if (!wid) throw new InvalidParameterError('Workspace id is required'); + if (!id) throw new InvalidParameterError('Resource id is required'); const resource = req.body as Partial; if (!resource) throw new InvalidParameterError('Body is required'); await service.updateResource(id, resource); @@ -59,6 +61,8 @@ function resourcesHandlers(service: ResourcesService, io: Server) { */ const deleteResource = async (req: Request, res: Response) => { const { wid, id } = req.params; + if (!wid) throw new InvalidParameterError('Workspace id is required'); + if (!id) throw new InvalidParameterError('Resource id is required'); await service.deleteResource(id); io.of('/workspaces').in(wid).emit('resources:delete', { id }); httpResponse.noContent(res).send(); diff --git a/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts b/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts index ca573f80..ca041d34 100644 --- a/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts +++ b/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts @@ -5,6 +5,7 @@ import { Request, Response } from 'express'; import { WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; import { Services } from '@services/Services'; import { Server } from 'socket.io'; +import { InvalidParameterError } from '@domain/errors/errors'; function workspaceHandlers(services: Services, io: Server) { /** @@ -14,6 +15,7 @@ function workspaceHandlers(services: Services, io: Server) { */ const createWorkspace = async (req: Request, res: Response) => { const { name } = req.body as WorkspaceMetaData; + if (!name) throw new InvalidParameterError('Workspace name is required'); const id = await services.workspace.createWorkspace(name); httpResponse.created(res).json({ id }); }; @@ -46,6 +48,8 @@ function workspaceHandlers(services: Services, io: Server) { */ const updateWorkspace = async (req: Request, res: Response) => { const { id, name } = req.body as WorkspaceMetaData; + if (!id) throw new InvalidParameterError('Workspace id is required'); + if (!name) throw new InvalidParameterError('Workspace name is required'); await services.workspace.updateWorkspace(id, name); io.of('/workspaces').in(id).emit('workspaces:update', { id, name }); httpResponse.noContent(res).send(); @@ -58,6 +62,7 @@ function workspaceHandlers(services: Services, io: Server) { */ const deleteWorkspace = async (req: Request, res: Response) => { const { wid } = req.params; + if (!wid) throw new InvalidParameterError('Workspace id is required'); await services.workspace.deleteWorkspace(wid); io.of('/workspaces').in(wid).emit('workspaces:delete', { id: wid }); httpResponse.noContent(res).send(); diff --git a/code/server/src/ts/database/postgres/PostgresResourceDatabase.ts b/code/server/src/ts/database/postgres/PostgresResourceDatabase.ts index 50bec591..d4c0aba8 100644 --- a/code/server/src/ts/database/postgres/PostgresResourceDatabase.ts +++ b/code/server/src/ts/database/postgres/PostgresResourceDatabase.ts @@ -4,8 +4,8 @@ import { InvalidParameterError, NotFoundError } from '@domain/errors/errors'; import sql from '@database/postgres/config'; export class PostgresResourceDatabase implements ResourceRepository { - async createResource(wid: string, name: string, type: ResourceType, parent: string): Promise { - const resource = { workspace: wid, name, type, parent }; + async createResource(wid: string, name: string, type: ResourceType, parent?: string): Promise { + const resource = { workspace: wid, name, type, parent: parent || 'IDKYET' }; const results = await sql` INSERT INTO resource ${sql(resource)} RETURNING id diff --git a/code/server/src/ts/database/types.d.ts b/code/server/src/ts/database/types.d.ts index c0c0b40d..3036e056 100644 --- a/code/server/src/ts/database/types.d.ts +++ b/code/server/src/ts/database/types.d.ts @@ -17,7 +17,7 @@ export interface DocumentRepository { * Resource Repository - Interface for handling resources metadata management */ export interface ResourceRepository { - createResource: (wid: string, name: string, type: ResourceType, parent: string) => Promise; + createResource: (wid: string, name: string, type: ResourceType, parent?: string) => Promise; getResource: (id: string) => Promise; updateResource: (id: string, newProps: Partial) => Promise; deleteResource: (id: string) => Promise; diff --git a/code/server/src/ts/services/ResourcesService.ts b/code/server/src/ts/services/ResourcesService.ts index 01693105..a8f62c3d 100644 --- a/code/server/src/ts/services/ResourcesService.ts +++ b/code/server/src/ts/services/ResourcesService.ts @@ -10,7 +10,7 @@ export class ResourcesService { this.documents = documents; } - async createResource(wid: string, name: string, type: ResourceType, parent: string): Promise { + async createResource(wid: string, name: string, type: ResourceType, parent?: string): Promise { return await this.resources.createResource(wid, name, type, parent); } diff --git a/code/server/src/ts/services/WorkspaceService.ts b/code/server/src/ts/services/WorkspaceService.ts index 25004f67..098f3240 100644 --- a/code/server/src/ts/services/WorkspaceService.ts +++ b/code/server/src/ts/services/WorkspaceService.ts @@ -9,8 +9,8 @@ export class WorkspaceService { this.database = database; } - async createWorkspace(title: string): Promise { - return await this.database.createWorkspace(title); + async createWorkspace(name: string): Promise { + return await this.database.createWorkspace(name); } async getWorkspaces(): Promise { diff --git a/code/server/test/document/conflict-resolution.test.ts b/code/server/test/documents/conflict-resolution.test.ts similarity index 76% rename from code/server/test/document/conflict-resolution.test.ts rename to code/server/test/documents/conflict-resolution.test.ts index d3bb86e6..d6d8ab2e 100644 --- a/code/server/test/document/conflict-resolution.test.ts +++ b/code/server/test/documents/conflict-resolution.test.ts @@ -1,18 +1,18 @@ import * as http from 'http'; -import request = require('supertest'); import { Server } from 'socket.io'; import { setup } from '../server.test'; import { applyOperations } from './utils'; -import { Express } from 'express'; import { io, Socket } from 'socket.io-client'; -import { InsertOperation, DeleteOperation, Operation } from '@notespace/shared/src/document/types/operations'; +import { InsertOperation, DeleteOperation } from '@notespace/shared/src/document/types/operations'; import { FugueTree } from '@notespace/shared/src/document/FugueTree'; +import { requests as requestOperations } from '../utils/requests'; +import { randomString } from '../utils'; const PORT = process.env.PORT || 8080; const BASE_URL = `http://localhost:${PORT}/document`; let ioServer: Server; let httpServer: http.Server; -let app: Express; +let requests: ReturnType; let client1: Socket; let client2: Socket; let tree = new FugueTree(); @@ -21,7 +21,7 @@ beforeAll(done => { const { _http, _io, _app } = setup(); httpServer = _http; ioServer = _io; - app = _app; + requests = requestOperations(_app); // ioServer.on('connection', onConnectionHandler); httpServer.listen(PORT, () => { @@ -47,6 +47,15 @@ beforeEach(() => { describe('Operations must be commutative', () => { test('insert operations should be commutative', async () => { + // setup document + const wid = await requests.workspace.createWorkspace(randomString()); + const id = await requests.document.createDocument(wid); + + // clients join the document + client1.emit('join', id); + client2.emit('join', id); + + // create insert operations const insert1: InsertOperation = { type: 'insert', id: { sender: 'A', counter: 0 }, @@ -61,14 +70,6 @@ describe('Operations must be commutative', () => { parent: { sender: 'root', counter: 0 }, side: 'R', }; - // create a document - const createdResponse = await request(app).post('/documents'); - expect(createdResponse.status).toBe(201); - const id = createdResponse.body.id; - - // clients join the document - client1.emit('join', id); - client2.emit('join', id); // client 1 inserts 'a' and client 2 inserts 'b' client1.emit('operation', [insert1]); @@ -77,12 +78,10 @@ describe('Operations must be commutative', () => { await new Promise(resolve => setTimeout(resolve, 500)); // get the document - const response = await request(app).get('/documents/' + id); - expect(response.status).toBe(200); - const operations = response.body.operations as Operation[]; + const document = await requests.document.getDocument(wid, id); // apply the operations to the tree - applyOperations(tree, operations); + applyOperations(tree, document.content); expect(tree.toString()).toBe('ab'); }); @@ -90,6 +89,15 @@ describe('Operations must be commutative', () => { describe('Operations must be idempotent', () => { test('delete operations should be idempotent', async () => { + // setup document + const wid = await requests.workspace.createWorkspace(randomString()); + const id = await requests.document.createDocument(wid); + + // clients join the document + client1.emit('join', id); + client2.emit('join', id); + + // create insert operations const insert1: InsertOperation = { type: 'insert', id: { sender: 'A', counter: 0 }, @@ -105,15 +113,6 @@ describe('Operations must be idempotent', () => { side: 'R', }; - // create a document - const createdResponse = await request(app).post('/documents'); - expect(createdResponse.status).toBe(201); - const id = createdResponse.body.id; - - // clients join the document - client1.emit('join', id); - client2.emit('join', id); - // both clients insert 'a' client1.emit('operation', [insert1]); client2.emit('operation', [insert2]); @@ -130,9 +129,8 @@ describe('Operations must be idempotent', () => { await new Promise(resolve => setTimeout(resolve, 500)); - const response = await request(app).get('/documents/' + id); - const operations = response.body.operations as Operation[]; - applyOperations(tree, operations); + const document = await requests.document.getDocument(wid, id); + applyOperations(tree, document.content); expect(tree.toString()).toBe('a'); }); }); diff --git a/code/server/test/document/utils.ts b/code/server/test/documents/utils.ts similarity index 100% rename from code/server/test/document/utils.ts rename to code/server/test/documents/utils.ts diff --git a/code/server/test/utils.ts b/code/server/test/utils.ts new file mode 100644 index 00000000..20cd08c4 --- /dev/null +++ b/code/server/test/utils.ts @@ -0,0 +1,5 @@ +export function randomString(length: number = 10): string { + return Math.random() + .toString(36) + .substring(2, 2 + length); +} diff --git a/code/server/test/utils/documentRequests.ts b/code/server/test/utils/documentRequests.ts new file mode 100644 index 00000000..edb00d77 --- /dev/null +++ b/code/server/test/utils/documentRequests.ts @@ -0,0 +1,35 @@ +import { Express } from 'express'; +import request = require('supertest'); +import { DocumentResource, ResourceInputModel, ResourceType } from '@notespace/shared/src/workspace/types/resource'; + +export function documentRequests(app: Express) { + async function createDocument(wid: string, name?: string): Promise { + const resource: ResourceInputModel = { + name: name || 'Untitled', + type: ResourceType.DOCUMENT, + parent: undefined, + }; + const response = await request(app).post(`/workspaces/${wid}`).send(resource); + expect(response.status).toBe(201); + return response.body.id; + } + + async function getDocument(wid: string, id: string): Promise { + const response = await request(app).get(`/workspaces/${wid}/${id}`); + expect(response.status).toBe(200); + if (response.body.type !== ResourceType.DOCUMENT) throw new Error('Resource is not a document'); + return response.body; + } + + async function updateDocument(wid: string, id: string, name: string) { + const response = await request(app).put(`/workspaces/${wid}/${id}`).send({ name }); + expect(response.status).toBe(204); + } + + async function deleteDocument(wid: string, id: string) { + const response = await request(app).delete(`/workspaces/${wid}/${id}`); + expect(response.status).toBe(204); + } + + return { createDocument, getDocument, updateDocument, deleteDocument }; +} diff --git a/code/server/test/utils/requests.ts b/code/server/test/utils/requests.ts new file mode 100644 index 00000000..bc08307e --- /dev/null +++ b/code/server/test/utils/requests.ts @@ -0,0 +1,10 @@ +import { Express } from 'express'; +import { documentRequests } from './documentRequests'; +import { workspaceRequests } from './workspaceRequests'; + +export function requests(app: Express) { + return { + document: documentRequests(app), + workspace: workspaceRequests(app), + }; +} diff --git a/code/server/test/utils/workspaceRequests.ts b/code/server/test/utils/workspaceRequests.ts new file mode 100644 index 00000000..009604af --- /dev/null +++ b/code/server/test/utils/workspaceRequests.ts @@ -0,0 +1,35 @@ +import { Express } from 'express'; +import request = require('supertest'); +import { WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; + +export function workspaceRequests(app: Express) { + async function createWorkspace(name: string): Promise { + const response = await request(app).post('/workspaces').send({ name }); + expect(response.status).toBe(201); + return response.body.id; + } + + async function getWorkspaces(): Promise { + const response = await request(app).get('/workspaces'); + expect(response.status).toBe(200); + return response.body; + } + + async function getWorkspace(id: string, metaOnly: boolean): Promise { + const response = await request(app).get(`/workspaces/${id}?metaOnly=${metaOnly}`); + expect(response.status).toBe(200); + return response.body; + } + + async function updateWorkspace(id: string, name: string) { + const response = await request(app).put(`/workspaces/${id}`).send({ name }); + expect(response.status).toBe(204); + } + + async function deleteWorkspace(id: string) { + const response = await request(app).delete(`/workspaces/${id}`); + expect(response.status).toBe(204); + } + + return { createWorkspace, getWorkspaces, getWorkspace, updateWorkspace, deleteWorkspace }; +} diff --git a/code/shared/src/workspace/types/resource.ts b/code/shared/src/workspace/types/resource.ts index 2511d2d0..d2f7a904 100644 --- a/code/shared/src/workspace/types/resource.ts +++ b/code/shared/src/workspace/types/resource.ts @@ -11,10 +11,9 @@ export interface WorkspaceResource { } export interface ResourceInputModel { - workspace: string; name: string; type: ResourceType; - parent: string; + parent?: string; } export enum ResourceType {