From 8a86046fb7856852ba316d3f67340b956d45cc72 Mon Sep 17 00:00:00 2001 From: Guilherme_F Date: Wed, 22 May 2024 20:40:46 +0100 Subject: [PATCH] Fixed workspace management on postgres --- .../src/domain/workspaces/tree/types.ts | 6 +- .../workspaces/tree/useWorkspaceTree.ts | 69 ++++++++------- .../src/domain/workspaces/tree/utils.ts | 11 +-- .../src/services/resource/resourceService.ts | 2 +- .../src/ui/components/sidebar/Sidebar.tsx | 7 ++ .../sidebar/components/WorkspaceTree.tsx | 8 +- .../contexts/workspace/WorkspaceContext.tsx | 34 ++++---- .../src/ui/contexts/workspace/useResources.ts | 40 ++++++--- code/server/src/sql/create_tables.sql | 13 ++- code/server/src/sql/populate_tables.sql | 21 +++++ .../src/sql/triggers/child_triggers.sql | 83 +++++++++++++++++++ .../src/sql/triggers/resources_triggers.sql | 26 ------ .../src/sql/triggers/workspace_triggers.sql | 16 ++++ .../controllers/http/handlers/errorHandler.ts | 1 + .../http/handlers/resourcesHandlers.ts | 9 +- .../http/handlers/workspacesHandlers.ts | 6 +- .../firestore/FirestoreDocumentsDB.ts | 6 +- .../ts/databases/memory/MemoryWorkspacesDB.ts | 5 +- .../databases/postgres/PostgresResourcesDB.ts | 17 ++-- .../postgres/PostgresWorkspacesDB.ts | 23 +++-- code/server/src/ts/databases/types.ts | 4 +- .../src/ts/services/WorkspacesService.ts | 7 +- code/server/test/utils/documentRequests.ts | 2 +- code/shared/src/workspace/types/resource.ts | 7 +- code/shared/src/workspace/types/workspace.ts | 6 +- 25 files changed, 286 insertions(+), 143 deletions(-) create mode 100644 code/server/src/sql/triggers/child_triggers.sql delete mode 100644 code/server/src/sql/triggers/resources_triggers.sql create mode 100644 code/server/src/sql/triggers/workspace_triggers.sql diff --git a/code/client/src/domain/workspaces/tree/types.ts b/code/client/src/domain/workspaces/tree/types.ts index 84c7c008..812ba20c 100644 --- a/code/client/src/domain/workspaces/tree/types.ts +++ b/code/client/src/domain/workspaces/tree/types.ts @@ -1,10 +1,10 @@ -import { WorkspaceResourceMetadata } from '@notespace/shared/src/workspace/types/resource'; +import { WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; export type TreeNode = { node: WorkspaceTreeNode; children: TreeNode[]; }; -export type WorkspaceTreeNode = WorkspaceResourceMetadata; +export type WorkspaceTreeNode = WorkspaceResource; -export type WorkspaceTreeNodes = Map; +export type WorkspaceTreeNodes = Record; diff --git a/code/client/src/domain/workspaces/tree/useWorkspaceTree.ts b/code/client/src/domain/workspaces/tree/useWorkspaceTree.ts index 659e0476..cbadde4d 100644 --- a/code/client/src/domain/workspaces/tree/useWorkspaceTree.ts +++ b/code/client/src/domain/workspaces/tree/useWorkspaceTree.ts @@ -1,74 +1,73 @@ import { useState } from 'react'; import { rootNode } from '@domain/workspaces/tree/utils'; -import { WorkspaceTreeNode } from '@domain/workspaces/tree/types'; +import { WorkspaceTreeNode, WorkspaceTreeNodes } from '@domain/workspaces/tree/types'; +import { WorkspaceResources } from '@notespace/shared/src/workspace/types/workspace'; function useWorkspaceTree() { - const [nodes, setNodes] = useState>(new Map()); - - function setTree(nodes: WorkspaceTreeNode[]) { - const nodesMap = new Map(nodes.map(node => [node.id, node])); - const root = rootNode( - Array.from(nodes?.values() || []) - .filter(node => node.parent === 'root') - .map(node => node.id) - ); - nodesMap.set('root', root); - setNodes(nodesMap); - } + const [nodes, setNodes] = useState({}); - function getNode(id: string) { - return nodes.get(id); + function setTree(nodes: WorkspaceResources) { + const wid = Object.values(nodes)[0].workspace; + const newNodes = { ...nodes }; + newNodes[wid] = rootNode(newNodes[wid] ? newNodes[wid].children : []); + setNodes(newNodes); } + const getNode = (id: string) => nodes[id]; + function addNode(node: WorkspaceTreeNode) { - const newNodes = new Map(nodes); - const parentNode = newNodes.get(node.parent); + const newNodes = { ...nodes }; + const parentNode = newNodes[node.parent]; if (!parentNode) throw new Error('Invalid parent id: ' + node.parent); - newNodes.set(node.id, node); - newNodes.set(node.parent, { ...parentNode, children: [...parentNode.children, node.id] }); + newNodes[node.id] = node; + newNodes[node.parent] = { ...parentNode, children: [...parentNode.children, node.id] }; setNodes(newNodes); } function removeNode(id: string) { - const node = nodes.get(id); + const node = nodes[id]; + if (!node) throw new Error('Invalid id: ' + id); const { parent } = node; - const parentNode = nodes.get(parent); + const parentNode = nodes[parent]; + if (!parentNode) throw new Error('Invalid parent id: ' + parent); - const newNodes = new Map(nodes); + const newNodes = { ...nodes }; const index = parentNode.children.indexOf(id); + if (index !== -1) parentNode.children.splice(index, 1); - newNodes.delete(id); - newNodes.set(parent, parentNode); + + delete newNodes[id]; + + newNodes[parent] = parentNode; setNodes(newNodes); } function updateNode(id: string, name: string) { - const node = nodes.get(id); + const node = nodes[id]; if (!node) throw new Error('Invalid id: ' + id); - const newNode = { ...node, name }; - nodes.set(id, newNode); - setNodes(new Map(nodes)); + nodes[id] = { ...node, name }; + setNodes({ ...nodes }); } function moveNode(id: string, newParent: string) { - const node = nodes.get(id); + const node = nodes[id]; if (!node) throw new Error('Invalid id: ' + id); const { parent } = node; - const parentNode = nodes.get(parent); + const parentNode = nodes[parent]; if (parentNode) { const index = parentNode.children.indexOf(node.id); if (index !== -1) parentNode.children.splice(index, 1); - nodes.set(parent, parentNode); + nodes[parent] = parentNode; } - const newParentNode = nodes.get(newParent); + const newParentNode = nodes[newParent]; if (!newParentNode) throw new Error('Invalid parent id: ' + newParent); newParentNode.children.push(node.id); node.parent = newParent; - nodes.set(id, node); - nodes.set(newParent, newParentNode); - setNodes(new Map(nodes)); + nodes[id] = node; + nodes[newParent] = newParentNode; + setNodes({ ...nodes }); } function isDescendant(parentId: string, nodeId: string): boolean { diff --git a/code/client/src/domain/workspaces/tree/utils.ts b/code/client/src/domain/workspaces/tree/utils.ts index b74627b2..228ae129 100644 --- a/code/client/src/domain/workspaces/tree/utils.ts +++ b/code/client/src/domain/workspaces/tree/utils.ts @@ -1,17 +1,18 @@ import { ResourceType } from '@notespace/shared/src/workspace/types/resource'; -import { TreeNode, WorkspaceTreeNode } from '@domain/workspaces/tree/types'; +import { TreeNode, WorkspaceTreeNode, WorkspaceTreeNodes } from '@domain/workspaces/tree/types'; -export function getTree(nodes: Map, id: string = 'root'): TreeNode { - const root = nodes.get(id)!; +export function getTree(nodes: WorkspaceTreeNodes, id: string): TreeNode { + const root = nodes[id]; return { node: root, children: root.children.map(id => getTree(nodes, id)), }; } -export function rootNode(children?: string[]): WorkspaceTreeNode { +export function rootNode(wid: string, children?: string[]): WorkspaceTreeNode { return { - id: 'root', + id: wid, + workspace: wid, name: 'root', parent: '', children: children || [], diff --git a/code/client/src/services/resource/resourceService.ts b/code/client/src/services/resource/resourceService.ts index 249cb6a4..54e2af3b 100644 --- a/code/client/src/services/resource/resourceService.ts +++ b/code/client/src/services/resource/resourceService.ts @@ -7,7 +7,7 @@ function resourceService(http: HttpCommunication, wid: string) { } async function createResource(name: string, type: ResourceType, parent?: string): Promise { - const resource: ResourceInputModel = { name, type, parent }; + const resource: ResourceInputModel = { name, type, parent: parent || wid }; const { id } = await http.post(`/workspaces/${wid}`, resource); return id; } diff --git a/code/client/src/ui/components/sidebar/Sidebar.tsx b/code/client/src/ui/components/sidebar/Sidebar.tsx index 616c1667..3351c4e6 100644 --- a/code/client/src/ui/components/sidebar/Sidebar.tsx +++ b/code/client/src/ui/components/sidebar/Sidebar.tsx @@ -7,11 +7,18 @@ import './Sidebar.scss'; import WorkspaceTree from '@ui/components/sidebar/components/WorkspaceTree'; import { FaHome } from 'react-icons/fa'; import { IoMdSettings } from 'react-icons/io'; +import { useEffect } from 'react'; function Sidebar() { const { isOpen, isLocked, isLoaded, handleClick, handleMouseEnter, handleMouseLeave } = useSidebarState(); const { workspace, nodes, operations } = useWorkspace(); + useEffect(() => { + if (workspace) { + console.log('nodes', nodes); + } + }, [nodes, workspace]); + if (!isLoaded) return null; return (
; nodes?: WorkspaceTreeNodes; }; @@ -25,7 +25,7 @@ function WorkspaceTree({ workspace, nodes, operations }: WorkspaceTreeProps) { async function onDrop(e: DragEvent) { if (!dragId) return; - const parentId = e.currentTarget.id || 'root'; + const parentId = e.currentTarget.id || workspace.id; await operations.moveResource(dragId, parentId); } @@ -33,7 +33,7 @@ function WorkspaceTree({ workspace, nodes, operations }: WorkspaceTreeProps) {
    {nodes && - getTree(nodes).children.map(node => ( + getTree(nodes, workspace.id).children.map(node => (
  • Promise; - deleteResource: (id: string) => Promise; - updateResource: (id: string, newProps: Partial) => Promise; - moveResource: (id: string, parent: string) => Promise; -}; - export type WorkspaceContextType = { workspace?: WorkspaceMetaData; resources?: WorkspaceResource[]; - operations?: ResourceOperationsType; + operations?: Omit; nodes?: WorkspaceTreeNodes; }; @@ -28,7 +21,7 @@ export const WorkspaceContext = createContext({}); export function WorkspaceProvider({ children }: { children: React.ReactNode }) { const services = useWorkspaceService(); const [workspace, setWorkspace] = useState(undefined); - const { resources, setResources, tree, operations } = useResources(); + const { resources, tree, operations } = useResources(); const { socket } = useCommunication(); const { publishError } = useError(); const { wid } = useParams(); @@ -38,9 +31,9 @@ export function WorkspaceProvider({ children }: { children: React.ReactNode }) { async function fetchWorkspace() { const { id, name, resources } = await services.getWorkspace(wid!); + console.log('fetchWorkspace', resources); setWorkspace({ id, name }); - setResources(resources); - tree.setTree(resources); + operations.setResources(resources); } socket.emit('joinWorkspace', wid); fetchWorkspace().catch(publishError); @@ -50,8 +43,21 @@ export function WorkspaceProvider({ children }: { children: React.ReactNode }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [wid, services, socket, publishError]); + const newOperations: WorkspaceContextType['operations'] = { + createResource: operations.createResource, + deleteResource: operations.deleteResource, + updateResource: operations.updateResource, + moveResource: operations.moveResource, + }; + + useEffect(() => { + console.log('resources: ', resources); + }, [resources]); + return ( - + {children} ); diff --git a/code/client/src/ui/contexts/workspace/useResources.ts b/code/client/src/ui/contexts/workspace/useResources.ts index edaaa2dc..468e4048 100644 --- a/code/client/src/ui/contexts/workspace/useResources.ts +++ b/code/client/src/ui/contexts/workspace/useResources.ts @@ -1,22 +1,36 @@ -import { - ResourceType, - WorkspaceResource, - WorkspaceResourceMetadata, -} from '@notespace/shared/src/workspace/types/resource'; +import { ResourceType, WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; import useResourceService from '@services/resource/useResourceService'; import useSocketListeners from '@services/communication/socket/useSocketListeners'; import { useCommunication } from '@ui/contexts/communication/useCommunication'; import { useState } from 'react'; import useWorkspaceTree from '@domain/workspaces/tree/useWorkspaceTree'; +import { WorkspaceTreeNodes } from '@domain/workspaces/tree/types'; -function useResources() { +export type UseResourcesType = { + resources: WorkspaceTreeNodes; + tree: ReturnType; + operations: { + setResources: (resources: WorkspaceTreeNodes) => void; + createResource: (name: string, type: ResourceType, parent?: string) => Promise; + deleteResource: (id: string) => Promise; + updateResource: (id: string, newProps: Partial) => Promise; + moveResource: (id: string, parent: string) => Promise; + }; +}; + +function useResources(): UseResourcesType { const service = useResourceService(); const { socket } = useCommunication(); - const [resources, setResources] = useState([]); + const [resources, setResources] = useState({}); const tree = useWorkspaceTree(); + function onSetResources(resources: WorkspaceTreeNodes) { + setResources(resources); + tree.setTree(resources); + } + function onCreateResource(resource: WorkspaceResource) { - setResources([...resources, resource]); + setResources({ ...resources, [resource.id]: resource }); tree.addNode(resource); } @@ -31,7 +45,9 @@ function useResources() { return [id, ...resource.children.flatMap(childId => getChildren(childId))]; }; const idsToRemove = getChildren(id); - setResources(resources.filter(resource => !idsToRemove.includes(resource.id))); + const newResources = { ...resources }; + idsToRemove.forEach(id => delete newResources[id]); + setResources(newResources); tree.removeNode(id); } @@ -39,9 +55,9 @@ function useResources() { await service.deleteResource(id); } - function onUpdateResource(resource: Partial) { + function onUpdateResource(resource: Partial) { if (!resource.id) throw new Error('Resource id is required'); - setResources(resources.map(res => (res.id === resource.id ? { ...res, ...resource } : res))); + setResources({ ...resources, [resource.id]: { ...resources[resource.id], ...resource } }); if (resource.name) tree.updateNode(resource.id, resource.name); if (resource.parent) tree.moveNode(resource.id, resource.parent); } @@ -63,9 +79,9 @@ function useResources() { return { resources, - setResources, tree, operations: { + setResources: onSetResources, createResource, deleteResource, updateResource, diff --git a/code/server/src/sql/create_tables.sql b/code/server/src/sql/create_tables.sql index e4bb8b33..ab20cb43 100644 --- a/code/server/src/sql/create_tables.sql +++ b/code/server/src/sql/create_tables.sql @@ -1,8 +1,12 @@ begin; + -- Create the pgcrypto extension if it doesn't already exist create extension if not exists "pgcrypto"; + + -- Create the enum type for resource_type create type resource_type as enum ('D', 'F'); + -- Create the workspace table create table if not exists workspace ( id char(12) primary key default encode(gen_random_bytes(8), 'base64'), name text not null, @@ -10,15 +14,16 @@ begin; updated_at timestamp not null default now() ); + -- Create the resource table create table if not exists resource ( id char(12) primary key default encode(gen_random_bytes(8), 'base64'), - workspace varchar not null references workspace(id) on delete cascade, + workspace char(12) not null references workspace(id) on delete cascade, name text not null, type resource_type not null, created_at timestamp not null default now(), updated_at timestamp not null default now(), - children char(12)[] not null default '{}', - parent char(12) references resource(id) on delete cascade + parent char(12) default null references resource(id) on delete cascade, + children char(12)[] not null default '{}'::char(12)[] -- Array of resource ids ); -commit; +commit; \ No newline at end of file diff --git a/code/server/src/sql/populate_tables.sql b/code/server/src/sql/populate_tables.sql index e69de29b..96f8c902 100644 --- a/code/server/src/sql/populate_tables.sql +++ b/code/server/src/sql/populate_tables.sql @@ -0,0 +1,21 @@ +begin ; + -- Insert workspaces + insert into workspace (name) values ('Workspace 1'); + insert into workspace (name) values ('Workspace 2'); + + -- Insert some resources + insert into resource (name, type, workspace) + values ('Fol 1', 'F', (select id from workspace where name = 'Workspace 1')); + + insert into resource (name, type, workspace) + values ('Fol 2', 'F', (select id from workspace where name = 'Workspace 2')); + + insert into resource (name, type, workspace) + values ('Doc 1', 'D', (select id from workspace where name = 'Workspace 1')); + + insert into resource (name, type, workspace) + values ('Doc 2', 'D', (select id from workspace where name = 'Workspace 2')); + + -- Insert some children resources + +commit ; \ No newline at end of file diff --git a/code/server/src/sql/triggers/child_triggers.sql b/code/server/src/sql/triggers/child_triggers.sql new file mode 100644 index 00000000..47a8df12 --- /dev/null +++ b/code/server/src/sql/triggers/child_triggers.sql @@ -0,0 +1,83 @@ +begin; + + -- NEW RESOURCE IS CREATED -> UPDATE SELF'S PARENT ID AND APPEND SELF TO PARENT'S CHILDREN ARRAY + create or replace function on_new_resource_created() returns trigger as $$ + begin + --- parent_id is null + if new.parent is null then + if new.id != new.workspace then ---- Workspace resource with root as parent + update resource + set parent = new.workspace + where id = new.id; + ---- Append self to root resource's children array + update resource + set children = array_append(children, new.id) + where id = new.workspace; + return new; + else return new; ---- Root resource - do nothing + end if; + --- parent_id is not null + else + ---- check if parent resource exists + if not exists (select 1 from resource where id = new.parent) then + raise exception 'Parent resource does not exist'; + end if; + --- Append self to children array of parent resource + update resource + set children = array_append(children, new.id) + where id = new.parent; + return new; + end if; + end; + $$ language plpgsql; + + create or replace trigger on_new_resource_created_trigger + after insert on resource + for each row execute function on_new_resource_created(); + +------------------------------------------------------------------------------------------------------------------------ + + -- Resource is deleted -> Remove self from parent's children array + create or replace function on_child_removed() returns trigger as $$ + begin + --- Check if parent resource exists + if old.parent is not null then + ---- Remove self from parent's children array + update resource + set children = array_remove(children, old.id) + where id = old.parent; + end if; + return old; + end; + $$ language plpgsql; + + create or replace trigger on_child_removed_trigger + after delete on resource + for each row execute function on_child_removed(); + + -- Resource is updated -> Update new and old parent's children array + create or replace function on_child_updated() returns trigger as $$ + begin + if new.parent = old.parent then + return new; + end if; + --- Append self to children array of new parent + if new.parent is not null then + update resource + set children = array_append(children, new.id) + where id = new.parent; + end if; + --- Remove self from children array of old parent + if old.parent is not null then + update resource + set children = array_remove(children, old.id) + where id = old.parent; + end if; + return new; + end; + $$ language plpgsql; + + create or replace trigger update_child_resource_trigger + after update on resource + for each row execute function on_child_updated(); +commit ; \ No newline at end of file diff --git a/code/server/src/sql/triggers/resources_triggers.sql b/code/server/src/sql/triggers/resources_triggers.sql deleted file mode 100644 index 06fb3c0d..00000000 --- a/code/server/src/sql/triggers/resources_triggers.sql +++ /dev/null @@ -1,26 +0,0 @@ -begin ; - - create or replace function delete_resource_on_child_delete() returns trigger as $$ - begin - delete from resource where id = old.child; - return old; - end; - $$ language plpgsql; - --- create or replace trigger delete_resource_trigger --- after delete on resource_child --- for each row execute function delete_resource_on_child_delete(); - - create or replace function create_root_resource_on_workspace_create() returns trigger as $$ - begin - insert into resource (id, name, type, parent, workspace) - values (new.id, 'root', 'F', null, new.id); - return new; - end; - $$ language plpgsql; - - create or replace trigger create_root_resource_trigger - after insert on workspace - for each row execute function create_root_resource_on_workspace_create(); - -commit ; \ No newline at end of file diff --git a/code/server/src/sql/triggers/workspace_triggers.sql b/code/server/src/sql/triggers/workspace_triggers.sql new file mode 100644 index 00000000..d1bac693 --- /dev/null +++ b/code/server/src/sql/triggers/workspace_triggers.sql @@ -0,0 +1,16 @@ +begin; + + -- Add root resource to resource table when a workspace is created + create or replace function add_root_resource() returns trigger as $$ + begin + insert into resource (id, workspace, name, type) + values (new.id, new.id, 'root', 'F'); + return new; + end; + $$ language plpgsql; + + create or replace trigger add_root_resource_trigger + after insert on workspace + for each row execute function add_root_resource(); + +commit; \ No newline at end of file diff --git a/code/server/src/ts/controllers/http/handlers/errorHandler.ts b/code/server/src/ts/controllers/http/handlers/errorHandler.ts index 75bfee15..48391334 100644 --- a/code/server/src/ts/controllers/http/handlers/errorHandler.ts +++ b/code/server/src/ts/controllers/http/handlers/errorHandler.ts @@ -22,4 +22,5 @@ export default function errorHandler(error: Error, req: Request, res: Response, const message = response.statusCode === 500 ? 'Internal server error' : error.message; response.send({ error: message }); ErrorLogger.logError(error.message); + console.error(error.stack); } diff --git a/code/server/src/ts/controllers/http/handlers/resourcesHandlers.ts b/code/server/src/ts/controllers/http/handlers/resourcesHandlers.ts index aa16dfef..211e17c0 100644 --- a/code/server/src/ts/controllers/http/handlers/resourcesHandlers.ts +++ b/code/server/src/ts/controllers/http/handlers/resourcesHandlers.ts @@ -1,9 +1,5 @@ import PromiseRouter from 'express-promise-router'; -import { - ResourceInputModel, - WorkspaceResource, - WorkspaceResourceMetadata, -} from '@notespace/shared/src/workspace/types/resource'; +import { ResourceInputModel, WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; import { httpResponse } from '@controllers/http/utils/httpResponse'; import { Request, Response } from 'express'; import { ResourcesService } from '@services/ResourcesService'; @@ -28,7 +24,8 @@ function resourcesHandlers(service: ResourcesService, io: Server) { if (!type) throw new InvalidParameterError('Resource type is required'); const id = await service.createResource(wid, name, type, parent); - const createdResource: WorkspaceResourceMetadata = { id, ...resource, children: [], parent: parent || wid }; + console.log('created resource', id, wid, resource, parent); + const createdResource: WorkspaceResource = { id, workspace: wid, ...resource, children: [], parent: parent || wid }; io.in(wid).emit('createdResource', createdResource); httpResponse.created(res).json({ id }); }; diff --git a/code/server/src/ts/controllers/http/handlers/workspacesHandlers.ts b/code/server/src/ts/controllers/http/handlers/workspacesHandlers.ts index 550b405f..466836f2 100644 --- a/code/server/src/ts/controllers/http/handlers/workspacesHandlers.ts +++ b/code/server/src/ts/controllers/http/handlers/workspacesHandlers.ts @@ -14,6 +14,7 @@ function workspacesHandlers(services: Services, io: Server) { const createWorkspace = async (req: Request, res: Response) => { const { name } = req.body as WorkspaceInputModel; if (!name) throw new InvalidParameterError('Workspace name is required'); + const id = await services.workspace.createWorkspace(name); io.emit('createdWorkspace', { id, name }); httpResponse.created(res).json({ id }); @@ -32,8 +33,9 @@ function workspacesHandlers(services: Services, io: Server) { */ const getWorkspace = async (req: Request, res: Response) => { const { wid } = req.params; - const { metaOnly } = req.query; if (!wid) throw new InvalidParameterError('Workspace id is required'); + + const { metaOnly } = req.query; const workspace = await services.workspace.getWorkspace(wid, metaOnly === 'true'); httpResponse.ok(res).json(workspace); }; @@ -41,6 +43,7 @@ function workspacesHandlers(services: Services, io: Server) { const updateWorkspace = async (req: Request, res: Response) => { const { wid } = req.params; if (!wid) throw new InvalidParameterError('Workspace id is required'); + const { name } = req.body as WorkspaceMetaData; if (!name) throw new InvalidParameterError('Workspace name is required'); @@ -57,6 +60,7 @@ function workspacesHandlers(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.emit('deletedWorkspace', wid); httpResponse.noContent(res).send(); diff --git a/code/server/src/ts/databases/firestore/FirestoreDocumentsDB.ts b/code/server/src/ts/databases/firestore/FirestoreDocumentsDB.ts index e5bc0c82..2d09a16e 100644 --- a/code/server/src/ts/databases/firestore/FirestoreDocumentsDB.ts +++ b/code/server/src/ts/databases/firestore/FirestoreDocumentsDB.ts @@ -12,7 +12,10 @@ export class FirestoreDocumentsDB implements DocumentsRepository { async createDocument(wid: string, id: string) { const documents = await this.getWorkspace(wid); const docData: DocumentContent = { operations: [] }; - await documents.doc(id).set(docData); + + const doc = documents.doc(id); + + await doc.set(docData); return id; } @@ -29,6 +32,7 @@ export class FirestoreDocumentsDB implements DocumentsRepository { async updateDocument(wid: string, id: string, newOperations: Operation[]) { const doc = await this.getDoc(wid, id); + console.log('updating document', id, newOperations); await doc.update({ operations: FieldValue.arrayUnion(...newOperations) }); } diff --git a/code/server/src/ts/databases/memory/MemoryWorkspacesDB.ts b/code/server/src/ts/databases/memory/MemoryWorkspacesDB.ts index 2c56151f..5e65ece1 100644 --- a/code/server/src/ts/databases/memory/MemoryWorkspacesDB.ts +++ b/code/server/src/ts/databases/memory/MemoryWorkspacesDB.ts @@ -1,7 +1,6 @@ import { WorkspacesRepository } from '@databases/types'; -import { WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; +import { WorkspaceMetaData, WorkspaceResources } from '@notespace/shared/src/workspace/types/workspace'; import { memoryDB } from '@databases/memory/Memory'; -import { WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; export class MemoryWorkspacesDB implements WorkspacesRepository { async createWorkspace(name: string): Promise { @@ -13,7 +12,7 @@ export class MemoryWorkspacesDB implements WorkspacesRepository { async getWorkspace(id: string): Promise { return memoryDB.getWorkspace(id); } - async getWorkspaceResources(id: string): Promise { + async getWorkspaceResources(id: string): Promise { return memoryDB.getWorkspace(id).resources; } async updateWorkspace(id: string, name: string): Promise { diff --git a/code/server/src/ts/databases/postgres/PostgresResourcesDB.ts b/code/server/src/ts/databases/postgres/PostgresResourcesDB.ts index 6418a547..5ed5da94 100644 --- a/code/server/src/ts/databases/postgres/PostgresResourcesDB.ts +++ b/code/server/src/ts/databases/postgres/PostgresResourcesDB.ts @@ -6,20 +6,25 @@ import sql from '@databases/postgres/config'; export class PostgresResourcesDB implements ResourcesRepository { async createResource(wid: string, name: string, type: ResourceType, parent?: string): Promise { - const resource = { workspace: wid, name, parent: parent || wid, type }; - console.log('resource', resource); + const resource = { + workspace: wid, + parent: parent || wid, + name, + type, + }; + const results = await sql` - insert into resource ${sql(resource)} + insert into resource ${sql(resource)} returning id `; + if (isEmpty(results)) throw new Error('Resource not created'); return results[0].id; } async getResource(id: string): Promise { - const results: WorkspaceResource[] = await sql` - select * from resource where id = ${id} - `; + const results: WorkspaceResource[] = await sql`select * from resource where id = ${id}`; + if (isEmpty(results)) throw new NotFoundError('Resource not found'); return results[0]; } diff --git a/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts b/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts index db98ea9c..9feee0d0 100644 --- a/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts +++ b/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts @@ -1,5 +1,5 @@ import { WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; -import { WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; +import { WorkspaceMetaData, WorkspaceResources } from '@notespace/shared/src/workspace/types/workspace'; import { NotFoundError } from '@domain/errors/errors'; import { WorkspacesRepository } from '@databases/types'; import { isEmpty } from 'lodash'; @@ -44,11 +44,20 @@ export class PostgresWorkspacesDB implements WorkspacesRepository { if (isEmpty(results)) throw new NotFoundError(`Workspace not found`); } - async getWorkspaceResources(id: string): Promise { - return sql` - select json_object_agg(id, r) - from resource r - where workspace = ${id} and id != ${id} - `; + async getWorkspaceResources(id: string): Promise { + const results: WorkspaceResource[] = ( + await sql` + select row_to_json(t) as resources + from( + select id, name, type, parent, children + from resource + where workspace = ${id} + group by id + order by created_at desc + ) as t + ` + ).map(r => r.resources); + + return Object.fromEntries(results.map(r => [r.id, r])); } } diff --git a/code/server/src/ts/databases/types.ts b/code/server/src/ts/databases/types.ts index 89027cb5..5ad9287a 100644 --- a/code/server/src/ts/databases/types.ts +++ b/code/server/src/ts/databases/types.ts @@ -1,7 +1,7 @@ import { DocumentContent } from '@notespace/shared/src/workspace/types/document'; import { Operation } from '@notespace/shared/src/document/types/operations'; import { ResourceType, WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; -import { WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; +import { WorkspaceMetaData, WorkspaceResources } from '@notespace/shared/src/workspace/types/workspace'; /** * Document Repository - Interface for handling resources content management @@ -34,7 +34,7 @@ export interface WorkspacesRepository { getWorkspace: (id: string) => Promise; updateWorkspace: (id: string, name: string) => Promise; deleteWorkspace: (id: string) => Promise; - getWorkspaceResources: (id: string) => Promise; + getWorkspaceResources: (id: string) => Promise; } export interface Databases { diff --git a/code/server/src/ts/services/WorkspacesService.ts b/code/server/src/ts/services/WorkspacesService.ts index 5270a9eb..197c085e 100644 --- a/code/server/src/ts/services/WorkspacesService.ts +++ b/code/server/src/ts/services/WorkspacesService.ts @@ -1,5 +1,4 @@ -import { WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; -import { Workspace, WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; +import { Workspace, WorkspaceMetaData, WorkspaceResources } from '@notespace/shared/src/workspace/types/workspace'; import { DocumentsRepository, WorkspacesRepository } from '@databases/types'; export class WorkspacesService { @@ -32,12 +31,12 @@ export class WorkspacesService { async getWorkspace(id: string, metaOnly: boolean = false): Promise { const metadata = await this.workspaces.getWorkspace(id); - if (metaOnly) return { ...metadata, resources: [] }; + if (metaOnly) return { ...metadata, resources: {} }; const resources = await this.getWorkspaceResources(id); return { ...metadata, resources }; } - private async getWorkspaceResources(id: string): Promise { + private async getWorkspaceResources(id: string): Promise { return await this.workspaces.getWorkspaceResources(id); } } diff --git a/code/server/test/utils/documentRequests.ts b/code/server/test/utils/documentRequests.ts index 60c119ca..4db8329d 100644 --- a/code/server/test/utils/documentRequests.ts +++ b/code/server/test/utils/documentRequests.ts @@ -7,7 +7,7 @@ export function documentRequests(app: Express) { const resource: ResourceInputModel = { name: name || 'Untitled', type: ResourceType.DOCUMENT, - parent: 'root', + parent: wid, }; const response = await request(app).post(`/workspaces/${wid}`).send(resource); expect(response.status).toBe(201); diff --git a/code/shared/src/workspace/types/resource.ts b/code/shared/src/workspace/types/resource.ts index 76465d31..2ce9b104 100644 --- a/code/shared/src/workspace/types/resource.ts +++ b/code/shared/src/workspace/types/resource.ts @@ -21,9 +21,6 @@ export enum ResourceType { FOLDER = "F", } -export interface WorkspaceResourceMetadata - extends Omit {} - export interface FolderResource extends WorkspaceResource { type: ResourceType.FOLDER; } @@ -47,11 +44,11 @@ export interface DocumentResource extends WorkspaceResource { // children: [], // }); -export interface DocumentResourceMetadata extends WorkspaceResourceMetadata { +export interface DocumentResourceMetadata extends WorkspaceResource { type: ResourceType.DOCUMENT; } -export interface FolderResourceMetadata extends WorkspaceResourceMetadata { +export interface FolderResourceMetadata extends WorkspaceResource { type: ResourceType.FOLDER; } diff --git a/code/shared/src/workspace/types/workspace.ts b/code/shared/src/workspace/types/workspace.ts index 8706015c..4cb8fe2d 100644 --- a/code/shared/src/workspace/types/workspace.ts +++ b/code/shared/src/workspace/types/workspace.ts @@ -1,4 +1,4 @@ -import { WorkspaceResource, WorkspaceResourceMetadata } from "./resource"; +import { WorkspaceResource } from "./resource"; export type WorkspaceMetaData = { name: string; @@ -6,7 +6,7 @@ export type WorkspaceMetaData = { }; export type Workspace = WorkspaceMetaData & { - resources: WorkspaceResource[]; + resources: WorkspaceResources; }; export interface WorkspaceInputModel { @@ -17,4 +17,4 @@ export interface WorkspaceInputModel { // members: string[]; } -export type WorkspaceResources = Map; +export type WorkspaceResources = Record;