mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Permission checks improvements
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -10,11 +10,10 @@ import { createCollaborationsTable } from './00007-create-collaborations-table';
|
||||
import { createFilesTable } from './00008-create-files-table';
|
||||
import { createMutationsTable } from './00009-create-mutations-table';
|
||||
import { createTombstonesTable } from './00010-create-tombstones-table';
|
||||
import { createTextsTable } from './00011-create-texts-table';
|
||||
import { createCursorsTable } from './00012-create-cursors-table';
|
||||
import { createMetadataTable } from './00013-create-metadata-table';
|
||||
import { createDocumentsTable } from './00014-create-documents-table';
|
||||
import { createDocumentUpdatesTable } from './00015-create-document-updates-table';
|
||||
import { createDocumentsTable } from './00011-create-documents-table';
|
||||
import { createDocumentUpdatesTable } from './00012-create-document-updates-table';
|
||||
import { createCursorsTable } from './00013-create-cursors-table';
|
||||
import { createMetadataTable } from './00014-create-metadata-table';
|
||||
|
||||
export const workspaceDatabaseMigrations: Record<string, Migration> = {
|
||||
'00001-create-users-table': createUsersTable,
|
||||
@@ -27,9 +26,8 @@ export const workspaceDatabaseMigrations: Record<string, Migration> = {
|
||||
'00008-create-files-table': createFilesTable,
|
||||
'00009-create-mutations-table': createMutationsTable,
|
||||
'00010-create-tombstones-table': createTombstonesTable,
|
||||
'00011-create-texts-table': createTextsTable,
|
||||
'00012-create-cursors-table': createCursorsTable,
|
||||
'00013-create-metadata-table': createMetadataTable,
|
||||
'00014-create-documents-table': createDocumentsTable,
|
||||
'00015-create-document-updates-table': createDocumentUpdatesTable,
|
||||
'00011-create-documents-table': createDocumentsTable,
|
||||
'00012-create-document-updates-table': createDocumentUpdatesTable,
|
||||
'00013-create-cursors-table': createCursorsTable,
|
||||
'00014-create-metadata-table': createMetadataTable,
|
||||
};
|
||||
|
||||
@@ -159,16 +159,6 @@ export type SelectTombsonte = Selectable<TombstoneTable>;
|
||||
export type CreateTombstone = Insertable<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 {
|
||||
key: ColumnType<string, string, never>;
|
||||
value: ColumnType<bigint, bigint, bigint>;
|
||||
@@ -227,9 +217,8 @@ export interface WorkspaceDatabaseSchema {
|
||||
files: FileTable;
|
||||
mutations: MutationTable;
|
||||
tombstones: TombstoneTable;
|
||||
texts: TextTable;
|
||||
cursors: CursorTable;
|
||||
metadata: MetadataTable;
|
||||
documents: DocumentTable;
|
||||
document_updates: DocumentUpdateTable;
|
||||
cursors: CursorTable;
|
||||
metadata: MetadataTable;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import {
|
||||
canCreateNode,
|
||||
generateId,
|
||||
IdType,
|
||||
FileAttributes,
|
||||
} from '@colanode/core';
|
||||
import { generateId, IdType, FileAttributes } from '@colanode/core';
|
||||
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import {
|
||||
FileCreateMutationInput,
|
||||
FileCreateMutationOutput,
|
||||
} 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';
|
||||
|
||||
export class FileCreateMutationHandler
|
||||
@@ -23,53 +15,20 @@ export class FileCreateMutationHandler
|
||||
input: FileCreateMutationInput
|
||||
): Promise<FileCreateMutationOutput> {
|
||||
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 = {
|
||||
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);
|
||||
await workspace.files.createFile(input.filePath, fileId, node.id, root);
|
||||
await workspace.nodes.createNode({
|
||||
id: fileId,
|
||||
attributes,
|
||||
parentId: node.id,
|
||||
parentId: input.parentId,
|
||||
});
|
||||
|
||||
await workspace.files.createFile(fileId, input.filePath);
|
||||
|
||||
return {
|
||||
id: fileId,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
canCreateNode,
|
||||
EditorNodeTypes,
|
||||
FileAttributes,
|
||||
generateId,
|
||||
IdType,
|
||||
MessageAttributes,
|
||||
@@ -12,10 +12,12 @@ import {
|
||||
MessageCreateMutationInput,
|
||||
MessageCreateMutationOutput,
|
||||
} 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 { mapNode } from '@/main/lib/mappers';
|
||||
|
||||
interface MessageFile {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class MessageCreateMutationHandler
|
||||
extends WorkspaceMutationHandlerBase
|
||||
@@ -26,43 +28,10 @@ export class MessageCreateMutationHandler
|
||||
): Promise<MessageCreateMutationOutput> {
|
||||
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 editorContent = input.content.content ?? [];
|
||||
const blocks = mapContentsToBlocks(messageId, editorContent, new Map());
|
||||
const filesToCreate: MessageFile[] = [];
|
||||
|
||||
// check if there are nested nodes (files, pages, folders etc.)
|
||||
for (const block of Object.values(blocks)) {
|
||||
@@ -70,7 +39,10 @@ export class MessageCreateMutationHandler
|
||||
const path = block.attrs?.path;
|
||||
const fileId = generateId(IdType.File);
|
||||
|
||||
await workspace.files.createFile(path, fileId, messageId, root);
|
||||
filesToCreate.push({
|
||||
id: fileId,
|
||||
path: path,
|
||||
});
|
||||
|
||||
block.id = fileId;
|
||||
block.type = 'file';
|
||||
@@ -92,6 +64,21 @@ export class MessageCreateMutationHandler
|
||||
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 {
|
||||
id: messageId,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sql } from 'kysely';
|
||||
|
||||
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
|
||||
import { SelectUser } from '@/main/databases/workspace';
|
||||
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
|
||||
@@ -82,26 +80,20 @@ export class UserSearchQueryHandler
|
||||
input: UserSearchQueryInput
|
||||
): Promise<SelectUser[]> {
|
||||
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
|
||||
|
||||
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);
|
||||
return result.rows;
|
||||
let queryBuilder = workspace.database
|
||||
.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[]> {
|
||||
|
||||
@@ -16,6 +16,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
fetchNode,
|
||||
fetchUserStorageUsed,
|
||||
getFileMetadata,
|
||||
getWorkspaceFilesDirectoryPath,
|
||||
@@ -26,7 +27,7 @@ import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { DownloadStatus, UploadStatus } from '@/shared/types/files';
|
||||
import { WorkspaceService } from '@/main/services/workspaces/workspace-service';
|
||||
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 { formatBytes } from '@/shared/lib/files';
|
||||
|
||||
@@ -93,12 +94,7 @@ export class FileService {
|
||||
this.cleanupEventLoop.start();
|
||||
}
|
||||
|
||||
public async createFile(
|
||||
path: string,
|
||||
id: string,
|
||||
parentId: string,
|
||||
root: SelectNode
|
||||
): Promise<void> {
|
||||
public async createFile(id: string, path: string): Promise<void> {
|
||||
const metadata = getFileMetadata(path);
|
||||
if (!metadata) {
|
||||
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);
|
||||
|
||||
const mutationData: CreateFileMutationData = {
|
||||
id,
|
||||
type: metadata.type,
|
||||
parentId: parentId,
|
||||
rootId: root.id,
|
||||
parentId: node.parent_id!,
|
||||
rootId: node.root_id,
|
||||
name: metadata.name,
|
||||
originalName: metadata.name,
|
||||
extension: metadata.extension,
|
||||
@@ -156,8 +160,8 @@ export class FileService {
|
||||
.values({
|
||||
id,
|
||||
type: metadata.type,
|
||||
parent_id: parentId,
|
||||
root_id: root.id,
|
||||
parent_id: node.parent_id!,
|
||||
root_id: node.root_id,
|
||||
name: metadata.name,
|
||||
original_name: metadata.name,
|
||||
mime_type: metadata.mimeType,
|
||||
|
||||
@@ -3,21 +3,19 @@ import {
|
||||
IdType,
|
||||
createDebugger,
|
||||
NodeAttributes,
|
||||
Node,
|
||||
canCreateNode,
|
||||
extractNodeText,
|
||||
canUpdateNode,
|
||||
canDeleteNode,
|
||||
DeleteNodeMutationData,
|
||||
SyncNodeData,
|
||||
SyncNodeTombstoneData,
|
||||
getNodeModel,
|
||||
CreateNodeMutationData,
|
||||
UpdateNodeMutationData,
|
||||
CanCreateNodeContext,
|
||||
CanUpdateAttributesContext,
|
||||
CanDeleteNodeContext,
|
||||
} from '@colanode/core';
|
||||
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 { eventBus } from '@/shared/lib/event-bus';
|
||||
import { WorkspaceService } from '@/main/services/workspaces/workspace-service';
|
||||
@@ -49,42 +47,26 @@ export class NodeService {
|
||||
public async createNode(input: CreateNodeInput): Promise<SelectNode> {
|
||||
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 parent = await fetchNode(this.workspace.database, input.parentId);
|
||||
|
||||
if (parent) {
|
||||
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(
|
||||
{
|
||||
const model = getNodeModel(input.attributes.type);
|
||||
const canCreateNodeContext: CanCreateNodeContext = {
|
||||
user: {
|
||||
userId: this.workspace.userId,
|
||||
id: this.workspace.userId,
|
||||
role: this.workspace.role,
|
||||
workspaceId: this.workspace.id,
|
||||
accountId: this.workspace.accountId,
|
||||
},
|
||||
root: root,
|
||||
},
|
||||
input.attributes.type
|
||||
)
|
||||
) {
|
||||
ancestors: ancestors.map(mapNode),
|
||||
attributes: input.attributes,
|
||||
};
|
||||
|
||||
if (!model.canCreate(canCreateNodeContext)) {
|
||||
throw new Error('Insufficient permissions');
|
||||
}
|
||||
|
||||
const model = getNodeModel(input.attributes.type);
|
||||
const ydoc = new YDoc();
|
||||
const update = ydoc.update(model.attributesSchema, input.attributes);
|
||||
|
||||
@@ -93,7 +75,7 @@ export class NodeService {
|
||||
}
|
||||
|
||||
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
|
||||
.transaction()
|
||||
@@ -103,7 +85,7 @@ export class NodeService {
|
||||
.returningAll()
|
||||
.values({
|
||||
id: input.id,
|
||||
root_id: root?.id ?? input.id,
|
||||
root_id: rootId,
|
||||
attributes: JSON.stringify(input.attributes),
|
||||
created_at: createdAt,
|
||||
created_by: this.workspace.userId,
|
||||
@@ -152,13 +134,6 @@ export class NodeService {
|
||||
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 {
|
||||
createdNode,
|
||||
createdMutation,
|
||||
@@ -206,13 +181,9 @@ export class NodeService {
|
||||
): Promise<UpdateNodeResult | null> {
|
||||
this.debug(`Updating node ${nodeId}`);
|
||||
|
||||
const nodeRow = await fetchNode(this.workspace.database, nodeId);
|
||||
if (!nodeRow) {
|
||||
return 'not_found';
|
||||
}
|
||||
|
||||
const root = await fetchNode(this.workspace.database, nodeRow.root_id);
|
||||
if (!root) {
|
||||
const ancestors = await fetchNodeAncestors(this.workspace.database, nodeId);
|
||||
const nodeRow = ancestors[ancestors.length - 1];
|
||||
if (!nodeRow || nodeRow.id !== nodeId) {
|
||||
return 'not_found';
|
||||
}
|
||||
|
||||
@@ -222,7 +193,22 @@ export class NodeService {
|
||||
const updatedAttributes = updater(node.attributes as T);
|
||||
|
||||
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
|
||||
.selectFrom('node_states')
|
||||
@@ -230,46 +216,31 @@ export class NodeService {
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
if (state) {
|
||||
ydoc.applyUpdate(state.state);
|
||||
}
|
||||
|
||||
const updates = await this.workspace.database
|
||||
.selectFrom('node_updates')
|
||||
.where('node_id', '=', nodeId)
|
||||
.selectAll()
|
||||
.execute();
|
||||
|
||||
const ydoc = new YDoc();
|
||||
|
||||
if (state) {
|
||||
ydoc.applyUpdate(state.state);
|
||||
}
|
||||
|
||||
for (const update of updates) {
|
||||
ydoc.applyUpdate(update.data);
|
||||
}
|
||||
|
||||
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) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
const attributes = ydoc.getObject<NodeAttributes>();
|
||||
const text = extractNodeText(nodeId, updatedAttributes);
|
||||
|
||||
const localRevision = BigInt(node.localRevision) + BigInt(1);
|
||||
|
||||
const { updatedNode, createdMutation } = await this.workspace.database
|
||||
.transaction()
|
||||
.execute(async (trx) => {
|
||||
@@ -328,17 +299,6 @@ export class NodeService {
|
||||
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 {
|
||||
updatedNode,
|
||||
createdMutation,
|
||||
@@ -372,26 +332,28 @@ export class NodeService {
|
||||
}
|
||||
|
||||
public async deleteNode(nodeId: string) {
|
||||
const node = await fetchNode(this.workspace.database, nodeId);
|
||||
if (!node) {
|
||||
throw new Error('Node not found');
|
||||
const ancestors = await fetchNodeAncestors(this.workspace.database, nodeId);
|
||||
const nodeRow = ancestors[ancestors.length - 1];
|
||||
if (!nodeRow || nodeRow.id !== nodeId) {
|
||||
return 'not_found';
|
||||
}
|
||||
|
||||
const root = await fetchNode(this.workspace.database, node.root_id);
|
||||
if (!root) {
|
||||
throw new Error('Root not found');
|
||||
}
|
||||
const node = mapNode(nodeRow);
|
||||
|
||||
if (
|
||||
!canDeleteNode({
|
||||
const model = getNodeModel(node.attributes.type);
|
||||
|
||||
const canDeleteNodeContext: CanDeleteNodeContext = {
|
||||
user: {
|
||||
userId: this.workspace.userId,
|
||||
id: this.workspace.userId,
|
||||
role: this.workspace.role,
|
||||
workspaceId: this.workspace.id,
|
||||
accountId: this.workspace.accountId,
|
||||
},
|
||||
root: mapNode(root),
|
||||
node: mapNode(node),
|
||||
})
|
||||
) {
|
||||
ancestors: ancestors.map(mapNode),
|
||||
node: node,
|
||||
};
|
||||
|
||||
if (!model.canDelete(canDeleteNodeContext)) {
|
||||
throw new Error('Insufficient permissions');
|
||||
}
|
||||
|
||||
@@ -419,7 +381,7 @@ export class NodeService {
|
||||
|
||||
const deleteMutationData: DeleteNodeMutationData = {
|
||||
id: nodeId,
|
||||
rootId: root.root_id,
|
||||
rootId: node.rootId,
|
||||
deletedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -477,7 +439,6 @@ export class NodeService {
|
||||
|
||||
const ydoc = new YDoc(node.state);
|
||||
const attributes = ydoc.getObject<NodeAttributes>();
|
||||
const text = extractNodeText(node.id, attributes);
|
||||
|
||||
const { createdNode } = await this.workspace.database
|
||||
.transaction()
|
||||
@@ -500,13 +461,6 @@ export class NodeService {
|
||||
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
|
||||
.insertInto('node_states')
|
||||
.returningAll()
|
||||
@@ -576,7 +530,6 @@ export class NodeService {
|
||||
}
|
||||
|
||||
const attributes = ydoc.getObject<NodeAttributes>();
|
||||
const text = extractNodeText(node.id, attributes);
|
||||
const localRevision = BigInt(existingNode.local_revision) + BigInt(1);
|
||||
|
||||
const { updatedNode } = await this.workspace.database
|
||||
@@ -609,17 +562,6 @@ export class NodeService {
|
||||
.where('id', '=', node.id)
|
||||
.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 };
|
||||
});
|
||||
|
||||
@@ -664,8 +606,6 @@ export class NodeService {
|
||||
.where('id', '=', tombstone.id)
|
||||
.execute();
|
||||
|
||||
await trx.deleteFrom('texts').where('id', '=', tombstone.id).execute();
|
||||
|
||||
await trx
|
||||
.deleteFrom('node_reactions')
|
||||
.where('node_id', '=', tombstone.id)
|
||||
@@ -687,6 +627,16 @@ export class NodeService {
|
||||
.where('id', '=', tombstone.id)
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.deleteFrom('documents')
|
||||
.where('id', '=', tombstone.id)
|
||||
.executeTakeFirst();
|
||||
|
||||
await trx
|
||||
.deleteFrom('document_updates')
|
||||
.where('document_id', '=', tombstone.id)
|
||||
.execute();
|
||||
|
||||
return { deletedNode, deletedFile };
|
||||
});
|
||||
|
||||
@@ -734,8 +684,6 @@ export class NodeService {
|
||||
.where('node_id', '=', mutation.id)
|
||||
.execute();
|
||||
|
||||
await tx.deleteFrom('texts').where('id', '=', mutation.id).execute();
|
||||
|
||||
await tx
|
||||
.deleteFrom('node_reactions')
|
||||
.where('node_id', '=', mutation.id)
|
||||
@@ -745,6 +693,13 @@ export class NodeService {
|
||||
.deleteFrom('node_states')
|
||||
.where('id', '=', mutation.id)
|
||||
.execute();
|
||||
|
||||
await tx.deleteFrom('documents').where('id', '=', mutation.id).execute();
|
||||
|
||||
await tx
|
||||
.deleteFrom('document_updates')
|
||||
.where('document_id', '=', mutation.id)
|
||||
.execute();
|
||||
});
|
||||
|
||||
eventBus.publish({
|
||||
@@ -786,11 +741,6 @@ export class NodeService {
|
||||
.where('node_id', '=', mutation.id)
|
||||
.execute();
|
||||
|
||||
await this.workspace.database
|
||||
.deleteFrom('texts')
|
||||
.where('id', '=', mutation.id)
|
||||
.execute();
|
||||
|
||||
await this.workspace.database
|
||||
.deleteFrom('node_reactions')
|
||||
.where('node_id', '=', mutation.id)
|
||||
@@ -801,6 +751,16 @@ export class NodeService {
|
||||
.where('id', '=', mutation.id)
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -843,8 +803,6 @@ export class NodeService {
|
||||
}
|
||||
|
||||
const attributes = ydoc.getObject<NodeAttributes>();
|
||||
const text = extractNodeText(mutation.id, attributes);
|
||||
|
||||
const updatedNode = await this.workspace.database
|
||||
.transaction()
|
||||
.execute(async (trx) => {
|
||||
@@ -862,21 +820,6 @@ export class NodeService {
|
||||
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
|
||||
.deleteFrom('node_updates')
|
||||
.where('id', '=', mutation.updateId)
|
||||
@@ -931,9 +874,8 @@ export class NodeService {
|
||||
}
|
||||
|
||||
const attributes = ydoc.getObject<NodeAttributes>();
|
||||
const text = extractNodeText(mutation.id, attributes);
|
||||
|
||||
const deletedNode = JSON.parse(tombstone.data) as SelectNode;
|
||||
|
||||
const createdNode = await this.workspace.database
|
||||
.transaction()
|
||||
.execute(async (trx) => {
|
||||
@@ -962,21 +904,6 @@ export class NodeService {
|
||||
.deleteFrom('tombstones')
|
||||
.where('id', '=', mutation.id)
|
||||
.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) {
|
||||
|
||||
@@ -50,16 +50,6 @@ export class UserService {
|
||||
)
|
||||
.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) {
|
||||
eventBus.publish({
|
||||
type: 'user_created',
|
||||
|
||||
@@ -29,9 +29,9 @@ const roles: NodeCollaboratorRole[] = [
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'Commenter',
|
||||
value: 'commenter',
|
||||
description: 'Can message or comment on content',
|
||||
name: 'Collaborator',
|
||||
value: 'collaborator',
|
||||
description: 'Can create records, messages or comments',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Conversation = ({
|
||||
};
|
||||
|
||||
const isAdmin = hasNodeRole(role, 'admin');
|
||||
const canCreateMessage = hasNodeRole(role, 'commenter');
|
||||
const canCreateMessage = hasNodeRole(role, 'collaborator');
|
||||
|
||||
return (
|
||||
<ConversationContext.Provider
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
canCreateNode,
|
||||
canDeleteNode,
|
||||
canUpdateNode,
|
||||
CanCreateNodeContext,
|
||||
CanDeleteNodeContext,
|
||||
CanUpdateAttributesContext,
|
||||
createDebugger,
|
||||
CreateNodeMutationData,
|
||||
extractNodeCollaborators,
|
||||
@@ -309,35 +309,34 @@ export const createNodeFromMutation = async (
|
||||
): Promise<CreateNodeOutput | null> => {
|
||||
const ydoc = new YDoc(mutation.data);
|
||||
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') {
|
||||
const parent = await fetchNode(attributes.parentId);
|
||||
if (!parent) {
|
||||
return null;
|
||||
parentId = attributes.parentId;
|
||||
}
|
||||
|
||||
root = await fetchNode(parent.root_id);
|
||||
}
|
||||
|
||||
if (
|
||||
!canCreateNode(
|
||||
{
|
||||
const ancestors = parentId ? await fetchNodeAncestors(parentId) : [];
|
||||
const canCreateNodeContext: CanCreateNodeContext = {
|
||||
user: {
|
||||
userId: user.id,
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
workspaceId: user.workspace_id,
|
||||
accountId: user.account_id,
|
||||
},
|
||||
root: root ? mapNode(root) : null,
|
||||
},
|
||||
attributes.type
|
||||
)
|
||||
) {
|
||||
ancestors: ancestors.map(mapNode),
|
||||
attributes,
|
||||
};
|
||||
|
||||
if (!model.canCreate(canCreateNodeContext)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootId = ancestors[0]?.id ?? mutation.id;
|
||||
const createNode: CreateNode = {
|
||||
id: mutation.id,
|
||||
root_id: root?.id ?? mutation.id,
|
||||
root_id: rootId,
|
||||
attributes: JSON.stringify(attributes),
|
||||
workspace_id: user.workspace_id,
|
||||
created_at: new Date(mutation.createdAt),
|
||||
@@ -386,7 +385,7 @@ export const createNodeFromMutation = async (
|
||||
eventBus.publish({
|
||||
type: 'node_created',
|
||||
nodeId: mutation.id,
|
||||
rootId: root?.id ?? mutation.id,
|
||||
rootId,
|
||||
workspaceId: user.workspace_id,
|
||||
});
|
||||
|
||||
@@ -431,35 +430,36 @@ const tryUpdateNodeFromMutation = async (
|
||||
user: SelectUser,
|
||||
mutation: UpdateNodeMutationData
|
||||
): Promise<ConcurrentUpdateResult<UpdateNodeOutput>> => {
|
||||
const node = await fetchNode(mutation.id);
|
||||
if (!node) {
|
||||
const ancestors = await fetchNodeAncestors(mutation.id);
|
||||
if (ancestors.length === 0) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
const root = await fetchNode(node.root_id);
|
||||
if (!root) {
|
||||
const node = ancestors[ancestors.length - 1];
|
||||
if (!node || node.id !== mutation.id) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
const model = getNodeModel(node.type);
|
||||
const ydoc = new YDoc(node.state);
|
||||
ydoc.applyUpdate(mutation.data);
|
||||
|
||||
const attributes = ydoc.getObject<NodeAttributes>();
|
||||
const attributesJson = JSON.stringify(attributes);
|
||||
|
||||
if (
|
||||
!canUpdateNode(
|
||||
{
|
||||
const canUpdateNodeContext: CanUpdateAttributesContext = {
|
||||
user: {
|
||||
userId: user.id,
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
workspaceId: user.workspace_id,
|
||||
accountId: user.account_id,
|
||||
},
|
||||
root: mapNode(root),
|
||||
ancestors: ancestors.map(mapNode),
|
||||
node: mapNode(node),
|
||||
},
|
||||
attributes
|
||||
)
|
||||
) {
|
||||
attributes,
|
||||
};
|
||||
|
||||
if (!model.canUpdateAttributes(canUpdateNodeContext)) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
@@ -507,7 +507,7 @@ const tryUpdateNodeFromMutation = async (
|
||||
eventBus.publish({
|
||||
type: 'node_updated',
|
||||
nodeId: mutation.id,
|
||||
rootId: root.id,
|
||||
rootId: node.root_id,
|
||||
workspaceId: user.workspace_id,
|
||||
});
|
||||
|
||||
@@ -544,26 +544,29 @@ export const deleteNode = async (
|
||||
user: SelectUser,
|
||||
input: DeleteNodeInput
|
||||
): Promise<DeleteNodeOutput | null> => {
|
||||
const node = await fetchNode(input.id);
|
||||
if (!node) {
|
||||
const ancestors = await fetchNodeAncestors(input.id);
|
||||
if (ancestors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = await fetchNode(node.root_id);
|
||||
if (!root) {
|
||||
const node = ancestors[ancestors.length - 1];
|
||||
if (!node || node.id !== input.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!canDeleteNode({
|
||||
const model = getNodeModel(node.type);
|
||||
const canDeleteNodeContext: CanDeleteNodeContext = {
|
||||
user: {
|
||||
userId: user.id,
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
workspaceId: user.workspace_id,
|
||||
accountId: user.account_id,
|
||||
},
|
||||
root: mapNode(root),
|
||||
ancestors: ancestors.map(mapNode),
|
||||
node: mapNode(node),
|
||||
})
|
||||
) {
|
||||
};
|
||||
|
||||
if (!model.canDelete(canDeleteNodeContext)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,124 +1,10 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import {
|
||||
extractNodeRole,
|
||||
Node,
|
||||
NodeAttributes,
|
||||
WorkspaceRole,
|
||||
NodeRole,
|
||||
NodeType,
|
||||
} from '../index';
|
||||
import { extractNodeRole, Node, WorkspaceRole, NodeRole } from '../index';
|
||||
|
||||
export type UserInput = {
|
||||
userId: string;
|
||||
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 = (
|
||||
currentRole: WorkspaceRole,
|
||||
targetRole: WorkspaceRole
|
||||
@@ -160,11 +46,11 @@ export const hasNodeRole = (currentRole: NodeRole, targetRole: NodeRole) => {
|
||||
return currentRole === 'admin' || currentRole === 'editor';
|
||||
}
|
||||
|
||||
if (targetRole === 'commenter') {
|
||||
if (targetRole === 'collaborator') {
|
||||
return (
|
||||
currentRole === 'admin' ||
|
||||
currentRole === 'editor' ||
|
||||
currentRole === 'commenter'
|
||||
currentRole === 'collaborator'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,7 +58,7 @@ export const hasNodeRole = (currentRole: NodeRole, targetRole: NodeRole) => {
|
||||
return (
|
||||
currentRole === 'admin' ||
|
||||
currentRole === 'editor' ||
|
||||
currentRole === 'commenter' ||
|
||||
currentRole === 'collaborator' ||
|
||||
currentRole === 'viewer'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,6 @@
|
||||
import { Block } from '../registry/block';
|
||||
import { NodeAttributes } from '../registry/nodes';
|
||||
|
||||
export type TextResult = {
|
||||
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 = (
|
||||
export const extractBlockTexts = (
|
||||
entryId: string,
|
||||
blocks: Record<string, Block> | undefined | null
|
||||
): string | null => {
|
||||
|
||||
@@ -2,6 +2,9 @@ import { z } from 'zod';
|
||||
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
|
||||
export const channelAttributesSchema = z.object({
|
||||
@@ -16,26 +19,59 @@ export type ChannelAttributes = z.infer<typeof channelAttributesSchema>;
|
||||
export const channelModel: NodeModel = {
|
||||
type: 'channel',
|
||||
attributesSchema: channelAttributesSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasAdminAccess();
|
||||
canUpdateDocument: () => {
|
||||
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,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
): string | null | undefined => {
|
||||
if (attributes.type !== 'channel') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributes.name;
|
||||
},
|
||||
getText: function (_: string, __: NodeAttributes): string | null | undefined {
|
||||
getAttributesText: (): string | null | undefined => {
|
||||
return undefined;
|
||||
},
|
||||
getDocumentText: (): string | null | undefined => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { NodeModel, nodeRoleEnum } from './core';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { hasWorkspaceRole } from '../../lib/permissions';
|
||||
|
||||
export const chatAttributesSchema = z.object({
|
||||
type: z.literal('chat'),
|
||||
@@ -14,19 +14,42 @@ export type ChatAttributes = z.infer<typeof chatAttributesSchema>;
|
||||
export const chatModel: NodeModel = {
|
||||
type: 'chat',
|
||||
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;
|
||||
},
|
||||
canUpdate: async (_, __) => {
|
||||
canUpdateAttributes: () => {
|
||||
return false;
|
||||
},
|
||||
canDelete: async (_, __) => {
|
||||
canUpdateDocument: () => {
|
||||
return false;
|
||||
},
|
||||
getName: function (_: string, __: NodeAttributes): string | null | undefined {
|
||||
canDelete: () => {
|
||||
return false;
|
||||
},
|
||||
getName: (): string | null | undefined => {
|
||||
return undefined;
|
||||
},
|
||||
getText: function (_: string, __: NodeAttributes): string | null | undefined {
|
||||
getAttributesText: (): string | null | undefined => {
|
||||
return undefined;
|
||||
},
|
||||
getDocumentText: (): string | null | undefined => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { z, ZodSchema } from 'zod';
|
||||
|
||||
import { WorkspaceRole } from '../../types/workspaces';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { DocumentContent } from '../documents';
|
||||
|
||||
import { Node, NodeAttributes } from '.';
|
||||
|
||||
export type NodeRole = 'admin' | 'editor' | 'commenter' | 'viewer';
|
||||
export const nodeRoleEnum = z.enum(['admin', 'editor', 'commenter', 'viewer']);
|
||||
export type NodeRole = 'admin' | 'editor' | 'collaborator' | 'viewer';
|
||||
export const nodeRoleEnum = z.enum([
|
||||
'admin',
|
||||
'editor',
|
||||
'collaborator',
|
||||
'viewer',
|
||||
]);
|
||||
|
||||
export interface NodeMutationUser {
|
||||
id: string;
|
||||
@@ -16,54 +20,49 @@ export interface NodeMutationUser {
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
export class NodeMutationContext {
|
||||
public readonly user: NodeMutationUser;
|
||||
public readonly root: Node | null;
|
||||
public readonly role: NodeRole | null;
|
||||
export type CanCreateNodeContext = {
|
||||
user: NodeMutationUser;
|
||||
ancestors: Node[];
|
||||
attributes: NodeAttributes;
|
||||
};
|
||||
|
||||
constructor(user: NodeMutationUser, root: Node | null) {
|
||||
this.user = user;
|
||||
this.root = root;
|
||||
this.role = root ? extractNodeRole(root, user.id) : null;
|
||||
}
|
||||
export type CanUpdateAttributesContext = {
|
||||
user: NodeMutationUser;
|
||||
ancestors: Node[];
|
||||
node: Node;
|
||||
attributes: NodeAttributes;
|
||||
};
|
||||
|
||||
public hasAdminAccess = () => {
|
||||
return this.role ? hasNodeRole(this.role, 'admin') : false;
|
||||
};
|
||||
export type CanUpdateDocumentContext = {
|
||||
user: NodeMutationUser;
|
||||
ancestors: Node[];
|
||||
node: Node;
|
||||
};
|
||||
|
||||
public hasEditorAccess = () => {
|
||||
return this.role ? hasNodeRole(this.role, 'editor') : false;
|
||||
};
|
||||
|
||||
public hasCollaboratorAccess = () => {
|
||||
return this.role ? hasNodeRole(this.role, 'commenter') : false;
|
||||
};
|
||||
|
||||
public hasViewerAccess = () => {
|
||||
return this.role ? hasNodeRole(this.role, 'viewer') : false;
|
||||
};
|
||||
}
|
||||
export type CanDeleteNodeContext = {
|
||||
user: NodeMutationUser;
|
||||
ancestors: Node[];
|
||||
node: Node;
|
||||
};
|
||||
|
||||
export interface NodeModel {
|
||||
type: string;
|
||||
attributesSchema: ZodSchema;
|
||||
documentSchema?: ZodSchema;
|
||||
canCreate: (
|
||||
context: NodeMutationContext,
|
||||
attributes: NodeAttributes
|
||||
) => Promise<boolean>;
|
||||
canUpdate: (
|
||||
context: NodeMutationContext,
|
||||
node: Node,
|
||||
attributes: NodeAttributes
|
||||
) => Promise<boolean>;
|
||||
canDelete: (context: NodeMutationContext, node: Node) => Promise<boolean>;
|
||||
canCreate: (context: CanCreateNodeContext) => boolean;
|
||||
canUpdateAttributes: (context: CanUpdateAttributesContext) => boolean;
|
||||
canUpdateDocument: (context: CanUpdateDocumentContext) => boolean;
|
||||
canDelete: (context: CanDeleteNodeContext) => boolean;
|
||||
getName: (
|
||||
id: string,
|
||||
attributes: NodeAttributes
|
||||
) => string | null | undefined;
|
||||
getText: (
|
||||
getAttributesText: (
|
||||
id: string,
|
||||
attributes: NodeAttributes
|
||||
) => string | null | undefined;
|
||||
getDocumentText: (
|
||||
id: string,
|
||||
content: DocumentContent
|
||||
) => string | null | undefined;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { z } from 'zod';
|
||||
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
export const databaseViewFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -91,26 +92,56 @@ export type DatabaseViewLayout = 'table' | 'board' | 'calendar';
|
||||
export const databaseViewModel: NodeModel = {
|
||||
type: 'database_view',
|
||||
attributesSchema: databaseViewAttributesSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateDocument: () => {
|
||||
return false;
|
||||
},
|
||||
getName: function (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
canDelete: (context) => {
|
||||
if (context.ancestors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const role = extractNodeRole(context.ancestors, context.user.id);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasNodeRole(role, 'editor');
|
||||
},
|
||||
getName: (_, attributes) => {
|
||||
if (attributes.type !== 'database_view') {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return attributes.name;
|
||||
},
|
||||
getText: function (_: string, __: NodeAttributes): string | null | undefined {
|
||||
getAttributesText: () => {
|
||||
return undefined;
|
||||
},
|
||||
getDocumentText: () => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import { z } from 'zod';
|
||||
import { fieldAttributesSchema } from './field';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
export const databaseAttributesSchema = z.object({
|
||||
type: z.literal('database'),
|
||||
@@ -18,26 +19,56 @@ export type DatabaseAttributes = z.infer<typeof databaseAttributesSchema>;
|
||||
export const databaseModel: NodeModel = {
|
||||
type: 'database',
|
||||
attributesSchema: databaseAttributesSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateDocument: () => {
|
||||
return false;
|
||||
},
|
||||
getName: function (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
canDelete: (context) => {
|
||||
if (context.ancestors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const role = extractNodeRole(context.ancestors, context.user.id);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasNodeRole(role, 'editor');
|
||||
},
|
||||
getName: (_, attributes) => {
|
||||
if (attributes.type !== 'database') {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return attributes.name;
|
||||
},
|
||||
getText: function (_: string, __: NodeAttributes): string | null | undefined {
|
||||
getAttributesText: () => {
|
||||
return undefined;
|
||||
},
|
||||
getDocumentText: () => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@ import { z } from 'zod';
|
||||
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
export const fileAttributesSchema = z.object({
|
||||
type: z.literal('file'),
|
||||
@@ -16,26 +17,71 @@ export type FileAttributes = z.infer<typeof fileAttributesSchema>;
|
||||
export const fileModel: NodeModel = {
|
||||
type: 'file',
|
||||
attributesSchema: fileAttributesSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateDocument: () => {
|
||||
return false;
|
||||
},
|
||||
getName: function (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
canDelete: (context) => {
|
||||
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');
|
||||
},
|
||||
getName: (_, attributes) => {
|
||||
if (attributes.type !== 'file') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributes.name;
|
||||
},
|
||||
getText: function (_: string, __: NodeAttributes): string | null | undefined {
|
||||
getAttributesText: () => {
|
||||
return undefined;
|
||||
},
|
||||
getDocumentText: () => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@ import { z } from 'zod';
|
||||
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
export const folderAttributesSchema = z.object({
|
||||
type: z.literal('folder'),
|
||||
@@ -16,26 +17,56 @@ export type FolderAttributes = z.infer<typeof folderAttributesSchema>;
|
||||
export const folderModel: NodeModel = {
|
||||
type: 'folder',
|
||||
attributesSchema: folderAttributesSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateDocument: () => {
|
||||
return false;
|
||||
},
|
||||
getName: function (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
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: (_, attributes) => {
|
||||
if (attributes.type !== 'folder') {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return attributes.name;
|
||||
},
|
||||
getText: function (_: string, __: NodeAttributes): string | null | undefined {
|
||||
getAttributesText: () => {
|
||||
return undefined;
|
||||
},
|
||||
getDocumentText: () => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,8 +3,9 @@ import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { blockSchema } from '../block';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractBlockTexts } from '../../lib/texts';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
export const messageAttributesSchema = z.object({
|
||||
type: z.literal('message'),
|
||||
@@ -20,26 +21,67 @@ export type MessageAttributes = z.infer<typeof messageAttributesSchema>;
|
||||
export const messageModel: NodeModel = {
|
||||
type: 'message',
|
||||
attributesSchema: messageAttributesSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canUpdateDocument: () => {
|
||||
return false;
|
||||
},
|
||||
getName: function (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
canDelete: (context) => {
|
||||
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 || hasNodeRole(role, 'admin')
|
||||
);
|
||||
},
|
||||
getName: (_, attributes) => {
|
||||
if (attributes.type !== 'message') {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@ import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { richTextContentSchema } from '../documents/rich-text';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
export const pageAttributesSchema = z.object({
|
||||
type: z.literal('page'),
|
||||
@@ -19,26 +19,69 @@ export const pageModel: NodeModel = {
|
||||
type: 'page',
|
||||
attributesSchema: pageAttributesSchema,
|
||||
documentSchema: richTextContentSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasEditorAccess();
|
||||
canUpdateDocument: (context) => {
|
||||
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 (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
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: (_, attributes) => {
|
||||
if (attributes.type !== 'page') {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ import { fieldValueSchema } from './field-value';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
import { richTextContentSchema } from '../documents/rich-text';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole } from '../../lib/permissions';
|
||||
|
||||
export const recordAttributesSchema = z.object({
|
||||
type: z.literal('record'),
|
||||
@@ -22,26 +22,81 @@ export const recordModel: NodeModel = {
|
||||
type: 'record',
|
||||
attributesSchema: recordAttributesSchema,
|
||||
documentSchema: richTextContentSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canUpdateDocument: (context) => {
|
||||
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 (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
canDelete: (context) => {
|
||||
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, 'admin');
|
||||
},
|
||||
getName: (_, attributes) => {
|
||||
if (attributes.type !== 'record') {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@ import { z } from 'zod';
|
||||
|
||||
import { NodeModel, nodeRoleEnum } from './core';
|
||||
|
||||
import { NodeAttributes } from '.';
|
||||
import { extractNodeRole } from '../../lib/nodes';
|
||||
import { hasNodeRole, hasWorkspaceRole } from '../../lib/permissions';
|
||||
|
||||
export const spaceAttributesSchema = z.object({
|
||||
type: z.literal('space'),
|
||||
@@ -18,26 +19,72 @@ export type SpaceAttributes = z.infer<typeof spaceAttributesSchema>;
|
||||
export const spaceModel: NodeModel = {
|
||||
type: 'space',
|
||||
attributesSchema: spaceAttributesSchema,
|
||||
canCreate: async (context, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canCreate: (context) => {
|
||||
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, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canUpdateAttributes: (context) => {
|
||||
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, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canUpdateDocument: () => {
|
||||
return false;
|
||||
},
|
||||
getName: function (
|
||||
_: string,
|
||||
attributes: NodeAttributes
|
||||
): string | null | undefined {
|
||||
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: (_, attributes) => {
|
||||
if (attributes.type !== 'space') {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user