mirror of
https://github.com/colanode/colanode.git
synced 2025-12-25 16:09:31 +01:00
Add workspace as root node
This commit is contained in:
@@ -17,6 +17,7 @@ const createNodesTable: Migration = {
|
||||
.stored()
|
||||
.references('nodes.id')
|
||||
.onDelete('cascade')
|
||||
.notNull()
|
||||
)
|
||||
.addColumn('index', 'text', (col) =>
|
||||
col.generatedAlwaysAs(sql`json_extract(attributes, '$.index')`).stored()
|
||||
@@ -140,15 +141,11 @@ const createNodePathsTable: Migration = {
|
||||
AFTER INSERT ON nodes
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Insert direct path from the new node to itself
|
||||
INSERT INTO node_paths (ancestor_id, descendant_id, level)
|
||||
VALUES (NEW.id, NEW.id, 0);
|
||||
|
||||
-- Insert paths from ancestors to the new node
|
||||
INSERT INTO node_paths (ancestor_id, descendant_id, level)
|
||||
SELECT ancestor_id, NEW.id, level + 1
|
||||
FROM node_paths
|
||||
WHERE descendant_id = NEW.parent_id;
|
||||
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
|
||||
END;
|
||||
`.execute(db);
|
||||
|
||||
@@ -166,7 +163,7 @@ const createNodePathsTable: Migration = {
|
||||
INSERT INTO node_paths (ancestor_id, descendant_id, level)
|
||||
SELECT ancestor_id, NEW.id, level + 1
|
||||
FROM node_paths
|
||||
WHERE descendant_id = NEW.parent_id;
|
||||
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
|
||||
END;
|
||||
`.execute(db);
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ColumnType, Insertable, Selectable, Updateable } from 'kysely';
|
||||
|
||||
interface NodeTable {
|
||||
id: ColumnType<string, string, never>;
|
||||
parent_id: ColumnType<string | null, never, never>;
|
||||
parent_id: ColumnType<string, never, never>;
|
||||
type: ColumnType<string, never, never>;
|
||||
index: ColumnType<string | null, never, never>;
|
||||
attributes: ColumnType<string, string, string>;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { databaseManager } from '@/main/data/database-manager';
|
||||
import { EmailLoginMutationInput } from '@/operations/mutations/email-login';
|
||||
import { MutationChange, MutationHandler, MutationResult } from '@/main/types';
|
||||
import { httpClient } from '@/lib/http-client';
|
||||
import { toUint8Array } from 'js-base64';
|
||||
|
||||
export class EmailLoginMutationHandler
|
||||
implements MutationHandler<EmailLoginMutationInput>
|
||||
@@ -84,37 +83,6 @@ export class EmailLoginMutationHandler
|
||||
});
|
||||
});
|
||||
|
||||
for (const workspace of data.workspaces) {
|
||||
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(
|
||||
workspace.user.id
|
||||
);
|
||||
|
||||
const user = workspace.user.node;
|
||||
await workspaceDatabase
|
||||
.insertInto('nodes')
|
||||
.values({
|
||||
id: user.id,
|
||||
attributes: JSON.stringify(user.attributes),
|
||||
state: toUint8Array(user.state),
|
||||
created_at: user.createdAt,
|
||||
created_by: user.createdBy,
|
||||
updated_at: user.updatedAt,
|
||||
updated_by: user.updatedBy,
|
||||
server_created_at: user.serverCreatedAt,
|
||||
server_updated_at: user.serverUpdatedAt,
|
||||
version_id: user.versionId,
|
||||
server_version_id: user.versionId,
|
||||
})
|
||||
.onConflict((cb) => cb.doNothing())
|
||||
.execute();
|
||||
|
||||
changedTables.push({
|
||||
type: 'workspace',
|
||||
table: 'nodes',
|
||||
userId: workspace.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
output: {
|
||||
success: true,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { databaseManager } from '@/main/data/database-manager';
|
||||
import { httpClient } from '@/lib/http-client';
|
||||
import { EmailRegisterMutationInput } from '@/operations/mutations/email-register';
|
||||
import { MutationChange, MutationHandler, MutationResult } from '@/main/types';
|
||||
import { toUint8Array } from 'js-base64';
|
||||
|
||||
export class EmailRegisterMutationHandler
|
||||
implements MutationHandler<EmailRegisterMutationInput>
|
||||
@@ -85,37 +84,6 @@ export class EmailRegisterMutationHandler
|
||||
});
|
||||
});
|
||||
|
||||
for (const workspace of data.workspaces) {
|
||||
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(
|
||||
workspace.user.id
|
||||
);
|
||||
|
||||
const user = workspace.user.node;
|
||||
await workspaceDatabase
|
||||
.insertInto('nodes')
|
||||
.values({
|
||||
id: user.id,
|
||||
attributes: JSON.stringify(user.attributes),
|
||||
state: toUint8Array(user.state),
|
||||
created_at: user.createdAt,
|
||||
created_by: user.createdBy,
|
||||
updated_at: user.updatedAt,
|
||||
updated_by: user.updatedBy,
|
||||
server_created_at: user.serverCreatedAt,
|
||||
server_updated_at: user.serverUpdatedAt,
|
||||
version_id: user.versionId,
|
||||
server_version_id: user.versionId,
|
||||
})
|
||||
.onConflict((cb) => cb.doNothing())
|
||||
.execute();
|
||||
|
||||
changedTables.push({
|
||||
type: 'workspace',
|
||||
table: 'nodes',
|
||||
userId: workspace.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
output: {
|
||||
success: true,
|
||||
|
||||
@@ -3,8 +3,7 @@ import { socketManager } from '@/main/sockets/socket-manager';
|
||||
import { hasInsertChanges, hasUpdateChanges } from '@/main/utils';
|
||||
import { MutationHandler, MutationResult } from '@/main/types';
|
||||
import { ServerNodeSyncMutationInput } from '@/operations/mutations/server-node-sync';
|
||||
import { toUint8Array } from 'js-base64';
|
||||
import * as Y from 'yjs';
|
||||
import { YDoc } from '@colanode/crdt';
|
||||
|
||||
export class ServerNodeSyncMutationHandler
|
||||
implements MutationHandler<ServerNodeSyncMutationInput>
|
||||
@@ -44,18 +43,15 @@ export class ServerNodeSyncMutationHandler
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!existingNode) {
|
||||
const doc = new Y.Doc({ guid: input.id });
|
||||
const state = toUint8Array(input.state);
|
||||
Y.applyUpdate(doc, state);
|
||||
|
||||
const attributesMap = doc.getMap('attributes');
|
||||
const attributes = JSON.stringify(attributesMap.toJSON());
|
||||
const ydoc = new YDoc(input.id, input.state);
|
||||
const attributes = ydoc.getAttributes();
|
||||
const state = ydoc.getState();
|
||||
|
||||
const result = await workspaceDatabase
|
||||
.insertInto('nodes')
|
||||
.values({
|
||||
id: input.id,
|
||||
attributes: attributes,
|
||||
attributes: JSON.stringify(attributes),
|
||||
state: state,
|
||||
created_at: input.createdAt,
|
||||
created_by: input.createdBy,
|
||||
@@ -89,19 +85,17 @@ export class ServerNodeSyncMutationHandler
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const doc = new Y.Doc({ guid: input.id });
|
||||
Y.applyUpdate(doc, existingNode.state);
|
||||
Y.applyUpdate(doc, toUint8Array(input.state));
|
||||
const ydoc = new YDoc(input.id, existingNode.state);
|
||||
ydoc.applyUpdate(input.state);
|
||||
|
||||
const attributesMap = doc.getMap('attributes');
|
||||
const attributes = JSON.stringify(attributesMap.toJSON());
|
||||
const state = Y.encodeStateAsUpdate(doc);
|
||||
const attributes = ydoc.getAttributes();
|
||||
const state = ydoc.getState();
|
||||
|
||||
const result = await workspaceDatabase
|
||||
.updateTable('nodes')
|
||||
.set({
|
||||
state: state,
|
||||
attributes: attributes,
|
||||
attributes: JSON.stringify(attributes),
|
||||
server_created_at: input.serverCreatedAt,
|
||||
server_updated_at: input.serverUpdatedAt,
|
||||
server_version_id: input.versionId,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { httpClient } from '@/lib/http-client';
|
||||
import { WorkspaceCreateMutationInput } from '@/operations/mutations/workspace-create';
|
||||
import { MutationHandler, MutationResult } from '@/main/types';
|
||||
import { WorkspaceOutput } from '@/types/workspaces';
|
||||
import { toUint8Array } from 'js-base64';
|
||||
|
||||
export class WorkspaceCreateMutationHandler
|
||||
implements MutationHandler<WorkspaceCreateMutationInput>
|
||||
@@ -60,29 +59,6 @@ export class WorkspaceCreateMutationHandler
|
||||
.onConflict((cb) => cb.doNothing())
|
||||
.execute();
|
||||
|
||||
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(
|
||||
data.user.id
|
||||
);
|
||||
|
||||
const user = data.user.node;
|
||||
await workspaceDatabase
|
||||
.insertInto('nodes')
|
||||
.values({
|
||||
id: user.id,
|
||||
attributes: JSON.stringify(user.attributes),
|
||||
state: toUint8Array(user.state),
|
||||
created_at: user.createdAt,
|
||||
created_by: user.createdBy,
|
||||
updated_at: user.updatedAt,
|
||||
updated_by: user.updatedBy,
|
||||
server_created_at: user.serverCreatedAt,
|
||||
server_updated_at: user.serverUpdatedAt,
|
||||
version_id: user.versionId,
|
||||
server_version_id: user.versionId,
|
||||
})
|
||||
.onConflict((cb) => cb.doNothing())
|
||||
.execute();
|
||||
|
||||
return {
|
||||
output: {
|
||||
id: data.id,
|
||||
@@ -95,7 +71,7 @@ export class WorkspaceCreateMutationHandler
|
||||
{
|
||||
type: 'workspace',
|
||||
table: 'nodes',
|
||||
userId: user.id,
|
||||
userId: data.user.id,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
QueryHandler,
|
||||
QueryResult,
|
||||
} from '@/main/types';
|
||||
import { NodeTypes } from '@colanode/core';
|
||||
|
||||
export class BreadcrumbListQueryHandler
|
||||
implements QueryHandler<BreadcrumbListQueryInput>
|
||||
@@ -79,6 +80,7 @@ export class BreadcrumbListQueryHandler
|
||||
SELECT n.*
|
||||
FROM nodes n
|
||||
INNER JOIN breadcrumb_nodes b ON n.id = b.parent_id
|
||||
WHERE n.type <> ${NodeTypes.Workspace}
|
||||
)
|
||||
SELECT n.*
|
||||
FROM breadcrumb_nodes n;
|
||||
|
||||
@@ -90,7 +90,7 @@ export class SidebarSpaceListQueryHandler
|
||||
WITH space_nodes AS (
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE parent_id IS NULL AND type = ${NodeTypes.Space}
|
||||
WHERE type = ${NodeTypes.Space}
|
||||
),
|
||||
space_children_nodes AS (
|
||||
SELECT *
|
||||
|
||||
@@ -47,11 +47,7 @@ class NodeManager {
|
||||
|
||||
await workspaceDatabase.transaction().execute(async (transaction) => {
|
||||
for (const input of inputs) {
|
||||
const model = registry[input.attributes.type];
|
||||
if (!model) {
|
||||
throw new Error('Invalid node type');
|
||||
}
|
||||
|
||||
const model = registry.getModel(input.attributes.type);
|
||||
if (!model.schema.safeParse(input.attributes).success) {
|
||||
throw new Error('Invalid attributes');
|
||||
}
|
||||
@@ -77,7 +73,7 @@ class NodeManager {
|
||||
}
|
||||
|
||||
const ydoc = new YDoc(input.id);
|
||||
ydoc.updateAttributes(model.schema, input.attributes);
|
||||
ydoc.updateAttributes(input.attributes);
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const versionId = generateId(IdType.Version);
|
||||
@@ -190,11 +186,7 @@ class NodeManager {
|
||||
const updatedAt = new Date().toISOString();
|
||||
const updatedAttributes = updater(node.attributes);
|
||||
|
||||
const model = registry[node.type];
|
||||
if (!model) {
|
||||
throw new Error('Invalid node type');
|
||||
}
|
||||
|
||||
const model = registry.getModel(node.type);
|
||||
if (!model.schema.safeParse(updatedAttributes).success) {
|
||||
throw new Error('Invalid attributes');
|
||||
}
|
||||
@@ -204,10 +196,7 @@ class NodeManager {
|
||||
}
|
||||
|
||||
const ydoc = new YDoc(nodeRow.id, nodeRow.state);
|
||||
ydoc.updateAttributes(
|
||||
registry[updatedAttributes.type].schema,
|
||||
updatedAttributes
|
||||
);
|
||||
ydoc.updateAttributes(updatedAttributes);
|
||||
|
||||
const updates = ydoc.getEncodedUpdates();
|
||||
if (updates.length === 0) {
|
||||
@@ -283,11 +272,7 @@ class NodeManager {
|
||||
throw new Error('Node not found');
|
||||
}
|
||||
|
||||
const model = registry[node.type];
|
||||
if (!model) {
|
||||
throw new Error('Invalid node type');
|
||||
}
|
||||
|
||||
const model = registry.getModel(node.type);
|
||||
const context = new NodeMutationContext(
|
||||
workspace.account_id,
|
||||
workspace.workspace_id,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { ServerNode } from '@/types/nodes';
|
||||
|
||||
export type WorkspaceRole = 'admin' | 'editor' | 'collaborator' | 'viewer';
|
||||
export type WorkspaceRole =
|
||||
| 'owner'
|
||||
| 'admin'
|
||||
| 'editor'
|
||||
| 'collaborator'
|
||||
| 'viewer';
|
||||
|
||||
export type Workspace = {
|
||||
id: string;
|
||||
@@ -26,7 +31,6 @@ export type WorkspaceUserOutput = {
|
||||
id: string;
|
||||
accountId: string;
|
||||
role: WorkspaceRole;
|
||||
node: ServerNode;
|
||||
};
|
||||
|
||||
export type WorkspaceUsersInviteOutput = {
|
||||
|
||||
@@ -109,6 +109,7 @@ const createNodesTable: Migration = {
|
||||
.stored()
|
||||
.references('nodes.id')
|
||||
.onDelete('cascade')
|
||||
.notNull()
|
||||
)
|
||||
.addColumn('index', 'varchar(30)', (col) =>
|
||||
col.generatedAlwaysAs(sql`(attributes->>'index')::VARCHAR(30)`).stored()
|
||||
@@ -158,7 +159,7 @@ const createNodePathsTable: Migration = {
|
||||
INSERT INTO node_paths (ancestor_id, descendant_id, workspace_id, level)
|
||||
SELECT ancestor_id, NEW.id, NEW.workspace_id, level + 1
|
||||
FROM node_paths
|
||||
WHERE descendant_id = NEW.parent_id;
|
||||
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
@@ -179,7 +180,7 @@ const createNodePathsTable: Migration = {
|
||||
INSERT INTO node_paths (ancestor_id, descendant_id, workspace_id, level)
|
||||
SELECT ancestor_id, NEW.id, NEW.workspace_id, level + 1
|
||||
FROM node_paths
|
||||
WHERE descendant_id = NEW.parent_id;
|
||||
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
@@ -5,24 +5,38 @@ import {
|
||||
WorkspaceStatus,
|
||||
WorkspaceUserStatus,
|
||||
} from '@/types/workspaces';
|
||||
import { generateId, IdType, NodeRoles } from '@colanode/core';
|
||||
import * as Y from 'yjs';
|
||||
import { fromUint8Array } from 'js-base64';
|
||||
import {
|
||||
ChannelAttributes,
|
||||
generateId,
|
||||
IdType,
|
||||
NodeRoles,
|
||||
PageAttributes,
|
||||
SpaceAttributes,
|
||||
UserAttributes,
|
||||
WorkspaceAttributes,
|
||||
} from '@colanode/core';
|
||||
import { NodeCreatedEvent } from '@/types/events';
|
||||
import { enqueueEvent } from '@/queues/events';
|
||||
import {} from './constants';
|
||||
import { YDoc } from '@colanode/crdt';
|
||||
|
||||
export const createDefaultWorkspace = async (account: SelectAccount) => {
|
||||
const createdAt = new Date();
|
||||
const workspaceId = generateId(IdType.Workspace);
|
||||
const workspaceName = `${account.name}'s Workspace`;
|
||||
const workspaceVersionId = generateId(IdType.Version);
|
||||
|
||||
const user = buildUserNodeCreate(workspaceId, account);
|
||||
const workspace = buildWorkspaceNodeCreate(
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
workspaceVersionId,
|
||||
user.id
|
||||
);
|
||||
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];
|
||||
const nodesToCreate = [workspace, user, space, page, channel];
|
||||
|
||||
await database.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
@@ -62,26 +76,54 @@ export const createDefaultWorkspace = async (account: SelectAccount) => {
|
||||
}
|
||||
};
|
||||
|
||||
const buildWorkspaceNodeCreate = (
|
||||
workspaceId: string,
|
||||
workspaceName: string,
|
||||
workspaceVersionId: string,
|
||||
userId: string
|
||||
): CreateNode => {
|
||||
const attributes: WorkspaceAttributes = {
|
||||
type: 'workspace',
|
||||
name: workspaceName,
|
||||
parentId: workspaceId,
|
||||
};
|
||||
|
||||
const ydoc = new YDoc(workspaceId);
|
||||
ydoc.updateAttributes(attributes);
|
||||
const state = ydoc.getState();
|
||||
|
||||
return {
|
||||
id: workspaceId,
|
||||
workspace_id: workspaceId,
|
||||
created_at: new Date(),
|
||||
created_by: userId,
|
||||
version_id: workspaceVersionId,
|
||||
server_created_at: new Date(),
|
||||
attributes: JSON.stringify(attributes),
|
||||
state,
|
||||
};
|
||||
};
|
||||
|
||||
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: UserAttributes = {
|
||||
type: 'user',
|
||||
name: account.name,
|
||||
avatar: account.avatar,
|
||||
email: account.email,
|
||||
role: 'owner',
|
||||
accountId: account.id,
|
||||
parentId: workspaceId,
|
||||
};
|
||||
|
||||
const attributes = JSON.stringify(attributesMap.toJSON());
|
||||
const state = Y.encodeStateAsUpdate(doc);
|
||||
const ydoc = new YDoc(id);
|
||||
ydoc.updateAttributes(attributes);
|
||||
const state = ydoc.getState();
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -90,7 +132,7 @@ const buildUserNodeCreate = (
|
||||
created_by: account.id,
|
||||
version_id: versionId,
|
||||
server_created_at: new Date(),
|
||||
attributes,
|
||||
attributes: JSON.stringify(attributes),
|
||||
state,
|
||||
};
|
||||
};
|
||||
@@ -101,24 +143,20 @@ const buildSpaceNodeCreate = (
|
||||
): 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');
|
||||
const attributes: SpaceAttributes = {
|
||||
type: 'space',
|
||||
name: 'Home',
|
||||
description: 'Home space',
|
||||
parentId: workspaceId,
|
||||
collaborators: {
|
||||
[userId]: NodeRoles.Admin,
|
||||
},
|
||||
};
|
||||
|
||||
attributesMap.set('collaborators', new Y.Map());
|
||||
const collaboratorsMap = attributesMap.get(
|
||||
'collaborators'
|
||||
) as Y.Map<string>;
|
||||
|
||||
collaboratorsMap.set(userId, NodeRoles.Admin);
|
||||
});
|
||||
|
||||
const attributes = JSON.stringify(attributesMap.toJSON());
|
||||
const state = Y.encodeStateAsUpdate(doc);
|
||||
const ydoc = new YDoc(id);
|
||||
ydoc.updateAttributes(attributes);
|
||||
const state = ydoc.getState();
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -127,7 +165,7 @@ const buildSpaceNodeCreate = (
|
||||
created_by: userId,
|
||||
version_id: versionId,
|
||||
server_created_at: new Date(),
|
||||
attributes,
|
||||
attributes: JSON.stringify(attributes),
|
||||
state,
|
||||
};
|
||||
};
|
||||
@@ -139,17 +177,17 @@ const buildPageNodeCreate = (
|
||||
): 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: PageAttributes = {
|
||||
type: 'page',
|
||||
name: 'Notes',
|
||||
parentId: spaceId,
|
||||
content: {},
|
||||
};
|
||||
|
||||
const attributes = JSON.stringify(attributesMap.toJSON());
|
||||
const state = Y.encodeStateAsUpdate(doc);
|
||||
const ydoc = new YDoc(id);
|
||||
ydoc.updateAttributes(attributes);
|
||||
const state = ydoc.getState();
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -158,7 +196,7 @@ const buildPageNodeCreate = (
|
||||
created_by: userId,
|
||||
version_id: versionId,
|
||||
server_created_at: new Date(),
|
||||
attributes,
|
||||
attributes: JSON.stringify(attributes),
|
||||
state,
|
||||
};
|
||||
};
|
||||
@@ -170,17 +208,17 @@ const buildChannelNodeCreate = (
|
||||
): 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: ChannelAttributes = {
|
||||
type: 'channel',
|
||||
name: 'Discussions',
|
||||
parentId: spaceId,
|
||||
index: '0',
|
||||
};
|
||||
|
||||
const attributes = JSON.stringify(attributesMap.toJSON());
|
||||
const state = Y.encodeStateAsUpdate(doc);
|
||||
const ydoc = new YDoc(id);
|
||||
ydoc.updateAttributes(attributes);
|
||||
const state = ydoc.getState();
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -189,7 +227,7 @@ const buildChannelNodeCreate = (
|
||||
created_by: userId,
|
||||
version_id: versionId,
|
||||
server_created_at: new Date(),
|
||||
attributes,
|
||||
attributes: JSON.stringify(attributes),
|
||||
state,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -99,7 +99,12 @@ const handleNodeDeletedEvent = async (
|
||||
|
||||
const createUserNodes = async (event: NodeCreatedEvent): Promise<void> => {
|
||||
const userNodesToCreate: CreateUserNode[] = [];
|
||||
if (event.attributes.type === NodeTypes.User) {
|
||||
|
||||
const isForEveryone =
|
||||
event.attributes.type === NodeTypes.User ||
|
||||
event.attributes.type === NodeTypes.Workspace;
|
||||
|
||||
if (isForEveryone) {
|
||||
const userIds = await fetchWorkspaceUsers(event.workspaceId);
|
||||
|
||||
for (const userId of userIds) {
|
||||
|
||||
@@ -11,21 +11,20 @@ import {
|
||||
} from '@/types/accounts';
|
||||
import axios from 'axios';
|
||||
import { ApiError, ColanodeRequest, ColanodeResponse } from '@/types/api';
|
||||
import { generateId, IdType, NodeAttributes } from '@colanode/core';
|
||||
import { generateId, IdType } from '@colanode/core';
|
||||
import { database } from '@/data/database';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { WorkspaceOutput, WorkspaceRole } from '@/types/workspaces';
|
||||
import { authMiddleware } from '@/middlewares/auth';
|
||||
import { generateToken } from '@/lib/tokens';
|
||||
import { mapServerNode } from '@/lib/nodes';
|
||||
import { enqueueTask } from '@/queues/tasks';
|
||||
import * as Y from 'yjs';
|
||||
import { CompiledQuery } from 'kysely';
|
||||
import { NodeUpdatedEvent } from '@/types/events';
|
||||
import { enqueueEvent } from '@/queues/events';
|
||||
import { SelectAccount } from '@/data/schema';
|
||||
import { createDefaultWorkspace } from '@/lib/workspaces';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { YDoc } from '@colanode/crdt';
|
||||
|
||||
const GoogleUserInfoUrl = 'https://www.googleapis.com/oauth2/v1/userinfo';
|
||||
const SaltRounds = 10;
|
||||
@@ -321,21 +320,15 @@ accountsRouter.put(
|
||||
continue;
|
||||
}
|
||||
|
||||
const doc = new Y.Doc({ guid: user.id });
|
||||
Y.applyUpdate(doc, user.state);
|
||||
const ydoc = new YDoc(user.id, user.state);
|
||||
ydoc.updateAttributes({
|
||||
...user.attributes,
|
||||
name: input.name,
|
||||
avatar: input.avatar ?? null,
|
||||
});
|
||||
|
||||
const attributesMap = doc.getMap('attributes');
|
||||
if (name != input.name) {
|
||||
attributesMap.set('name', input.name);
|
||||
}
|
||||
|
||||
if (avatar != input.avatar) {
|
||||
attributesMap.set('avatar', input.avatar);
|
||||
}
|
||||
|
||||
const attributes = attributesMap.toJSON() as NodeAttributes;
|
||||
const attributesJson = JSON.stringify(attributes);
|
||||
const state = Y.encodeStateAsUpdate(doc);
|
||||
const attributes = ydoc.getAttributes();
|
||||
const state = ydoc.getState();
|
||||
|
||||
const updatedAt = new Date();
|
||||
const versionId = generateId(IdType.Version);
|
||||
@@ -344,7 +337,7 @@ accountsRouter.put(
|
||||
database
|
||||
.updateTable('nodes')
|
||||
.set({
|
||||
attributes: attributesJson,
|
||||
attributes: JSON.stringify(attributes),
|
||||
state: state,
|
||||
updated_at: updatedAt,
|
||||
updated_by: user.id,
|
||||
@@ -418,13 +411,6 @@ const buildLoginOutput = async (
|
||||
.selectAll()
|
||||
.execute();
|
||||
|
||||
const userIds = workspaceUsers.map((wu) => wu.id);
|
||||
const userNodes = await database
|
||||
.selectFrom('nodes')
|
||||
.selectAll()
|
||||
.where('id', 'in', userIds)
|
||||
.execute();
|
||||
|
||||
for (const workspaceUser of workspaceUsers) {
|
||||
const workspace = workspaces.find(
|
||||
(w) => w.id === workspaceUser.workspace_id
|
||||
@@ -434,11 +420,6 @@ const buildLoginOutput = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
const userNode = userNodes.find((n) => n.id === workspaceUser.id);
|
||||
if (!userNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
workspaceOutputs.push({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
@@ -449,7 +430,6 @@ const buildLoginOutput = async (
|
||||
id: workspaceUser.id,
|
||||
accountId: workspaceUser.account_id,
|
||||
role: workspaceUser.role as WorkspaceRole,
|
||||
node: mapServerNode(userNode),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,13 +118,7 @@ const handleCreateNodeChange = async (
|
||||
const ydoc = new YDoc(changeData.id, changeData.state);
|
||||
const attributes = ydoc.getAttributes();
|
||||
|
||||
const model = registry[attributes.type];
|
||||
if (!model) {
|
||||
return {
|
||||
status: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
const model = registry.getModel(attributes.type);
|
||||
if (!model.schema.safeParse(attributes).success) {
|
||||
return {
|
||||
status: 'error',
|
||||
@@ -212,13 +206,7 @@ const handleUpdateNodeChange = async (
|
||||
const attributesJson = JSON.stringify(attributes);
|
||||
const state = ydoc.getState();
|
||||
|
||||
const model = registry[attributes.type];
|
||||
if (!model) {
|
||||
return {
|
||||
status: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
const model = registry.getModel(attributes.type);
|
||||
if (!model.schema.safeParse(attributes).success) {
|
||||
return {
|
||||
status: 'error',
|
||||
@@ -309,13 +297,7 @@ const handleDeleteNodeChange = async (
|
||||
};
|
||||
}
|
||||
|
||||
const model = registry[existingNode.type];
|
||||
if (!model) {
|
||||
return {
|
||||
status: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
const model = registry.getModel(existingNode.type);
|
||||
const ancestorRows = await fetchNodeAncestors(existingNode.id);
|
||||
const ancestors = ancestorRows.map(mapNode);
|
||||
const node = ancestors.find((ancestor) => ancestor.id === existingNode.id);
|
||||
|
||||
@@ -8,11 +8,14 @@ import {
|
||||
WorkspaceStatus,
|
||||
} from '@/types/workspaces';
|
||||
import { ApiError, ColanodeRequest, ColanodeResponse } from '@/types/api';
|
||||
import { generateId, IdType } from '@colanode/core';
|
||||
import {
|
||||
generateId,
|
||||
IdType,
|
||||
UserAttributes,
|
||||
WorkspaceAttributes,
|
||||
} from '@colanode/core';
|
||||
import { database } from '@/data/database';
|
||||
import { Router } from 'express';
|
||||
import * as Y from 'yjs';
|
||||
import { fromUint8Array, toUint8Array } from 'js-base64';
|
||||
import {
|
||||
CreateAccount,
|
||||
CreateNode,
|
||||
@@ -26,6 +29,7 @@ import { ServerNode } from '@/types/nodes';
|
||||
import { mapServerNode } from '@/lib/nodes';
|
||||
import { NodeCreatedEvent } from '@/types/events';
|
||||
import { enqueueEvent } from '@/queues/events';
|
||||
import { YDoc } from '@colanode/crdt';
|
||||
|
||||
export const workspacesRouter = Router();
|
||||
|
||||
@@ -62,25 +66,36 @@ workspacesRouter.post(
|
||||
}
|
||||
|
||||
const createdAt = new Date();
|
||||
|
||||
const workspaceId = generateId(IdType.Workspace);
|
||||
const workspaceVersionId = generateId(IdType.Version);
|
||||
const workspaceDoc = new YDoc(workspaceId);
|
||||
|
||||
const workspaceAttributes: WorkspaceAttributes = {
|
||||
type: 'workspace',
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
avatar: input.avatar,
|
||||
parentId: workspaceId,
|
||||
};
|
||||
|
||||
workspaceDoc.updateAttributes(workspaceAttributes);
|
||||
|
||||
const userId = generateId(IdType.User);
|
||||
const userVersionId = generateId(IdType.Version);
|
||||
const userDoc = new Y.Doc({ guid: userId });
|
||||
const userDoc = new YDoc(userId);
|
||||
|
||||
const userAttributesMap = userDoc.getMap('attributes');
|
||||
userDoc.transact(() => {
|
||||
userAttributesMap.set('type', 'user');
|
||||
userAttributesMap.set('name', account.name);
|
||||
userAttributesMap.set('avatar', account.avatar);
|
||||
userAttributesMap.set('email', account.email);
|
||||
userAttributesMap.set('role', WorkspaceRole.Owner);
|
||||
userAttributesMap.set('accountId', account.id);
|
||||
});
|
||||
const userAttributes: UserAttributes = {
|
||||
type: 'user',
|
||||
name: account.name,
|
||||
avatar: account.avatar,
|
||||
email: account.email,
|
||||
accountId: account.id,
|
||||
role: WorkspaceRole.Owner,
|
||||
parentId: workspaceId,
|
||||
};
|
||||
|
||||
const userAttributes = JSON.stringify(userAttributesMap.toJSON());
|
||||
const userState = Y.encodeStateAsUpdate(userDoc);
|
||||
userDoc.updateAttributes(userAttributes);
|
||||
|
||||
await database.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
@@ -111,13 +126,27 @@ workspacesRouter.post(
|
||||
})
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto('nodes')
|
||||
.values({
|
||||
id: workspaceId,
|
||||
workspace_id: workspaceId,
|
||||
attributes: JSON.stringify(workspaceAttributes),
|
||||
state: workspaceDoc.getState(),
|
||||
created_at: createdAt,
|
||||
created_by: account.id,
|
||||
version_id: workspaceVersionId,
|
||||
server_created_at: createdAt,
|
||||
})
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto('nodes')
|
||||
.values({
|
||||
id: userId,
|
||||
workspace_id: workspaceId,
|
||||
attributes: userAttributes,
|
||||
state: userState,
|
||||
attributes: JSON.stringify(userAttributes),
|
||||
state: userDoc.getState(),
|
||||
created_at: createdAt,
|
||||
created_by: account.id,
|
||||
version_id: userVersionId,
|
||||
@@ -126,11 +155,24 @@ workspacesRouter.post(
|
||||
.execute();
|
||||
});
|
||||
|
||||
const workspaceEvent: NodeCreatedEvent = {
|
||||
type: 'node_created',
|
||||
id: workspaceId,
|
||||
workspaceId: workspaceId,
|
||||
attributes: workspaceAttributes,
|
||||
createdBy: account.id,
|
||||
createdAt: createdAt.toISOString(),
|
||||
versionId: workspaceVersionId,
|
||||
serverCreatedAt: createdAt.toISOString(),
|
||||
};
|
||||
|
||||
await enqueueEvent(workspaceEvent);
|
||||
|
||||
const userEvent: NodeCreatedEvent = {
|
||||
type: 'node_created',
|
||||
id: userId,
|
||||
workspaceId: workspaceId,
|
||||
attributes: JSON.parse(userAttributes),
|
||||
attributes: userAttributes,
|
||||
createdBy: account.id,
|
||||
createdAt: createdAt.toISOString(),
|
||||
versionId: userVersionId,
|
||||
@@ -149,18 +191,6 @@ workspacesRouter.post(
|
||||
id: userId,
|
||||
accountId: account.id,
|
||||
role: WorkspaceRole.Owner,
|
||||
node: {
|
||||
id: userId,
|
||||
workspaceId: workspaceId,
|
||||
type: 'user',
|
||||
attributes: JSON.parse(userAttributes),
|
||||
state: fromUint8Array(userState),
|
||||
createdAt: new Date(),
|
||||
createdBy: account.id,
|
||||
versionId: userVersionId,
|
||||
serverCreatedAt: new Date(),
|
||||
index: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -215,33 +245,6 @@ workspacesRouter.put(
|
||||
});
|
||||
}
|
||||
|
||||
if (!workspaceUser) {
|
||||
return res.status(403).json({
|
||||
code: ApiError.Forbidden,
|
||||
message: 'Forbidden.',
|
||||
});
|
||||
}
|
||||
|
||||
if (workspaceUser.role !== WorkspaceRole.Owner) {
|
||||
return res.status(403).json({
|
||||
code: ApiError.Forbidden,
|
||||
message: 'Forbidden.',
|
||||
});
|
||||
}
|
||||
|
||||
const userNode = await database
|
||||
.selectFrom('nodes')
|
||||
.selectAll()
|
||||
.where('id', '=', workspaceUser.id)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!userNode) {
|
||||
return res.status(500).json({
|
||||
code: ApiError.InternalServerError,
|
||||
message: 'Internal server error.',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedWorkspace = await database
|
||||
.updateTable('workspaces')
|
||||
.set({
|
||||
@@ -272,7 +275,6 @@ workspacesRouter.put(
|
||||
id: workspaceUser.id,
|
||||
accountId: workspaceUser.account_id,
|
||||
role: workspaceUser.role,
|
||||
node: mapServerNode(userNode),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -373,19 +375,6 @@ workspacesRouter.get(
|
||||
});
|
||||
}
|
||||
|
||||
const userNode = await database
|
||||
.selectFrom('nodes')
|
||||
.selectAll()
|
||||
.where('id', '=', workspaceUser.id)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!userNode) {
|
||||
return res.status(500).json({
|
||||
code: ApiError.InternalServerError,
|
||||
message: 'Internal server error.',
|
||||
});
|
||||
}
|
||||
|
||||
const output: WorkspaceOutput = {
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
@@ -396,7 +385,6 @@ workspacesRouter.get(
|
||||
id: workspaceUser.id,
|
||||
accountId: workspaceUser.account_id,
|
||||
role: workspaceUser.role as WorkspaceRole,
|
||||
node: mapServerNode(userNode),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -427,18 +415,7 @@ workspacesRouter.get(
|
||||
.where('id', 'in', workspaceIds)
|
||||
.execute();
|
||||
|
||||
const userNodes = await database
|
||||
.selectFrom('nodes')
|
||||
.selectAll()
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
workspaceUsers.map((wa) => wa.id)
|
||||
)
|
||||
.execute();
|
||||
|
||||
const outputs: WorkspaceOutput[] = [];
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
const workspaceUser = workspaceUsers.find(
|
||||
(wa) => wa.workspace_id === workspace.id
|
||||
@@ -448,12 +425,6 @@ workspacesRouter.get(
|
||||
continue;
|
||||
}
|
||||
|
||||
const userNode = userNodes.find((un) => un.id === workspaceUser.id);
|
||||
|
||||
if (!userNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const output: WorkspaceOutput = {
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
@@ -464,7 +435,6 @@ workspacesRouter.get(
|
||||
id: workspaceUser.id,
|
||||
accountId: workspaceUser.account_id,
|
||||
role: workspaceUser.role as WorkspaceRole,
|
||||
node: mapServerNode(userNode),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -616,20 +586,19 @@ workspacesRouter.post(
|
||||
|
||||
const userId = generateId(IdType.User);
|
||||
const userVersionId = generateId(IdType.Version);
|
||||
const userDoc = new Y.Doc({ guid: userId });
|
||||
const userDoc = new YDoc(userId);
|
||||
|
||||
const userAttributesMap = userDoc.getMap('attributes');
|
||||
userDoc.transact(() => {
|
||||
userAttributesMap.set('type', 'user');
|
||||
userAttributesMap.set('name', account!.name);
|
||||
userAttributesMap.set('avatar', account!.avatar);
|
||||
userAttributesMap.set('email', account!.email);
|
||||
userAttributesMap.set('role', WorkspaceRole.Collaborator);
|
||||
userAttributesMap.set('accountId', account!.id);
|
||||
});
|
||||
const userAttributes: UserAttributes = {
|
||||
type: 'user',
|
||||
name: account!.name,
|
||||
avatar: account!.avatar,
|
||||
email: account!.email,
|
||||
role: WorkspaceRole.Collaborator,
|
||||
accountId: account!.id,
|
||||
parentId: workspace.id,
|
||||
};
|
||||
|
||||
const userAttributes = JSON.stringify(userAttributesMap.toJSON());
|
||||
const userState = Y.encodeStateAsUpdate(userDoc);
|
||||
userDoc.updateAttributes(userAttributes);
|
||||
|
||||
workspaceUsersToCreate.push({
|
||||
id: userId,
|
||||
@@ -639,14 +608,14 @@ workspacesRouter.post(
|
||||
created_at: new Date(),
|
||||
created_by: req.account.id,
|
||||
status: WorkspaceUserStatus.Active,
|
||||
version_id: generateId(IdType.Version),
|
||||
version_id: userVersionId,
|
||||
});
|
||||
|
||||
const user: ServerNode = {
|
||||
id: userId,
|
||||
type: 'user',
|
||||
attributes: JSON.parse(userAttributes),
|
||||
state: fromUint8Array(userState),
|
||||
attributes: userAttributes,
|
||||
state: userDoc.getEncodedState(),
|
||||
createdAt: new Date(),
|
||||
createdBy: workspaceUser.id,
|
||||
serverCreatedAt: new Date(),
|
||||
@@ -657,8 +626,8 @@ workspacesRouter.post(
|
||||
|
||||
usersToCreate.push({
|
||||
id: user.id,
|
||||
attributes: userAttributes,
|
||||
state: userState,
|
||||
attributes: JSON.stringify(userAttributes),
|
||||
state: userDoc.getState(),
|
||||
created_at: user.createdAt,
|
||||
created_by: user.createdBy,
|
||||
server_created_at: user.serverCreatedAt,
|
||||
@@ -790,20 +759,20 @@ workspacesRouter.put(
|
||||
});
|
||||
}
|
||||
|
||||
const userDoc = new Y.Doc({ guid: user.id });
|
||||
Y.applyUpdate(userDoc, user.state);
|
||||
const attributes = user.attributes;
|
||||
if (attributes.type !== 'user') {
|
||||
return res.status(400).json({
|
||||
code: ApiError.BadRequest,
|
||||
message: 'BadRequest.',
|
||||
});
|
||||
}
|
||||
|
||||
const userUpdates: string[] = [];
|
||||
userDoc.on('update', (update) => {
|
||||
userUpdates.push(fromUint8Array(update));
|
||||
});
|
||||
|
||||
const userAttributesMap = userDoc.getMap('attributes');
|
||||
userAttributesMap.set('role', input.role);
|
||||
|
||||
const userAttributes = JSON.stringify(userAttributesMap.toJSON());
|
||||
const state = Y.encodeStateAsUpdate(userDoc);
|
||||
const updatedAt = new Date();
|
||||
const userDoc = new YDoc(user.id, user.state);
|
||||
userDoc.updateAttributes({
|
||||
...attributes,
|
||||
role: input.role,
|
||||
});
|
||||
|
||||
const userNode: ServerNode = {
|
||||
id: user.id,
|
||||
@@ -811,8 +780,8 @@ workspacesRouter.put(
|
||||
workspaceId: user.workspace_id,
|
||||
index: null,
|
||||
parentId: null,
|
||||
attributes: JSON.parse(userAttributes),
|
||||
state: fromUint8Array(state),
|
||||
attributes: userDoc.getAttributes(),
|
||||
state: userDoc.getEncodedState(),
|
||||
createdAt: user.created_at,
|
||||
createdBy: user.created_by,
|
||||
serverCreatedAt: user.server_created_at,
|
||||
@@ -837,8 +806,8 @@ workspacesRouter.put(
|
||||
await trx
|
||||
.updateTable('nodes')
|
||||
.set({
|
||||
attributes: userAttributes,
|
||||
state: state,
|
||||
attributes: JSON.stringify(userDoc.getAttributes()),
|
||||
state: userDoc.getState(),
|
||||
server_updated_at: updatedAt,
|
||||
updated_at: updatedAt,
|
||||
updated_by: currentWorkspaceUser.id,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ServerNode } from '@/types/nodes';
|
||||
|
||||
export enum WorkspaceStatus {
|
||||
Active = 1,
|
||||
Inactive = 2,
|
||||
@@ -49,7 +47,6 @@ export type WorkspaceUserOutput = {
|
||||
id: string;
|
||||
accountId: string;
|
||||
role: WorkspaceRole;
|
||||
node: ServerNode;
|
||||
};
|
||||
|
||||
export type WorkspaceAccountsInviteInput = {
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
* The common package is using the internal packages approach, so it needs to
|
||||
* be transpiled / bundled together with the deployed code.
|
||||
*/
|
||||
noExternal: ['@colanode/core'],
|
||||
noExternal: ['@colanode/core', '@colanode/crdt'],
|
||||
/**
|
||||
* Do not use tsup for generating d.ts files because it can not generate type
|
||||
* the definition maps required for go-to-definition to work in our IDE. We
|
||||
|
||||
@@ -16,3 +16,4 @@ export * from './registry/record';
|
||||
export * from './registry/space';
|
||||
export * from './registry/user';
|
||||
export * from './registry/zod';
|
||||
export * from './registry/workspace';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const NodeTypes = {
|
||||
User: 'user',
|
||||
Space: 'space',
|
||||
Workspace: 'workspace',
|
||||
Page: 'page',
|
||||
Channel: 'channel',
|
||||
Chat: 'chat',
|
||||
|
||||
@@ -9,23 +9,11 @@ import { DatabaseAttributes, databaseModel } from './database';
|
||||
import { FileAttributes, fileModel } from './file';
|
||||
import { FolderAttributes, folderModel } from './folder';
|
||||
import { RecordAttributes, recordModel } from './record';
|
||||
|
||||
export const registry: Record<string, NodeModel> = {
|
||||
channel: channelModel,
|
||||
chat: chatModel,
|
||||
database: databaseModel,
|
||||
file: fileModel,
|
||||
folder: folderModel,
|
||||
message: messageModel,
|
||||
page: pageModel,
|
||||
record: recordModel,
|
||||
space: spaceModel,
|
||||
user: userModel,
|
||||
};
|
||||
import { WorkspaceAttributes, workspaceModel } from './workspace';
|
||||
|
||||
type NodeBase = {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
parentId: string;
|
||||
index: string | null;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
@@ -87,6 +75,11 @@ export type UserNode = NodeBase & {
|
||||
attributes: UserAttributes;
|
||||
};
|
||||
|
||||
export type WorkspaceNode = NodeBase & {
|
||||
type: 'workspace';
|
||||
attributes: WorkspaceAttributes;
|
||||
};
|
||||
|
||||
export type NodeType =
|
||||
| 'channel'
|
||||
| 'chat'
|
||||
@@ -97,7 +90,8 @@ export type NodeType =
|
||||
| 'page'
|
||||
| 'record'
|
||||
| 'space'
|
||||
| 'user';
|
||||
| 'user'
|
||||
| 'workspace';
|
||||
|
||||
export type Node =
|
||||
| ChannelNode
|
||||
@@ -109,7 +103,8 @@ export type Node =
|
||||
| PageNode
|
||||
| RecordNode
|
||||
| SpaceNode
|
||||
| UserNode;
|
||||
| UserNode
|
||||
| WorkspaceNode;
|
||||
|
||||
export type NodeAttributes =
|
||||
| UserAttributes
|
||||
@@ -121,4 +116,34 @@ export type NodeAttributes =
|
||||
| FolderAttributes
|
||||
| MessageAttributes
|
||||
| PageAttributes
|
||||
| RecordAttributes;
|
||||
| RecordAttributes
|
||||
| WorkspaceAttributes;
|
||||
|
||||
class Registry {
|
||||
private models: Map<string, NodeModel> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.models.set('channel', channelModel);
|
||||
this.models.set('chat', chatModel);
|
||||
this.models.set('database', databaseModel);
|
||||
this.models.set('file', fileModel);
|
||||
this.models.set('folder', folderModel);
|
||||
this.models.set('message', messageModel);
|
||||
this.models.set('page', pageModel);
|
||||
this.models.set('record', recordModel);
|
||||
this.models.set('space', spaceModel);
|
||||
this.models.set('user', userModel);
|
||||
this.models.set('workspace', workspaceModel);
|
||||
}
|
||||
|
||||
getModel(type: string): NodeModel {
|
||||
const model = this.models.get(type);
|
||||
if (!model) {
|
||||
throw new Error(`Model for type ${type} not found`);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
export const registry = new Registry();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isEqual } from 'lodash-es';
|
||||
|
||||
export const messageAttributesSchema = z.object({
|
||||
type: z.literal('message'),
|
||||
parentId: z.string().nullable(),
|
||||
parentId: z.string(),
|
||||
content: z.record(z.string(), blockSchema),
|
||||
reactions: z.record(z.string(), z.array(z.string())),
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ export const userAttributesSchema = z.object({
|
||||
email: z.string().email(),
|
||||
avatar: z.string().nullable(),
|
||||
accountId: z.string(),
|
||||
role: z.enum(['admin', 'editor', 'collaborator', 'viewer']),
|
||||
role: z.enum(['owner', 'admin', 'editor', 'collaborator', 'viewer']),
|
||||
});
|
||||
|
||||
export type UserAttributes = z.infer<typeof userAttributesSchema>;
|
||||
|
||||
26
packages/core/src/registry/workspace.ts
Normal file
26
packages/core/src/registry/workspace.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
export const workspaceAttributesSchema = z.object({
|
||||
type: z.literal('workspace'),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
parentId: z.string(),
|
||||
});
|
||||
|
||||
export type WorkspaceAttributes = z.infer<typeof workspaceAttributesSchema>;
|
||||
|
||||
export const workspaceModel: NodeModel = {
|
||||
type: 'workspace',
|
||||
schema: workspaceAttributesSchema,
|
||||
canCreate: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
canUpdate: async (_, __, ___) => {
|
||||
return true;
|
||||
},
|
||||
canDelete: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import * as Y from 'yjs';
|
||||
import { NodeAttributes, ZodText } from '@colanode/core';
|
||||
import { NodeAttributes, registry, ZodText } from '@colanode/core';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { diffChars } from 'diff';
|
||||
import { fromUint8Array, toUint8Array } from 'js-base64';
|
||||
@@ -23,10 +23,10 @@ export class YDoc {
|
||||
});
|
||||
}
|
||||
|
||||
public updateAttributes(
|
||||
schema: z.ZodSchema,
|
||||
attributes: z.infer<typeof schema>
|
||||
) {
|
||||
public updateAttributes(attributes: NodeAttributes) {
|
||||
const model = registry.getModel(attributes.type);
|
||||
|
||||
const schema = model.schema;
|
||||
if (!(schema instanceof z.ZodObject)) {
|
||||
throw new Error('Schema must be a ZodObject');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user