Permission checks improvements

This commit is contained in:
Hakan Shehu
2025-02-12 12:16:05 +01:00
parent d8745d84cd
commit 95999b4f9e
29 changed files with 775 additions and 700 deletions

View File

@@ -1,14 +0,0 @@
import { Migration, sql } from 'kysely';
export const createTextsTable: Migration = {
up: async (db) => {
await sql`
CREATE VIRTUAL TABLE texts USING fts5(id UNINDEXED, name, text);
`.execute(db);
},
down: async (db) => {
await sql`
DROP TABLE IF EXISTS texts;
`.execute(db);
},
};

View File

@@ -10,11 +10,10 @@ import { createCollaborationsTable } from './00007-create-collaborations-table';
import { createFilesTable } from './00008-create-files-table'; import { createFilesTable } from './00008-create-files-table';
import { createMutationsTable } from './00009-create-mutations-table'; import { createMutationsTable } from './00009-create-mutations-table';
import { createTombstonesTable } from './00010-create-tombstones-table'; import { createTombstonesTable } from './00010-create-tombstones-table';
import { createTextsTable } from './00011-create-texts-table'; import { createDocumentsTable } from './00011-create-documents-table';
import { createCursorsTable } from './00012-create-cursors-table'; import { createDocumentUpdatesTable } from './00012-create-document-updates-table';
import { createMetadataTable } from './00013-create-metadata-table'; import { createCursorsTable } from './00013-create-cursors-table';
import { createDocumentsTable } from './00014-create-documents-table'; import { createMetadataTable } from './00014-create-metadata-table';
import { createDocumentUpdatesTable } from './00015-create-document-updates-table';
export const workspaceDatabaseMigrations: Record<string, Migration> = { export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00001-create-users-table': createUsersTable, '00001-create-users-table': createUsersTable,
@@ -27,9 +26,8 @@ export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00008-create-files-table': createFilesTable, '00008-create-files-table': createFilesTable,
'00009-create-mutations-table': createMutationsTable, '00009-create-mutations-table': createMutationsTable,
'00010-create-tombstones-table': createTombstonesTable, '00010-create-tombstones-table': createTombstonesTable,
'00011-create-texts-table': createTextsTable, '00011-create-documents-table': createDocumentsTable,
'00012-create-cursors-table': createCursorsTable, '00012-create-document-updates-table': createDocumentUpdatesTable,
'00013-create-metadata-table': createMetadataTable, '00013-create-cursors-table': createCursorsTable,
'00014-create-documents-table': createDocumentsTable, '00014-create-metadata-table': createMetadataTable,
'00015-create-document-updates-table': createDocumentUpdatesTable,
}; };

View File

@@ -159,16 +159,6 @@ export type SelectTombsonte = Selectable<TombstoneTable>;
export type CreateTombstone = Insertable<TombstoneTable>; export type CreateTombstone = Insertable<TombstoneTable>;
export type UpdateTombstone = Updateable<TombstoneTable>; export type UpdateTombstone = Updateable<TombstoneTable>;
interface TextTable {
id: ColumnType<string, string, never>;
name: ColumnType<string | null, string | null, string | null>;
text: ColumnType<string | null, string | null, string | null>;
}
export type SelectText = Selectable<TextTable>;
export type CreateText = Insertable<TextTable>;
export type UpdateText = Updateable<TextTable>;
interface CursorTable { interface CursorTable {
key: ColumnType<string, string, never>; key: ColumnType<string, string, never>;
value: ColumnType<bigint, bigint, bigint>; value: ColumnType<bigint, bigint, bigint>;
@@ -227,9 +217,8 @@ export interface WorkspaceDatabaseSchema {
files: FileTable; files: FileTable;
mutations: MutationTable; mutations: MutationTable;
tombstones: TombstoneTable; tombstones: TombstoneTable;
texts: TextTable;
cursors: CursorTable;
metadata: MetadataTable;
documents: DocumentTable; documents: DocumentTable;
document_updates: DocumentUpdateTable; document_updates: DocumentUpdateTable;
cursors: CursorTable;
metadata: MetadataTable;
} }

View File

@@ -1,18 +1,10 @@
import { import { generateId, IdType, FileAttributes } from '@colanode/core';
canCreateNode,
generateId,
IdType,
FileAttributes,
} from '@colanode/core';
import { MutationHandler } from '@/main/lib/types'; import { MutationHandler } from '@/main/lib/types';
import { import {
FileCreateMutationInput, FileCreateMutationInput,
FileCreateMutationOutput, FileCreateMutationOutput,
} from '@/shared/mutations/files/file-create'; } from '@/shared/mutations/files/file-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { fetchNode } from '@/main/lib/utils';
import { mapNode } from '@/main/lib/mappers';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base'; import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileCreateMutationHandler export class FileCreateMutationHandler
@@ -23,53 +15,20 @@ export class FileCreateMutationHandler
input: FileCreateMutationInput input: FileCreateMutationInput
): Promise<FileCreateMutationOutput> { ): Promise<FileCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const node = await fetchNode(workspace.database, input.parentId);
if (!node) {
throw new MutationError(
MutationErrorCode.NodeNotFound,
'There was an error while fetching the node. Please make sure you have access to this node.'
);
}
const root = await fetchNode(workspace.database, node.root_id);
if (!root) {
throw new MutationError(
MutationErrorCode.RootNotFound,
'There was an error while fetching the root. Please make sure you have access to this root.'
);
}
const attributes: FileAttributes = { const attributes: FileAttributes = {
type: 'file', type: 'file',
parentId: node.id, parentId: input.parentId,
}; };
if (
!canCreateNode(
{
user: {
userId: workspace.userId,
role: workspace.role,
},
root: mapNode(root),
},
'file'
)
) {
throw new MutationError(
MutationErrorCode.FileCreateForbidden,
'You are not allowed to upload a file in this node.'
);
}
const fileId = generateId(IdType.File); const fileId = generateId(IdType.File);
await workspace.files.createFile(input.filePath, fileId, node.id, root);
await workspace.nodes.createNode({ await workspace.nodes.createNode({
id: fileId, id: fileId,
attributes, attributes,
parentId: node.id, parentId: input.parentId,
}); });
await workspace.files.createFile(fileId, input.filePath);
return { return {
id: fileId, id: fileId,
}; };

View File

@@ -1,6 +1,6 @@
import { import {
canCreateNode,
EditorNodeTypes, EditorNodeTypes,
FileAttributes,
generateId, generateId,
IdType, IdType,
MessageAttributes, MessageAttributes,
@@ -12,10 +12,12 @@ import {
MessageCreateMutationInput, MessageCreateMutationInput,
MessageCreateMutationOutput, MessageCreateMutationOutput,
} from '@/shared/mutations/messages/message-create'; } from '@/shared/mutations/messages/message-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { fetchNode } from '@/main/lib/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base'; import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
import { mapNode } from '@/main/lib/mappers';
interface MessageFile {
id: string;
path: string;
}
export class MessageCreateMutationHandler export class MessageCreateMutationHandler
extends WorkspaceMutationHandlerBase extends WorkspaceMutationHandlerBase
@@ -26,43 +28,10 @@ export class MessageCreateMutationHandler
): Promise<MessageCreateMutationOutput> { ): Promise<MessageCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const node = await fetchNode(workspace.database, input.parentId);
if (!node) {
throw new MutationError(
MutationErrorCode.NodeNotFound,
'There was an error while fetching the conversation. Please make sure you have access to this conversation.'
);
}
const root = await fetchNode(workspace.database, node.root_id);
if (!root) {
throw new MutationError(
MutationErrorCode.RootNotFound,
'There was an error while fetching the root. Please make sure you have access to this root.'
);
}
if (
!canCreateNode(
{
user: {
userId: workspace.userId,
role: workspace.role,
},
root: mapNode(root),
},
'message'
)
) {
throw new MutationError(
MutationErrorCode.MessageCreateForbidden,
'You are not allowed to create a message in this conversation.'
);
}
const messageId = generateId(IdType.Message); const messageId = generateId(IdType.Message);
const editorContent = input.content.content ?? []; const editorContent = input.content.content ?? [];
const blocks = mapContentsToBlocks(messageId, editorContent, new Map()); const blocks = mapContentsToBlocks(messageId, editorContent, new Map());
const filesToCreate: MessageFile[] = [];
// check if there are nested nodes (files, pages, folders etc.) // check if there are nested nodes (files, pages, folders etc.)
for (const block of Object.values(blocks)) { for (const block of Object.values(blocks)) {
@@ -70,7 +39,10 @@ export class MessageCreateMutationHandler
const path = block.attrs?.path; const path = block.attrs?.path;
const fileId = generateId(IdType.File); const fileId = generateId(IdType.File);
await workspace.files.createFile(path, fileId, messageId, root); filesToCreate.push({
id: fileId,
path: path,
});
block.id = fileId; block.id = fileId;
block.type = 'file'; block.type = 'file';
@@ -92,6 +64,21 @@ export class MessageCreateMutationHandler
parentId: input.parentId, parentId: input.parentId,
}); });
for (const file of filesToCreate) {
const fileAttributes: FileAttributes = {
type: 'file',
parentId: messageId,
};
await workspace.nodes.createNode({
id: file.id,
attributes: fileAttributes,
parentId: messageId,
});
await workspace.files.createFile(file.id, file.path);
}
return { return {
id: messageId, id: messageId,
}; };

View File

@@ -1,5 +1,3 @@
import { sql } from 'kysely';
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base'; import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
import { SelectUser } from '@/main/databases/workspace'; import { SelectUser } from '@/main/databases/workspace';
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types'; import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
@@ -82,26 +80,20 @@ export class UserSearchQueryHandler
input: UserSearchQueryInput input: UserSearchQueryInput
): Promise<SelectUser[]> { ): Promise<SelectUser[]> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const exclude = input.exclude ?? []; const exclude = input.exclude ?? [];
const query = sql<SelectUser>`
SELECT u.*
FROM users u
JOIN texts t ON u.id = t.id
WHERE u.id != ${workspace.userId}
AND t.name MATCH ${input.searchQuery + '*'}
${
exclude.length > 0
? sql`AND u.id NOT IN (${sql.join(
exclude.map((id) => sql`${id}`),
sql`, `
)})`
: sql``
}
`.compile(workspace.database);
const result = await workspace.database.executeQuery(query); let queryBuilder = workspace.database
return result.rows; .selectFrom('users')
.selectAll()
.where('id', '!=', workspace.userId)
.where('name', 'like', `%${input.searchQuery}%`);
if (exclude.length > 0) {
queryBuilder = queryBuilder.where('id', 'not in', exclude);
}
const rows = await queryBuilder.execute();
return rows;
} }
private async fetchUsers(input: UserSearchQueryInput): Promise<SelectUser[]> { private async fetchUsers(input: UserSearchQueryInput): Promise<SelectUser[]> {

View File

@@ -16,6 +16,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { import {
fetchNode,
fetchUserStorageUsed, fetchUserStorageUsed,
getFileMetadata, getFileMetadata,
getWorkspaceFilesDirectoryPath, getWorkspaceFilesDirectoryPath,
@@ -26,7 +27,7 @@ import { eventBus } from '@/shared/lib/event-bus';
import { DownloadStatus, UploadStatus } from '@/shared/types/files'; import { DownloadStatus, UploadStatus } from '@/shared/types/files';
import { WorkspaceService } from '@/main/services/workspaces/workspace-service'; import { WorkspaceService } from '@/main/services/workspaces/workspace-service';
import { EventLoop } from '@/main/lib/event-loop'; import { EventLoop } from '@/main/lib/event-loop';
import { SelectFile, SelectNode } from '@/main/databases/workspace'; import { SelectFile } from '@/main/databases/workspace';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { formatBytes } from '@/shared/lib/files'; import { formatBytes } from '@/shared/lib/files';
@@ -93,12 +94,7 @@ export class FileService {
this.cleanupEventLoop.start(); this.cleanupEventLoop.start();
} }
public async createFile( public async createFile(id: string, path: string): Promise<void> {
path: string,
id: string,
parentId: string,
root: SelectNode
): Promise<void> {
const metadata = getFileMetadata(path); const metadata = getFileMetadata(path);
if (!metadata) { if (!metadata) {
throw new MutationError( throw new MutationError(
@@ -132,13 +128,21 @@ export class FileService {
); );
} }
const node = await fetchNode(this.workspace.database, id);
if (!node || node.type !== 'file') {
throw new MutationError(
MutationErrorCode.NodeNotFound,
'There was an error while creating the file. Please make sure you have access to this node.'
);
}
this.copyFileToWorkspace(path, id, metadata.extension); this.copyFileToWorkspace(path, id, metadata.extension);
const mutationData: CreateFileMutationData = { const mutationData: CreateFileMutationData = {
id, id,
type: metadata.type, type: metadata.type,
parentId: parentId, parentId: node.parent_id!,
rootId: root.id, rootId: node.root_id,
name: metadata.name, name: metadata.name,
originalName: metadata.name, originalName: metadata.name,
extension: metadata.extension, extension: metadata.extension,
@@ -156,8 +160,8 @@ export class FileService {
.values({ .values({
id, id,
type: metadata.type, type: metadata.type,
parent_id: parentId, parent_id: node.parent_id!,
root_id: root.id, root_id: node.root_id,
name: metadata.name, name: metadata.name,
original_name: metadata.name, original_name: metadata.name,
mime_type: metadata.mimeType, mime_type: metadata.mimeType,

View File

@@ -3,21 +3,19 @@ import {
IdType, IdType,
createDebugger, createDebugger,
NodeAttributes, NodeAttributes,
Node,
canCreateNode,
extractNodeText,
canUpdateNode,
canDeleteNode,
DeleteNodeMutationData, DeleteNodeMutationData,
SyncNodeData, SyncNodeData,
SyncNodeTombstoneData, SyncNodeTombstoneData,
getNodeModel, getNodeModel,
CreateNodeMutationData, CreateNodeMutationData,
UpdateNodeMutationData, UpdateNodeMutationData,
CanCreateNodeContext,
CanUpdateAttributesContext,
CanDeleteNodeContext,
} from '@colanode/core'; } from '@colanode/core';
import { decodeState, encodeState, YDoc } from '@colanode/crdt'; import { decodeState, encodeState, YDoc } from '@colanode/crdt';
import { fetchNode } from '@/main/lib/utils'; import { fetchNodeAncestors } from '@/main/lib/utils';
import { mapFile, mapNode } from '@/main/lib/mappers'; import { mapFile, mapNode } from '@/main/lib/mappers';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { WorkspaceService } from '@/main/services/workspaces/workspace-service'; import { WorkspaceService } from '@/main/services/workspaces/workspace-service';
@@ -49,42 +47,26 @@ export class NodeService {
public async createNode(input: CreateNodeInput): Promise<SelectNode> { public async createNode(input: CreateNodeInput): Promise<SelectNode> {
this.debug(`Creating ${Array.isArray(input) ? 'nodes' : 'node'}`); this.debug(`Creating ${Array.isArray(input) ? 'nodes' : 'node'}`);
let root: Node | null = null; const ancestors = input.parentId
? await fetchNodeAncestors(this.workspace.database, input.parentId)
: [];
if (input.parentId) { const model = getNodeModel(input.attributes.type);
const parent = await fetchNode(this.workspace.database, input.parentId); const canCreateNodeContext: CanCreateNodeContext = {
user: {
id: this.workspace.userId,
role: this.workspace.role,
workspaceId: this.workspace.id,
accountId: this.workspace.accountId,
},
ancestors: ancestors.map(mapNode),
attributes: input.attributes,
};
if (parent) { if (!model.canCreate(canCreateNodeContext)) {
if (parent.id === parent.root_id) {
root = mapNode(parent);
} else {
const rootRow = await fetchNode(
this.workspace.database,
parent.root_id
);
if (rootRow) {
root = mapNode(rootRow);
}
}
}
}
if (
!canCreateNode(
{
user: {
userId: this.workspace.userId,
role: this.workspace.role,
},
root: root,
},
input.attributes.type
)
) {
throw new Error('Insufficient permissions'); throw new Error('Insufficient permissions');
} }
const model = getNodeModel(input.attributes.type);
const ydoc = new YDoc(); const ydoc = new YDoc();
const update = ydoc.update(model.attributesSchema, input.attributes); const update = ydoc.update(model.attributesSchema, input.attributes);
@@ -93,7 +75,7 @@ export class NodeService {
} }
const createdAt = new Date().toISOString(); const createdAt = new Date().toISOString();
const text = extractNodeText(input.id, input.attributes); const rootId = ancestors[0]?.id ?? input.id;
const { createdNode, createdMutation } = await this.workspace.database const { createdNode, createdMutation } = await this.workspace.database
.transaction() .transaction()
@@ -103,7 +85,7 @@ export class NodeService {
.returningAll() .returningAll()
.values({ .values({
id: input.id, id: input.id,
root_id: root?.id ?? input.id, root_id: rootId,
attributes: JSON.stringify(input.attributes), attributes: JSON.stringify(input.attributes),
created_at: createdAt, created_at: createdAt,
created_by: this.workspace.userId, created_by: this.workspace.userId,
@@ -152,13 +134,6 @@ export class NodeService {
throw new Error('Failed to create mutation'); throw new Error('Failed to create mutation');
} }
if (text) {
await trx
.insertInto('texts')
.values({ id: input.id, name: text.name, text: text.text })
.execute();
}
return { return {
createdNode, createdNode,
createdMutation, createdMutation,
@@ -206,13 +181,9 @@ export class NodeService {
): Promise<UpdateNodeResult | null> { ): Promise<UpdateNodeResult | null> {
this.debug(`Updating node ${nodeId}`); this.debug(`Updating node ${nodeId}`);
const nodeRow = await fetchNode(this.workspace.database, nodeId); const ancestors = await fetchNodeAncestors(this.workspace.database, nodeId);
if (!nodeRow) { const nodeRow = ancestors[ancestors.length - 1];
return 'not_found'; if (!nodeRow || nodeRow.id !== nodeId) {
}
const root = await fetchNode(this.workspace.database, nodeRow.root_id);
if (!root) {
return 'not_found'; return 'not_found';
} }
@@ -222,7 +193,22 @@ export class NodeService {
const updatedAttributes = updater(node.attributes as T); const updatedAttributes = updater(node.attributes as T);
const model = getNodeModel(updatedAttributes.type); const model = getNodeModel(updatedAttributes.type);
const ydoc = new YDoc();
const canUpdateAttributesContext: CanUpdateAttributesContext = {
user: {
id: this.workspace.userId,
role: this.workspace.role,
workspaceId: this.workspace.id,
accountId: this.workspace.accountId,
},
ancestors: ancestors.map(mapNode),
node: node,
attributes: updatedAttributes,
};
if (!model.canUpdateAttributes(canUpdateAttributesContext)) {
return 'unauthorized';
}
const state = await this.workspace.database const state = await this.workspace.database
.selectFrom('node_states') .selectFrom('node_states')
@@ -230,46 +216,31 @@ export class NodeService {
.selectAll() .selectAll()
.executeTakeFirst(); .executeTakeFirst();
if (state) {
ydoc.applyUpdate(state.state);
}
const updates = await this.workspace.database const updates = await this.workspace.database
.selectFrom('node_updates') .selectFrom('node_updates')
.where('node_id', '=', nodeId) .where('node_id', '=', nodeId)
.selectAll() .selectAll()
.execute(); .execute();
const ydoc = new YDoc();
if (state) {
ydoc.applyUpdate(state.state);
}
for (const update of updates) { for (const update of updates) {
ydoc.applyUpdate(update.data); ydoc.applyUpdate(update.data);
} }
const update = ydoc.update(model.attributesSchema, updatedAttributes); const update = ydoc.update(model.attributesSchema, updatedAttributes);
if (
!canUpdateNode(
{
user: {
userId: this.workspace.userId,
role: this.workspace.role,
},
root: mapNode(root),
node: node,
},
updatedAttributes
)
) {
return 'unauthorized';
}
if (!update) { if (!update) {
return 'success'; return 'success';
} }
const attributes = ydoc.getObject<NodeAttributes>(); const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(nodeId, updatedAttributes);
const localRevision = BigInt(node.localRevision) + BigInt(1); const localRevision = BigInt(node.localRevision) + BigInt(1);
const { updatedNode, createdMutation } = await this.workspace.database const { updatedNode, createdMutation } = await this.workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
@@ -328,17 +299,6 @@ export class NodeService {
throw new Error('Failed to create mutation'); throw new Error('Failed to create mutation');
} }
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', nodeId).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({ id: nodeId, name: text.name, text: text.text })
.execute();
}
return { return {
updatedNode, updatedNode,
createdMutation, createdMutation,
@@ -372,26 +332,28 @@ export class NodeService {
} }
public async deleteNode(nodeId: string) { public async deleteNode(nodeId: string) {
const node = await fetchNode(this.workspace.database, nodeId); const ancestors = await fetchNodeAncestors(this.workspace.database, nodeId);
if (!node) { const nodeRow = ancestors[ancestors.length - 1];
throw new Error('Node not found'); if (!nodeRow || nodeRow.id !== nodeId) {
return 'not_found';
} }
const root = await fetchNode(this.workspace.database, node.root_id); const node = mapNode(nodeRow);
if (!root) {
throw new Error('Root not found');
}
if ( const model = getNodeModel(node.attributes.type);
!canDeleteNode({
user: { const canDeleteNodeContext: CanDeleteNodeContext = {
userId: this.workspace.userId, user: {
role: this.workspace.role, id: this.workspace.userId,
}, role: this.workspace.role,
root: mapNode(root), workspaceId: this.workspace.id,
node: mapNode(node), accountId: this.workspace.accountId,
}) },
) { ancestors: ancestors.map(mapNode),
node: node,
};
if (!model.canDelete(canDeleteNodeContext)) {
throw new Error('Insufficient permissions'); throw new Error('Insufficient permissions');
} }
@@ -419,7 +381,7 @@ export class NodeService {
const deleteMutationData: DeleteNodeMutationData = { const deleteMutationData: DeleteNodeMutationData = {
id: nodeId, id: nodeId,
rootId: root.root_id, rootId: node.rootId,
deletedAt: new Date().toISOString(), deletedAt: new Date().toISOString(),
}; };
@@ -477,7 +439,6 @@ export class NodeService {
const ydoc = new YDoc(node.state); const ydoc = new YDoc(node.state);
const attributes = ydoc.getObject<NodeAttributes>(); const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(node.id, attributes);
const { createdNode } = await this.workspace.database const { createdNode } = await this.workspace.database
.transaction() .transaction()
@@ -500,13 +461,6 @@ export class NodeService {
throw new Error('Failed to create node'); throw new Error('Failed to create node');
} }
if (text) {
await trx
.insertInto('texts')
.values({ id: node.id, name: text.name, text: text.text })
.execute();
}
await trx await trx
.insertInto('node_states') .insertInto('node_states')
.returningAll() .returningAll()
@@ -576,7 +530,6 @@ export class NodeService {
} }
const attributes = ydoc.getObject<NodeAttributes>(); const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(node.id, attributes);
const localRevision = BigInt(existingNode.local_revision) + BigInt(1); const localRevision = BigInt(existingNode.local_revision) + BigInt(1);
const { updatedNode } = await this.workspace.database const { updatedNode } = await this.workspace.database
@@ -609,17 +562,6 @@ export class NodeService {
.where('id', '=', node.id) .where('id', '=', node.id)
.executeTakeFirst(); .executeTakeFirst();
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', node.id).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({ id: node.id, name: text.name, text: text.text })
.execute();
}
return { updatedNode }; return { updatedNode };
}); });
@@ -664,8 +606,6 @@ export class NodeService {
.where('id', '=', tombstone.id) .where('id', '=', tombstone.id)
.execute(); .execute();
await trx.deleteFrom('texts').where('id', '=', tombstone.id).execute();
await trx await trx
.deleteFrom('node_reactions') .deleteFrom('node_reactions')
.where('node_id', '=', tombstone.id) .where('node_id', '=', tombstone.id)
@@ -687,6 +627,16 @@ export class NodeService {
.where('id', '=', tombstone.id) .where('id', '=', tombstone.id)
.execute(); .execute();
await trx
.deleteFrom('documents')
.where('id', '=', tombstone.id)
.executeTakeFirst();
await trx
.deleteFrom('document_updates')
.where('document_id', '=', tombstone.id)
.execute();
return { deletedNode, deletedFile }; return { deletedNode, deletedFile };
}); });
@@ -734,8 +684,6 @@ export class NodeService {
.where('node_id', '=', mutation.id) .where('node_id', '=', mutation.id)
.execute(); .execute();
await tx.deleteFrom('texts').where('id', '=', mutation.id).execute();
await tx await tx
.deleteFrom('node_reactions') .deleteFrom('node_reactions')
.where('node_id', '=', mutation.id) .where('node_id', '=', mutation.id)
@@ -745,6 +693,13 @@ export class NodeService {
.deleteFrom('node_states') .deleteFrom('node_states')
.where('id', '=', mutation.id) .where('id', '=', mutation.id)
.execute(); .execute();
await tx.deleteFrom('documents').where('id', '=', mutation.id).execute();
await tx
.deleteFrom('document_updates')
.where('document_id', '=', mutation.id)
.execute();
}); });
eventBus.publish({ eventBus.publish({
@@ -786,11 +741,6 @@ export class NodeService {
.where('node_id', '=', mutation.id) .where('node_id', '=', mutation.id)
.execute(); .execute();
await this.workspace.database
.deleteFrom('texts')
.where('id', '=', mutation.id)
.execute();
await this.workspace.database await this.workspace.database
.deleteFrom('node_reactions') .deleteFrom('node_reactions')
.where('node_id', '=', mutation.id) .where('node_id', '=', mutation.id)
@@ -801,6 +751,16 @@ export class NodeService {
.where('id', '=', mutation.id) .where('id', '=', mutation.id)
.execute(); .execute();
await this.workspace.database
.deleteFrom('documents')
.where('id', '=', mutation.id)
.execute();
await this.workspace.database
.deleteFrom('document_updates')
.where('document_id', '=', mutation.id)
.execute();
return true; return true;
} }
@@ -843,8 +803,6 @@ export class NodeService {
} }
const attributes = ydoc.getObject<NodeAttributes>(); const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(mutation.id, attributes);
const updatedNode = await this.workspace.database const updatedNode = await this.workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
@@ -862,21 +820,6 @@ export class NodeService {
return undefined; return undefined;
} }
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', mutation.id).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({
id: mutation.id,
name: text.name,
text: text.text,
})
.execute();
}
await trx await trx
.deleteFrom('node_updates') .deleteFrom('node_updates')
.where('id', '=', mutation.updateId) .where('id', '=', mutation.updateId)
@@ -931,9 +874,8 @@ export class NodeService {
} }
const attributes = ydoc.getObject<NodeAttributes>(); const attributes = ydoc.getObject<NodeAttributes>();
const text = extractNodeText(mutation.id, attributes);
const deletedNode = JSON.parse(tombstone.data) as SelectNode; const deletedNode = JSON.parse(tombstone.data) as SelectNode;
const createdNode = await this.workspace.database const createdNode = await this.workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
@@ -962,21 +904,6 @@ export class NodeService {
.deleteFrom('tombstones') .deleteFrom('tombstones')
.where('id', '=', mutation.id) .where('id', '=', mutation.id)
.execute(); .execute();
if (text !== undefined) {
await trx.deleteFrom('texts').where('id', '=', mutation.id).execute();
}
if (text) {
await trx
.insertInto('texts')
.values({
id: mutation.id,
name: text.name,
text: text.text,
})
.execute();
}
}); });
if (createdNode) { if (createdNode) {

View File

@@ -50,16 +50,6 @@ export class UserService {
) )
.executeTakeFirst(); .executeTakeFirst();
await this.workspace.database
.deleteFrom('texts')
.where('id', '=', user.id)
.execute();
await this.workspace.database
.insertInto('texts')
.values({ id: user.id, name: user.name, text: null })
.execute();
if (createdUser) { if (createdUser) {
eventBus.publish({ eventBus.publish({
type: 'user_created', type: 'user_created',

View File

@@ -29,9 +29,9 @@ const roles: NodeCollaboratorRole[] = [
enabled: true, enabled: true,
}, },
{ {
name: 'Commenter', name: 'Collaborator',
value: 'commenter', value: 'collaborator',
description: 'Can message or comment on content', description: 'Can create records, messages or comments',
enabled: true, enabled: true,
}, },
{ {

View File

@@ -73,7 +73,7 @@ export const Conversation = ({
}; };
const isAdmin = hasNodeRole(role, 'admin'); const isAdmin = hasNodeRole(role, 'admin');
const canCreateMessage = hasNodeRole(role, 'commenter'); const canCreateMessage = hasNodeRole(role, 'collaborator');
return ( return (
<ConversationContext.Provider <ConversationContext.Provider

View File

@@ -1,7 +1,7 @@
import { import {
canCreateNode, CanCreateNodeContext,
canDeleteNode, CanDeleteNodeContext,
canUpdateNode, CanUpdateAttributesContext,
createDebugger, createDebugger,
CreateNodeMutationData, CreateNodeMutationData,
extractNodeCollaborators, extractNodeCollaborators,
@@ -309,35 +309,34 @@ export const createNodeFromMutation = async (
): Promise<CreateNodeOutput | null> => { ): Promise<CreateNodeOutput | null> => {
const ydoc = new YDoc(mutation.data); const ydoc = new YDoc(mutation.data);
const attributes = ydoc.getObject<NodeAttributes>(); const attributes = ydoc.getObject<NodeAttributes>();
let root: SelectNode | null = null; const model = getNodeModel(attributes.type);
let parentId: string | null = null;
if (attributes.type !== 'space' && attributes.type !== 'chat') { if (attributes.type !== 'space' && attributes.type !== 'chat') {
const parent = await fetchNode(attributes.parentId); parentId = attributes.parentId;
if (!parent) {
return null;
}
root = await fetchNode(parent.root_id);
} }
if ( const ancestors = parentId ? await fetchNodeAncestors(parentId) : [];
!canCreateNode( const canCreateNodeContext: CanCreateNodeContext = {
{ user: {
user: { id: user.id,
userId: user.id, role: user.role,
role: user.role, workspaceId: user.workspace_id,
}, accountId: user.account_id,
root: root ? mapNode(root) : null, },
}, ancestors: ancestors.map(mapNode),
attributes.type attributes,
) };
) {
if (!model.canCreate(canCreateNodeContext)) {
return null; return null;
} }
const rootId = ancestors[0]?.id ?? mutation.id;
const createNode: CreateNode = { const createNode: CreateNode = {
id: mutation.id, id: mutation.id,
root_id: root?.id ?? mutation.id, root_id: rootId,
attributes: JSON.stringify(attributes), attributes: JSON.stringify(attributes),
workspace_id: user.workspace_id, workspace_id: user.workspace_id,
created_at: new Date(mutation.createdAt), created_at: new Date(mutation.createdAt),
@@ -386,7 +385,7 @@ export const createNodeFromMutation = async (
eventBus.publish({ eventBus.publish({
type: 'node_created', type: 'node_created',
nodeId: mutation.id, nodeId: mutation.id,
rootId: root?.id ?? mutation.id, rootId,
workspaceId: user.workspace_id, workspaceId: user.workspace_id,
}); });
@@ -431,35 +430,36 @@ const tryUpdateNodeFromMutation = async (
user: SelectUser, user: SelectUser,
mutation: UpdateNodeMutationData mutation: UpdateNodeMutationData
): Promise<ConcurrentUpdateResult<UpdateNodeOutput>> => { ): Promise<ConcurrentUpdateResult<UpdateNodeOutput>> => {
const node = await fetchNode(mutation.id); const ancestors = await fetchNodeAncestors(mutation.id);
if (!node) { if (ancestors.length === 0) {
return { type: 'error', output: null }; return { type: 'error', output: null };
} }
const root = await fetchNode(node.root_id); const node = ancestors[ancestors.length - 1];
if (!root) { if (!node || node.id !== mutation.id) {
return { type: 'error', output: null }; return { type: 'error', output: null };
} }
const model = getNodeModel(node.type);
const ydoc = new YDoc(node.state); const ydoc = new YDoc(node.state);
ydoc.applyUpdate(mutation.data); ydoc.applyUpdate(mutation.data);
const attributes = ydoc.getObject<NodeAttributes>(); const attributes = ydoc.getObject<NodeAttributes>();
const attributesJson = JSON.stringify(attributes); const attributesJson = JSON.stringify(attributes);
if ( const canUpdateNodeContext: CanUpdateAttributesContext = {
!canUpdateNode( user: {
{ id: user.id,
user: { role: user.role,
userId: user.id, workspaceId: user.workspace_id,
role: user.role, accountId: user.account_id,
}, },
root: mapNode(root), ancestors: ancestors.map(mapNode),
node: mapNode(node), node: mapNode(node),
}, attributes,
attributes };
)
) { if (!model.canUpdateAttributes(canUpdateNodeContext)) {
return { type: 'error', output: null }; return { type: 'error', output: null };
} }
@@ -507,7 +507,7 @@ const tryUpdateNodeFromMutation = async (
eventBus.publish({ eventBus.publish({
type: 'node_updated', type: 'node_updated',
nodeId: mutation.id, nodeId: mutation.id,
rootId: root.id, rootId: node.root_id,
workspaceId: user.workspace_id, workspaceId: user.workspace_id,
}); });
@@ -544,26 +544,29 @@ export const deleteNode = async (
user: SelectUser, user: SelectUser,
input: DeleteNodeInput input: DeleteNodeInput
): Promise<DeleteNodeOutput | null> => { ): Promise<DeleteNodeOutput | null> => {
const node = await fetchNode(input.id); const ancestors = await fetchNodeAncestors(input.id);
if (!node) { if (ancestors.length === 0) {
return null; return null;
} }
const root = await fetchNode(node.root_id); const node = ancestors[ancestors.length - 1];
if (!root) { if (!node || node.id !== input.id) {
return null; return null;
} }
if ( const model = getNodeModel(node.type);
!canDeleteNode({ const canDeleteNodeContext: CanDeleteNodeContext = {
user: { user: {
userId: user.id, id: user.id,
role: user.role, role: user.role,
}, workspaceId: user.workspace_id,
root: mapNode(root), accountId: user.account_id,
node: mapNode(node), },
}) ancestors: ancestors.map(mapNode),
) { node: mapNode(node),
};
if (!model.canDelete(canDeleteNodeContext)) {
return null; return null;
} }

View File

@@ -1,124 +1,10 @@
import { isEqual } from 'lodash-es'; import { extractNodeRole, Node, WorkspaceRole, NodeRole } from '../index';
import {
extractNodeRole,
Node,
NodeAttributes,
WorkspaceRole,
NodeRole,
NodeType,
} from '../index';
export type UserInput = { export type UserInput = {
userId: string; userId: string;
role: WorkspaceRole; role: WorkspaceRole;
}; };
export type CanCreateNodeInput = {
user: UserInput;
root: Node | null;
};
export const canCreateNode = (
input: CanCreateNodeInput,
type: NodeType
): boolean => {
if (input.user.role === 'none') {
return false;
}
if (type === 'chat') {
return true;
}
if (type === 'space') {
return hasWorkspaceRole(input.user.role, 'collaborator');
}
const root = input.root;
if (!root) {
return false;
}
const rootRole = extractNodeRole(root, input.user.userId);
if (!rootRole) {
return false;
}
return hasNodeRole(rootRole, 'editor');
};
export type CanUpdateNodeInput = {
user: UserInput;
root: Node;
node: Node;
};
export const canUpdateNode = (
input: CanUpdateNodeInput,
attributes: NodeAttributes
): boolean => {
if (input.user.role === 'none') {
return false;
}
if (attributes.type === 'chat') {
return false;
}
const rootRole = extractNodeRole(input.root, input.user.userId);
if (!rootRole) {
return false;
}
if (attributes.type === 'space') {
if (input.node.type !== 'space') {
return false;
}
const afterCollaborators = attributes.collaborators;
const beforeCollaborators = input.node.attributes.collaborators;
if (!isEqual(afterCollaborators, beforeCollaborators)) {
return hasNodeRole(rootRole, 'admin');
}
}
return hasNodeRole(rootRole, 'editor');
};
export type CanDeleteNodeInput = {
user: UserInput;
root: Node;
node: Node;
};
export const canDeleteNode = (input: CanDeleteNodeInput): boolean => {
if (input.user.role === 'none') {
return false;
}
const node = input.node;
if (node.attributes.type === 'chat') {
return false;
}
const rootRole = extractNodeRole(input.root, input.user.userId);
if (!rootRole) {
return false;
}
if (node.attributes.type === 'record') {
return node.createdBy === input.user.userId;
}
if (node.attributes.type === 'space') {
return hasNodeRole(rootRole, 'admin');
}
return hasNodeRole(rootRole, 'editor');
};
export const hasWorkspaceRole = ( export const hasWorkspaceRole = (
currentRole: WorkspaceRole, currentRole: WorkspaceRole,
targetRole: WorkspaceRole targetRole: WorkspaceRole
@@ -160,11 +46,11 @@ export const hasNodeRole = (currentRole: NodeRole, targetRole: NodeRole) => {
return currentRole === 'admin' || currentRole === 'editor'; return currentRole === 'admin' || currentRole === 'editor';
} }
if (targetRole === 'commenter') { if (targetRole === 'collaborator') {
return ( return (
currentRole === 'admin' || currentRole === 'admin' ||
currentRole === 'editor' || currentRole === 'editor' ||
currentRole === 'commenter' currentRole === 'collaborator'
); );
} }
@@ -172,7 +58,7 @@ export const hasNodeRole = (currentRole: NodeRole, targetRole: NodeRole) => {
return ( return (
currentRole === 'admin' || currentRole === 'admin' ||
currentRole === 'editor' || currentRole === 'editor' ||
currentRole === 'commenter' || currentRole === 'collaborator' ||
currentRole === 'viewer' currentRole === 'viewer'
); );
} }

View File

@@ -1,36 +1,6 @@
import { Block } from '../registry/block'; import { Block } from '../registry/block';
import { NodeAttributes } from '../registry/nodes';
export type TextResult = { export const extractBlockTexts = (
id: string;
name: string | null;
text: string | null;
};
export const extractNodeText = (
id: string,
attributes: NodeAttributes
): TextResult | undefined => {
// if (attributes.type === 'page') {
// return {
// id,
// name: attributes.name,
// text: extractBlockTexts(id, attributes.content),
// };
// }
// if (attributes.type === 'record') {
// return {
// id,
// name: attributes.name,
// text: extractBlockTexts(id, attributes.content),
// };
// }
return undefined;
};
const extractBlockTexts = (
entryId: string, entryId: string,
blocks: Record<string, Block> | undefined | null blocks: Record<string, Block> | undefined | null
): string | null => { ): string | null => {

View File

@@ -2,6 +2,9 @@ import { z } from 'zod';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
import { NodeAttributes } from '.'; import { NodeAttributes } from '.';
export const channelAttributesSchema = z.object({ export const channelAttributesSchema = z.object({
@@ -16,26 +19,59 @@ export type ChannelAttributes = z.infer<typeof channelAttributesSchema>;
export const channelModel: NodeModel = { export const channelModel: NodeModel = {
type: 'channel', type: 'channel',
attributesSchema: channelAttributesSchema, attributesSchema: channelAttributesSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canDelete: async (context, _) => { canUpdateDocument: () => {
return context.hasAdminAccess(); return false;
}, },
getName: function ( canDelete: (context) => {
if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (
_: string, _: string,
attributes: NodeAttributes attributes: NodeAttributes
): string | null | undefined { ): string | null | undefined => {
if (attributes.type !== 'channel') { if (attributes.type !== 'channel') {
return null; return null;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: (): string | null | undefined => {
return undefined;
},
getDocumentText: (): string | null | undefined => {
return undefined; return undefined;
}, },
}; };

View File

@@ -2,7 +2,7 @@ import { z } from 'zod';
import { NodeModel, nodeRoleEnum } from './core'; import { NodeModel, nodeRoleEnum } from './core';
import { NodeAttributes } from '.'; import { hasWorkspaceRole } from '../../lib/permissions';
export const chatAttributesSchema = z.object({ export const chatAttributesSchema = z.object({
type: z.literal('chat'), type: z.literal('chat'),
@@ -14,19 +14,42 @@ export type ChatAttributes = z.infer<typeof chatAttributesSchema>;
export const chatModel: NodeModel = { export const chatModel: NodeModel = {
type: 'chat', type: 'chat',
attributesSchema: chatAttributesSchema, attributesSchema: chatAttributesSchema,
canCreate: async (_, __) => { canCreate: (context) => {
if (!hasWorkspaceRole(context.user.role, 'guest')) {
return false;
}
if (context.attributes.type !== 'chat') {
return false;
}
const collaborators = context.attributes.collaborators;
if (Object.keys(collaborators).length !== 2) {
return false;
}
if (!collaborators[context.user.id]) {
return false;
}
return true; return true;
}, },
canUpdate: async (_, __) => { canUpdateAttributes: () => {
return false; return false;
}, },
canDelete: async (_, __) => { canUpdateDocument: () => {
return false; return false;
}, },
getName: function (_: string, __: NodeAttributes): string | null | undefined { canDelete: () => {
return false;
},
getName: (): string | null | undefined => {
return undefined; return undefined;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: (): string | null | undefined => {
return undefined;
},
getDocumentText: (): string | null | undefined => {
return undefined; return undefined;
}, },
}; };

View File

@@ -1,13 +1,17 @@
import { z, ZodSchema } from 'zod'; import { z, ZodSchema } from 'zod';
import { WorkspaceRole } from '../../types/workspaces'; import { WorkspaceRole } from '../../types/workspaces';
import { hasNodeRole } from '../../lib/permissions'; import { DocumentContent } from '../documents';
import { extractNodeRole } from '../../lib/nodes';
import { Node, NodeAttributes } from '.'; import { Node, NodeAttributes } from '.';
export type NodeRole = 'admin' | 'editor' | 'commenter' | 'viewer'; export type NodeRole = 'admin' | 'editor' | 'collaborator' | 'viewer';
export const nodeRoleEnum = z.enum(['admin', 'editor', 'commenter', 'viewer']); export const nodeRoleEnum = z.enum([
'admin',
'editor',
'collaborator',
'viewer',
]);
export interface NodeMutationUser { export interface NodeMutationUser {
id: string; id: string;
@@ -16,54 +20,49 @@ export interface NodeMutationUser {
accountId: string; accountId: string;
} }
export class NodeMutationContext { export type CanCreateNodeContext = {
public readonly user: NodeMutationUser; user: NodeMutationUser;
public readonly root: Node | null; ancestors: Node[];
public readonly role: NodeRole | null; attributes: NodeAttributes;
};
constructor(user: NodeMutationUser, root: Node | null) { export type CanUpdateAttributesContext = {
this.user = user; user: NodeMutationUser;
this.root = root; ancestors: Node[];
this.role = root ? extractNodeRole(root, user.id) : null; node: Node;
} attributes: NodeAttributes;
};
public hasAdminAccess = () => { export type CanUpdateDocumentContext = {
return this.role ? hasNodeRole(this.role, 'admin') : false; user: NodeMutationUser;
}; ancestors: Node[];
node: Node;
};
public hasEditorAccess = () => { export type CanDeleteNodeContext = {
return this.role ? hasNodeRole(this.role, 'editor') : false; user: NodeMutationUser;
}; ancestors: Node[];
node: Node;
public hasCollaboratorAccess = () => { };
return this.role ? hasNodeRole(this.role, 'commenter') : false;
};
public hasViewerAccess = () => {
return this.role ? hasNodeRole(this.role, 'viewer') : false;
};
}
export interface NodeModel { export interface NodeModel {
type: string; type: string;
attributesSchema: ZodSchema; attributesSchema: ZodSchema;
documentSchema?: ZodSchema; documentSchema?: ZodSchema;
canCreate: ( canCreate: (context: CanCreateNodeContext) => boolean;
context: NodeMutationContext, canUpdateAttributes: (context: CanUpdateAttributesContext) => boolean;
attributes: NodeAttributes canUpdateDocument: (context: CanUpdateDocumentContext) => boolean;
) => Promise<boolean>; canDelete: (context: CanDeleteNodeContext) => boolean;
canUpdate: (
context: NodeMutationContext,
node: Node,
attributes: NodeAttributes
) => Promise<boolean>;
canDelete: (context: NodeMutationContext, node: Node) => Promise<boolean>;
getName: ( getName: (
id: string, id: string,
attributes: NodeAttributes attributes: NodeAttributes
) => string | null | undefined; ) => string | null | undefined;
getText: ( getAttributesText: (
id: string, id: string,
attributes: NodeAttributes attributes: NodeAttributes
) => string | null | undefined; ) => string | null | undefined;
getDocumentText: (
id: string,
content: DocumentContent
) => string | null | undefined;
} }

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { NodeAttributes } from '.'; import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const databaseViewFieldAttributesSchema = z.object({ export const databaseViewFieldAttributesSchema = z.object({
id: z.string(), id: z.string(),
@@ -91,26 +92,56 @@ export type DatabaseViewLayout = 'table' | 'board' | 'calendar';
export const databaseViewModel: NodeModel = { export const databaseViewModel: NodeModel = {
type: 'database_view', type: 'database_view',
attributesSchema: databaseViewAttributesSchema, attributesSchema: databaseViewAttributesSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canDelete: async (context, _) => { canUpdateDocument: () => {
return context.hasEditorAccess(); return false;
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
getName: (_, attributes) => {
if (attributes.type !== 'database_view') { if (attributes.type !== 'database_view') {
return null; return undefined;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };

View File

@@ -3,7 +3,8 @@ import { z } from 'zod';
import { fieldAttributesSchema } from './field'; import { fieldAttributesSchema } from './field';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { NodeAttributes } from '.'; import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const databaseAttributesSchema = z.object({ export const databaseAttributesSchema = z.object({
type: z.literal('database'), type: z.literal('database'),
@@ -18,26 +19,56 @@ export type DatabaseAttributes = z.infer<typeof databaseAttributesSchema>;
export const databaseModel: NodeModel = { export const databaseModel: NodeModel = {
type: 'database', type: 'database',
attributesSchema: databaseAttributesSchema, attributesSchema: databaseAttributesSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canDelete: async (context, _) => { canUpdateDocument: () => {
return context.hasEditorAccess(); return false;
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
},
getName: (_, attributes) => {
if (attributes.type !== 'database') { if (attributes.type !== 'database') {
return null; return undefined;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { NodeAttributes } from '.'; import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const fileAttributesSchema = z.object({ export const fileAttributesSchema = z.object({
type: z.literal('file'), type: z.literal('file'),
@@ -16,26 +17,71 @@ export type FileAttributes = z.infer<typeof fileAttributesSchema>;
export const fileModel: NodeModel = { export const fileModel: NodeModel = {
type: 'file', type: 'file',
attributesSchema: fileAttributesSchema, attributesSchema: fileAttributesSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
const parent = context.ancestors[context.ancestors.length - 1]!;
if (parent.type === 'message') {
return hasNodeRole(role, 'collaborator');
}
return hasNodeRole(role, 'editor');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
const parent = context.ancestors[context.ancestors.length - 1]!;
if (parent.type === 'message') {
return parent.createdBy === context.user.id || hasNodeRole(role, 'admin');
}
return hasNodeRole(role, 'editor');
}, },
canDelete: async (context, _) => { canUpdateDocument: () => {
return context.hasEditorAccess(); return false;
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
const parent = context.ancestors[context.ancestors.length - 1]!;
if (parent.type === 'message') {
return parent.createdBy === context.user.id || hasNodeRole(role, 'admin');
}
return hasNodeRole(role, 'editor');
},
getName: (_, attributes) => {
if (attributes.type !== 'file') { if (attributes.type !== 'file') {
return null; return null;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { NodeAttributes } from '.'; import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const folderAttributesSchema = z.object({ export const folderAttributesSchema = z.object({
type: z.literal('folder'), type: z.literal('folder'),
@@ -16,26 +17,56 @@ export type FolderAttributes = z.infer<typeof folderAttributesSchema>;
export const folderModel: NodeModel = { export const folderModel: NodeModel = {
type: 'folder', type: 'folder',
attributesSchema: folderAttributesSchema, attributesSchema: folderAttributesSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canDelete: async (context, _) => { canUpdateDocument: () => {
return context.hasEditorAccess(); return false;
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'folder') { if (attributes.type !== 'folder') {
return null; return undefined;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: () => {
return undefined;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };

View File

@@ -3,8 +3,9 @@ import { z } from 'zod';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { blockSchema } from '../block'; import { blockSchema } from '../block';
import { extractBlockTexts } from '../../lib/texts';
import { NodeAttributes } from '.'; import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole } from '../../lib/permissions';
export const messageAttributesSchema = z.object({ export const messageAttributesSchema = z.object({
type: z.literal('message'), type: z.literal('message'),
@@ -20,26 +21,67 @@ export type MessageAttributes = z.infer<typeof messageAttributesSchema>;
export const messageModel: NodeModel = { export const messageModel: NodeModel = {
type: 'message', type: 'message',
attributesSchema: messageAttributesSchema, attributesSchema: messageAttributesSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasCollaboratorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'collaborator');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasCollaboratorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return context.node.createdBy === context.user.id;
}, },
canDelete: async (context, _) => { canUpdateDocument: () => {
return context.hasCollaboratorAccess(); return false;
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return (
context.node.createdBy === context.user.id || hasNodeRole(role, 'admin')
);
},
getName: (_, attributes) => {
if (attributes.type !== 'message') { if (attributes.type !== 'message') {
return null; return undefined;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: (id, attributes) => {
if (attributes.type !== 'message') {
return undefined;
}
const text = extractBlockTexts(id, attributes.content);
if (!text) {
return null;
}
return text;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };

View File

@@ -3,8 +3,8 @@ import { z } from 'zod';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { richTextContentSchema } from '../documents/rich-text'; import { richTextContentSchema } from '../documents/rich-text';
import { extractNodeRole } from '../../lib/nodes';
import { NodeAttributes } from '.'; import { hasNodeRole } from '../../lib/permissions';
export const pageAttributesSchema = z.object({ export const pageAttributesSchema = z.object({
type: z.literal('page'), type: z.literal('page'),
@@ -19,26 +19,69 @@ export const pageModel: NodeModel = {
type: 'page', type: 'page',
attributesSchema: pageAttributesSchema, attributesSchema: pageAttributesSchema,
documentSchema: richTextContentSchema, documentSchema: richTextContentSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
canDelete: async (context, _) => { canUpdateDocument: (context) => {
return context.hasEditorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'editor');
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'page') { if (attributes.type !== 'page') {
return null; return undefined;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: (id, attributes) => {
if (attributes.type !== 'page') {
return undefined;
}
return attributes.name;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };

View File

@@ -4,8 +4,8 @@ import { fieldValueSchema } from './field-value';
import { NodeModel } from './core'; import { NodeModel } from './core';
import { richTextContentSchema } from '../documents/rich-text'; import { richTextContentSchema } from '../documents/rich-text';
import { extractNodeRole } from '../../lib/nodes';
import { NodeAttributes } from '.'; import { hasNodeRole } from '../../lib/permissions';
export const recordAttributesSchema = z.object({ export const recordAttributesSchema = z.object({
type: z.literal('record'), type: z.literal('record'),
@@ -22,26 +22,81 @@ export const recordModel: NodeModel = {
type: 'record', type: 'record',
attributesSchema: recordAttributesSchema, attributesSchema: recordAttributesSchema,
documentSchema: richTextContentSchema, documentSchema: richTextContentSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasCollaboratorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'collaborator');
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasCollaboratorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
if (context.node.createdBy === context.user.id) {
return true;
}
return hasNodeRole(role, 'editor');
}, },
canDelete: async (context, _) => { canUpdateDocument: (context) => {
return context.hasCollaboratorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
if (context.node.createdBy === context.user.id) {
return true;
}
return hasNodeRole(role, 'editor');
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
if (context.node.createdBy === context.user.id) {
return true;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'record') { if (attributes.type !== 'record') {
return null; return undefined;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: (id, attributes) => {
if (attributes.type !== 'record') {
return undefined;
}
return attributes.name;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };

View File

@@ -2,7 +2,8 @@ import { z } from 'zod';
import { NodeModel, nodeRoleEnum } from './core'; import { NodeModel, nodeRoleEnum } from './core';
import { NodeAttributes } from '.'; import { extractNodeRole } from '../../lib/nodes';
import { hasNodeRole, hasWorkspaceRole } from '../../lib/permissions';
export const spaceAttributesSchema = z.object({ export const spaceAttributesSchema = z.object({
type: z.literal('space'), type: z.literal('space'),
@@ -18,26 +19,72 @@ export type SpaceAttributes = z.infer<typeof spaceAttributesSchema>;
export const spaceModel: NodeModel = { export const spaceModel: NodeModel = {
type: 'space', type: 'space',
attributesSchema: spaceAttributesSchema, attributesSchema: spaceAttributesSchema,
canCreate: async (context, _) => { canCreate: (context) => {
return context.hasCollaboratorAccess(); if (context.ancestors.length > 0) {
return false;
}
if (!hasWorkspaceRole(context.user.role, 'collaborator')) {
return false;
}
if (context.attributes.type !== 'space') {
return false;
}
const collaborators = context.attributes.collaborators;
if (Object.keys(collaborators).length === 0) {
return false;
}
if (collaborators[context.user.id] !== 'admin') {
return false;
}
return true;
}, },
canUpdate: async (context, _) => { canUpdateAttributes: (context) => {
return context.hasCollaboratorAccess(); if (context.ancestors.length === 0) {
return false;
}
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
}, },
canDelete: async (context, _) => { canUpdateDocument: () => {
return context.hasCollaboratorAccess(); return false;
}, },
getName: function ( canDelete: (context) => {
_: string, if (context.ancestors.length === 0) {
attributes: NodeAttributes return false;
): string | null | undefined { }
const role = extractNodeRole(context.ancestors, context.user.id);
if (!role) {
return false;
}
return hasNodeRole(role, 'admin');
},
getName: (_, attributes) => {
if (attributes.type !== 'space') { if (attributes.type !== 'space') {
return null; return undefined;
} }
return attributes.name; return attributes.name;
}, },
getText: function (_: string, __: NodeAttributes): string | null | undefined { getAttributesText: (id, attributes) => {
if (attributes.type !== 'space') {
return undefined;
}
return attributes.name;
},
getDocumentText: () => {
return undefined; return undefined;
}, },
}; };