mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Add document update validation and revert
This commit is contained in:
@@ -105,6 +105,19 @@ export class DocumentGetQueryHandler
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'document_update_deleted' &&
|
||||
event.accountId === input.accountId &&
|
||||
event.workspaceId === input.workspaceId &&
|
||||
event.documentId === input.documentId
|
||||
) {
|
||||
const newOutput = await this.handleQuery(input);
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newOutput,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'node_deleted' &&
|
||||
event.accountId === input.accountId &&
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
CanUpdateDocumentContext,
|
||||
createDebugger,
|
||||
DocumentContent,
|
||||
generateId,
|
||||
getNodeModel,
|
||||
IdType,
|
||||
@@ -10,6 +12,8 @@ import { encodeState, YDoc } from '@colanode/crdt';
|
||||
|
||||
import { WorkspaceService } from '@/main/services/workspaces/workspace-service';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { fetchNodeTree } from '@/main/lib/utils';
|
||||
import { mapNode } from '@/main/lib/mappers';
|
||||
|
||||
const UPDATE_RETRIES_LIMIT = 10;
|
||||
|
||||
@@ -22,12 +26,12 @@ export class DocumentService {
|
||||
}
|
||||
|
||||
public async updateDocument(id: string, update: Uint8Array) {
|
||||
const node = await this.workspace.database
|
||||
.selectFrom('nodes')
|
||||
.selectAll()
|
||||
.where('id', '=', id)
|
||||
.executeTakeFirst();
|
||||
const tree = await fetchNodeTree(this.workspace.database, id);
|
||||
if (!tree) {
|
||||
throw new Error('Node not found');
|
||||
}
|
||||
|
||||
const node = tree[tree.length - 1];
|
||||
if (!node) {
|
||||
throw new Error('Node not found');
|
||||
}
|
||||
@@ -37,6 +41,21 @@ export class DocumentService {
|
||||
throw new Error('Node does not have a document schema');
|
||||
}
|
||||
|
||||
const context: CanUpdateDocumentContext = {
|
||||
user: {
|
||||
id: this.workspace.userId,
|
||||
role: this.workspace.role,
|
||||
accountId: this.workspace.accountId,
|
||||
workspaceId: this.workspace.id,
|
||||
},
|
||||
node: mapNode(node),
|
||||
tree: tree.map((node) => mapNode(node)),
|
||||
};
|
||||
|
||||
if (!model.canUpdateDocument(context)) {
|
||||
throw new Error('User does not have permission to update document');
|
||||
}
|
||||
|
||||
const document = await this.workspace.database
|
||||
.selectFrom('documents')
|
||||
.selectAll()
|
||||
@@ -57,6 +76,11 @@ export class DocumentService {
|
||||
|
||||
ydoc.applyUpdate(update);
|
||||
|
||||
const content = ydoc.getObject<DocumentContent>();
|
||||
if (!model.documentSchema.safeParse(content).success) {
|
||||
throw new Error('Invalid document state');
|
||||
}
|
||||
|
||||
const updateId = generateId(IdType.Update);
|
||||
const updatedAt = new Date().toISOString();
|
||||
|
||||
@@ -122,6 +146,35 @@ export class DocumentService {
|
||||
}
|
||||
}
|
||||
|
||||
public async revertDocumentUpdate(data: UpdateDocumentMutationData) {
|
||||
const update = await this.workspace.database
|
||||
.selectFrom('document_updates')
|
||||
.selectAll()
|
||||
.where('id', '=', data.updateId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deletedUpdate = await this.workspace.database
|
||||
.deleteFrom('document_updates')
|
||||
.where('id', '=', update.id)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!deletedUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'document_update_deleted',
|
||||
accountId: this.workspace.accountId,
|
||||
workspaceId: this.workspace.id,
|
||||
documentId: data.documentId,
|
||||
updateId: data.updateId,
|
||||
});
|
||||
}
|
||||
|
||||
public async syncServerDocumentUpdate(data: SyncDocumentUpdateData) {
|
||||
for (let count = 0; count < UPDATE_RETRIES_LIMIT; count++) {
|
||||
try {
|
||||
|
||||
@@ -157,6 +157,8 @@ export class MutationService {
|
||||
await this.workspace.nodeReactions.revertNodeReactionDelete(
|
||||
mutation.data
|
||||
);
|
||||
} else if (mutation.type === 'update_document') {
|
||||
await this.workspace.documents.revertDocumentUpdate(mutation.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,11 +252,6 @@ export class MutationService {
|
||||
previousMutation.data.nodeId === mutation.data.id
|
||||
) {
|
||||
deletedMutationIds.add(previousMutation.id);
|
||||
} else if (
|
||||
previousMutation.type === 'create_file' &&
|
||||
previousMutation.data.id === mutation.data.id
|
||||
) {
|
||||
deletedMutationIds.add(previousMutation.id);
|
||||
}
|
||||
}
|
||||
} else if (mutation.type === 'delete_node_reaction') {
|
||||
|
||||
@@ -203,6 +203,14 @@ export type DocumentUpdateCreatedEvent = {
|
||||
updateId: string;
|
||||
};
|
||||
|
||||
export type DocumentUpdateDeletedEvent = {
|
||||
type: 'document_update_deleted';
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
documentId: string;
|
||||
updateId: string;
|
||||
};
|
||||
|
||||
export type Event =
|
||||
| UserCreatedEvent
|
||||
| UserUpdatedEvent
|
||||
@@ -235,4 +243,5 @@ export type Event =
|
||||
| WorkspaceMetadataUpdatedEvent
|
||||
| WorkspaceMetadataDeletedEvent
|
||||
| DocumentUpdatedEvent
|
||||
| DocumentUpdateCreatedEvent;
|
||||
| DocumentUpdateCreatedEvent
|
||||
| DocumentUpdateDeletedEvent;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
CanUpdateDocumentContext,
|
||||
createDebugger,
|
||||
DocumentContent,
|
||||
getNodeModel,
|
||||
UpdateDocumentMutationData,
|
||||
} from '@colanode/core';
|
||||
import { decodeState, YDoc } from '@colanode/crdt';
|
||||
@@ -9,7 +11,7 @@ import { database } from '@/data/database';
|
||||
import { SelectUser } from '@/data/schema';
|
||||
import { ConcurrentUpdateResult, UpdateDocumentOutput } from '@/types/nodes';
|
||||
import { eventBus } from '@/lib/event-bus';
|
||||
import { fetchNode } from '@/lib/nodes';
|
||||
import { fetchNodeTree, mapNode } from '@/lib/nodes';
|
||||
|
||||
const debug = createDebugger('server:lib:documents');
|
||||
|
||||
@@ -38,13 +40,33 @@ const tryUpdateDocumentFromMutation = async (
|
||||
user: SelectUser,
|
||||
mutation: UpdateDocumentMutationData
|
||||
): Promise<ConcurrentUpdateResult<UpdateDocumentOutput>> => {
|
||||
const node = await fetchNode(mutation.documentId);
|
||||
const tree = await fetchNodeTree(mutation.documentId);
|
||||
if (!tree) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
const node = tree[tree.length - 1];
|
||||
if (!node) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
const root = await fetchNode(node.root_id);
|
||||
if (!root) {
|
||||
const model = getNodeModel(node.type);
|
||||
if (!model.documentSchema) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
const context: CanUpdateDocumentContext = {
|
||||
user: {
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
workspaceId: user.workspace_id,
|
||||
accountId: user.account_id,
|
||||
},
|
||||
node: mapNode(node),
|
||||
tree: tree.map((node) => mapNode(node)),
|
||||
};
|
||||
|
||||
if (!model.canUpdateDocument(context)) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
@@ -68,6 +90,10 @@ const tryUpdateDocumentFromMutation = async (
|
||||
ydoc.applyUpdate(mutation.data);
|
||||
const content = ydoc.getObject<DocumentContent>();
|
||||
|
||||
if (!model.documentSchema.safeParse(content).success) {
|
||||
return { type: 'error', output: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const { updatedDocument, createdDocumentUpdate } = await database
|
||||
.transaction()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FileSubtype } from './files';
|
||||
|
||||
export type SyncMutationsInput = {
|
||||
mutations: Mutation[];
|
||||
};
|
||||
@@ -105,24 +103,6 @@ export type MarkNodeOpenedMutation = MutationBase & {
|
||||
data: MarkNodeOpenedMutationData;
|
||||
};
|
||||
|
||||
export type CreateFileMutationData = {
|
||||
id: string;
|
||||
type: FileSubtype;
|
||||
parentId: string;
|
||||
rootId: string;
|
||||
name: string;
|
||||
originalName: string;
|
||||
extension: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type CreateFileMutation = MutationBase & {
|
||||
type: 'create_file';
|
||||
data: CreateFileMutationData;
|
||||
};
|
||||
|
||||
export type UpdateDocumentMutationData = {
|
||||
documentId: string;
|
||||
updateId: string;
|
||||
@@ -143,7 +123,6 @@ export type Mutation =
|
||||
| DeleteNodeReactionMutation
|
||||
| MarkNodeSeenMutation
|
||||
| MarkNodeOpenedMutation
|
||||
| CreateFileMutation
|
||||
| UpdateDocumentMutation;
|
||||
|
||||
export type MutationType = Mutation['type'];
|
||||
|
||||
Reference in New Issue
Block a user