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 { 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,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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[]> {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user