openworkgraph 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/api-image-agent.d.ts +32 -0
- package/dist/api-image-agent.js +132 -0
- package/dist/api-image-output.d.ts +8 -0
- package/dist/api-image-output.js +41 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +153 -0
- package/dist/auth.d.ts +58 -0
- package/dist/auth.js +273 -0
- package/dist/backend/adapter.d.ts +35 -0
- package/dist/backend/adapter.js +439 -0
- package/dist/backend/capabilities.d.ts +23 -0
- package/dist/backend/capabilities.js +75 -0
- package/dist/backend/diagnostics.d.ts +9 -0
- package/dist/backend/diagnostics.js +80 -0
- package/dist/backend/index.d.ts +6 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/models.d.ts +15 -0
- package/dist/backend/models.js +43 -0
- package/dist/backend/paths.d.ts +6 -0
- package/dist/backend/paths.js +69 -0
- package/dist/backend/stdio.d.ts +66 -0
- package/dist/backend/stdio.js +172 -0
- package/dist/backend/types.d.ts +90 -0
- package/dist/backend/types.js +11 -0
- package/dist/backup-api.d.ts +10 -0
- package/dist/backup-api.js +40 -0
- package/dist/blob-store.d.ts +40 -0
- package/dist/blob-store.js +218 -0
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.js +63 -0
- package/dist/cli-output.d.ts +4 -0
- package/dist/cli-output.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -0
- package/dist/client-pairing.d.ts +8 -0
- package/dist/client-pairing.js +30 -0
- package/dist/directories.d.ts +14 -0
- package/dist/directories.js +34 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +7 -0
- package/dist/events.d.ts +22 -0
- package/dist/events.js +155 -0
- package/dist/execution-chains.d.ts +24 -0
- package/dist/execution-chains.js +231 -0
- package/dist/execution-settings.d.ts +10 -0
- package/dist/execution-settings.js +38 -0
- package/dist/graph-document-history.d.ts +43 -0
- package/dist/graph-document-history.js +275 -0
- package/dist/graph-files.d.ts +14 -0
- package/dist/graph-files.js +59 -0
- package/dist/graph-transfer.d.ts +53 -0
- package/dist/graph-transfer.js +501 -0
- package/dist/graphs.d.ts +68 -0
- package/dist/graphs.js +771 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +61 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +42 -0
- package/dist/image-credentials.d.ts +11 -0
- package/dist/image-credentials.js +130 -0
- package/dist/image-providers.d.ts +51 -0
- package/dist/image-providers.js +271 -0
- package/dist/input-materialization.d.ts +17 -0
- package/dist/input-materialization.js +95 -0
- package/dist/inputs.d.ts +74 -0
- package/dist/inputs.js +301 -0
- package/dist/local-control.d.ts +13 -0
- package/dist/local-control.js +37 -0
- package/dist/network.d.ts +11 -0
- package/dist/network.js +48 -0
- package/dist/openai-image-driver.d.ts +42 -0
- package/dist/openai-image-driver.js +368 -0
- package/dist/operations/backups.d.ts +40 -0
- package/dist/operations/backups.js +391 -0
- package/dist/operations/capacity.d.ts +7 -0
- package/dist/operations/capacity.js +35 -0
- package/dist/operations/cli-context.d.ts +7 -0
- package/dist/operations/cli-context.js +58 -0
- package/dist/operations/daemon.d.ts +36 -0
- package/dist/operations/daemon.js +159 -0
- package/dist/operations/files.d.ts +12 -0
- package/dist/operations/files.js +84 -0
- package/dist/operations/lifecycle.d.ts +28 -0
- package/dist/operations/lifecycle.js +155 -0
- package/dist/operations/local-admin.d.ts +6 -0
- package/dist/operations/local-admin.js +38 -0
- package/dist/operations/logs.d.ts +16 -0
- package/dist/operations/logs.js +37 -0
- package/dist/operations/plugins.d.ts +10 -0
- package/dist/operations/plugins.js +11 -0
- package/dist/operations/snapshot-worker.d.ts +1 -0
- package/dist/operations/snapshot-worker.js +18 -0
- package/dist/operations/snapshot.d.ts +1 -0
- package/dist/operations/snapshot.js +14 -0
- package/dist/operations/update.d.ts +26 -0
- package/dist/operations/update.js +216 -0
- package/dist/persistence/asset-business-schema.d.ts +2 -0
- package/dist/persistence/asset-business-schema.js +36 -0
- package/dist/persistence/database.d.ts +7 -0
- package/dist/persistence/database.js +91 -0
- package/dist/persistence/plugin-schema.d.ts +9 -0
- package/dist/persistence/plugin-schema.js +38 -0
- package/dist/persistence/repositories.d.ts +20 -0
- package/dist/persistence/repositories.js +106 -0
- package/dist/persistence/resource-schema.d.ts +2 -0
- package/dist/persistence/resource-schema.js +44 -0
- package/dist/persistence/schema.d.ts +6 -0
- package/dist/persistence/schema.js +236 -0
- package/dist/persistence/workflow-schema.d.ts +2 -0
- package/dist/persistence/workflow-schema.js +19 -0
- package/dist/plugin-api.d.ts +10 -0
- package/dist/plugin-api.js +40 -0
- package/dist/plugin-management.d.ts +36 -0
- package/dist/plugin-management.js +112 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +239 -0
- package/dist/project-discovery.d.ts +3 -0
- package/dist/project-discovery.js +57 -0
- package/dist/project-files-api.d.ts +16 -0
- package/dist/project-files-api.js +243 -0
- package/dist/project-files.d.ts +97 -0
- package/dist/project-files.js +557 -0
- package/dist/project-lock.d.ts +4 -0
- package/dist/project-lock.js +35 -0
- package/dist/project-probe-worker.d.ts +1 -0
- package/dist/project-probe-worker.js +17 -0
- package/dist/project-probe.d.ts +22 -0
- package/dist/project-probe.js +67 -0
- package/dist/projects.d.ts +29 -0
- package/dist/projects.js +219 -0
- package/dist/publication.d.ts +31 -0
- package/dist/publication.js +642 -0
- package/dist/resource-api.d.ts +15 -0
- package/dist/resource-api.js +256 -0
- package/dist/resources-orphans.d.ts +41 -0
- package/dist/resources-orphans.js +235 -0
- package/dist/resources.d.ts +246 -0
- package/dist/resources.js +714 -0
- package/dist/run-api.d.ts +10 -0
- package/dist/run-api.js +308 -0
- package/dist/run-context.d.ts +54 -0
- package/dist/run-context.js +137 -0
- package/dist/runs.d.ts +82 -0
- package/dist/runs.js +506 -0
- package/dist/runtime-backend.d.ts +24 -0
- package/dist/runtime-backend.js +214 -0
- package/dist/runtime.d.ts +97 -0
- package/dist/runtime.js +553 -0
- package/dist/scheduler.d.ts +85 -0
- package/dist/scheduler.js +344 -0
- package/dist/service.d.ts +29 -0
- package/dist/service.js +515 -0
- package/dist/skills-api.d.ts +18 -0
- package/dist/skills-api.js +120 -0
- package/dist/storage-api.d.ts +21 -0
- package/dist/storage-api.js +92 -0
- package/dist/transfer-api.d.ts +10 -0
- package/dist/transfer-api.js +60 -0
- package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
- package/dist/vendor/protocol/contracts.typecheck.js +35 -0
- package/dist/vendor/protocol/execution-chain.d.ts +6 -0
- package/dist/vendor/protocol/execution-chain.js +26 -0
- package/dist/vendor/protocol/graph-binary.d.ts +15 -0
- package/dist/vendor/protocol/graph-binary.js +121 -0
- package/dist/vendor/protocol/graph-title.d.ts +2 -0
- package/dist/vendor/protocol/graph-title.js +23 -0
- package/dist/vendor/protocol/index.d.ts +617 -0
- package/dist/vendor/protocol/index.js +20 -0
- package/dist/vendor/protocol/pairing.d.ts +29 -0
- package/dist/vendor/protocol/pairing.js +71 -0
- package/dist/vendor/protocol/preview.d.ts +2 -0
- package/dist/vendor/protocol/preview.js +9 -0
- package/dist/vendor/protocol/resource-mime.d.ts +2 -0
- package/dist/vendor/protocol/resource-mime.js +35 -0
- package/dist/video-thumbnail.d.ts +2 -0
- package/dist/video-thumbnail.js +34 -0
- package/package.json +9 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { atomic } from './persistence/database.js';
|
|
5
|
+
import { identifier, safePath } from './operations/files.js';
|
|
6
|
+
/** Only explicit graph purges enqueue IDs. Never enumerate user projects or
|
|
7
|
+
* infer orphan ownership. Retain failed entries across restarts for retry. */
|
|
8
|
+
export class GraphFiles {
|
|
9
|
+
db;
|
|
10
|
+
runsDirectory;
|
|
11
|
+
pending;
|
|
12
|
+
constructor(db, runsDirectory) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.runsDirectory = runsDirectory;
|
|
15
|
+
}
|
|
16
|
+
drain() {
|
|
17
|
+
if (this.db.isTransaction)
|
|
18
|
+
throw new Error('Run file cleanup must follow transaction commit');
|
|
19
|
+
if (this.pending)
|
|
20
|
+
return this.pending;
|
|
21
|
+
this.pending = this.work().finally(() => { this.pending = undefined; });
|
|
22
|
+
return this.pending;
|
|
23
|
+
}
|
|
24
|
+
async work() {
|
|
25
|
+
const counts = { removed: 0, failed: 0 }, lease = randomUUID();
|
|
26
|
+
const acquired = atomic(this.db, () => {
|
|
27
|
+
if (this.db.prepare("SELECT 1 FROM resource_maintenance_leases WHERE kind='backup'").get())
|
|
28
|
+
return false;
|
|
29
|
+
this.db.prepare("INSERT INTO resource_maintenance_leases VALUES(?,'drain',?)").run(lease, Date.now());
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
if (!acquired)
|
|
33
|
+
return counts;
|
|
34
|
+
try {
|
|
35
|
+
for (const row of this.db.prepare('SELECT run_id FROM run_file_deletions WHERE completed=0').all()) {
|
|
36
|
+
try {
|
|
37
|
+
const id = identifier(String(row['run_id']));
|
|
38
|
+
if (this.db.prepare('SELECT 1 FROM runs WHERE id=?').get(id))
|
|
39
|
+
throw new Error('Run is still retained');
|
|
40
|
+
// Every parent and the target must be free of symlinks. rm never
|
|
41
|
+
// follows nested symlinks; it unlinks those directory entries only.
|
|
42
|
+
const root = await safePath(this.runsDirectory);
|
|
43
|
+
const target = await safePath(join(root, id), true);
|
|
44
|
+
await rm(target, { recursive: true, force: true });
|
|
45
|
+
atomic(this.db, () => this.db.prepare('UPDATE run_file_deletions SET completed=1 WHERE run_id=?').run(id));
|
|
46
|
+
counts.removed++;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
counts.failed++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return counts;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
atomic(this.db, () => this.db.prepare('DELETE FROM resource_maintenance_leases WHERE id=?').run(lease));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=graph-files.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { GraphScope, Scope } from "./vendor/protocol/index.js";
|
|
2
|
+
import { Graphs } from './graphs.js';
|
|
3
|
+
import { Resources } from './resources.js';
|
|
4
|
+
import { ProjectFiles } from './project-files.js';
|
|
5
|
+
import type { CopiedProvenance, ImportedGraphSnapshot, GraphBundle } from "./vendor/protocol/index.js";
|
|
6
|
+
export type { BundleResource, BundlePluginRequirement, CopiedProvenance, ImportedGraphSnapshot, GraphBundle } from "./vendor/protocol/index.js";
|
|
7
|
+
/** Opaque instance-bound token. Contains no filesystem paths or mutable payload. */
|
|
8
|
+
export interface PreparedGraphImport {
|
|
9
|
+
readonly digest: string;
|
|
10
|
+
readonly projectId: string;
|
|
11
|
+
readonly serviceId: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DisposedGraphImport {
|
|
14
|
+
retired: number;
|
|
15
|
+
retained: number;
|
|
16
|
+
removed: number;
|
|
17
|
+
failed: number;
|
|
18
|
+
}
|
|
19
|
+
export interface GraphTransferLimits {
|
|
20
|
+
maxBundleBytes: number;
|
|
21
|
+
maxResourceBytes: number;
|
|
22
|
+
maxTotalResourceBytes: number;
|
|
23
|
+
maxNodes: number;
|
|
24
|
+
maxResources: number;
|
|
25
|
+
}
|
|
26
|
+
/** Portable graph data only: no Run records, thread identities, filesystem paths or executable plugin activation. */
|
|
27
|
+
export declare class GraphTransfer {
|
|
28
|
+
readonly graphs: Graphs;
|
|
29
|
+
readonly resources: Resources;
|
|
30
|
+
readonly projectFiles: ProjectFiles;
|
|
31
|
+
private readonly prepared;
|
|
32
|
+
readonly limits: Readonly<GraphTransferLimits>;
|
|
33
|
+
constructor(graphs: Graphs, resources: Resources, limits?: Partial<GraphTransferLimits>, projectFiles?: ProjectFiles);
|
|
34
|
+
private target;
|
|
35
|
+
private outside;
|
|
36
|
+
getCopiedProvenance(scope: GraphScope): CopiedProvenance[];
|
|
37
|
+
exportGraph(scope: GraphScope): Promise<GraphBundle>;
|
|
38
|
+
private validate;
|
|
39
|
+
/** trustedTopology is a local authorization decision, NEVER a field accepted from an HTTP bundle. */
|
|
40
|
+
prepareImport(scope: Scope, input: unknown, options?: {
|
|
41
|
+
trustedTopology?: boolean;
|
|
42
|
+
}): Promise<PreparedGraphImport>;
|
|
43
|
+
/** Call from API finally AFTER the surrounding authorization transaction settles,
|
|
44
|
+
* on success, failure and replay. No active session/project access is needed to
|
|
45
|
+
* release this instance-owned token. Durable tombstones fence late commits;
|
|
46
|
+
* deletion failure remains queued for Resources cleanup, never an orphan scan. */
|
|
47
|
+
disposePreparedImport(token: PreparedGraphImport): Promise<DisposedGraphImport>;
|
|
48
|
+
/** Recheck auth outside this helper, in the same surrounding transaction as this synchronous commit. */
|
|
49
|
+
commitImport(scope: Scope, token: PreparedGraphImport, options: {
|
|
50
|
+
idempotencyKey: string;
|
|
51
|
+
principal?: string;
|
|
52
|
+
}): ImportedGraphSnapshot;
|
|
53
|
+
}
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { WORKGRAPH_UPLOAD_MAX_BYTES, WORKGRAPH_TRANSFER_TOTAL_BYTES, WORKGRAPH_BUNDLE_MAX_BYTES, executionOrder, importedGraphTitle } from "./vendor/protocol/index.js";
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { previewEdgeError } from "./vendor/protocol/index.js";
|
|
4
|
+
import { Graphs, assertProjectWritable, resourceLinks, nodeDimension } from './graphs.js';
|
|
5
|
+
import { Resources } from './resources.js';
|
|
6
|
+
import { mimeMatchesBytes, sniffMime } from './blob-store.js';
|
|
7
|
+
import { checkedJson, PluginRegistry } from './plugins.js';
|
|
8
|
+
import { atomic } from './persistence/database.js';
|
|
9
|
+
import { canonicalJsonHash } from './persistence/repositories.js';
|
|
10
|
+
import { ServiceError } from './errors.js';
|
|
11
|
+
import { ProjectFiles, classifyProjectFile } from './project-files.js';
|
|
12
|
+
const DEFAULT_LIMITS = { maxBundleBytes: WORKGRAPH_BUNDLE_MAX_BYTES, maxResourceBytes: WORKGRAPH_UPLOAD_MAX_BYTES, maxTotalResourceBytes: WORKGRAPH_TRANSFER_TOTAL_BYTES, maxNodes: 2000, maxResources: 256 };
|
|
13
|
+
const core = new Set(['text', 'image', 'document', 'video', 'file', 'preview', 'execution', 'group']);
|
|
14
|
+
const key = (id, version) => JSON.stringify([id, version]);
|
|
15
|
+
function portableNode(node) {
|
|
16
|
+
return structuredClone(node);
|
|
17
|
+
}
|
|
18
|
+
function invalid(message) { throw new ServiceError('INVALID_REQUEST', message); }
|
|
19
|
+
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
20
|
+
function id(value) { if (typeof value !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(value))
|
|
21
|
+
invalid('Invalid bundle identity'); }
|
|
22
|
+
function positive(value) { if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1)
|
|
23
|
+
invalid('Invalid bundle version'); }
|
|
24
|
+
function keys(value, allowed) { if (Object.keys(value).some(k => !allowed.includes(k)))
|
|
25
|
+
invalid('Unsupported bundle field'); }
|
|
26
|
+
function text(value, max) { if (typeof value !== 'string' || !value.trim() || value.length > max || [...value].some(character => character.charCodeAt(0) < 32))
|
|
27
|
+
invalid('Invalid bundle title/name'); }
|
|
28
|
+
function hash(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
|
|
29
|
+
/** Portable graph data only: no Run records, thread identities, filesystem paths or executable plugin activation. */
|
|
30
|
+
export class GraphTransfer {
|
|
31
|
+
graphs;
|
|
32
|
+
resources;
|
|
33
|
+
projectFiles;
|
|
34
|
+
prepared = new WeakMap();
|
|
35
|
+
limits;
|
|
36
|
+
constructor(graphs, resources, limits = {}, projectFiles = new ProjectFiles(graphs.db)) {
|
|
37
|
+
this.graphs = graphs;
|
|
38
|
+
this.resources = resources;
|
|
39
|
+
this.projectFiles = projectFiles;
|
|
40
|
+
if (graphs.db !== resources.db)
|
|
41
|
+
throw new Error('Graph transfer requires one shared database');
|
|
42
|
+
this.limits = { ...DEFAULT_LIMITS, ...limits };
|
|
43
|
+
if (Object.values(this.limits).some(v => !Number.isSafeInteger(v) || v < 1))
|
|
44
|
+
invalid('Invalid transfer limits');
|
|
45
|
+
}
|
|
46
|
+
target(scope) {
|
|
47
|
+
if (scope.serviceId !== this.graphs.serviceId)
|
|
48
|
+
throw new ServiceError('SERVICE_MISMATCH', 'Wrong target service');
|
|
49
|
+
assertProjectWritable(this.graphs.db, scope.projectId);
|
|
50
|
+
}
|
|
51
|
+
outside() { if (this.graphs.db.isTransaction)
|
|
52
|
+
throw new Error('Graph transfer preparation must run outside transactions'); }
|
|
53
|
+
getCopiedProvenance(scope) {
|
|
54
|
+
this.graphs.scope(scope);
|
|
55
|
+
const value = this.graphs.repo.setting('graph-copied-provenance:' + scope.graphId);
|
|
56
|
+
const live = new Set(this.graphs.db.prepare('SELECT id FROM nodes WHERE graph_id=? AND deleted=0').all(scope.graphId).map(r => String(r['id'])));
|
|
57
|
+
return (Array.isArray(value) ? value : []).filter(p => p.kind === 'copied-delivery' ? live.has(p.sourceId) && live.has(p.targetId) : live.has(p.nodeId));
|
|
58
|
+
}
|
|
59
|
+
async exportGraph(scope) {
|
|
60
|
+
this.outside();
|
|
61
|
+
const bundle = atomic(this.graphs.db, () => {
|
|
62
|
+
const snapshot = this.graphs.snapshot(scope);
|
|
63
|
+
const portableNodes = snapshot.nodes.map(portableNode);
|
|
64
|
+
const links = new Map();
|
|
65
|
+
for (const node of snapshot.nodes)
|
|
66
|
+
for (const link of resourceLinks(node.content))
|
|
67
|
+
links.set(key(link.resourceId, link.version), link);
|
|
68
|
+
// Include explicit unplaced graph resources, not unrelated Run/history versions.
|
|
69
|
+
for (const row of this.graphs.db.prepare("SELECT resource_id,resource_version FROM canvas_resource_references WHERE graph_id=? AND owner_kind='graph' ORDER BY resource_id,resource_version").all(scope.graphId)) {
|
|
70
|
+
const link = { resourceId: String(row['resource_id']), version: Number(row['resource_version']) };
|
|
71
|
+
links.set(key(link.resourceId, link.version), link);
|
|
72
|
+
}
|
|
73
|
+
const resources = [...links.values()].map(link => {
|
|
74
|
+
const version = this.resources.readCanvasVersion(scope, link.resourceId, link.version);
|
|
75
|
+
return { ...link, name: this.resources.getCanvas(scope, link.resourceId).name, mime: version.mime, bytes: version.bytes, sha256: version.sha256, base64: '' };
|
|
76
|
+
});
|
|
77
|
+
const requirements = new Map();
|
|
78
|
+
const retained = this.graphs.repo.setting('graph-import-requirements:' + scope.graphId);
|
|
79
|
+
if (Array.isArray(retained))
|
|
80
|
+
for (const item of retained) {
|
|
81
|
+
const r = item;
|
|
82
|
+
requirements.set(key(r.typeId, r.schemaVersion), r);
|
|
83
|
+
}
|
|
84
|
+
for (const node of snapshot.nodes)
|
|
85
|
+
if (!core.has(node.type)) {
|
|
86
|
+
const k = key(node.type, node.schemaVersion);
|
|
87
|
+
if (requirements.has(k))
|
|
88
|
+
continue;
|
|
89
|
+
const row = this.graphs.db.prepare('SELECT contract FROM plugin_contracts WHERE type=? AND schema_version=?').get(node.type, node.schemaVersion);
|
|
90
|
+
requirements.set(k, { typeId: node.type, schemaVersion: node.schemaVersion, contract: row ? JSON.parse(String(row['contract'])) : null });
|
|
91
|
+
}
|
|
92
|
+
return { format: 'openworkgraph.graph', version: 1, graph: { title: snapshot.title, nodes: portableNodes, edges: snapshot.edges }, resources, pluginRequirements: [...requirements.values()], copiedProvenance: this.getCopiedProvenance(scope) };
|
|
93
|
+
});
|
|
94
|
+
const projectNodes = bundle.graph.nodes.filter(node => {
|
|
95
|
+
const content = record(node.content) ? node.content : undefined;
|
|
96
|
+
const source = content && record(content.source) ? content.source : undefined;
|
|
97
|
+
return source?.kind === 'project-file';
|
|
98
|
+
});
|
|
99
|
+
if (bundle.graph.nodes.length > this.limits.maxNodes || bundle.resources.length + projectNodes.length > this.limits.maxResources)
|
|
100
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'Graph exceeds export budget');
|
|
101
|
+
const projectPlans = [];
|
|
102
|
+
let totalResourceBytes = bundle.resources.reduce((sum, resource) => sum + resource.bytes, 0);
|
|
103
|
+
const maxResourceBytes = Math.min(this.limits.maxResourceBytes, this.resources.blobs.maxBytes);
|
|
104
|
+
if (bundle.resources.some(resource => resource.bytes > maxResourceBytes) || totalResourceBytes > this.limits.maxTotalResourceBytes)
|
|
105
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'Graph exceeds export budget');
|
|
106
|
+
for (let offset = 0; offset < projectNodes.length; offset += 100) {
|
|
107
|
+
const batch = projectNodes.slice(offset, offset + 100).map(node => {
|
|
108
|
+
const content = record(node.content) ? node.content : undefined;
|
|
109
|
+
const source = content && record(content.source) ? content.source : undefined;
|
|
110
|
+
const relativePath = source && typeof source.relativePath === 'string' ? source.relativePath : '';
|
|
111
|
+
if (!relativePath || source?.serviceId !== scope.serviceId || source?.projectId !== scope.projectId)
|
|
112
|
+
throw new ServiceError('INVALID_REQUEST', '图中项目文件引用不属于当前运行时项目。');
|
|
113
|
+
return { node, relativePath, classified: classifyProjectFile(relativePath) };
|
|
114
|
+
});
|
|
115
|
+
const observations = await this.projectFiles.stat(scope.projectId, batch.map(item => item.relativePath));
|
|
116
|
+
for (let index = 0; index < batch.length; index++) {
|
|
117
|
+
const item = batch[index], observation = observations[index];
|
|
118
|
+
if (!observation || observation.state !== 'available')
|
|
119
|
+
throw new ServiceError(observation?.state === 'missing' ? 'NOT_FOUND' : 'PROJECT_UNAVAILABLE', observation?.state === 'missing' ? '引用文件已不存在,工作图导出失败。' : '引用文件当前不可用,工作图导出失败。');
|
|
120
|
+
if (observation.bytes === null || !Number.isSafeInteger(observation.bytes) || observation.bytes < 0)
|
|
121
|
+
throw new ServiceError('PROJECT_UNAVAILABLE', '无法确认引用文件大小,工作图导出失败。');
|
|
122
|
+
if (observation.bytes > maxResourceBytes)
|
|
123
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', '引用文件超过 300 MB,工作图导出失败。');
|
|
124
|
+
totalResourceBytes += observation.bytes;
|
|
125
|
+
if (totalResourceBytes > this.limits.maxTotalResourceBytes)
|
|
126
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'Graph exceeds export budget');
|
|
127
|
+
projectPlans.push({ ...item, bytes: observation.bytes });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const resourceIds = new Set(bundle.resources.map(resource => resource.resourceId));
|
|
131
|
+
const projectResourceIds = new Set();
|
|
132
|
+
for (const plan of projectPlans) {
|
|
133
|
+
const { node, relativePath, classified } = plan;
|
|
134
|
+
const content = record(node.content) ? node.content : undefined;
|
|
135
|
+
const bytes = await this.projectFiles.read(scope.projectId, relativePath, maxResourceBytes);
|
|
136
|
+
if (bytes.length !== plan.bytes)
|
|
137
|
+
throw new ServiceError('CONFLICT', '引用文件在导出期间已变化,请重试。');
|
|
138
|
+
let resourceId = 'project-file-' + node.id;
|
|
139
|
+
let suffix = 1;
|
|
140
|
+
while (resourceIds.has(resourceId))
|
|
141
|
+
resourceId = 'project-file-' + node.id + '-' + suffix++;
|
|
142
|
+
resourceIds.add(resourceId);
|
|
143
|
+
const title = content && typeof content.title === 'string' && content.title.trim() ? content.title : relativePath.split('/').at(-1) ?? '项目文件';
|
|
144
|
+
const resource = { resourceId, version: 1, name: title, mime: classified.mime, bytes: bytes.length, sha256: hash(bytes), base64: bytes.toString('base64') };
|
|
145
|
+
bundle.resources.push(resource);
|
|
146
|
+
projectResourceIds.add(resourceId);
|
|
147
|
+
node.type = classified.type;
|
|
148
|
+
const decoded = classified.type === 'text' ? new TextDecoder('utf-8', { fatal: true }).decode(bytes) : '';
|
|
149
|
+
const text = classified.type === 'text' && Buffer.byteLength(JSON.stringify(decoded)) <= 2 * 1024 * 1024 - 2048 ? decoded : '';
|
|
150
|
+
node.content = { title, text, prompt: '', resourceId, resourceVersion: 1, mime: classified.mime, ...(classified.type === 'file' ? { bytes: bytes.length } : {}) };
|
|
151
|
+
}
|
|
152
|
+
for (const resource of bundle.resources) {
|
|
153
|
+
if (projectResourceIds.has(resource.resourceId))
|
|
154
|
+
continue;
|
|
155
|
+
const content = await this.resources.readContent(scope, 'canvas', resource.resourceId, resource.version);
|
|
156
|
+
if (content.bytes.length !== resource.bytes || hash(content.bytes) !== resource.sha256 || content.mime !== resource.mime)
|
|
157
|
+
throw new ServiceError('CONFLICT', 'Resource changed during export');
|
|
158
|
+
resource.base64 = content.bytes.toString('base64');
|
|
159
|
+
}
|
|
160
|
+
// Reuse import validation so exported data is portable and structurally complete.
|
|
161
|
+
return this.validate(bundle, true).bundle;
|
|
162
|
+
}
|
|
163
|
+
validate(input, trustedTopology) {
|
|
164
|
+
const bundle = checkedJson(input, this.limits.maxBundleBytes, 40);
|
|
165
|
+
if (!record(bundle) || bundle.format !== 'openworkgraph.graph' || bundle.version !== 1 || !record(bundle.graph))
|
|
166
|
+
invalid('Unsupported graph bundle');
|
|
167
|
+
keys(bundle, ['format', 'version', 'graph', 'resources', 'pluginRequirements', 'copiedProvenance']);
|
|
168
|
+
keys(bundle.graph, ['title', 'nodes', 'edges']);
|
|
169
|
+
text(bundle.graph.title, 1024);
|
|
170
|
+
if (!Array.isArray(bundle.graph.nodes) || bundle.graph.nodes.length > this.limits.maxNodes || !Array.isArray(bundle.graph.edges) || bundle.graph.edges.length > this.limits.maxNodes * 9 || !Array.isArray(bundle.resources) || bundle.resources.length > this.limits.maxResources || !Array.isArray(bundle.pluginRequirements) || bundle.pluginRequirements.length > this.limits.maxNodes)
|
|
171
|
+
invalid('Invalid bundle collection limits');
|
|
172
|
+
const nodes = new Map();
|
|
173
|
+
for (const n of bundle.graph.nodes) {
|
|
174
|
+
if (!record(n))
|
|
175
|
+
invalid('Invalid node');
|
|
176
|
+
keys(n, ['id', 'type', 'schemaVersion', 'contentVersion', 'content', 'x', 'y', 'width', 'height', 'memberIds', 'readOnly']);
|
|
177
|
+
id(n.id);
|
|
178
|
+
id(n.type);
|
|
179
|
+
positive(n.schemaVersion);
|
|
180
|
+
positive(n.contentVersion);
|
|
181
|
+
if (nodes.has(n.id) || typeof n.readOnly !== 'boolean' || !Number.isFinite(n.x) || !Number.isFinite(n.y) || Math.abs(n.x) > 1e9 || Math.abs(n.y) > 1e9)
|
|
182
|
+
invalid('Invalid or duplicate node');
|
|
183
|
+
checkedJson(n.content, 2 * 1024 * 1024, 32);
|
|
184
|
+
if (n.width !== undefined)
|
|
185
|
+
nodeDimension(n.width);
|
|
186
|
+
if (n.height !== undefined)
|
|
187
|
+
nodeDimension(n.height);
|
|
188
|
+
if (n.type !== 'group' && n.memberIds !== undefined)
|
|
189
|
+
invalid('Only layout groups have members');
|
|
190
|
+
if (n.type === 'group') {
|
|
191
|
+
if (n.readOnly || new PluginRegistry(this.graphs.db).inspect(n.type, n.schemaVersion, n.content).state !== 'available')
|
|
192
|
+
invalid('Invalid layout group');
|
|
193
|
+
if (record(n.content) && n.content['title'] !== undefined)
|
|
194
|
+
text(n.content['title'], 1024);
|
|
195
|
+
if (n.memberIds !== undefined && (!Array.isArray(n.memberIds) || n.memberIds.some(value => typeof value !== 'string') || new Set(n.memberIds).size !== n.memberIds.length))
|
|
196
|
+
invalid('Invalid group members');
|
|
197
|
+
}
|
|
198
|
+
let values = 0;
|
|
199
|
+
const count = (v) => { if (++values > 100000)
|
|
200
|
+
invalid('Node content has too many values'); if (v && typeof v === 'object')
|
|
201
|
+
for (const item of Object.values(v))
|
|
202
|
+
count(item); };
|
|
203
|
+
count(n.content);
|
|
204
|
+
nodes.set(n.id, n);
|
|
205
|
+
}
|
|
206
|
+
const members = new Set();
|
|
207
|
+
for (const n of nodes.values())
|
|
208
|
+
if (n.type === 'group')
|
|
209
|
+
for (const memberId of n.memberIds ?? []) {
|
|
210
|
+
const member = nodes.get(memberId);
|
|
211
|
+
if (!member || member.type === 'group' || members.has(memberId))
|
|
212
|
+
invalid('Missing, nested or overlapping group member');
|
|
213
|
+
members.add(memberId);
|
|
214
|
+
}
|
|
215
|
+
const pairs = new Set(), edges = new Set(), counts = new Map(), deliveryTargets = new Set();
|
|
216
|
+
for (const edge of bundle.graph.edges) {
|
|
217
|
+
if (!record(edge))
|
|
218
|
+
invalid('Invalid edge');
|
|
219
|
+
keys(edge, ['id', 'sourceId', 'targetId', 'kind']);
|
|
220
|
+
id(edge.id);
|
|
221
|
+
const source = nodes.get(edge.sourceId), target = nodes.get(edge.targetId), pair = JSON.stringify([edge.sourceId, edge.targetId]);
|
|
222
|
+
if (!source || !target || source.id === target.id || edges.has(edge.id) || pairs.has(pair))
|
|
223
|
+
throw new ServiceError('INVALID_EDGE', 'Missing, self or duplicate edge endpoint');
|
|
224
|
+
if (source.type === 'group' || target.type === 'group')
|
|
225
|
+
throw new ServiceError('INVALID_EDGE', 'Layout groups cannot be edge endpoints');
|
|
226
|
+
edges.add(edge.id);
|
|
227
|
+
pairs.add(pair);
|
|
228
|
+
if (edge.kind === 'delivery') {
|
|
229
|
+
if (source.type !== 'execution' || ['execution', 'group', 'preview'].includes(target.type) || !target.readOnly || deliveryTargets.has(target.id))
|
|
230
|
+
throw new ServiceError('INVALID_EDGE', 'Delivery topology requires one execution source to a readonly output');
|
|
231
|
+
deliveryTargets.add(target.id);
|
|
232
|
+
}
|
|
233
|
+
else if (edge.kind === 'execution') {
|
|
234
|
+
if (source.type !== 'execution' || target.type !== 'execution')
|
|
235
|
+
throw new ServiceError('INVALID_EDGE', 'Execution chain edges require execution endpoints');
|
|
236
|
+
}
|
|
237
|
+
else if (edge.kind === 'reference') {
|
|
238
|
+
if (source.type === 'execution')
|
|
239
|
+
throw new ServiceError('INVALID_EDGE', 'Execution cannot be a reference source');
|
|
240
|
+
const previewError = previewEdgeError(source.type, target.type, counts.get(target.id) ?? 0);
|
|
241
|
+
if (previewError)
|
|
242
|
+
throw new ServiceError('INVALID_EDGE', previewError);
|
|
243
|
+
const count = (counts.get(target.id) ?? 0) + 1;
|
|
244
|
+
counts.set(target.id, count);
|
|
245
|
+
if (count > 8)
|
|
246
|
+
throw new ServiceError('INVALID_EDGE', 'More than 8 direct predecessors');
|
|
247
|
+
}
|
|
248
|
+
else
|
|
249
|
+
throw new ServiceError('INVALID_EDGE', 'Unknown edge kind');
|
|
250
|
+
}
|
|
251
|
+
if (!executionOrder(bundle.graph.edges))
|
|
252
|
+
throw new ServiceError('INVALID_EDGE', 'Execution chain contains a cycle');
|
|
253
|
+
if (bundle.copiedProvenance !== undefined) {
|
|
254
|
+
if (!Array.isArray(bundle.copiedProvenance) || bundle.copiedProvenance.length > this.limits.maxNodes * 10)
|
|
255
|
+
invalid('Invalid copied provenance collection');
|
|
256
|
+
for (const p of bundle.copiedProvenance) {
|
|
257
|
+
if (!record(p) || p.verified !== false)
|
|
258
|
+
invalid('Imported provenance is always unverified');
|
|
259
|
+
if (p.kind === 'copied-delivery') {
|
|
260
|
+
keys(p, ['kind', 'sourceId', 'targetId', 'verified']);
|
|
261
|
+
if (nodes.get(p.sourceId)?.type !== 'execution' || !nodes.has(p.targetId) || ['execution', 'group'].includes(nodes.get(p.targetId).type) || p.sourceId === p.targetId)
|
|
262
|
+
invalid('Invalid copied delivery endpoints');
|
|
263
|
+
}
|
|
264
|
+
else if (p.kind === 'copied-node') {
|
|
265
|
+
keys(p, ['kind', 'nodeId', 'metadata', 'verified']);
|
|
266
|
+
if (!nodes.has(p.nodeId))
|
|
267
|
+
invalid('Missing copied provenance node');
|
|
268
|
+
checkedJson(p.metadata);
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
invalid('Unknown copied provenance kind');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const requirements = new Set();
|
|
275
|
+
for (const r of bundle.pluginRequirements) {
|
|
276
|
+
if (!record(r))
|
|
277
|
+
invalid('Invalid plugin requirement');
|
|
278
|
+
id(r.typeId);
|
|
279
|
+
positive(r.schemaVersion);
|
|
280
|
+
keys(r, ['typeId', 'schemaVersion', 'contract']);
|
|
281
|
+
const k = key(r.typeId, r.schemaVersion);
|
|
282
|
+
if (requirements.has(k))
|
|
283
|
+
invalid('Duplicate plugin requirement');
|
|
284
|
+
requirements.add(k);
|
|
285
|
+
if (r.contract !== null)
|
|
286
|
+
checkedJson(r.contract, 65_536, 24); // Preserve opaque requirements; never register or evaluate them.
|
|
287
|
+
}
|
|
288
|
+
for (const n of nodes.values())
|
|
289
|
+
if (!core.has(n.type) && !requirements.has(key(n.type, n.schemaVersion)))
|
|
290
|
+
invalid('Missing plugin requirement');
|
|
291
|
+
const resources = new Set(), buffers = [];
|
|
292
|
+
let total = 0;
|
|
293
|
+
for (const r of bundle.resources) {
|
|
294
|
+
if (!record(r))
|
|
295
|
+
invalid('Invalid resource');
|
|
296
|
+
keys(r, ['resourceId', 'version', 'name', 'mime', 'bytes', 'sha256', 'base64']);
|
|
297
|
+
id(r.resourceId);
|
|
298
|
+
positive(r.version);
|
|
299
|
+
text(r.name, 512);
|
|
300
|
+
if (typeof r.mime !== 'string' || !/^[a-zA-Z0-9.+-]+[/][a-zA-Z0-9.+-]+$/.test(r.mime) || typeof r.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(r.sha256) || !Number.isSafeInteger(r.bytes) || r.bytes < 0 || r.bytes > Math.min(this.limits.maxResourceBytes, this.resources.blobs.maxBytes))
|
|
301
|
+
invalid('Invalid resource metadata');
|
|
302
|
+
total += r.bytes;
|
|
303
|
+
if (total > this.limits.maxTotalResourceBytes)
|
|
304
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'Bundle resources exceed total budget');
|
|
305
|
+
const k = key(r.resourceId, r.version);
|
|
306
|
+
if (resources.has(k))
|
|
307
|
+
invalid('Duplicate resource version');
|
|
308
|
+
resources.add(k);
|
|
309
|
+
if (typeof r.base64 !== 'string' || r.base64.length !== 4 * Math.ceil(r.bytes / 3) || !/^[A-Za-z0-9+/]*={0,2}$/.test(r.base64))
|
|
310
|
+
invalid('Invalid base64 encoding');
|
|
311
|
+
const bytes = Buffer.from(r.base64, 'base64');
|
|
312
|
+
if (bytes.length !== r.bytes || bytes.toString('base64') !== r.base64 || hash(bytes) !== r.sha256)
|
|
313
|
+
invalid('Resource hash/size mismatch');
|
|
314
|
+
const sniffed = sniffMime(bytes), declared = r.mime.toLowerCase();
|
|
315
|
+
if (!mimeMatchesBytes(declared, sniffed))
|
|
316
|
+
throw new ServiceError('UNSUPPORTED_MEDIA_TYPE', 'Bundle MIME does not match bytes');
|
|
317
|
+
if (declared === 'application/json') {
|
|
318
|
+
try {
|
|
319
|
+
JSON.parse(bytes.toString('utf8'));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
invalid('Invalid JSON resource');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
buffers.push(bytes);
|
|
326
|
+
}
|
|
327
|
+
for (const n of nodes.values()) {
|
|
328
|
+
for (const link of resourceLinks(n.content))
|
|
329
|
+
if (!resources.has(key(link.resourceId, link.version)))
|
|
330
|
+
invalid('Referenced canvas resource/version is missing from bundle');
|
|
331
|
+
const rejectAssets = (value) => { if (!value || typeof value !== 'object')
|
|
332
|
+
return; if (!Array.isArray(value) && Object.hasOwn(value, 'assetId'))
|
|
333
|
+
invalid('Legacy/remote asset references are not portable canvas resources'); for (const v of Object.values(value))
|
|
334
|
+
rejectAssets(v); };
|
|
335
|
+
rejectAssets(n.content);
|
|
336
|
+
}
|
|
337
|
+
return { bundle, bytes: buffers };
|
|
338
|
+
}
|
|
339
|
+
/** trustedTopology is a local authorization decision, NEVER a field accepted from an HTTP bundle. */
|
|
340
|
+
async prepareImport(scope, input, options = {}) {
|
|
341
|
+
this.outside();
|
|
342
|
+
this.target(scope);
|
|
343
|
+
const stableScope = { serviceId: scope.serviceId, projectId: scope.projectId };
|
|
344
|
+
const trustedTopology = options.trustedTopology === true;
|
|
345
|
+
const { bundle, bytes } = this.validate(input, trustedTopology);
|
|
346
|
+
const digest = canonicalJsonHash(bundle);
|
|
347
|
+
const blobs = [];
|
|
348
|
+
try {
|
|
349
|
+
for (let i = 0; i < bundle.resources.length; i++) {
|
|
350
|
+
const r = bundle.resources[i];
|
|
351
|
+
blobs.push(await this.resources.prepareBytes(bytes[i], r.mime, r.sha256));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
// A token cannot be returned on partial preparation failure. Retire the
|
|
356
|
+
// already prepared generations durably before attempting physical cleanup.
|
|
357
|
+
atomic(this.graphs.db, () => { for (const blob of blobs)
|
|
358
|
+
this.resources.discardPrepared(blob); });
|
|
359
|
+
for (const blob of blobs) {
|
|
360
|
+
try {
|
|
361
|
+
await this.resources.blobs.remove(blob.path);
|
|
362
|
+
atomic(this.graphs.db, () => this.graphs.db.prepare('UPDATE resource_file_deletions SET completed=1 WHERE path=?').run(blob.path));
|
|
363
|
+
}
|
|
364
|
+
catch { } // Explicit file tombstone remains pending for Resources cleanup.
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
const token = Object.freeze({ digest, ...stableScope });
|
|
369
|
+
this.prepared.set(token, { bundle, blobs, trustedTopology, disposed: false, disposal: null });
|
|
370
|
+
return token;
|
|
371
|
+
}
|
|
372
|
+
/** Call from API finally AFTER the surrounding authorization transaction settles,
|
|
373
|
+
* on success, failure and replay. No active session/project access is needed to
|
|
374
|
+
* release this instance-owned token. Durable tombstones fence late commits;
|
|
375
|
+
* deletion failure remains queued for Resources cleanup, never an orphan scan. */
|
|
376
|
+
async disposePreparedImport(token) {
|
|
377
|
+
this.outside();
|
|
378
|
+
const data = this.prepared.get(token);
|
|
379
|
+
if (!data)
|
|
380
|
+
invalid('Invalid prepared graph import');
|
|
381
|
+
if (data.disposal)
|
|
382
|
+
return data.disposal;
|
|
383
|
+
data.disposed = true;
|
|
384
|
+
const work = async () => {
|
|
385
|
+
const retired = [];
|
|
386
|
+
const result = { retired: 0, retained: 0, removed: 0, failed: 0 };
|
|
387
|
+
atomic(this.graphs.db, () => {
|
|
388
|
+
for (const blob of data.blobs) {
|
|
389
|
+
// Skip the live physical mapping even if this import just committed it.
|
|
390
|
+
if (this.resources.discardPrepared(blob))
|
|
391
|
+
retired.push(blob);
|
|
392
|
+
else
|
|
393
|
+
result.retained++;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
result.retired = retired.length;
|
|
397
|
+
for (const blob of retired) {
|
|
398
|
+
try {
|
|
399
|
+
await this.resources.blobs.remove(blob.path);
|
|
400
|
+
atomic(this.graphs.db, () => this.graphs.db.prepare('UPDATE resource_file_deletions SET completed=1 WHERE path=?').run(blob.path));
|
|
401
|
+
result.removed++;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
result.failed++;
|
|
405
|
+
} // Remains durable and retryable by normal resource cleanup.
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
};
|
|
409
|
+
const disposal = work();
|
|
410
|
+
data.disposal = disposal;
|
|
411
|
+
try {
|
|
412
|
+
return await disposal;
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
data.disposal = null;
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** Recheck auth outside this helper, in the same surrounding transaction as this synchronous commit. */
|
|
420
|
+
commitImport(scope, token, options) {
|
|
421
|
+
const data = this.prepared.get(token);
|
|
422
|
+
if (!data || data.disposed || scope.serviceId !== token.serviceId || scope.projectId !== token.projectId)
|
|
423
|
+
invalid('Invalid or disposed prepared graph import');
|
|
424
|
+
return atomic(this.graphs.db, () => {
|
|
425
|
+
this.target(scope);
|
|
426
|
+
return this.graphs.repo.idempotent((options.principal ?? 'local') + ':graph.import:' + scope.projectId, options.idempotencyKey, { digest: token.digest, trustedTopology: data.trustedTopology }, () => {
|
|
427
|
+
const graphId = randomUUID(), target = { ...scope, graphId };
|
|
428
|
+
const existingTitles = this.graphs.db.prepare('SELECT title FROM graphs WHERE project_id=?').all(scope.projectId).map(row => String(row['title']));
|
|
429
|
+
const graphTitle = importedGraphTitle(data.bundle.graph.title, existingTitles);
|
|
430
|
+
this.graphs.db.prepare('INSERT INTO graphs(id,project_id,title) VALUES(?,?,?)').run(graphId, scope.projectId, graphTitle);
|
|
431
|
+
const ids = new Map(), resourceIds = new Map();
|
|
432
|
+
for (const n of data.bundle.graph.nodes)
|
|
433
|
+
ids.set(n.id, randomUUID());
|
|
434
|
+
for (let i = 0; i < data.bundle.resources.length; i++) {
|
|
435
|
+
const r = data.bundle.resources[i];
|
|
436
|
+
const created = this.resources.createCanvasFromPrepared(target, data.blobs[i], r.name);
|
|
437
|
+
resourceIds.set(key(r.resourceId, r.version), { id: created.resource.id, bytes: created.resource.current.bytes, mime: created.resource.current.mime });
|
|
438
|
+
}
|
|
439
|
+
const remap = (value) => {
|
|
440
|
+
if (Array.isArray(value))
|
|
441
|
+
return value.map(remap);
|
|
442
|
+
if (value === null || typeof value !== 'object')
|
|
443
|
+
return value;
|
|
444
|
+
const result = {};
|
|
445
|
+
for (const [k, v] of Object.entries(value))
|
|
446
|
+
result[k] = remap(v);
|
|
447
|
+
if (typeof value['resourceId'] === 'string') {
|
|
448
|
+
const v = value['resourceVersion'] ?? value['version'];
|
|
449
|
+
const mapped = resourceIds.get(key(value['resourceId'], v));
|
|
450
|
+
if (!mapped)
|
|
451
|
+
invalid('Missing resource remapping');
|
|
452
|
+
result['resourceId'] = mapped.id;
|
|
453
|
+
result['bytes'] = mapped.bytes;
|
|
454
|
+
result['mime'] = mapped.mime;
|
|
455
|
+
if (Object.hasOwn(value, 'resourceVersion'))
|
|
456
|
+
result['resourceVersion'] = 1;
|
|
457
|
+
if (Object.hasOwn(value, 'version'))
|
|
458
|
+
result['version'] = 1;
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
};
|
|
462
|
+
const copiedProvenance = (data.bundle.copiedProvenance ?? []).map(p => p.kind === 'copied-delivery' ? { ...p, sourceId: ids.get(p.sourceId), targetId: ids.get(p.targetId) } : { ...p, nodeId: ids.get(p.nodeId) });
|
|
463
|
+
for (const n of data.bundle.graph.nodes) {
|
|
464
|
+
const nodeId = ids.get(n.id), content = remap(n.content);
|
|
465
|
+
// Public import copies content, not publication privileges. Preserve
|
|
466
|
+
// source runtime identifiers only as explicitly non-authoritative data.
|
|
467
|
+
if (!data.trustedTopology && core.has(n.type) && content !== null && typeof content === 'object' && !Array.isArray(content)) {
|
|
468
|
+
const metadata = {};
|
|
469
|
+
for (const field of ['runId', 'originRunId', 'outputKey'])
|
|
470
|
+
if (Object.hasOwn(content, field)) {
|
|
471
|
+
metadata[field] = content[field];
|
|
472
|
+
delete content[field];
|
|
473
|
+
}
|
|
474
|
+
if (n.readOnly)
|
|
475
|
+
metadata['sourceReadOnly'] = true;
|
|
476
|
+
if (Object.keys(metadata).length)
|
|
477
|
+
copiedProvenance.push({ kind: 'copied-node', nodeId, metadata, verified: false });
|
|
478
|
+
}
|
|
479
|
+
this.graphs.insertNode(target, { ...n, ...(n.type === 'group' ? { memberIds: [] } : {}), id: nodeId, contentVersion: 1, content, readOnly: !data.trustedTopology && core.has(n.type) ? false : n.readOnly }, true);
|
|
480
|
+
}
|
|
481
|
+
for (const n of data.bundle.graph.nodes)
|
|
482
|
+
if (n.type === 'group')
|
|
483
|
+
this.graphs.setGroupMembers(target, ids.get(n.id), (n.memberIds ?? []).map(id => ids.get(id)));
|
|
484
|
+
for (const e of data.bundle.graph.edges) {
|
|
485
|
+
if (e.kind === 'delivery' && !data.trustedTopology) {
|
|
486
|
+
copiedProvenance.push({ kind: 'copied-delivery', sourceId: ids.get(e.sourceId), targetId: ids.get(e.targetId), verified: false });
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const edge = { ...e, id: randomUUID(), sourceId: ids.get(e.sourceId), targetId: ids.get(e.targetId) };
|
|
490
|
+
this.graphs.validateEdge(target, edge, data.trustedTopology);
|
|
491
|
+
this.graphs.db.prepare('INSERT INTO edges(id,graph_id,source_id,target_id,kind) VALUES(?,?,?,?,?)').run(edge.id, graphId, edge.sourceId, edge.targetId, edge.kind);
|
|
492
|
+
}
|
|
493
|
+
this.graphs.db.prepare('INSERT INTO settings(key,value) VALUES(?,?)').run('graph-import-requirements:' + graphId, JSON.stringify(data.bundle.pluginRequirements));
|
|
494
|
+
this.graphs.db.prepare('INSERT INTO settings(key,value) VALUES(?,?)').run('graph-copied-provenance:' + graphId, JSON.stringify(copiedProvenance));
|
|
495
|
+
this.graphs.event(target);
|
|
496
|
+
return { ...this.graphs.snapshot(target), copiedProvenance };
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
//# sourceMappingURL=graph-transfer.js.map
|