diff --git a/server/src/lib/workspaces.ts b/server/src/lib/workspaces.ts new file mode 100644 index 00000000..4d25d329 --- /dev/null +++ b/server/src/lib/workspaces.ts @@ -0,0 +1,215 @@ +import { database } from '@/data/database'; +import { CreateNode, SelectAccount } from '@/data/schema'; +import { + WorkspaceRole, + WorkspaceStatus, + WorkspaceUserStatus, +} from '@/types/workspaces'; +import { generateId, IdType } from '@/lib/id'; +import * as Y from 'yjs'; +import { fromUint8Array } from 'js-base64'; +import { NodeCreatedEvent } from '@/types/events'; +import { enqueueEvent } from '@/queues/events'; + +export const createDefaultWorkspace = async (account: SelectAccount) => { + const createdAt = new Date(); + const workspaceId = generateId(IdType.Workspace); + const workspaceName = `${account.name}'s Workspace`; + + const user = buildUserNodeCreate(workspaceId, account); + const space = buildSpaceNodeCreate(workspaceId, user.id); + const page = buildPageNodeCreate(workspaceId, space.id, user.id); + const channel = buildChannelNodeCreate(workspaceId, space.id, user.id); + + const nodesToCreate = [user, space, page, channel]; + + await database.transaction().execute(async (trx) => { + await trx + .insertInto('workspaces') + .values({ + id: workspaceId, + name: workspaceName, + description: 'Personal workspace for ' + account.name, + avatar: account.avatar, + created_at: createdAt, + created_by: account.id, + status: WorkspaceStatus.Active, + version_id: generateId(IdType.Version), + }) + .execute(); + + await trx + .insertInto('workspace_users') + .values({ + id: user.id, + account_id: account.id, + workspace_id: workspaceId, + role: WorkspaceRole.Owner, + created_at: createdAt, + created_by: account.id, + status: WorkspaceUserStatus.Active, + version_id: generateId(IdType.Version), + }) + .execute(); + + await trx.insertInto('nodes').values(nodesToCreate).execute(); + }); + + for (const node of nodesToCreate) { + const event = buildNodeCreateEvent(node); + await enqueueEvent(event); + } +}; + +const buildUserNodeCreate = ( + workspaceId: string, + account: SelectAccount, +): CreateNode => { + const id = generateId(IdType.User); + const versionId = generateId(IdType.Version); + const doc = new Y.Doc({ + guid: id, + }); + + const attributesMap = doc.getMap('attributes'); + doc.transact(() => { + attributesMap.set('type', 'user'); + attributesMap.set('name', account.name); + attributesMap.set('avatar', account.avatar); + attributesMap.set('email', account.email); + attributesMap.set('role', WorkspaceRole.Owner); + attributesMap.set('accountId', account.id); + }); + + const attributes = JSON.stringify(attributesMap.toJSON()); + const state = fromUint8Array(Y.encodeStateAsUpdate(doc)); + + return { + id, + workspace_id: workspaceId, + created_at: new Date(), + created_by: account.id, + version_id: versionId, + server_created_at: new Date(), + attributes, + state, + }; +}; + +const buildSpaceNodeCreate = ( + workspaceId: string, + userId: string, +): CreateNode => { + const id = generateId(IdType.Space); + const versionId = generateId(IdType.Version); + const doc = new Y.Doc({ + guid: id, + }); + + const attributesMap = doc.getMap('attributes'); + doc.transact(() => { + attributesMap.set('type', 'space'); + attributesMap.set('name', 'Home'); + attributesMap.set('description', 'Home space'); + + attributesMap.set('collaborators', new Y.Map()); + const collaboratorsMap = attributesMap.get( + 'collaborators', + ) as Y.Map; + + collaboratorsMap.set(userId, WorkspaceRole.Owner); + }); + + const attributes = JSON.stringify(attributesMap.toJSON()); + const state = fromUint8Array(Y.encodeStateAsUpdate(doc)); + + return { + id, + workspace_id: workspaceId, + created_at: new Date(), + created_by: userId, + version_id: versionId, + server_created_at: new Date(), + attributes, + state, + }; +}; + +const buildPageNodeCreate = ( + workspaceId: string, + spaceId: string, + userId: string, +): CreateNode => { + const id = generateId(IdType.Page); + const versionId = generateId(IdType.Version); + const doc = new Y.Doc({ + guid: id, + }); + + const attributesMap = doc.getMap('attributes'); + doc.transact(() => { + attributesMap.set('type', 'page'); + attributesMap.set('name', 'Notes'); + attributesMap.set('parentId', spaceId); + }); + + const attributes = JSON.stringify(attributesMap.toJSON()); + const state = fromUint8Array(Y.encodeStateAsUpdate(doc)); + + return { + id, + workspace_id: workspaceId, + created_at: new Date(), + created_by: userId, + version_id: versionId, + server_created_at: new Date(), + attributes, + state, + }; +}; + +const buildChannelNodeCreate = ( + workspaceId: string, + spaceId: string, + userId: string, +): CreateNode => { + const id = generateId(IdType.Channel); + const versionId = generateId(IdType.Version); + const doc = new Y.Doc({ + guid: id, + }); + + const attributesMap = doc.getMap('attributes'); + doc.transact(() => { + attributesMap.set('type', 'channel'); + attributesMap.set('parentId', spaceId); + attributesMap.set('name', 'Discussions'); + }); + + const attributes = JSON.stringify(attributesMap.toJSON()); + const state = fromUint8Array(Y.encodeStateAsUpdate(doc)); + + return { + id, + workspace_id: workspaceId, + created_at: new Date(), + created_by: userId, + version_id: versionId, + server_created_at: new Date(), + attributes, + state, + }; +}; + +const buildNodeCreateEvent = (node: CreateNode): NodeCreatedEvent => { + return { + type: 'node_created', + id: node.id, + workspaceId: node.workspace_id, + attributes: JSON.parse(node.attributes ?? '{}'), + createdBy: node.created_by, + createdAt: node.created_at.toISOString(), + versionId: node.version_id, + serverCreatedAt: node.server_created_at.toISOString(), + }; +}; diff --git a/server/src/routes/accounts.ts b/server/src/routes/accounts.ts index 9ffaf25d..194bc4c6 100644 --- a/server/src/routes/accounts.ts +++ b/server/src/routes/accounts.ts @@ -26,6 +26,7 @@ import { ServerNodeAttributes } from '@/types/nodes'; import { NodeUpdatedEvent } from '@/types/events'; import { enqueueEvent } from '@/queues/events'; import { SelectAccount } from '@/data/schema'; +import { createDefaultWorkspace } from '@/lib/workspaces'; const GoogleUserInfoUrl = 'https://www.googleapis.com/oauth2/v1/userinfo'; const SaltRounds = 10; @@ -87,12 +88,7 @@ accountsRouter.post('/register/email', async (req: Request, res: Response) => { }); } - const output = await buildLoginOutput( - account.id, - account.name, - account.email, - ); - + const output = await buildLoginOutput(account); return res.status(200).json(output); }); @@ -135,11 +131,7 @@ accountsRouter.post('/login/email', async (req: Request, res: Response) => { }); } - const output = await buildLoginOutput( - account.id, - account.name, - account.email, - ); + const output = await buildLoginOutput(account); return res.status(200).json(output); }); @@ -187,11 +179,7 @@ accountsRouter.post('/login/google', async (req: Request, res: Response) => { .execute(); } - const output = await buildLoginOutput( - existingAccount.id, - existingAccount.name, - existingAccount.email, - ); + const output = await buildLoginOutput(existingAccount); return res.status(200).json(output); } @@ -215,11 +203,7 @@ accountsRouter.post('/login/google', async (req: Request, res: Response) => { }); } - const output = await buildLoginOutput( - newAccount.id, - newAccount.name, - newAccount.email, - ); + const output = await buildLoginOutput(newAccount); return res.status(200).json(output); }); @@ -400,16 +384,24 @@ accountsRouter.put( ); const buildLoginOutput = async ( - id: string, - name: string, - email: string, + account: SelectAccount, ): Promise => { - const workspaceUsers = await database + let workspaceUsers = await database .selectFrom('workspace_users') - .where('account_id', '=', id) + .where('account_id', '=', account.id) .selectAll() .execute(); + if (workspaceUsers.length === 0) { + await createDefaultWorkspace(account); + + workspaceUsers = await database + .selectFrom('workspace_users') + .where('account_id', '=', account.id) + .selectAll() + .execute(); + } + const workspaceOutputs: WorkspaceOutput[] = []; if (workspaceUsers.length > 0) { const workspaceIds = workspaceUsers.map((wu) => wu.workspace_id); @@ -463,7 +455,7 @@ const buildLoginOutput = async ( .insertInto('devices') .values({ id: deviceId, - account_id: id, + account_id: account.id, token_hash: hash, token_salt: salt, token_generated_at: new Date(), @@ -481,9 +473,9 @@ const buildLoginOutput = async ( return { account: { token, - id, - name, - email, + id: account.id, + name: account.name, + email: account.email, deviceId: device.id, }, workspaces: workspaceOutputs,