92 lines
4.1 KiB
TypeScript
92 lines
4.1 KiB
TypeScript
import { blockedGate, capabilityIssue, type CapabilityGateResult } from "./capability-gates";
|
|
import type { ErrorCode } from "./error";
|
|
|
|
export const LIBRARY_LINKED_MUTATION_SCHEMA = 1 as const;
|
|
export const LINKED_DATA_WRITER_OPERATIONS = [
|
|
"OBJECT_TRANSFORM",
|
|
"MESH_GEOMETRY",
|
|
"MESH_MATERIAL_SLOT",
|
|
"MATERIAL_PROPERTIES",
|
|
"MATERIAL_IMAGE_NODE",
|
|
"IMAGE_PACKED_DATA",
|
|
] as const;
|
|
export type LinkedDataWriterOperation = typeof LINKED_DATA_WRITER_OPERATIONS[number];
|
|
|
|
export interface LinkedDataMutationIR {
|
|
schemaVersion: typeof LIBRARY_LINKED_MUTATION_SCHEMA;
|
|
operation: LinkedDataWriterOperation;
|
|
dataBlockId: string;
|
|
baseRevision: number;
|
|
owner: "SOURCE_LIBRARY";
|
|
linkedLibrary: true;
|
|
readOnly: true;
|
|
}
|
|
|
|
export class LinkedDataMutationError extends Error {
|
|
readonly code: ErrorCode;
|
|
readonly path?: string;
|
|
|
|
constructor(code: ErrorCode, message: string, path?: string) {
|
|
super(message);
|
|
this.name = "LinkedDataMutationError";
|
|
this.code = code;
|
|
this.path = path;
|
|
}
|
|
}
|
|
|
|
function record(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function exactKeys(value: Record<string, unknown>): void {
|
|
const expected = ["schemaVersion", "operation", "dataBlockId", "baseRevision", "owner", "linkedLibrary", "readOnly"];
|
|
const actual = Object.keys(value).sort();
|
|
if (actual.length !== expected.length || actual.some((key, index) => key !== expected.slice().sort()[index])) {
|
|
throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "linked data mutation contains unsupported fields");
|
|
}
|
|
}
|
|
|
|
export function parseLinkedDataMutation(value: unknown): LinkedDataMutationIR {
|
|
if (!record(value) || value.schemaVersion !== LIBRARY_LINKED_MUTATION_SCHEMA) {
|
|
throw new LinkedDataMutationError("PROTOCOL_MISMATCH", "Unsupported linked data mutation schema");
|
|
}
|
|
exactKeys(value);
|
|
if (!LINKED_DATA_WRITER_OPERATIONS.includes(value.operation as LinkedDataWriterOperation)) {
|
|
throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "linked data mutation operation is unsupported", "operation");
|
|
}
|
|
if (typeof value.dataBlockId !== "string" || value.dataBlockId.length === 0 || value.dataBlockId.length > 256) {
|
|
throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "dataBlockId is invalid", "dataBlockId");
|
|
}
|
|
if (typeof value.baseRevision !== "number" || !Number.isSafeInteger(value.baseRevision) || value.baseRevision < 0) {
|
|
throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "baseRevision is invalid", "baseRevision");
|
|
}
|
|
if (value.owner !== "SOURCE_LIBRARY" || value.linkedLibrary !== true || value.readOnly !== true) {
|
|
throw new LinkedDataMutationError("LINKED_DATA_MUTATION_BLOCKED", "linked data must remain SOURCE_LIBRARY/readOnly", "ownership");
|
|
}
|
|
return {
|
|
schemaVersion: LIBRARY_LINKED_MUTATION_SCHEMA,
|
|
operation: value.operation as LinkedDataWriterOperation,
|
|
dataBlockId: value.dataBlockId,
|
|
baseRevision: value.baseRevision,
|
|
owner: "SOURCE_LIBRARY",
|
|
linkedLibrary: true,
|
|
readOnly: true,
|
|
};
|
|
}
|
|
|
|
export function gateLinkedDataMutation(value: unknown, currentRevision: number): CapabilityGateResult {
|
|
let request: LinkedDataMutationIR;
|
|
try {
|
|
request = parseLinkedDataMutation(value);
|
|
}
|
|
catch (error) {
|
|
const code = error instanceof LinkedDataMutationError ? error.code : "TASK_VALIDATION_FAILED";
|
|
const message = error instanceof Error ? error.message : "linked data mutation is invalid";
|
|
return blockedGate("N-023", "LINKED_DATA_WRITER", [capabilityIssue(code, message, error instanceof LinkedDataMutationError ? error.path : undefined, false)]);
|
|
}
|
|
if (!Number.isSafeInteger(currentRevision) || currentRevision < 0 || request.baseRevision !== currentRevision) {
|
|
return blockedGate("N-023", `LINKED_${request.operation}`, [capabilityIssue("REVISION_CONFLICT", "linked data mutation revision is stale", "baseRevision", false)]);
|
|
}
|
|
return blockedGate("N-023", `LINKED_${request.operation}`, [capabilityIssue("LINKED_DATA_MUTATION_BLOCKED", "linked-library data is read-only", "readOnly", false)]);
|
|
}
|