feat: 收口文档桥接与 OnlyOffice/Sidebar 回归
- 为 documents.save/meta/content、blocks.patch 与 sidebar.dataset.list 补齐 Rust 协议映射、共享契约与桥接执行器 - 对齐 BlockNote、Mindmap、OnlyOffice 的保存/路由元信息,并补真实浏览器回归脚本与 OnlyOffice 部署基线 - 忽略 Rust 本地构建产物与 Harness 调试状态文件,避免临时产物进入仓库历史
This commit is contained in:
@@ -29,11 +29,14 @@ import {
|
||||
assertOptionsPatch,
|
||||
assertStats,
|
||||
assertTitle,
|
||||
buildDocumentBridgeMutationRequest,
|
||||
buildDocumentCommandEnvelope,
|
||||
buildDocumentQueryEnvelope,
|
||||
type BridgeContext,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { executeMetadataBridgeCommand } from "@/lib/documents/metadata-command-adapter";
|
||||
import { executeSaveBridgeCommand } from "@/lib/documents/save-command-adapter";
|
||||
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
|
||||
|
||||
vi.mock("@/lib/convex/route", () => ({
|
||||
getAuthedConvexClient: vi.fn(),
|
||||
@@ -131,6 +134,100 @@ describe("documents bridge helpers", () => {
|
||||
expect(envelope.payload).toEqual({ documentId: "doc_1" });
|
||||
});
|
||||
|
||||
it("buildDocumentBridgeMutationRequest builds title update runtime request", () => {
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "documents.title.update",
|
||||
payload: {
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
title: "新标题",
|
||||
},
|
||||
context: mockContext,
|
||||
target: { workspaceId: "ws_1", pageId: "doc_1" },
|
||||
});
|
||||
|
||||
const request = buildDocumentBridgeMutationRequest({
|
||||
context: mockContext,
|
||||
envelope,
|
||||
mapConvexArgs: (payload) => ({
|
||||
id: payload.documentId,
|
||||
title: payload.title,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(request.functionName).toBe("documents:updateTitle");
|
||||
expect(request.workspaceId).toBe("ws_1");
|
||||
expect(request.args).toEqual({
|
||||
id: "doc_1",
|
||||
title: "新标题",
|
||||
});
|
||||
expect(JSON.parse(request.payloadJson)).toEqual({
|
||||
kind: "command",
|
||||
name: "documents.title.update",
|
||||
request_id: "req_1",
|
||||
trace_id: "trace_1",
|
||||
deployment_id: null,
|
||||
project_id: null,
|
||||
workspace_id: "ws_1",
|
||||
tenant_id: null,
|
||||
idempotency_key: "idem_1",
|
||||
actor: {
|
||||
type: "user",
|
||||
id: "user_1",
|
||||
session_id: "sess_1",
|
||||
},
|
||||
source: {
|
||||
channel: "next-route",
|
||||
client: "vitest",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("buildDocumentBridgeMutationRequest builds documents.save runtime request", () => {
|
||||
const payload = buildDocumentSavePayload({
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
revision: 7,
|
||||
content: [{ id: "block_1" }],
|
||||
conflictDetectionKey: "conflict_1",
|
||||
snapshotCapturedAt: "2026-04-14T14:30:00.000Z",
|
||||
blockCount: 1,
|
||||
});
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "documents.save",
|
||||
payload,
|
||||
context: mockContext,
|
||||
target: { workspaceId: "ws_1", pageId: "doc_1" },
|
||||
});
|
||||
|
||||
const request = buildDocumentBridgeMutationRequest({
|
||||
context: mockContext,
|
||||
envelope,
|
||||
mapConvexArgs: (nextPayload) => ({
|
||||
id: nextPayload.documentId,
|
||||
content: nextPayload.content,
|
||||
expectedRevision: nextPayload.revision,
|
||||
conflictDetectionKey: nextPayload.conflictDetectionKey,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(request.functionName).toBe("documents:updateContent");
|
||||
expect(request.workspaceId).toBe("ws_1");
|
||||
expect(request.args).toEqual({
|
||||
id: "doc_1",
|
||||
content: [{ id: "block_1" }],
|
||||
expectedRevision: 7,
|
||||
conflictDetectionKey: "conflict_1",
|
||||
});
|
||||
expect(JSON.parse(request.payloadJson)).toMatchObject({
|
||||
kind: "command",
|
||||
name: "documents.save",
|
||||
workspace_id: "ws_1",
|
||||
request_id: "req_1",
|
||||
trace_id: "trace_1",
|
||||
});
|
||||
});
|
||||
|
||||
it("executeMetadataBridgeCommand routes title update through adapter", async () => {
|
||||
const mutation = vi.fn().mockResolvedValue({ ok: true });
|
||||
const { getAuthedConvexClient } = await import("@/lib/convex/route");
|
||||
@@ -212,4 +309,95 @@ describe("documents bridge helpers", () => {
|
||||
});
|
||||
expect(result.commandName).toBe("documents.options.update");
|
||||
});
|
||||
|
||||
it("executeSaveBridgeCommand routes documents.save through adapter", async () => {
|
||||
const mutation = vi.fn().mockResolvedValue({ ok: true });
|
||||
const { getAuthedConvexClient } = await import("@/lib/convex/route");
|
||||
const { recordBridgeCommandArtifacts } = await import("@/lib/documents/bridge-log");
|
||||
vi.mocked(getAuthedConvexClient).mockResolvedValue({
|
||||
auth: { userId: "user_1" },
|
||||
client: {
|
||||
mutation,
|
||||
} as unknown as ConvexHttpClient,
|
||||
});
|
||||
vi.mocked(recordBridgeCommandArtifacts).mockResolvedValue(undefined);
|
||||
const previousBridgeArtifactCalls = vi.mocked(recordBridgeCommandArtifacts).mock.calls.length;
|
||||
|
||||
const payload = buildDocumentSavePayload({
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
revision: 7,
|
||||
content: [{ id: "block_1" }],
|
||||
conflictDetectionKey: "conflict_1",
|
||||
snapshotCapturedAt: "2026-04-14T14:30:00.000Z",
|
||||
blockCount: 1,
|
||||
});
|
||||
|
||||
const result = await executeSaveBridgeCommand({
|
||||
context: mockContext,
|
||||
envelope: buildDocumentCommandEnvelope({
|
||||
name: "documents.save",
|
||||
payload,
|
||||
context: mockContext,
|
||||
target: { workspaceId: "ws_1", pageId: "doc_1" },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(mutation).toHaveBeenCalledTimes(1);
|
||||
expect(mutation.mock.calls[0]?.[1]).toEqual({
|
||||
id: "doc_1",
|
||||
content: [{ id: "block_1" }],
|
||||
expectedRevision: 7,
|
||||
conflictDetectionKey: "conflict_1",
|
||||
});
|
||||
expect(vi.mocked(recordBridgeCommandArtifacts).mock.calls.length).toBe(
|
||||
previousBridgeArtifactCalls + 1,
|
||||
);
|
||||
expect(recordBridgeCommandArtifacts).toHaveBeenCalledWith({
|
||||
context: mockContext,
|
||||
envelope: expect.objectContaining({
|
||||
name: "documents.save",
|
||||
target: { workspaceId: "ws_1", pageId: "doc_1" },
|
||||
payload,
|
||||
}),
|
||||
});
|
||||
expect(result.requestId).toBe("req_1");
|
||||
expect(result.traceId).toBe("trace_1");
|
||||
expect(result.commandName).toBe("documents.save");
|
||||
});
|
||||
|
||||
it("executeSaveBridgeCommand 将冲突错误归一为 bridge rejected", async () => {
|
||||
const mutation = vi.fn().mockRejectedValue(new Error("正文内容已变更,请刷新后重试"));
|
||||
const { getAuthedConvexClient } = await import("@/lib/convex/route");
|
||||
vi.mocked(getAuthedConvexClient).mockResolvedValue({
|
||||
auth: { userId: "user_1" },
|
||||
client: {
|
||||
mutation,
|
||||
} as unknown as ConvexHttpClient,
|
||||
});
|
||||
|
||||
const payload = buildDocumentSavePayload({
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
revision: 7,
|
||||
content: [{ id: "block_1" }],
|
||||
conflictDetectionKey: "conflict_1",
|
||||
});
|
||||
|
||||
await expect(
|
||||
executeSaveBridgeCommand({
|
||||
context: mockContext,
|
||||
envelope: buildDocumentCommandEnvelope({
|
||||
name: "documents.save",
|
||||
payload,
|
||||
context: mockContext,
|
||||
target: { workspaceId: "ws_1", pageId: "doc_1" },
|
||||
}),
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
name: "DocumentBridgeError",
|
||||
status: 409,
|
||||
code: "REJECTED",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import type { ConvexHttpClient } from "convex/browser";
|
||||
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
|
||||
import { apiErrorResponse } from "@/lib/api-utils";
|
||||
import type { PageOptionsState } from "@/types/page-options";
|
||||
@@ -80,6 +81,28 @@ export type QueryEnvelope<T> = {
|
||||
payload: T;
|
||||
};
|
||||
|
||||
export type DocumentBridgeMutationRequest<
|
||||
TArgs extends Record<string, unknown> = Record<string, unknown>,
|
||||
> = {
|
||||
functionName: string;
|
||||
deploymentId: string | null;
|
||||
projectId: string | null;
|
||||
workspaceId: string | null;
|
||||
requestId: string;
|
||||
traceId: string;
|
||||
idempotencyKey: string | null;
|
||||
actorId: string;
|
||||
payloadJson: string;
|
||||
args: TArgs;
|
||||
};
|
||||
|
||||
const DOCUMENT_BRIDGE_MUTATION_FUNCTIONS = {
|
||||
"documents.title.update": "documents:updateTitle",
|
||||
"documents.stats.update": "documents:updateStats",
|
||||
"documents.options.update": "documents:updateOptions",
|
||||
"documents.save": "documents:updateContent",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
function readHeaderValue(headerList: Headers, ...candidates: string[]): string | null {
|
||||
for (const candidate of candidates) {
|
||||
const value = headerList.get(candidate);
|
||||
@@ -187,6 +210,94 @@ export function buildDocumentQueryEnvelope<T>(input: { name: string; payload: T
|
||||
};
|
||||
}
|
||||
|
||||
function getDocumentBridgeMutationFunctionName(commandName: string): string {
|
||||
const functionName =
|
||||
DOCUMENT_BRIDGE_MUTATION_FUNCTIONS[
|
||||
commandName as keyof typeof DOCUMENT_BRIDGE_MUTATION_FUNCTIONS
|
||||
];
|
||||
if (!functionName) {
|
||||
throw new DocumentBridgeError(
|
||||
`未注册文档 bridge mutation: ${commandName}`,
|
||||
500,
|
||||
"TRANSPORT_ERROR",
|
||||
);
|
||||
}
|
||||
return functionName;
|
||||
}
|
||||
|
||||
function buildDocumentCommandPayloadJson(input: {
|
||||
context: BridgeContext;
|
||||
commandName: string;
|
||||
workspaceId: string | null;
|
||||
idempotencyKey: string | null;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
kind: "command",
|
||||
name: input.commandName,
|
||||
request_id: input.context.requestId,
|
||||
trace_id: input.context.traceId,
|
||||
deployment_id: input.context.deploymentId,
|
||||
project_id: input.context.projectId,
|
||||
workspace_id: input.workspaceId,
|
||||
tenant_id: input.context.tenantId,
|
||||
idempotency_key: input.idempotencyKey,
|
||||
actor: {
|
||||
type: input.context.actor.actorType,
|
||||
id: input.context.actor.actorId,
|
||||
session_id: input.context.actor.sessionId,
|
||||
},
|
||||
source: {
|
||||
channel: input.context.source.channel,
|
||||
client: input.context.source.client,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function buildDocumentBridgeMutationRequest<
|
||||
TPayload,
|
||||
TArgs extends Record<string, unknown>,
|
||||
>(input: {
|
||||
context: BridgeContext;
|
||||
envelope: CommandEnvelope<TPayload>;
|
||||
mapConvexArgs: (payload: TPayload) => TArgs;
|
||||
}): DocumentBridgeMutationRequest<TArgs> {
|
||||
const workspaceId = input.envelope.target?.workspaceId ?? input.context.workspaceId ?? null;
|
||||
const idempotencyKey = input.envelope.idempotencyKey ?? input.context.idempotencyKey;
|
||||
|
||||
return {
|
||||
functionName: getDocumentBridgeMutationFunctionName(input.envelope.name),
|
||||
deploymentId: input.context.deploymentId,
|
||||
projectId: input.context.projectId,
|
||||
workspaceId,
|
||||
requestId: input.context.requestId,
|
||||
traceId: input.context.traceId,
|
||||
idempotencyKey,
|
||||
actorId: input.context.actor.actorId,
|
||||
payloadJson: buildDocumentCommandPayloadJson({
|
||||
context: input.context,
|
||||
commandName: input.envelope.name,
|
||||
workspaceId,
|
||||
idempotencyKey,
|
||||
}),
|
||||
args: input.mapConvexArgs(input.envelope.payload),
|
||||
};
|
||||
}
|
||||
|
||||
export async function executeDocumentBridgeMutationRequest<
|
||||
TArgs extends Record<string, unknown>,
|
||||
TResult,
|
||||
>(input: {
|
||||
client: ConvexHttpClient;
|
||||
mutation: unknown;
|
||||
request: DocumentBridgeMutationRequest<TArgs>;
|
||||
}): Promise<TResult> {
|
||||
const mutate = input.client.mutation.bind(input.client) as (
|
||||
mutation: unknown,
|
||||
args: TArgs,
|
||||
) => Promise<TResult>;
|
||||
return mutate(input.mutation, input.request.args);
|
||||
}
|
||||
|
||||
export function assertDocumentId(documentId: string | null | undefined): string {
|
||||
const normalized = typeof documentId === "string" ? documentId.trim() : "";
|
||||
if (!normalized) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import type { CommandEnvelope, BridgeContext } from "@/lib/documents/bridge";
|
||||
import {
|
||||
buildDocumentBridgeMutationRequest,
|
||||
executeDocumentBridgeMutationRequest,
|
||||
type CommandEnvelope,
|
||||
type BridgeContext,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { recordBridgeCommandArtifacts } from "@/lib/documents/bridge-log";
|
||||
import type { PageOptionsState } from "@/types/page-options";
|
||||
|
||||
@@ -35,9 +40,11 @@ export type MetadataCommandExecutionResult = {
|
||||
commandName: string;
|
||||
};
|
||||
|
||||
type MetadataMutationArgs = Record<string, unknown>;
|
||||
|
||||
type MetadataWriteAdapter<TPayload> = {
|
||||
convexMutation: unknown;
|
||||
mapConvexArgs: (payload: TPayload) => Record<string, unknown>;
|
||||
mapConvexArgs: (payload: TPayload) => MetadataMutationArgs;
|
||||
};
|
||||
|
||||
function mapDocumentOptionsToConvexArgs(payload: DocumentOptionsUpdatePayload) {
|
||||
@@ -101,11 +108,17 @@ export async function executeMetadataBridgeCommand<TPayload>(input: {
|
||||
}): Promise<MetadataCommandExecutionResult> {
|
||||
const adapter = getMetadataWriteAdapter<TPayload>(input.envelope.name);
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const mutationRequest = buildDocumentBridgeMutationRequest({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
mapConvexArgs: adapter.mapConvexArgs,
|
||||
});
|
||||
|
||||
await client.mutation(
|
||||
adapter.convexMutation as Parameters<typeof client.mutation>[0],
|
||||
adapter.mapConvexArgs(input.envelope.payload) as Parameters<typeof client.mutation>[1],
|
||||
);
|
||||
await executeDocumentBridgeMutationRequest({
|
||||
client,
|
||||
mutation: adapter.convexMutation,
|
||||
request: mutationRequest,
|
||||
});
|
||||
await recordBridgeCommandArtifacts({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import {
|
||||
buildDocumentBridgeMutationRequest,
|
||||
executeDocumentBridgeMutationRequest,
|
||||
type CommandEnvelope,
|
||||
type BridgeContext,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { recordBridgeCommandArtifacts } from "@/lib/documents/bridge-log";
|
||||
import type { DocumentSavePayload } from "@/lib/documents/save-contract";
|
||||
import { DocumentBridgeError } from "@/lib/documents/bridge";
|
||||
|
||||
export type DocumentSaveExecutionResult = {
|
||||
requestId: string;
|
||||
traceId: string;
|
||||
commandId: string;
|
||||
commandName: string;
|
||||
revision: number | null;
|
||||
conflictDetectionKey: string | null;
|
||||
};
|
||||
|
||||
export async function executeSaveBridgeCommand(input: {
|
||||
context: BridgeContext;
|
||||
envelope: CommandEnvelope<DocumentSavePayload>;
|
||||
}): Promise<DocumentSaveExecutionResult> {
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const mutationRequest = buildDocumentBridgeMutationRequest({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
mapConvexArgs: (payload) => ({
|
||||
id: payload.documentId,
|
||||
content: payload.content,
|
||||
expectedRevision: payload.revision,
|
||||
conflictDetectionKey: payload.conflictDetectionKey,
|
||||
}),
|
||||
});
|
||||
|
||||
let mutationResult;
|
||||
try {
|
||||
mutationResult = await executeDocumentBridgeMutationRequest({
|
||||
client,
|
||||
mutation: api.documents.updateContent,
|
||||
request: mutationRequest,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && /正文(内容已变更|冲突检测失败)/.test(error.message)) {
|
||||
throw new DocumentBridgeError(error.message, 409, "REJECTED", {
|
||||
reason: "content_conflict",
|
||||
revision: input.envelope.payload.revision,
|
||||
conflictDetectionKey: input.envelope.payload.conflictDetectionKey,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await recordBridgeCommandArtifacts({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
});
|
||||
|
||||
return {
|
||||
requestId: input.context.requestId,
|
||||
traceId: input.context.traceId,
|
||||
commandId: input.envelope.commandId,
|
||||
commandName: input.envelope.name,
|
||||
revision:
|
||||
typeof mutationResult?.revision === "number" && Number.isInteger(mutationResult.revision)
|
||||
? mutationResult.revision
|
||||
: null,
|
||||
conflictDetectionKey:
|
||||
typeof mutationResult?.conflict_detection_key === "string" &&
|
||||
mutationResult.conflict_detection_key.trim()
|
||||
? mutationResult.conflict_detection_key
|
||||
: null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
|
||||
|
||||
describe("buildDocumentSavePayload", () => {
|
||||
it("统一规范 documents.save 的共享 payload", () => {
|
||||
const payload = buildDocumentSavePayload({
|
||||
documentId: " doc_1 ",
|
||||
workspaceId: " ws_1 ",
|
||||
revision: 3,
|
||||
content: [{ id: "block_1", type: "paragraph", content: [] }],
|
||||
conflictDetectionKey: " conflict_1 ",
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
revision: 3,
|
||||
content: [{ id: "block_1", type: "paragraph", content: [] }],
|
||||
conflictDetectionKey: "conflict_1",
|
||||
snapshotCapturedAt: null,
|
||||
blockCount: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("非法 revision/conflictDetectionKey 会回退到 null", () => {
|
||||
const payload = buildDocumentSavePayload({
|
||||
documentId: "doc_1",
|
||||
workspaceId: "",
|
||||
revision: -1,
|
||||
content: [],
|
||||
conflictDetectionKey: " ",
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
documentId: "doc_1",
|
||||
workspaceId: null,
|
||||
revision: null,
|
||||
content: [],
|
||||
conflictDetectionKey: null,
|
||||
snapshotCapturedAt: null,
|
||||
blockCount: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("保留正文快照采集元数据", () => {
|
||||
const payload = buildDocumentSavePayload({
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
revision: 4,
|
||||
content: [{ id: "block_1" }],
|
||||
conflictDetectionKey: "doc_1:4",
|
||||
snapshotCapturedAt: "2026-04-14T14:30:00.000Z",
|
||||
blockCount: 1,
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
revision: 4,
|
||||
content: [{ id: "block_1" }],
|
||||
conflictDetectionKey: "doc_1:4",
|
||||
snapshotCapturedAt: "2026-04-14T14:30:00.000Z",
|
||||
blockCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Json } from "@/types/supabase";
|
||||
|
||||
export type DocumentSavePayload = {
|
||||
documentId: string;
|
||||
workspaceId: string | null;
|
||||
revision: number | null;
|
||||
content: Json;
|
||||
conflictDetectionKey: string | null;
|
||||
snapshotCapturedAt: string | null;
|
||||
blockCount: number | null;
|
||||
};
|
||||
|
||||
export function buildDocumentSavePayload(input: {
|
||||
documentId: string;
|
||||
workspaceId?: string | null;
|
||||
revision?: number | null;
|
||||
content: Json;
|
||||
conflictDetectionKey?: string | null;
|
||||
snapshotCapturedAt?: string | null;
|
||||
blockCount?: number | null;
|
||||
}): DocumentSavePayload {
|
||||
const revision =
|
||||
typeof input.revision === "number" && Number.isInteger(input.revision) && input.revision >= 0
|
||||
? input.revision
|
||||
: null;
|
||||
const conflictDetectionKey =
|
||||
typeof input.conflictDetectionKey === "string" && input.conflictDetectionKey.trim()
|
||||
? input.conflictDetectionKey.trim()
|
||||
: null;
|
||||
const snapshotCapturedAt =
|
||||
typeof input.snapshotCapturedAt === "string" && input.snapshotCapturedAt.trim()
|
||||
? input.snapshotCapturedAt.trim()
|
||||
: null;
|
||||
const blockCount =
|
||||
typeof input.blockCount === "number" && Number.isInteger(input.blockCount) && input.blockCount >= 0
|
||||
? input.blockCount
|
||||
: null;
|
||||
|
||||
return {
|
||||
documentId: input.documentId.trim(),
|
||||
workspaceId: input.workspaceId?.trim() || null,
|
||||
revision,
|
||||
content: input.content,
|
||||
conflictDetectionKey,
|
||||
snapshotCapturedAt,
|
||||
blockCount,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user