@typicalday/firegraph 0.9.0 → 0.11.0
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 +93 -90
- package/bin/firegraph.mjs +21 -7
- package/dist/backend-U-MLShlg.d.ts +97 -0
- package/dist/backend-np4gEVhB.d.cts +97 -0
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +7 -6
- package/dist/backend.d.ts +7 -6
- package/dist/backend.js +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/{chunk-EVUM6ORB.js → chunk-6SB34IPQ.js} +76 -8
- package/dist/chunk-6SB34IPQ.js.map +1 -0
- package/dist/{chunk-SU4FNLC3.js → chunk-EEKWRX5E.js} +1 -1
- package/dist/{chunk-SU4FNLC3.js.map → chunk-EEKWRX5E.js.map} +1 -1
- package/dist/{chunk-YLGXLEUE.js → chunk-GJVVRTQT.js} +5 -14
- package/dist/chunk-GJVVRTQT.js.map +1 -0
- package/dist/{chunk-GLOVWKQH.js → chunk-R7CRGYY4.js} +1 -1
- package/dist/{chunk-GLOVWKQH.js.map → chunk-R7CRGYY4.js.map} +1 -1
- package/dist/{do-sqlite.cjs → cloudflare/index.cjs} +1659 -1422
- package/dist/cloudflare/index.cjs.map +1 -0
- package/dist/cloudflare/index.d.cts +529 -0
- package/dist/cloudflare/index.d.ts +529 -0
- package/dist/cloudflare/index.js +934 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/codegen/index.cjs +4 -13
- package/dist/codegen/index.cjs.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +1 -1
- package/dist/index.cjs +144 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -27
- package/dist/index.d.ts +116 -27
- package/dist/index.js +77 -123
- package/dist/index.js.map +1 -1
- package/dist/query-client/index.cjs.map +1 -1
- package/dist/query-client/index.js +1 -1
- package/dist/{scope-path-BtajqNK5.d.ts → scope-path-B1G3YiA7.d.cts} +5 -100
- package/dist/{scope-path-D2mNENJ-.d.cts → scope-path-B1G3YiA7.d.ts} +5 -100
- package/dist/{types-DfWVTsMn.d.ts → types-BGWxcpI_.d.cts} +92 -1
- package/dist/{types-DfWVTsMn.d.cts → types-BGWxcpI_.d.ts} +92 -1
- package/package.json +13 -17
- package/dist/chunk-EVUM6ORB.js.map +0 -1
- package/dist/chunk-SZ6W4VAS.js +0 -701
- package/dist/chunk-SZ6W4VAS.js.map +0 -1
- package/dist/chunk-YLGXLEUE.js.map +0 -1
- package/dist/d1.cjs +0 -2421
- package/dist/d1.cjs.map +0 -1
- package/dist/d1.d.cts +0 -54
- package/dist/d1.d.ts +0 -54
- package/dist/d1.js +0 -76
- package/dist/d1.js.map +0 -1
- package/dist/do-sqlite.cjs.map +0 -1
- package/dist/do-sqlite.d.cts +0 -41
- package/dist/do-sqlite.d.ts +0 -41
- package/dist/do-sqlite.js +0 -79
- package/dist/do-sqlite.js.map +0 -1
- package/dist/editor/client/assets/index-Bq2bfzeY.js +0 -411
- package/dist/editor/client/assets/index-CJ4m_EOL.css +0 -1
- package/dist/editor/client/index.html +0 -16
- package/dist/editor/server/index.mjs +0 -51511
|
@@ -154,101 +154,16 @@ var init_serialization = __esm({
|
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
// src/
|
|
158
|
-
var
|
|
159
|
-
__export(
|
|
160
|
-
|
|
157
|
+
// src/cloudflare/index.ts
|
|
158
|
+
var cloudflare_exports = {};
|
|
159
|
+
__export(cloudflare_exports, {
|
|
160
|
+
DORPCBackend: () => DORPCBackend,
|
|
161
|
+
FiregraphDO: () => FiregraphDO,
|
|
162
|
+
buildDOSchemaStatements: () => buildDOSchemaStatements,
|
|
163
|
+
createDOClient: () => createDOClient,
|
|
164
|
+
createSiblingClient: () => createSiblingClient
|
|
161
165
|
});
|
|
162
|
-
module.exports = __toCommonJS(
|
|
163
|
-
|
|
164
|
-
// src/docid.ts
|
|
165
|
-
var import_node_crypto = require("crypto");
|
|
166
|
-
|
|
167
|
-
// src/internal/constants.ts
|
|
168
|
-
var NODE_RELATION = "is";
|
|
169
|
-
var DEFAULT_QUERY_LIMIT = 500;
|
|
170
|
-
var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
|
|
171
|
-
"aType",
|
|
172
|
-
"aUid",
|
|
173
|
-
"axbType",
|
|
174
|
-
"bType",
|
|
175
|
-
"bUid",
|
|
176
|
-
"createdAt",
|
|
177
|
-
"updatedAt"
|
|
178
|
-
]);
|
|
179
|
-
var SHARD_SEPARATOR = ":";
|
|
180
|
-
|
|
181
|
-
// src/docid.ts
|
|
182
|
-
function computeNodeDocId(uid) {
|
|
183
|
-
return uid;
|
|
184
|
-
}
|
|
185
|
-
function computeEdgeDocId(aUid, axbType, bUid) {
|
|
186
|
-
const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
187
|
-
const hash = (0, import_node_crypto.createHash)("sha256").update(composite).digest("hex");
|
|
188
|
-
const shard = hash[0];
|
|
189
|
-
return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// src/batch.ts
|
|
193
|
-
function buildWritableNodeRecord(aType, uid, data) {
|
|
194
|
-
return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
|
|
195
|
-
}
|
|
196
|
-
function buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
|
|
197
|
-
return { aType, aUid, axbType, bType, bUid, data };
|
|
198
|
-
}
|
|
199
|
-
var GraphBatchImpl = class {
|
|
200
|
-
constructor(backend, registry, scopePath = "") {
|
|
201
|
-
this.backend = backend;
|
|
202
|
-
this.registry = registry;
|
|
203
|
-
this.scopePath = scopePath;
|
|
204
|
-
}
|
|
205
|
-
async putNode(aType, uid, data) {
|
|
206
|
-
if (this.registry) {
|
|
207
|
-
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
208
|
-
}
|
|
209
|
-
const docId = computeNodeDocId(uid);
|
|
210
|
-
const record = buildWritableNodeRecord(aType, uid, data);
|
|
211
|
-
if (this.registry) {
|
|
212
|
-
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
213
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
214
|
-
record.v = entry.schemaVersion;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
this.backend.setDoc(docId, record);
|
|
218
|
-
}
|
|
219
|
-
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
220
|
-
if (this.registry) {
|
|
221
|
-
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
222
|
-
}
|
|
223
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
224
|
-
const record = buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
225
|
-
if (this.registry) {
|
|
226
|
-
const entry = this.registry.lookup(aType, axbType, bType);
|
|
227
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
228
|
-
record.v = entry.schemaVersion;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
this.backend.setDoc(docId, record);
|
|
232
|
-
}
|
|
233
|
-
async updateNode(uid, data) {
|
|
234
|
-
const docId = computeNodeDocId(uid);
|
|
235
|
-
this.backend.updateDoc(docId, { dataFields: data });
|
|
236
|
-
}
|
|
237
|
-
async removeNode(uid) {
|
|
238
|
-
const docId = computeNodeDocId(uid);
|
|
239
|
-
this.backend.deleteDoc(docId);
|
|
240
|
-
}
|
|
241
|
-
async removeEdge(aUid, axbType, bUid) {
|
|
242
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
243
|
-
this.backend.deleteDoc(docId);
|
|
244
|
-
}
|
|
245
|
-
async commit() {
|
|
246
|
-
await this.backend.commit();
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// src/dynamic-registry.ts
|
|
251
|
-
var import_node_crypto3 = require("crypto");
|
|
166
|
+
module.exports = __toCommonJS(cloudflare_exports);
|
|
252
167
|
|
|
253
168
|
// src/errors.ts
|
|
254
169
|
var FiregraphError = class extends Error {
|
|
@@ -305,238 +220,963 @@ var MigrationError = class extends FiregraphError {
|
|
|
305
220
|
}
|
|
306
221
|
};
|
|
307
222
|
|
|
308
|
-
// src/
|
|
309
|
-
var
|
|
310
|
-
var
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
errors
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
}
|
|
223
|
+
// src/internal/constants.ts
|
|
224
|
+
var NODE_RELATION = "is";
|
|
225
|
+
var DEFAULT_QUERY_LIMIT = 500;
|
|
226
|
+
var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
|
|
227
|
+
"aType",
|
|
228
|
+
"aUid",
|
|
229
|
+
"axbType",
|
|
230
|
+
"bType",
|
|
231
|
+
"bUid",
|
|
232
|
+
"createdAt",
|
|
233
|
+
"updatedAt"
|
|
234
|
+
]);
|
|
235
|
+
var SHARD_SEPARATOR = ":";
|
|
326
236
|
|
|
327
|
-
// src/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
for (const step of sorted) {
|
|
333
|
-
if (step.fromVersion === version) {
|
|
334
|
-
try {
|
|
335
|
-
result = await step.up(result);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
if (err instanceof MigrationError) throw err;
|
|
338
|
-
throw new MigrationError(
|
|
339
|
-
`Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
if (!result || typeof result !== "object") {
|
|
343
|
-
throw new MigrationError(
|
|
344
|
-
`Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
version = step.toVersion;
|
|
348
|
-
}
|
|
237
|
+
// src/timestamp.ts
|
|
238
|
+
var GraphTimestampImpl = class _GraphTimestampImpl {
|
|
239
|
+
constructor(seconds, nanoseconds) {
|
|
240
|
+
this.seconds = seconds;
|
|
241
|
+
this.nanoseconds = nanoseconds;
|
|
349
242
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
243
|
+
toDate() {
|
|
244
|
+
return new Date(this.toMillis());
|
|
245
|
+
}
|
|
246
|
+
toMillis() {
|
|
247
|
+
return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
|
|
248
|
+
}
|
|
249
|
+
toJSON() {
|
|
250
|
+
return { seconds: this.seconds, nanoseconds: this.nanoseconds };
|
|
251
|
+
}
|
|
252
|
+
static fromMillis(ms) {
|
|
253
|
+
const seconds = Math.floor(ms / 1e3);
|
|
254
|
+
const nanoseconds = (ms - seconds * 1e3) * 1e6;
|
|
255
|
+
return new _GraphTimestampImpl(seconds, nanoseconds);
|
|
256
|
+
}
|
|
257
|
+
static now() {
|
|
258
|
+
return _GraphTimestampImpl.fromMillis(Date.now());
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/default-indexes.ts
|
|
263
|
+
var DEFAULT_CORE_INDEXES = Object.freeze([
|
|
264
|
+
{ fields: ["aUid"] },
|
|
265
|
+
{ fields: ["bUid"] },
|
|
266
|
+
{ fields: ["aType"] },
|
|
267
|
+
{ fields: ["bType"] },
|
|
268
|
+
{ fields: ["aUid", "axbType"] },
|
|
269
|
+
{ fields: ["axbType", "bUid"] },
|
|
270
|
+
{ fields: ["aType", "axbType"] },
|
|
271
|
+
{ fields: ["axbType", "bType"] }
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
// src/internal/sqlite-index-ddl.ts
|
|
275
|
+
var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
276
|
+
var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
277
|
+
function quoteIdent(name) {
|
|
278
|
+
if (!IDENT_RE.test(name)) {
|
|
279
|
+
throw new FiregraphError(
|
|
280
|
+
`Invalid SQL identifier in index DDL: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
|
|
281
|
+
"INVALID_INDEX"
|
|
353
282
|
);
|
|
354
283
|
}
|
|
355
|
-
return
|
|
284
|
+
return `"${name}"`;
|
|
356
285
|
}
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
286
|
+
function fnv1a32(str) {
|
|
287
|
+
let h = 2166136261;
|
|
288
|
+
for (let i = 0; i < str.length; i++) {
|
|
289
|
+
h ^= str.charCodeAt(i);
|
|
290
|
+
h = Math.imul(h, 16777619);
|
|
291
|
+
}
|
|
292
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
293
|
+
}
|
|
294
|
+
function normalizeFields(fields) {
|
|
295
|
+
return fields.map((f) => {
|
|
296
|
+
if (typeof f === "string") return { path: f, desc: false };
|
|
297
|
+
if (!f.path || typeof f.path !== "string") {
|
|
298
|
+
throw new FiregraphError(
|
|
299
|
+
`IndexSpec field must be a string or { path: string, desc?: boolean }; got ${JSON.stringify(f)}`,
|
|
300
|
+
"INVALID_INDEX"
|
|
364
301
|
);
|
|
365
302
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
303
|
+
return { path: f.path, desc: !!f.desc };
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function specFingerprint(spec, leadingColumns) {
|
|
307
|
+
const normalized = {
|
|
308
|
+
lead: leadingColumns,
|
|
309
|
+
fields: normalizeFields(spec.fields),
|
|
310
|
+
where: spec.where ?? ""
|
|
311
|
+
};
|
|
312
|
+
return fnv1a32(JSON.stringify(normalized));
|
|
313
|
+
}
|
|
314
|
+
function compileFieldExpr(path, fieldToColumn) {
|
|
315
|
+
const col = fieldToColumn[path];
|
|
316
|
+
if (col) return quoteIdent(col);
|
|
317
|
+
if (path === "data") {
|
|
318
|
+
return `json_extract("data", '$')`;
|
|
319
|
+
}
|
|
320
|
+
if (path.startsWith("data.")) {
|
|
321
|
+
const suffix = path.slice(5);
|
|
322
|
+
const parts = suffix.split(".");
|
|
323
|
+
for (const part of parts) {
|
|
324
|
+
if (!JSON_PATH_KEY_RE.test(part)) {
|
|
325
|
+
throw new FiregraphError(
|
|
326
|
+
`IndexSpec data path "${path}" has invalid component "${part}". Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,
|
|
327
|
+
"INVALID_INDEX"
|
|
328
|
+
);
|
|
329
|
+
}
|
|
370
330
|
}
|
|
371
|
-
|
|
331
|
+
return `json_extract("data", '$.${suffix}')`;
|
|
372
332
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
);
|
|
383
|
-
}
|
|
333
|
+
throw new FiregraphError(
|
|
334
|
+
`IndexSpec field "${path}" is not a known firegraph field. Use a top-level field (aType, aUid, axbType, bType, bUid, createdAt, updatedAt, v) or a dotted data path like 'data.status'.`,
|
|
335
|
+
"INVALID_INDEX"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
function buildIndexDDL(spec, options) {
|
|
339
|
+
const { table, fieldToColumn, leadingColumns = [] } = options;
|
|
340
|
+
if (!spec.fields || spec.fields.length === 0) {
|
|
341
|
+
throw new FiregraphError("IndexSpec.fields must be a non-empty array", "INVALID_INDEX");
|
|
384
342
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
343
|
+
const normalized = normalizeFields(spec.fields);
|
|
344
|
+
const hash = specFingerprint(spec, leadingColumns);
|
|
345
|
+
const indexName = `${table}_idx_${hash}`;
|
|
346
|
+
const cols = [];
|
|
347
|
+
for (const col of leadingColumns) {
|
|
348
|
+
cols.push(quoteIdent(col));
|
|
389
349
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (!entry?.migrations?.length || !entry.schemaVersion) {
|
|
394
|
-
return { record, migrated: false, writeBack: "off" };
|
|
350
|
+
for (const f of normalized) {
|
|
351
|
+
const expr = compileFieldExpr(f.path, fieldToColumn);
|
|
352
|
+
cols.push(f.desc ? `${expr} DESC` : expr);
|
|
395
353
|
}
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
354
|
+
let ddl = `CREATE INDEX IF NOT EXISTS ${quoteIdent(indexName)} ON ${quoteIdent(table)}(${cols.join(", ")})`;
|
|
355
|
+
if (spec.where) {
|
|
356
|
+
ddl += ` WHERE ${spec.where}`;
|
|
399
357
|
}
|
|
400
|
-
|
|
401
|
-
record.data,
|
|
402
|
-
currentVersion,
|
|
403
|
-
entry.schemaVersion,
|
|
404
|
-
entry.migrations
|
|
405
|
-
);
|
|
406
|
-
const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
|
|
407
|
-
return {
|
|
408
|
-
record: { ...record, data: migratedData, v: entry.schemaVersion },
|
|
409
|
-
migrated: true,
|
|
410
|
-
writeBack
|
|
411
|
-
};
|
|
358
|
+
return ddl;
|
|
412
359
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
)
|
|
360
|
+
function dedupeIndexSpecs(specs, leadingColumns = []) {
|
|
361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
362
|
+
const out = [];
|
|
363
|
+
for (const spec of specs) {
|
|
364
|
+
const fp = specFingerprint(spec, leadingColumns);
|
|
365
|
+
if (seen.has(fp)) continue;
|
|
366
|
+
seen.add(fp);
|
|
367
|
+
out.push(spec);
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
417
370
|
}
|
|
418
371
|
|
|
419
|
-
// src/
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
372
|
+
// src/cloudflare/schema.ts
|
|
373
|
+
var DO_FIELD_TO_COLUMN = {
|
|
374
|
+
aType: "a_type",
|
|
375
|
+
aUid: "a_uid",
|
|
376
|
+
axbType: "axb_type",
|
|
377
|
+
bType: "b_type",
|
|
378
|
+
bUid: "b_uid",
|
|
379
|
+
v: "v",
|
|
380
|
+
createdAt: "created_at",
|
|
381
|
+
updatedAt: "updated_at"
|
|
382
|
+
};
|
|
383
|
+
var IDENT_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
384
|
+
function validateDOTableName(name) {
|
|
385
|
+
if (!IDENT_RE2.test(name)) {
|
|
386
|
+
throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
387
|
+
}
|
|
426
388
|
}
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
return
|
|
389
|
+
function quoteDOIdent(name) {
|
|
390
|
+
validateDOTableName(name);
|
|
391
|
+
return `"${name}"`;
|
|
430
392
|
}
|
|
431
|
-
function
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
393
|
+
function buildDOSchemaStatements(table, options = {}) {
|
|
394
|
+
const t = quoteDOIdent(table);
|
|
395
|
+
const statements = [
|
|
396
|
+
`CREATE TABLE IF NOT EXISTS ${t} (
|
|
397
|
+
doc_id TEXT NOT NULL PRIMARY KEY,
|
|
398
|
+
a_type TEXT NOT NULL,
|
|
399
|
+
a_uid TEXT NOT NULL,
|
|
400
|
+
axb_type TEXT NOT NULL,
|
|
401
|
+
b_type TEXT NOT NULL,
|
|
402
|
+
b_uid TEXT NOT NULL,
|
|
403
|
+
data TEXT NOT NULL,
|
|
404
|
+
v INTEGER,
|
|
405
|
+
created_at INTEGER NOT NULL,
|
|
406
|
+
updated_at INTEGER NOT NULL
|
|
407
|
+
)`
|
|
408
|
+
];
|
|
409
|
+
const core = options.coreIndexes ?? [...DEFAULT_CORE_INDEXES];
|
|
410
|
+
const fromRegistry = options.registry?.entries().flatMap((e) => e.indexes ?? []) ?? [];
|
|
411
|
+
const deduped = dedupeIndexSpecs([...core, ...fromRegistry]);
|
|
412
|
+
for (const spec of deduped) {
|
|
413
|
+
statements.push(buildIndexDDL(spec, { table, fieldToColumn: DO_FIELD_TO_COLUMN }));
|
|
448
414
|
}
|
|
449
|
-
return
|
|
415
|
+
return statements;
|
|
450
416
|
}
|
|
451
417
|
|
|
452
|
-
// src/
|
|
453
|
-
function
|
|
454
|
-
|
|
418
|
+
// src/cloudflare/sql.ts
|
|
419
|
+
function compileFieldRef(field) {
|
|
420
|
+
const column = DO_FIELD_TO_COLUMN[field];
|
|
421
|
+
if (column) {
|
|
422
|
+
return { expr: quoteDOIdent(column) };
|
|
423
|
+
}
|
|
424
|
+
if (field.startsWith("data.")) {
|
|
425
|
+
const suffix = field.slice(5);
|
|
426
|
+
for (const part of suffix.split(".")) {
|
|
427
|
+
validateJsonPathKey(part);
|
|
428
|
+
}
|
|
429
|
+
return { expr: `json_extract("data", '$.${suffix}')` };
|
|
430
|
+
}
|
|
431
|
+
if (field === "data") {
|
|
432
|
+
return { expr: `json_extract("data", '$')` };
|
|
433
|
+
}
|
|
434
|
+
throw new FiregraphError(
|
|
435
|
+
`DO SQLite backend cannot resolve filter field: ${field}`,
|
|
436
|
+
"INVALID_QUERY"
|
|
437
|
+
);
|
|
455
438
|
}
|
|
456
|
-
|
|
457
|
-
|
|
439
|
+
var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
440
|
+
"Timestamp",
|
|
441
|
+
"GeoPoint",
|
|
442
|
+
"VectorValue",
|
|
443
|
+
"DocumentReference",
|
|
444
|
+
"FieldValue"
|
|
445
|
+
]);
|
|
446
|
+
function isFirestoreSpecialType(value) {
|
|
447
|
+
const ctorName = value.constructor?.name;
|
|
448
|
+
if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
|
|
449
|
+
return null;
|
|
458
450
|
}
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
entries = input;
|
|
464
|
-
} else {
|
|
465
|
-
entries = discoveryToEntries(input);
|
|
451
|
+
function bindValue(value) {
|
|
452
|
+
if (value === null || value === void 0) return null;
|
|
453
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
454
|
+
return value;
|
|
466
455
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
456
|
+
if (value instanceof Date) return value.getTime();
|
|
457
|
+
if (typeof value === "object") {
|
|
458
|
+
const firestoreType = isFirestoreSpecialType(value);
|
|
459
|
+
if (firestoreType) {
|
|
460
|
+
throw new FiregraphError(
|
|
461
|
+
`DO SQLite backend cannot bind a Firestore ${firestoreType} value \u2014 JSON serialization would silently drop fields and the resulting bind would never match a stored row. Convert to a primitive (e.g. \`ts.toMillis()\` for Timestamp) before filtering or updating.`,
|
|
462
|
+
"INVALID_QUERY"
|
|
472
463
|
);
|
|
473
464
|
}
|
|
474
|
-
|
|
475
|
-
const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
|
|
476
|
-
validateMigrationChain(entry.migrations, label);
|
|
477
|
-
entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
|
|
478
|
-
} else {
|
|
479
|
-
entry.schemaVersion = void 0;
|
|
480
|
-
}
|
|
481
|
-
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
482
|
-
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
483
|
-
map.set(key, { entry, validate: validator });
|
|
465
|
+
return JSON.stringify(value);
|
|
484
466
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
467
|
+
return String(value);
|
|
468
|
+
}
|
|
469
|
+
var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
470
|
+
function validateJsonPathKey(key) {
|
|
471
|
+
if (key.length === 0) {
|
|
472
|
+
throw new FiregraphError(
|
|
473
|
+
"DO SQLite backend: empty JSON path component is not allowed",
|
|
474
|
+
"INVALID_QUERY"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
if (!JSON_PATH_KEY_RE2.test(key)) {
|
|
478
|
+
throw new FiregraphError(
|
|
479
|
+
`DO SQLite backend: data field path component "${key}" is not a safe JSON-path identifier. Allowed pattern: /^[A-Za-z_][A-Za-z0-9_-]*$/. Use replaceData (full-data overwrite) for keys with reserved characters (whitespace, dots, brackets, quotes, etc.).`,
|
|
480
|
+
"INVALID_QUERY"
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function compileFilter(filter, params) {
|
|
485
|
+
const { expr } = compileFieldRef(filter.field);
|
|
486
|
+
switch (filter.op) {
|
|
487
|
+
case "==":
|
|
488
|
+
params.push(bindValue(filter.value));
|
|
489
|
+
return `${expr} = ?`;
|
|
490
|
+
case "!=":
|
|
491
|
+
params.push(bindValue(filter.value));
|
|
492
|
+
return `${expr} != ?`;
|
|
493
|
+
case "<":
|
|
494
|
+
params.push(bindValue(filter.value));
|
|
495
|
+
return `${expr} < ?`;
|
|
496
|
+
case "<=":
|
|
497
|
+
params.push(bindValue(filter.value));
|
|
498
|
+
return `${expr} <= ?`;
|
|
499
|
+
case ">":
|
|
500
|
+
params.push(bindValue(filter.value));
|
|
501
|
+
return `${expr} > ?`;
|
|
502
|
+
case ">=":
|
|
503
|
+
params.push(bindValue(filter.value));
|
|
504
|
+
return `${expr} >= ?`;
|
|
505
|
+
case "in": {
|
|
506
|
+
const values = asArray(filter.value, "in");
|
|
507
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
508
|
+
for (const v of values) params.push(bindValue(v));
|
|
509
|
+
return `${expr} IN (${placeholders})`;
|
|
510
|
+
}
|
|
511
|
+
case "not-in": {
|
|
512
|
+
const values = asArray(filter.value, "not-in");
|
|
513
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
514
|
+
for (const v of values) params.push(bindValue(v));
|
|
515
|
+
return `${expr} NOT IN (${placeholders})`;
|
|
516
|
+
}
|
|
517
|
+
case "array-contains": {
|
|
518
|
+
params.push(bindValue(filter.value));
|
|
519
|
+
return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value = ?)`;
|
|
520
|
+
}
|
|
521
|
+
case "array-contains-any": {
|
|
522
|
+
const values = asArray(filter.value, "array-contains-any");
|
|
523
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
524
|
+
for (const v of values) params.push(bindValue(v));
|
|
525
|
+
return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value IN (${placeholders}))`;
|
|
493
526
|
}
|
|
527
|
+
default:
|
|
528
|
+
throw new FiregraphError(
|
|
529
|
+
`DO SQLite backend does not support filter operator: ${String(filter.op)}`,
|
|
530
|
+
"INVALID_QUERY"
|
|
531
|
+
);
|
|
494
532
|
}
|
|
495
|
-
|
|
496
|
-
|
|
533
|
+
}
|
|
534
|
+
function asArray(value, op) {
|
|
535
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
536
|
+
throw new FiregraphError(`Operator "${op}" requires a non-empty array value`, "INVALID_QUERY");
|
|
537
|
+
}
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
function compileOrderBy(options, _params) {
|
|
541
|
+
if (!options?.orderBy) return "";
|
|
542
|
+
const { field, direction } = options.orderBy;
|
|
543
|
+
const { expr } = compileFieldRef(field);
|
|
544
|
+
const dir = direction === "desc" ? "DESC" : "ASC";
|
|
545
|
+
return ` ORDER BY ${expr} ${dir}`;
|
|
546
|
+
}
|
|
547
|
+
function compileLimit(options, params) {
|
|
548
|
+
if (options?.limit === void 0) return "";
|
|
549
|
+
params.push(options.limit);
|
|
550
|
+
return ` LIMIT ?`;
|
|
551
|
+
}
|
|
552
|
+
function compileDOSelect(table, filters, options) {
|
|
553
|
+
const params = [];
|
|
554
|
+
const conditions = [];
|
|
555
|
+
for (const f of filters) {
|
|
556
|
+
conditions.push(compileFilter(f, params));
|
|
497
557
|
}
|
|
558
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
559
|
+
let sql = `SELECT * FROM ${quoteDOIdent(table)}${where}`;
|
|
560
|
+
sql += compileOrderBy(options, params);
|
|
561
|
+
sql += compileLimit(options, params);
|
|
562
|
+
return { sql, params };
|
|
563
|
+
}
|
|
564
|
+
function compileDOSelectByDocId(table, docId) {
|
|
498
565
|
return {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
566
|
+
sql: `SELECT * FROM ${quoteDOIdent(table)} WHERE "doc_id" = ? LIMIT 1`,
|
|
567
|
+
params: [docId]
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function compileDOSet(table, docId, record, nowMillis) {
|
|
571
|
+
const sql = `INSERT OR REPLACE INTO ${quoteDOIdent(table)} (
|
|
572
|
+
doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
|
|
573
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
574
|
+
const params = [
|
|
575
|
+
docId,
|
|
576
|
+
record.aType,
|
|
577
|
+
record.aUid,
|
|
578
|
+
record.axbType,
|
|
579
|
+
record.bType,
|
|
580
|
+
record.bUid,
|
|
581
|
+
JSON.stringify(record.data ?? {}),
|
|
582
|
+
record.v ?? null,
|
|
583
|
+
nowMillis,
|
|
584
|
+
nowMillis
|
|
585
|
+
];
|
|
586
|
+
return { sql, params };
|
|
587
|
+
}
|
|
588
|
+
function compileDOUpdate(table, docId, update, nowMillis) {
|
|
589
|
+
const setClauses = [];
|
|
590
|
+
const params = [];
|
|
591
|
+
if (update.replaceData) {
|
|
592
|
+
setClauses.push(`"data" = ?`);
|
|
593
|
+
params.push(JSON.stringify(update.replaceData));
|
|
594
|
+
} else if (update.dataFields && Object.keys(update.dataFields).length > 0) {
|
|
595
|
+
const entries = Object.entries(update.dataFields);
|
|
596
|
+
const pathArgs = entries.map(() => `?, ?`).join(", ");
|
|
597
|
+
setClauses.push(`"data" = json_set(COALESCE("data", '{}'), ${pathArgs})`);
|
|
598
|
+
for (const [k, v] of entries) {
|
|
599
|
+
validateJsonPathKey(k);
|
|
600
|
+
params.push(`$.${k}`);
|
|
601
|
+
params.push(bindValue(v));
|
|
529
602
|
}
|
|
603
|
+
}
|
|
604
|
+
if (update.v !== void 0) {
|
|
605
|
+
setClauses.push(`"v" = ?`);
|
|
606
|
+
params.push(update.v);
|
|
607
|
+
}
|
|
608
|
+
setClauses.push(`"updated_at" = ?`);
|
|
609
|
+
params.push(nowMillis);
|
|
610
|
+
params.push(docId);
|
|
611
|
+
return {
|
|
612
|
+
sql: `UPDATE ${quoteDOIdent(table)} SET ${setClauses.join(", ")} WHERE "doc_id" = ?`,
|
|
613
|
+
params
|
|
530
614
|
};
|
|
531
615
|
}
|
|
532
|
-
function
|
|
533
|
-
const baseKeys = new Set(base.entries().map(tripleKeyFor));
|
|
616
|
+
function compileDODelete(table, docId) {
|
|
534
617
|
return {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
618
|
+
sql: `DELETE FROM ${quoteDOIdent(table)} WHERE "doc_id" = ?`,
|
|
619
|
+
params: [docId]
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function compileDODeleteAll(table) {
|
|
623
|
+
return {
|
|
624
|
+
sql: `DELETE FROM ${quoteDOIdent(table)}`,
|
|
625
|
+
params: []
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function rowToDORecord(row) {
|
|
629
|
+
const dataString = row.data;
|
|
630
|
+
const data = dataString ? JSON.parse(dataString) : {};
|
|
631
|
+
const createdAtMs = toMillis(row.created_at);
|
|
632
|
+
const updatedAtMs = toMillis(row.updated_at);
|
|
633
|
+
const record = {
|
|
634
|
+
aType: row.a_type,
|
|
635
|
+
aUid: row.a_uid,
|
|
636
|
+
axbType: row.axb_type,
|
|
637
|
+
bType: row.b_type,
|
|
638
|
+
bUid: row.b_uid,
|
|
639
|
+
data,
|
|
640
|
+
createdAtMs,
|
|
641
|
+
updatedAtMs
|
|
642
|
+
};
|
|
643
|
+
if (row.v !== null && row.v !== void 0) {
|
|
644
|
+
record.v = Number(row.v);
|
|
645
|
+
}
|
|
646
|
+
return record;
|
|
647
|
+
}
|
|
648
|
+
function hydrateDORecord(wire) {
|
|
649
|
+
const record = {
|
|
650
|
+
aType: wire.aType,
|
|
651
|
+
aUid: wire.aUid,
|
|
652
|
+
axbType: wire.axbType,
|
|
653
|
+
bType: wire.bType,
|
|
654
|
+
bUid: wire.bUid,
|
|
655
|
+
data: wire.data,
|
|
656
|
+
createdAt: GraphTimestampImpl.fromMillis(wire.createdAtMs),
|
|
657
|
+
updatedAt: GraphTimestampImpl.fromMillis(wire.updatedAtMs)
|
|
658
|
+
};
|
|
659
|
+
if (wire.v !== void 0) {
|
|
660
|
+
record.v = wire.v;
|
|
661
|
+
}
|
|
662
|
+
return record;
|
|
663
|
+
}
|
|
664
|
+
function toMillis(value) {
|
|
665
|
+
if (typeof value === "number") return value;
|
|
666
|
+
if (typeof value === "bigint") return Number(value);
|
|
667
|
+
if (typeof value === "string") {
|
|
668
|
+
const n = Number(value);
|
|
669
|
+
if (Number.isFinite(n)) return n;
|
|
670
|
+
}
|
|
671
|
+
throw new FiregraphError(
|
|
672
|
+
`DO SQLite row has non-numeric timestamp column: ${typeof value} (${String(value)})`,
|
|
673
|
+
"INVALID_QUERY"
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/cloudflare/backend.ts
|
|
678
|
+
function validateSegment(value, label) {
|
|
679
|
+
if (!value || value.includes("/")) {
|
|
680
|
+
throw new FiregraphError(
|
|
681
|
+
`Invalid ${label} for subgraph: "${value}". Must be non-empty and not contain "/".`,
|
|
682
|
+
"INVALID_SUBGRAPH"
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function transactionsUnsupported() {
|
|
687
|
+
return new FiregraphError(
|
|
688
|
+
"Interactive transactions are not supported by the Cloudflare DO backend. Use `batch()` for atomic multi-write commits, or restructure the read-then-conditional-write as an explicit read \u2192 decide \u2192 batch sequence.",
|
|
689
|
+
"UNSUPPORTED_OPERATION"
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
var DORPCBatchBackend = class {
|
|
693
|
+
constructor(getStub) {
|
|
694
|
+
this.getStub = getStub;
|
|
695
|
+
}
|
|
696
|
+
ops = [];
|
|
697
|
+
setDoc(docId, record) {
|
|
698
|
+
this.ops.push({ kind: "set", docId, record });
|
|
699
|
+
}
|
|
700
|
+
updateDoc(docId, update) {
|
|
701
|
+
this.ops.push({ kind: "update", docId, update });
|
|
702
|
+
}
|
|
703
|
+
deleteDoc(docId) {
|
|
704
|
+
this.ops.push({ kind: "delete", docId });
|
|
705
|
+
}
|
|
706
|
+
async commit() {
|
|
707
|
+
if (this.ops.length === 0) return;
|
|
708
|
+
const ops = this.ops.slice();
|
|
709
|
+
this.ops.length = 0;
|
|
710
|
+
await this.getStub()._fgBatch(ops);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
var DORPCBackend = class _DORPCBackend {
|
|
714
|
+
collectionPath = "firegraph";
|
|
715
|
+
scopePath;
|
|
716
|
+
/** @internal */
|
|
717
|
+
storageKey;
|
|
718
|
+
/** @internal */
|
|
719
|
+
namespace;
|
|
720
|
+
registryAccessor;
|
|
721
|
+
/** @internal — see `DORPCBackendOptions.makeSiblingClient` for the union-type rationale. */
|
|
722
|
+
makeSiblingClient;
|
|
723
|
+
cachedStub = null;
|
|
724
|
+
constructor(namespace, options) {
|
|
725
|
+
this.namespace = namespace;
|
|
726
|
+
this.scopePath = options.scopePath ?? "";
|
|
727
|
+
this.storageKey = options.storageKey;
|
|
728
|
+
this.registryAccessor = options.registryAccessor;
|
|
729
|
+
this.makeSiblingClient = options.makeSiblingClient;
|
|
730
|
+
}
|
|
731
|
+
get stub() {
|
|
732
|
+
if (!this.cachedStub) {
|
|
733
|
+
const id = this.namespace.idFromName(this.storageKey);
|
|
734
|
+
this.cachedStub = this.namespace.get(id);
|
|
735
|
+
}
|
|
736
|
+
return this.cachedStub;
|
|
737
|
+
}
|
|
738
|
+
// --- Reads ---
|
|
739
|
+
async getDoc(docId) {
|
|
740
|
+
const wire = await this.stub._fgGetDoc(docId);
|
|
741
|
+
return wire ? hydrateDORecord(wire) : null;
|
|
742
|
+
}
|
|
743
|
+
async query(filters, options) {
|
|
744
|
+
const wires = await this.stub._fgQuery(filters, options);
|
|
745
|
+
return wires.map(hydrateDORecord);
|
|
746
|
+
}
|
|
747
|
+
// --- Writes ---
|
|
748
|
+
async setDoc(docId, record) {
|
|
749
|
+
return this.stub._fgSetDoc(docId, record);
|
|
750
|
+
}
|
|
751
|
+
async updateDoc(docId, update) {
|
|
752
|
+
return this.stub._fgUpdateDoc(docId, update);
|
|
753
|
+
}
|
|
754
|
+
async deleteDoc(docId) {
|
|
755
|
+
return this.stub._fgDeleteDoc(docId);
|
|
756
|
+
}
|
|
757
|
+
// --- Transactions / batches ---
|
|
758
|
+
async runTransaction(_fn) {
|
|
759
|
+
void _fn;
|
|
760
|
+
throw transactionsUnsupported();
|
|
761
|
+
}
|
|
762
|
+
createBatch() {
|
|
763
|
+
return new DORPCBatchBackend(() => this.stub);
|
|
764
|
+
}
|
|
765
|
+
// --- Subgraphs ---
|
|
766
|
+
subgraph(parentNodeUid, name) {
|
|
767
|
+
validateSegment(parentNodeUid, "parentNodeUid");
|
|
768
|
+
validateSegment(name, "subgraph name");
|
|
769
|
+
const newStorageKey = `${this.storageKey}/${parentNodeUid}/${name}`;
|
|
770
|
+
const newScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
771
|
+
return new _DORPCBackend(this.namespace, {
|
|
772
|
+
scopePath: newScopePath,
|
|
773
|
+
storageKey: newStorageKey,
|
|
774
|
+
// Subgraph backends share the same live registry accessor so a cascade
|
|
775
|
+
// invoked on a subgraph client still fans out correctly. The sibling
|
|
776
|
+
// factory is also carried forward so `createSiblingClient` works from
|
|
777
|
+
// any subgraph client in the chain.
|
|
778
|
+
registryAccessor: this.registryAccessor,
|
|
779
|
+
makeSiblingClient: this.makeSiblingClient
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
// --- Cascade & bulk ---
|
|
783
|
+
async removeNodeCascade(uid, reader, options) {
|
|
784
|
+
const shouldDeleteSubgraphs = options?.deleteSubcollections !== false;
|
|
785
|
+
const registry = this.registryAccessor?.();
|
|
786
|
+
if (shouldDeleteSubgraphs && registry) {
|
|
787
|
+
const node = await reader.getNode(uid);
|
|
788
|
+
if (node) {
|
|
789
|
+
const topology = registry.getSubgraphTopology(node.aType);
|
|
790
|
+
for (const entry of topology) {
|
|
791
|
+
const target = entry.targetGraph;
|
|
792
|
+
const childBackend = this.subgraph(uid, target);
|
|
793
|
+
await childBackend.destroyRecursively(registry);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return this.stub._fgRemoveNodeCascade(uid);
|
|
798
|
+
}
|
|
799
|
+
async bulkRemoveEdges(params, _reader, options) {
|
|
800
|
+
void _reader;
|
|
801
|
+
return this.stub._fgBulkRemoveEdges(params, options);
|
|
802
|
+
}
|
|
803
|
+
// --- Cross-scope queries ---
|
|
804
|
+
//
|
|
805
|
+
// `findEdgesGlobal` is deliberately NOT defined on this class. The
|
|
806
|
+
// GraphClient checks for its presence before running query planning and
|
|
807
|
+
// throws `UNSUPPORTED_OPERATION` when absent, giving the caller an
|
|
808
|
+
// immediate, accurate error. Defining the method with a throwing body
|
|
809
|
+
// would only surface the same error AFTER `checkQuerySafety` had already
|
|
810
|
+
// fired — and for scan-unsafe calls that results in a misleading
|
|
811
|
+
// `QuerySafetyError` ("add filters like aUid+axbType") when no filter
|
|
812
|
+
// combination would actually make the call work on this backend. See the
|
|
813
|
+
// "What's not supported" section in `createDOClient` for the design
|
|
814
|
+
// rationale (no collection-group index across DOs).
|
|
815
|
+
// --- Destroy helpers ---
|
|
816
|
+
/**
|
|
817
|
+
* Wipe this DO's storage. The DO itself can't be deleted — its ID
|
|
818
|
+
* persists forever — but its rows can be emptied, which is what the
|
|
819
|
+
* cascade walk does on every descendant subgraph DO.
|
|
820
|
+
*
|
|
821
|
+
* Exposed on the concrete class (not `StorageBackend`) so generic
|
|
822
|
+
* backend code doesn't reach for it.
|
|
823
|
+
*/
|
|
824
|
+
async destroy() {
|
|
825
|
+
await this.stub._fgDestroy();
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Tear down every descendant subgraph DO, then wipe this DO's own rows.
|
|
829
|
+
*
|
|
830
|
+
* Invoked by cross-DO cascade: for each node in this DO we enumerate the
|
|
831
|
+
* subgraph topology and recurse into child DOs depth-first before
|
|
832
|
+
* wiping the current DO. The current DO's own rows are destroyed last so
|
|
833
|
+
* that a partial failure mid-recursion leaves the caller's reader able
|
|
834
|
+
* to discover what's still present.
|
|
835
|
+
*
|
|
836
|
+
* @internal
|
|
837
|
+
*/
|
|
838
|
+
async destroyRecursively(registry) {
|
|
839
|
+
const nodes = await this.query([{ field: "axbType", op: "==", value: NODE_RELATION }]);
|
|
840
|
+
for (const node of nodes) {
|
|
841
|
+
const topology = registry.getSubgraphTopology(node.aType);
|
|
842
|
+
for (const entry of topology) {
|
|
843
|
+
const target = entry.targetGraph;
|
|
844
|
+
const childBackend = this.subgraph(node.aUid, target);
|
|
845
|
+
await childBackend.destroyRecursively(registry);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
await this.destroy();
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// src/docid.ts
|
|
853
|
+
var import_node_crypto = require("crypto");
|
|
854
|
+
function computeNodeDocId(uid) {
|
|
855
|
+
return uid;
|
|
856
|
+
}
|
|
857
|
+
function computeEdgeDocId(aUid, axbType, bUid) {
|
|
858
|
+
const composite = `${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
859
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(composite).digest("hex");
|
|
860
|
+
const shard = hash[0];
|
|
861
|
+
return `${shard}${SHARD_SEPARATOR}${aUid}${SHARD_SEPARATOR}${axbType}${SHARD_SEPARATOR}${bUid}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/batch.ts
|
|
865
|
+
function buildWritableNodeRecord(aType, uid, data) {
|
|
866
|
+
return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
|
|
867
|
+
}
|
|
868
|
+
function buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
|
|
869
|
+
return { aType, aUid, axbType, bType, bUid, data };
|
|
870
|
+
}
|
|
871
|
+
var GraphBatchImpl = class {
|
|
872
|
+
constructor(backend, registry, scopePath = "") {
|
|
873
|
+
this.backend = backend;
|
|
874
|
+
this.registry = registry;
|
|
875
|
+
this.scopePath = scopePath;
|
|
876
|
+
}
|
|
877
|
+
async putNode(aType, uid, data) {
|
|
878
|
+
if (this.registry) {
|
|
879
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
880
|
+
}
|
|
881
|
+
const docId = computeNodeDocId(uid);
|
|
882
|
+
const record = buildWritableNodeRecord(aType, uid, data);
|
|
883
|
+
if (this.registry) {
|
|
884
|
+
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
885
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
886
|
+
record.v = entry.schemaVersion;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
this.backend.setDoc(docId, record);
|
|
890
|
+
}
|
|
891
|
+
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
892
|
+
if (this.registry) {
|
|
893
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
894
|
+
}
|
|
895
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
896
|
+
const record = buildWritableEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
897
|
+
if (this.registry) {
|
|
898
|
+
const entry = this.registry.lookup(aType, axbType, bType);
|
|
899
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
900
|
+
record.v = entry.schemaVersion;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
this.backend.setDoc(docId, record);
|
|
904
|
+
}
|
|
905
|
+
async updateNode(uid, data) {
|
|
906
|
+
const docId = computeNodeDocId(uid);
|
|
907
|
+
this.backend.updateDoc(docId, { dataFields: data });
|
|
908
|
+
}
|
|
909
|
+
async removeNode(uid) {
|
|
910
|
+
const docId = computeNodeDocId(uid);
|
|
911
|
+
this.backend.deleteDoc(docId);
|
|
912
|
+
}
|
|
913
|
+
async removeEdge(aUid, axbType, bUid) {
|
|
914
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
915
|
+
this.backend.deleteDoc(docId);
|
|
916
|
+
}
|
|
917
|
+
async commit() {
|
|
918
|
+
await this.backend.commit();
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/dynamic-registry.ts
|
|
923
|
+
var import_node_crypto3 = require("crypto");
|
|
924
|
+
|
|
925
|
+
// src/json-schema.ts
|
|
926
|
+
var import_ajv = __toESM(require("ajv"), 1);
|
|
927
|
+
var import_ajv_formats = __toESM(require("ajv-formats"), 1);
|
|
928
|
+
var ajv = new import_ajv.default({ allErrors: true, strict: false });
|
|
929
|
+
(0, import_ajv_formats.default)(ajv);
|
|
930
|
+
function compileSchema(schema, label) {
|
|
931
|
+
const validate = ajv.compile(schema);
|
|
932
|
+
return (data) => {
|
|
933
|
+
if (!validate(data)) {
|
|
934
|
+
const errors = validate.errors ?? [];
|
|
935
|
+
const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
|
|
936
|
+
throw new ValidationError(
|
|
937
|
+
`Data validation failed${label ? " for " + label : ""}: ${messages}`,
|
|
938
|
+
errors
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/migration.ts
|
|
945
|
+
async function applyMigrationChain(data, currentVersion, targetVersion, migrations) {
|
|
946
|
+
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
947
|
+
let result = { ...data };
|
|
948
|
+
let version = currentVersion;
|
|
949
|
+
for (const step of sorted) {
|
|
950
|
+
if (step.fromVersion === version) {
|
|
951
|
+
try {
|
|
952
|
+
result = await step.up(result);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
if (err instanceof MigrationError) throw err;
|
|
955
|
+
throw new MigrationError(
|
|
956
|
+
`Migration from v${step.fromVersion} to v${step.toVersion} failed: ${err.message}`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
if (!result || typeof result !== "object") {
|
|
960
|
+
throw new MigrationError(
|
|
961
|
+
`Migration from v${step.fromVersion} to v${step.toVersion} returned invalid data (expected object)`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
version = step.toVersion;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (version !== targetVersion) {
|
|
968
|
+
throw new MigrationError(
|
|
969
|
+
`Incomplete migration chain: reached v${version} but target is v${targetVersion}`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
function validateMigrationChain(migrations, label) {
|
|
975
|
+
if (migrations.length === 0) return;
|
|
976
|
+
const seen = /* @__PURE__ */ new Set();
|
|
977
|
+
for (const step of migrations) {
|
|
978
|
+
if (step.toVersion <= step.fromVersion) {
|
|
979
|
+
throw new MigrationError(
|
|
980
|
+
`${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (seen.has(step.fromVersion)) {
|
|
984
|
+
throw new MigrationError(
|
|
985
|
+
`${label}: duplicate migration step for fromVersion ${step.fromVersion}`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
seen.add(step.fromVersion);
|
|
989
|
+
}
|
|
990
|
+
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
991
|
+
const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
|
|
992
|
+
let version = 0;
|
|
993
|
+
for (const step of sorted) {
|
|
994
|
+
if (step.fromVersion === version) {
|
|
995
|
+
version = step.toVersion;
|
|
996
|
+
} else if (step.fromVersion > version) {
|
|
997
|
+
throw new MigrationError(
|
|
998
|
+
`${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (version !== targetVersion) {
|
|
1003
|
+
throw new MigrationError(
|
|
1004
|
+
`${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async function migrateRecord(record, registry, globalWriteBack = "off") {
|
|
1009
|
+
const entry = registry.lookup(record.aType, record.axbType, record.bType);
|
|
1010
|
+
if (!entry?.migrations?.length || !entry.schemaVersion) {
|
|
1011
|
+
return { record, migrated: false, writeBack: "off" };
|
|
1012
|
+
}
|
|
1013
|
+
const currentVersion = record.v ?? 0;
|
|
1014
|
+
if (currentVersion >= entry.schemaVersion) {
|
|
1015
|
+
return { record, migrated: false, writeBack: "off" };
|
|
1016
|
+
}
|
|
1017
|
+
const migratedData = await applyMigrationChain(
|
|
1018
|
+
record.data,
|
|
1019
|
+
currentVersion,
|
|
1020
|
+
entry.schemaVersion,
|
|
1021
|
+
entry.migrations
|
|
1022
|
+
);
|
|
1023
|
+
const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
|
|
1024
|
+
return {
|
|
1025
|
+
record: { ...record, data: migratedData, v: entry.schemaVersion },
|
|
1026
|
+
migrated: true,
|
|
1027
|
+
writeBack
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
async function migrateRecords(records, registry, globalWriteBack = "off") {
|
|
1031
|
+
return Promise.all(records.map((r) => migrateRecord(r, registry, globalWriteBack)));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/scope.ts
|
|
1035
|
+
function matchScope(scopePath, pattern) {
|
|
1036
|
+
if (pattern === "root") return scopePath === "";
|
|
1037
|
+
if (pattern === "**") return true;
|
|
1038
|
+
const pathSegments = scopePath === "" ? [] : scopePath.split("/");
|
|
1039
|
+
const patternSegments = pattern.split("/");
|
|
1040
|
+
return matchSegments(pathSegments, 0, patternSegments, 0);
|
|
1041
|
+
}
|
|
1042
|
+
function matchScopeAny(scopePath, patterns) {
|
|
1043
|
+
if (!patterns || patterns.length === 0) return true;
|
|
1044
|
+
return patterns.some((p) => matchScope(scopePath, p));
|
|
1045
|
+
}
|
|
1046
|
+
function matchSegments(path, pi, pattern, qi) {
|
|
1047
|
+
if (pi === path.length && qi === pattern.length) return true;
|
|
1048
|
+
if (qi === pattern.length) return false;
|
|
1049
|
+
const seg = pattern[qi];
|
|
1050
|
+
if (seg === "**") {
|
|
1051
|
+
if (qi === pattern.length - 1) return true;
|
|
1052
|
+
for (let skip = 0; skip <= path.length - pi; skip++) {
|
|
1053
|
+
if (matchSegments(path, pi + skip, pattern, qi + 1)) return true;
|
|
1054
|
+
}
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
if (pi === path.length) return false;
|
|
1058
|
+
if (seg === "*") {
|
|
1059
|
+
return matchSegments(path, pi + 1, pattern, qi + 1);
|
|
1060
|
+
}
|
|
1061
|
+
if (path[pi] === seg) {
|
|
1062
|
+
return matchSegments(path, pi + 1, pattern, qi + 1);
|
|
1063
|
+
}
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/registry.ts
|
|
1068
|
+
function tripleKey(aType, axbType, bType) {
|
|
1069
|
+
return `${aType}:${axbType}:${bType}`;
|
|
1070
|
+
}
|
|
1071
|
+
function tripleKeyFor(e) {
|
|
1072
|
+
return tripleKey(e.aType, e.axbType, e.bType);
|
|
1073
|
+
}
|
|
1074
|
+
function createRegistry(input) {
|
|
1075
|
+
const map = /* @__PURE__ */ new Map();
|
|
1076
|
+
let entries;
|
|
1077
|
+
if (Array.isArray(input)) {
|
|
1078
|
+
entries = input;
|
|
1079
|
+
} else {
|
|
1080
|
+
entries = discoveryToEntries(input);
|
|
1081
|
+
}
|
|
1082
|
+
const entryList = Object.freeze([...entries]);
|
|
1083
|
+
for (const entry of entries) {
|
|
1084
|
+
if (entry.targetGraph && entry.targetGraph.includes("/")) {
|
|
1085
|
+
throw new ValidationError(
|
|
1086
|
+
`Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (entry.migrations?.length) {
|
|
1090
|
+
const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
|
|
1091
|
+
validateMigrationChain(entry.migrations, label);
|
|
1092
|
+
entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
|
|
1093
|
+
} else {
|
|
1094
|
+
entry.schemaVersion = void 0;
|
|
1095
|
+
}
|
|
1096
|
+
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
1097
|
+
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
1098
|
+
map.set(key, { entry, validate: validator });
|
|
1099
|
+
}
|
|
1100
|
+
const axbIndex = /* @__PURE__ */ new Map();
|
|
1101
|
+
const axbBuild = /* @__PURE__ */ new Map();
|
|
1102
|
+
for (const entry of entries) {
|
|
1103
|
+
const existing = axbBuild.get(entry.axbType);
|
|
1104
|
+
if (existing) {
|
|
1105
|
+
existing.push(entry);
|
|
1106
|
+
} else {
|
|
1107
|
+
axbBuild.set(entry.axbType, [entry]);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
for (const [key, arr] of axbBuild) {
|
|
1111
|
+
axbIndex.set(key, Object.freeze(arr));
|
|
1112
|
+
}
|
|
1113
|
+
const topologyIndex = /* @__PURE__ */ new Map();
|
|
1114
|
+
const topologyBuild = /* @__PURE__ */ new Map();
|
|
1115
|
+
const topologySeen = /* @__PURE__ */ new Map();
|
|
1116
|
+
for (const entry of entries) {
|
|
1117
|
+
if (!entry.targetGraph) continue;
|
|
1118
|
+
let seen = topologySeen.get(entry.aType);
|
|
1119
|
+
if (!seen) {
|
|
1120
|
+
seen = /* @__PURE__ */ new Set();
|
|
1121
|
+
topologySeen.set(entry.aType, seen);
|
|
1122
|
+
}
|
|
1123
|
+
if (seen.has(entry.targetGraph)) continue;
|
|
1124
|
+
seen.add(entry.targetGraph);
|
|
1125
|
+
const existing = topologyBuild.get(entry.aType);
|
|
1126
|
+
if (existing) {
|
|
1127
|
+
existing.push(entry);
|
|
1128
|
+
} else {
|
|
1129
|
+
topologyBuild.set(entry.aType, [entry]);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
for (const [key, arr] of topologyBuild) {
|
|
1133
|
+
topologyIndex.set(key, Object.freeze(arr));
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
lookup(aType, axbType, bType) {
|
|
1137
|
+
return map.get(tripleKey(aType, axbType, bType))?.entry;
|
|
1138
|
+
},
|
|
1139
|
+
lookupByAxbType(axbType) {
|
|
1140
|
+
return axbIndex.get(axbType) ?? [];
|
|
1141
|
+
},
|
|
1142
|
+
getSubgraphTopology(aType) {
|
|
1143
|
+
return topologyIndex.get(aType) ?? [];
|
|
1144
|
+
},
|
|
1145
|
+
validate(aType, axbType, bType, data, scopePath) {
|
|
1146
|
+
const rec = map.get(tripleKey(aType, axbType, bType));
|
|
1147
|
+
if (!rec) {
|
|
1148
|
+
throw new RegistryViolationError(aType, axbType, bType);
|
|
1149
|
+
}
|
|
1150
|
+
if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
|
|
1151
|
+
if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
|
|
1152
|
+
throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (rec.validate) {
|
|
1156
|
+
try {
|
|
1157
|
+
rec.validate(data);
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
if (err instanceof ValidationError) throw err;
|
|
1160
|
+
throw new ValidationError(
|
|
1161
|
+
`Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
|
|
1162
|
+
err
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
entries() {
|
|
1168
|
+
return entryList;
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
function createMergedRegistry(base, extension) {
|
|
1173
|
+
const baseKeys = new Set(base.entries().map(tripleKeyFor));
|
|
1174
|
+
return {
|
|
1175
|
+
lookup(aType, axbType, bType) {
|
|
1176
|
+
return base.lookup(aType, axbType, bType) ?? extension.lookup(aType, axbType, bType);
|
|
1177
|
+
},
|
|
1178
|
+
lookupByAxbType(axbType) {
|
|
1179
|
+
const baseResults = base.lookupByAxbType(axbType);
|
|
540
1180
|
const extResults = extension.lookupByAxbType(axbType);
|
|
541
1181
|
if (extResults.length === 0) return baseResults;
|
|
542
1182
|
if (baseResults.length === 0) return extResults;
|
|
@@ -549,6 +1189,21 @@ function createMergedRegistry(base, extension) {
|
|
|
549
1189
|
}
|
|
550
1190
|
return Object.freeze(merged);
|
|
551
1191
|
},
|
|
1192
|
+
getSubgraphTopology(aType) {
|
|
1193
|
+
const baseResults = base.getSubgraphTopology(aType);
|
|
1194
|
+
const extResults = extension.getSubgraphTopology(aType);
|
|
1195
|
+
if (extResults.length === 0) return baseResults;
|
|
1196
|
+
if (baseResults.length === 0) return extResults;
|
|
1197
|
+
const seen = new Set(baseResults.map((e) => e.targetGraph));
|
|
1198
|
+
const merged = [...baseResults];
|
|
1199
|
+
for (const entry of extResults) {
|
|
1200
|
+
if (!seen.has(entry.targetGraph)) {
|
|
1201
|
+
seen.add(entry.targetGraph);
|
|
1202
|
+
merged.push(entry);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return Object.freeze(merged);
|
|
1206
|
+
},
|
|
552
1207
|
validate(aType, axbType, bType, data, scopePath) {
|
|
553
1208
|
if (baseKeys.has(tripleKey(aType, axbType, bType))) {
|
|
554
1209
|
return base.validate(aType, axbType, bType, data, scopePath);
|
|
@@ -581,7 +1236,8 @@ function discoveryToEntries(discovery) {
|
|
|
581
1236
|
subtitleField: entity.subtitleField,
|
|
582
1237
|
allowedIn: entity.allowedIn,
|
|
583
1238
|
migrations: entity.migrations,
|
|
584
|
-
migrationWriteBack: entity.migrationWriteBack
|
|
1239
|
+
migrationWriteBack: entity.migrationWriteBack,
|
|
1240
|
+
indexes: entity.indexes
|
|
585
1241
|
});
|
|
586
1242
|
}
|
|
587
1243
|
for (const [axbType, entity] of discovery.edges) {
|
|
@@ -609,7 +1265,8 @@ function discoveryToEntries(discovery) {
|
|
|
609
1265
|
allowedIn: entity.allowedIn,
|
|
610
1266
|
targetGraph: resolvedTargetGraph,
|
|
611
1267
|
migrations: entity.migrations,
|
|
612
|
-
migrationWriteBack: entity.migrationWriteBack
|
|
1268
|
+
migrationWriteBack: entity.migrationWriteBack,
|
|
1269
|
+
indexes: entity.indexes
|
|
613
1270
|
});
|
|
614
1271
|
}
|
|
615
1272
|
}
|
|
@@ -1132,246 +1789,22 @@ var GraphTransactionImpl = class {
|
|
|
1132
1789
|
v: result.record.v
|
|
1133
1790
|
});
|
|
1134
1791
|
}
|
|
1135
|
-
return result.record;
|
|
1136
|
-
}
|
|
1137
|
-
async edgeExists(aUid, axbType, bUid) {
|
|
1138
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1139
|
-
const record = await this.backend.getDoc(docId);
|
|
1140
|
-
return record !== null;
|
|
1141
|
-
}
|
|
1142
|
-
checkQuerySafety(filters, allowCollectionScan) {
|
|
1143
|
-
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
1144
|
-
const result = analyzeQuerySafety(filters);
|
|
1145
|
-
if (result.safe) return;
|
|
1146
|
-
if (this.scanProtection === "error") {
|
|
1147
|
-
throw new QuerySafetyError(result.reason);
|
|
1148
|
-
}
|
|
1149
|
-
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1150
|
-
}
|
|
1151
|
-
async findEdges(params) {
|
|
1152
|
-
const plan = buildEdgeQueryPlan(params);
|
|
1153
|
-
let records;
|
|
1154
|
-
if (plan.strategy === "get") {
|
|
1155
|
-
const record = await this.backend.getDoc(plan.docId);
|
|
1156
|
-
records = record ? [record] : [];
|
|
1157
|
-
} else {
|
|
1158
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1159
|
-
records = await this.backend.query(plan.filters, plan.options);
|
|
1160
|
-
}
|
|
1161
|
-
return this.applyMigrations(records);
|
|
1162
|
-
}
|
|
1163
|
-
async findNodes(params) {
|
|
1164
|
-
const plan = buildNodeQueryPlan(params);
|
|
1165
|
-
let records;
|
|
1166
|
-
if (plan.strategy === "get") {
|
|
1167
|
-
const record = await this.backend.getDoc(plan.docId);
|
|
1168
|
-
records = record ? [record] : [];
|
|
1169
|
-
} else {
|
|
1170
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1171
|
-
records = await this.backend.query(plan.filters, plan.options);
|
|
1172
|
-
}
|
|
1173
|
-
return this.applyMigrations(records);
|
|
1174
|
-
}
|
|
1175
|
-
async applyMigrations(records) {
|
|
1176
|
-
if (!this.registry || records.length === 0) return records;
|
|
1177
|
-
const results = await migrateRecords(records, this.registry, this.globalWriteBack);
|
|
1178
|
-
for (const result of results) {
|
|
1179
|
-
if (result.migrated && result.writeBack !== "off") {
|
|
1180
|
-
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
1181
|
-
await this.backend.updateDoc(docId, {
|
|
1182
|
-
replaceData: result.record.data,
|
|
1183
|
-
v: result.record.v
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
return results.map((r) => r.record);
|
|
1188
|
-
}
|
|
1189
|
-
async putNode(aType, uid, data) {
|
|
1190
|
-
if (this.registry) {
|
|
1191
|
-
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
1192
|
-
}
|
|
1193
|
-
const docId = computeNodeDocId(uid);
|
|
1194
|
-
const record = buildWritableNodeRecord2(aType, uid, data);
|
|
1195
|
-
if (this.registry) {
|
|
1196
|
-
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
1197
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1198
|
-
record.v = entry.schemaVersion;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
await this.backend.setDoc(docId, record);
|
|
1202
|
-
}
|
|
1203
|
-
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
1204
|
-
if (this.registry) {
|
|
1205
|
-
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
1206
|
-
}
|
|
1207
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1208
|
-
const record = buildWritableEdgeRecord2(aType, aUid, axbType, bType, bUid, data);
|
|
1209
|
-
if (this.registry) {
|
|
1210
|
-
const entry = this.registry.lookup(aType, axbType, bType);
|
|
1211
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1212
|
-
record.v = entry.schemaVersion;
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
await this.backend.setDoc(docId, record);
|
|
1216
|
-
}
|
|
1217
|
-
async updateNode(uid, data) {
|
|
1218
|
-
const docId = computeNodeDocId(uid);
|
|
1219
|
-
await this.backend.updateDoc(docId, { dataFields: data });
|
|
1220
|
-
}
|
|
1221
|
-
async removeNode(uid) {
|
|
1222
|
-
const docId = computeNodeDocId(uid);
|
|
1223
|
-
await this.backend.deleteDoc(docId);
|
|
1224
|
-
}
|
|
1225
|
-
async removeEdge(aUid, axbType, bUid) {
|
|
1226
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1227
|
-
await this.backend.deleteDoc(docId);
|
|
1228
|
-
}
|
|
1229
|
-
};
|
|
1230
|
-
|
|
1231
|
-
// src/client.ts
|
|
1232
|
-
var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
|
|
1233
|
-
function buildWritableNodeRecord3(aType, uid, data) {
|
|
1234
|
-
return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
|
|
1235
|
-
}
|
|
1236
|
-
function buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data) {
|
|
1237
|
-
return { aType, aUid, axbType, bType, bUid, data };
|
|
1238
|
-
}
|
|
1239
|
-
var GraphClientImpl = class _GraphClientImpl {
|
|
1240
|
-
constructor(backend, options, metaBackend) {
|
|
1241
|
-
this.backend = backend;
|
|
1242
|
-
this.globalWriteBack = options?.migrationWriteBack ?? "off";
|
|
1243
|
-
this.migrationSandbox = options?.migrationSandbox;
|
|
1244
|
-
if (options?.registryMode) {
|
|
1245
|
-
this.dynamicConfig = options.registryMode;
|
|
1246
|
-
this.bootstrapRegistry = createBootstrapRegistry();
|
|
1247
|
-
if (options.registry) {
|
|
1248
|
-
this.staticRegistry = options.registry;
|
|
1249
|
-
}
|
|
1250
|
-
this.metaBackend = metaBackend;
|
|
1251
|
-
} else {
|
|
1252
|
-
this.staticRegistry = options?.registry;
|
|
1253
|
-
}
|
|
1254
|
-
this.scanProtection = options?.scanProtection ?? "error";
|
|
1255
|
-
}
|
|
1256
|
-
scanProtection;
|
|
1257
|
-
// Static mode
|
|
1258
|
-
staticRegistry;
|
|
1259
|
-
// Dynamic mode
|
|
1260
|
-
dynamicConfig;
|
|
1261
|
-
bootstrapRegistry;
|
|
1262
|
-
dynamicRegistry;
|
|
1263
|
-
metaBackend;
|
|
1264
|
-
// Migration settings
|
|
1265
|
-
globalWriteBack;
|
|
1266
|
-
migrationSandbox;
|
|
1267
|
-
// ---------------------------------------------------------------------------
|
|
1268
|
-
// Backend access (exposed for traversal helpers and subgraph cloning)
|
|
1269
|
-
// ---------------------------------------------------------------------------
|
|
1270
|
-
/** @internal */
|
|
1271
|
-
getBackend() {
|
|
1272
|
-
return this.backend;
|
|
1273
|
-
}
|
|
1274
|
-
// ---------------------------------------------------------------------------
|
|
1275
|
-
// Registry routing
|
|
1276
|
-
// ---------------------------------------------------------------------------
|
|
1277
|
-
getRegistryForType(aType) {
|
|
1278
|
-
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1279
|
-
if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
|
|
1280
|
-
return this.bootstrapRegistry;
|
|
1281
|
-
}
|
|
1282
|
-
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1283
|
-
}
|
|
1284
|
-
getBackendForType(aType) {
|
|
1285
|
-
if (this.metaBackend && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
|
|
1286
|
-
return this.metaBackend;
|
|
1287
|
-
}
|
|
1288
|
-
return this.backend;
|
|
1289
|
-
}
|
|
1290
|
-
getCombinedRegistry() {
|
|
1291
|
-
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1292
|
-
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1293
|
-
}
|
|
1294
|
-
// ---------------------------------------------------------------------------
|
|
1295
|
-
// Query safety
|
|
1296
|
-
// ---------------------------------------------------------------------------
|
|
1297
|
-
checkQuerySafety(filters, allowCollectionScan) {
|
|
1298
|
-
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
1299
|
-
const result = analyzeQuerySafety(filters);
|
|
1300
|
-
if (result.safe) return;
|
|
1301
|
-
if (this.scanProtection === "error") {
|
|
1302
|
-
throw new QuerySafetyError(result.reason);
|
|
1303
|
-
}
|
|
1304
|
-
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1305
|
-
}
|
|
1306
|
-
// ---------------------------------------------------------------------------
|
|
1307
|
-
// Migration helpers
|
|
1308
|
-
// ---------------------------------------------------------------------------
|
|
1309
|
-
async applyMigration(record, docId) {
|
|
1310
|
-
const registry = this.getCombinedRegistry();
|
|
1311
|
-
if (!registry) return record;
|
|
1312
|
-
const result = await migrateRecord(record, registry, this.globalWriteBack);
|
|
1313
|
-
if (result.migrated) {
|
|
1314
|
-
this.handleWriteBack(result, docId);
|
|
1315
|
-
}
|
|
1316
|
-
return result.record;
|
|
1317
|
-
}
|
|
1318
|
-
async applyMigrations(records) {
|
|
1319
|
-
const registry = this.getCombinedRegistry();
|
|
1320
|
-
if (!registry || records.length === 0) return records;
|
|
1321
|
-
const results = await migrateRecords(records, registry, this.globalWriteBack);
|
|
1322
|
-
for (const result of results) {
|
|
1323
|
-
if (result.migrated) {
|
|
1324
|
-
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
1325
|
-
this.handleWriteBack(result, docId);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
return results.map((r) => r.record);
|
|
1329
|
-
}
|
|
1330
|
-
/**
|
|
1331
|
-
* Fire-and-forget write-back for a migrated record. Both `'eager'` and
|
|
1332
|
-
* `'background'` are non-blocking; the difference is the log level on
|
|
1333
|
-
* failure. For synchronous write-back, use a transaction — see
|
|
1334
|
-
* `GraphTransactionImpl`.
|
|
1335
|
-
*/
|
|
1336
|
-
handleWriteBack(result, docId) {
|
|
1337
|
-
if (result.writeBack === "off") return;
|
|
1338
|
-
const doWriteBack = async () => {
|
|
1339
|
-
try {
|
|
1340
|
-
await this.backend.updateDoc(docId, {
|
|
1341
|
-
replaceData: result.record.data,
|
|
1342
|
-
v: result.record.v
|
|
1343
|
-
});
|
|
1344
|
-
} catch (err) {
|
|
1345
|
-
const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
|
|
1346
|
-
if (result.writeBack === "eager") {
|
|
1347
|
-
console.error(msg);
|
|
1348
|
-
} else {
|
|
1349
|
-
console.warn(msg);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1353
|
-
void doWriteBack();
|
|
1354
|
-
}
|
|
1355
|
-
// ---------------------------------------------------------------------------
|
|
1356
|
-
// GraphReader
|
|
1357
|
-
// ---------------------------------------------------------------------------
|
|
1358
|
-
async getNode(uid) {
|
|
1359
|
-
const docId = computeNodeDocId(uid);
|
|
1360
|
-
const record = await this.backend.getDoc(docId);
|
|
1361
|
-
if (!record) return null;
|
|
1362
|
-
return this.applyMigration(record, docId);
|
|
1363
|
-
}
|
|
1364
|
-
async getEdge(aUid, axbType, bUid) {
|
|
1365
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1366
|
-
const record = await this.backend.getDoc(docId);
|
|
1367
|
-
if (!record) return null;
|
|
1368
|
-
return this.applyMigration(record, docId);
|
|
1792
|
+
return result.record;
|
|
1369
1793
|
}
|
|
1370
1794
|
async edgeExists(aUid, axbType, bUid) {
|
|
1371
1795
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1372
1796
|
const record = await this.backend.getDoc(docId);
|
|
1373
1797
|
return record !== null;
|
|
1374
1798
|
}
|
|
1799
|
+
checkQuerySafety(filters, allowCollectionScan) {
|
|
1800
|
+
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
1801
|
+
const result = analyzeQuerySafety(filters);
|
|
1802
|
+
if (result.safe) return;
|
|
1803
|
+
if (this.scanProtection === "error") {
|
|
1804
|
+
throw new QuerySafetyError(result.reason);
|
|
1805
|
+
}
|
|
1806
|
+
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1807
|
+
}
|
|
1375
1808
|
async findEdges(params) {
|
|
1376
1809
|
const plan = buildEdgeQueryPlan(params);
|
|
1377
1810
|
let records;
|
|
@@ -1396,40 +1829,47 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1396
1829
|
}
|
|
1397
1830
|
return this.applyMigrations(records);
|
|
1398
1831
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1832
|
+
async applyMigrations(records) {
|
|
1833
|
+
if (!this.registry || records.length === 0) return records;
|
|
1834
|
+
const results = await migrateRecords(records, this.registry, this.globalWriteBack);
|
|
1835
|
+
for (const result of results) {
|
|
1836
|
+
if (result.migrated && result.writeBack !== "off") {
|
|
1837
|
+
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
1838
|
+
await this.backend.updateDoc(docId, {
|
|
1839
|
+
replaceData: result.record.data,
|
|
1840
|
+
v: result.record.v
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return results.map((r) => r.record);
|
|
1845
|
+
}
|
|
1402
1846
|
async putNode(aType, uid, data) {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
registry.validate(aType, NODE_RELATION, aType, data, this.backend.scopePath);
|
|
1847
|
+
if (this.registry) {
|
|
1848
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
1406
1849
|
}
|
|
1407
|
-
const backend = this.getBackendForType(aType);
|
|
1408
1850
|
const docId = computeNodeDocId(uid);
|
|
1409
|
-
const record =
|
|
1410
|
-
if (registry) {
|
|
1411
|
-
const entry = registry.lookup(aType, NODE_RELATION, aType);
|
|
1851
|
+
const record = buildWritableNodeRecord2(aType, uid, data);
|
|
1852
|
+
if (this.registry) {
|
|
1853
|
+
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
1412
1854
|
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1413
1855
|
record.v = entry.schemaVersion;
|
|
1414
1856
|
}
|
|
1415
1857
|
}
|
|
1416
|
-
await backend.setDoc(docId, record);
|
|
1858
|
+
await this.backend.setDoc(docId, record);
|
|
1417
1859
|
}
|
|
1418
1860
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
registry.validate(aType, axbType, bType, data, this.backend.scopePath);
|
|
1861
|
+
if (this.registry) {
|
|
1862
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
1422
1863
|
}
|
|
1423
|
-
const backend = this.getBackendForType(aType);
|
|
1424
1864
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1425
|
-
const record =
|
|
1426
|
-
if (registry) {
|
|
1427
|
-
const entry = registry.lookup(aType, axbType, bType);
|
|
1865
|
+
const record = buildWritableEdgeRecord2(aType, aUid, axbType, bType, bUid, data);
|
|
1866
|
+
if (this.registry) {
|
|
1867
|
+
const entry = this.registry.lookup(aType, axbType, bType);
|
|
1428
1868
|
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1429
1869
|
record.v = entry.schemaVersion;
|
|
1430
1870
|
}
|
|
1431
1871
|
}
|
|
1432
|
-
await backend.setDoc(docId, record);
|
|
1872
|
+
await this.backend.setDoc(docId, record);
|
|
1433
1873
|
}
|
|
1434
1874
|
async updateNode(uid, data) {
|
|
1435
1875
|
const docId = computeNodeDocId(uid);
|
|
@@ -1443,982 +1883,779 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1443
1883
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1444
1884
|
await this.backend.deleteDoc(docId);
|
|
1445
1885
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
this.
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
);
|
|
1467
|
-
}
|
|
1468
|
-
// ---------------------------------------------------------------------------
|
|
1469
|
-
// Subgraph
|
|
1470
|
-
// ---------------------------------------------------------------------------
|
|
1471
|
-
subgraph(parentNodeUid, name = "graph") {
|
|
1472
|
-
if (!parentNodeUid || parentNodeUid.includes("/")) {
|
|
1473
|
-
throw new FiregraphError(
|
|
1474
|
-
`Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
|
|
1475
|
-
"INVALID_SUBGRAPH"
|
|
1476
|
-
);
|
|
1477
|
-
}
|
|
1478
|
-
if (name.includes("/")) {
|
|
1479
|
-
throw new FiregraphError(
|
|
1480
|
-
`Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
1481
|
-
"INVALID_SUBGRAPH"
|
|
1482
|
-
);
|
|
1483
|
-
}
|
|
1484
|
-
const childBackend = this.backend.subgraph(parentNodeUid, name);
|
|
1485
|
-
return new _GraphClientImpl(
|
|
1486
|
-
childBackend,
|
|
1487
|
-
{
|
|
1488
|
-
registry: this.getCombinedRegistry(),
|
|
1489
|
-
scanProtection: this.scanProtection,
|
|
1490
|
-
migrationWriteBack: this.globalWriteBack,
|
|
1491
|
-
migrationSandbox: this.migrationSandbox
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
// src/client.ts
|
|
1889
|
+
var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
|
|
1890
|
+
function buildWritableNodeRecord3(aType, uid, data) {
|
|
1891
|
+
return { aType, aUid: uid, axbType: NODE_RELATION, bType: aType, bUid: uid, data };
|
|
1892
|
+
}
|
|
1893
|
+
function buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data) {
|
|
1894
|
+
return { aType, aUid, axbType, bType, bUid, data };
|
|
1895
|
+
}
|
|
1896
|
+
var GraphClientImpl = class _GraphClientImpl {
|
|
1897
|
+
constructor(backend, options, metaBackend) {
|
|
1898
|
+
this.backend = backend;
|
|
1899
|
+
this.globalWriteBack = options?.migrationWriteBack ?? "off";
|
|
1900
|
+
this.migrationSandbox = options?.migrationSandbox;
|
|
1901
|
+
if (options?.registryMode) {
|
|
1902
|
+
this.dynamicConfig = options.registryMode;
|
|
1903
|
+
this.bootstrapRegistry = createBootstrapRegistry();
|
|
1904
|
+
if (options.registry) {
|
|
1905
|
+
this.staticRegistry = options.registry;
|
|
1492
1906
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
// ---------------------------------------------------------------------------
|
|
1497
|
-
// Collection group query
|
|
1498
|
-
// ---------------------------------------------------------------------------
|
|
1499
|
-
async findEdgesGlobal(params, collectionName) {
|
|
1500
|
-
if (!this.backend.findEdgesGlobal) {
|
|
1501
|
-
throw new FiregraphError(
|
|
1502
|
-
"findEdgesGlobal() is not supported by the current storage backend.",
|
|
1503
|
-
"UNSUPPORTED_OPERATION"
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
const plan = buildEdgeQueryPlan(params);
|
|
1507
|
-
if (plan.strategy === "get") {
|
|
1508
|
-
throw new FiregraphError(
|
|
1509
|
-
"findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
|
|
1510
|
-
"INVALID_QUERY"
|
|
1511
|
-
);
|
|
1907
|
+
this.metaBackend = metaBackend;
|
|
1908
|
+
} else {
|
|
1909
|
+
this.staticRegistry = options?.registry;
|
|
1512
1910
|
}
|
|
1513
|
-
this.
|
|
1514
|
-
const records = await this.backend.findEdgesGlobal(params, collectionName);
|
|
1515
|
-
return this.applyMigrations(records);
|
|
1911
|
+
this.scanProtection = options?.scanProtection ?? "error";
|
|
1516
1912
|
}
|
|
1913
|
+
scanProtection;
|
|
1914
|
+
// Static mode
|
|
1915
|
+
staticRegistry;
|
|
1916
|
+
// Dynamic mode
|
|
1917
|
+
dynamicConfig;
|
|
1918
|
+
bootstrapRegistry;
|
|
1919
|
+
dynamicRegistry;
|
|
1920
|
+
metaBackend;
|
|
1921
|
+
// Migration settings
|
|
1922
|
+
globalWriteBack;
|
|
1923
|
+
migrationSandbox;
|
|
1517
1924
|
// ---------------------------------------------------------------------------
|
|
1518
|
-
//
|
|
1925
|
+
// Backend access (exposed for traversal helpers and subgraph cloning)
|
|
1519
1926
|
// ---------------------------------------------------------------------------
|
|
1520
|
-
|
|
1521
|
-
|
|
1927
|
+
/** @internal */
|
|
1928
|
+
getBackend() {
|
|
1929
|
+
return this.backend;
|
|
1522
1930
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1931
|
+
/**
|
|
1932
|
+
* Snapshot of the currently-effective registry. Returns the merged view
|
|
1933
|
+
* used for domain-type validation and migration — in dynamic mode this is
|
|
1934
|
+
* `dynamicRegistry ?? staticRegistry ?? bootstrapRegistry`, so callers see
|
|
1935
|
+
* updates after `reloadRegistry()` without having to re-resolve anything.
|
|
1936
|
+
*
|
|
1937
|
+
* Exposed for backends that need topology access during bulk operations
|
|
1938
|
+
* (e.g. the Cloudflare DO backend's cross-DO cascade). Not part of the
|
|
1939
|
+
* public `GraphClient` surface.
|
|
1940
|
+
*
|
|
1941
|
+
* @internal
|
|
1942
|
+
*/
|
|
1943
|
+
getRegistrySnapshot() {
|
|
1944
|
+
return this.getCombinedRegistry();
|
|
1525
1945
|
}
|
|
1526
1946
|
// ---------------------------------------------------------------------------
|
|
1527
|
-
//
|
|
1947
|
+
// Registry routing
|
|
1528
1948
|
// ---------------------------------------------------------------------------
|
|
1529
|
-
|
|
1530
|
-
if (!this.dynamicConfig)
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
);
|
|
1534
|
-
}
|
|
1535
|
-
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
1536
|
-
throw new DynamicRegistryError(
|
|
1537
|
-
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
1538
|
-
);
|
|
1539
|
-
}
|
|
1540
|
-
if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
|
|
1541
|
-
throw new DynamicRegistryError(
|
|
1542
|
-
`Cannot define node type "${name}": already defined in the static registry.`
|
|
1543
|
-
);
|
|
1544
|
-
}
|
|
1545
|
-
const uid = generateDeterministicUid(META_NODE_TYPE, name);
|
|
1546
|
-
const data = { name, jsonSchema };
|
|
1547
|
-
if (description !== void 0) data.description = description;
|
|
1548
|
-
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
1549
|
-
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
1550
|
-
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1551
|
-
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1552
|
-
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
1553
|
-
if (options?.migrationWriteBack !== void 0)
|
|
1554
|
-
data.migrationWriteBack = options.migrationWriteBack;
|
|
1555
|
-
if (options?.migrations !== void 0) {
|
|
1556
|
-
data.migrations = await this.serializeMigrations(options.migrations);
|
|
1949
|
+
getRegistryForType(aType) {
|
|
1950
|
+
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1951
|
+
if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
|
|
1952
|
+
return this.bootstrapRegistry;
|
|
1557
1953
|
}
|
|
1558
|
-
|
|
1954
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1559
1955
|
}
|
|
1560
|
-
|
|
1561
|
-
if (
|
|
1562
|
-
|
|
1563
|
-
'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
1564
|
-
);
|
|
1565
|
-
}
|
|
1566
|
-
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
1567
|
-
throw new DynamicRegistryError(
|
|
1568
|
-
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
if (this.staticRegistry) {
|
|
1572
|
-
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
1573
|
-
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
1574
|
-
for (const aType of fromTypes) {
|
|
1575
|
-
for (const bType of toTypes) {
|
|
1576
|
-
if (this.staticRegistry.lookup(aType, name, bType)) {
|
|
1577
|
-
throw new DynamicRegistryError(
|
|
1578
|
-
`Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
|
|
1579
|
-
);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
const uid = generateDeterministicUid(META_EDGE_TYPE, name);
|
|
1585
|
-
const data = {
|
|
1586
|
-
name,
|
|
1587
|
-
from: topology.from,
|
|
1588
|
-
to: topology.to
|
|
1589
|
-
};
|
|
1590
|
-
if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
|
|
1591
|
-
if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
|
|
1592
|
-
if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
|
|
1593
|
-
if (description !== void 0) data.description = description;
|
|
1594
|
-
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
1595
|
-
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
1596
|
-
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1597
|
-
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1598
|
-
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
1599
|
-
if (options?.migrationWriteBack !== void 0)
|
|
1600
|
-
data.migrationWriteBack = options.migrationWriteBack;
|
|
1601
|
-
if (options?.migrations !== void 0) {
|
|
1602
|
-
data.migrations = await this.serializeMigrations(options.migrations);
|
|
1956
|
+
getBackendForType(aType) {
|
|
1957
|
+
if (this.metaBackend && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
|
|
1958
|
+
return this.metaBackend;
|
|
1603
1959
|
}
|
|
1604
|
-
|
|
1960
|
+
return this.backend;
|
|
1961
|
+
}
|
|
1962
|
+
getCombinedRegistry() {
|
|
1963
|
+
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1964
|
+
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1605
1965
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1966
|
+
// ---------------------------------------------------------------------------
|
|
1967
|
+
// Query safety
|
|
1968
|
+
// ---------------------------------------------------------------------------
|
|
1969
|
+
checkQuerySafety(filters, allowCollectionScan) {
|
|
1970
|
+
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
1971
|
+
const result = analyzeQuerySafety(filters);
|
|
1972
|
+
if (result.safe) return;
|
|
1973
|
+
if (this.scanProtection === "error") {
|
|
1974
|
+
throw new QuerySafetyError(result.reason);
|
|
1611
1975
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1976
|
+
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1977
|
+
}
|
|
1978
|
+
// ---------------------------------------------------------------------------
|
|
1979
|
+
// Migration helpers
|
|
1980
|
+
// ---------------------------------------------------------------------------
|
|
1981
|
+
async applyMigration(record, docId) {
|
|
1982
|
+
const registry = this.getCombinedRegistry();
|
|
1983
|
+
if (!registry) return record;
|
|
1984
|
+
const result = await migrateRecord(record, registry, this.globalWriteBack);
|
|
1985
|
+
if (result.migrated) {
|
|
1986
|
+
this.handleWriteBack(result, docId);
|
|
1618
1987
|
}
|
|
1988
|
+
return result.record;
|
|
1619
1989
|
}
|
|
1620
|
-
async
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1990
|
+
async applyMigrations(records) {
|
|
1991
|
+
const registry = this.getCombinedRegistry();
|
|
1992
|
+
if (!registry || records.length === 0) return records;
|
|
1993
|
+
const results = await migrateRecords(records, registry, this.globalWriteBack);
|
|
1994
|
+
for (const result of results) {
|
|
1995
|
+
if (result.migrated) {
|
|
1996
|
+
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
1997
|
+
this.handleWriteBack(result, docId);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return results.map((r) => r.record);
|
|
1627
2001
|
}
|
|
1628
2002
|
/**
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
2003
|
+
* Fire-and-forget write-back for a migrated record. Both `'eager'` and
|
|
2004
|
+
* `'background'` are non-blocking; the difference is the log level on
|
|
2005
|
+
* failure. For synchronous write-back, use a transaction — see
|
|
2006
|
+
* `GraphTransactionImpl`.
|
|
1631
2007
|
*/
|
|
1632
|
-
|
|
1633
|
-
if (
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
},
|
|
1647
|
-
async findEdges(params) {
|
|
1648
|
-
const plan = buildEdgeQueryPlan(params);
|
|
1649
|
-
if (plan.strategy === "get") {
|
|
1650
|
-
const record = await backend.getDoc(plan.docId);
|
|
1651
|
-
return record ? [record] : [];
|
|
1652
|
-
}
|
|
1653
|
-
return executeMetaQuery(plan.filters, plan.options);
|
|
1654
|
-
},
|
|
1655
|
-
async findNodes(params) {
|
|
1656
|
-
const plan = buildNodeQueryPlan(params);
|
|
1657
|
-
if (plan.strategy === "get") {
|
|
1658
|
-
const record = await backend.getDoc(plan.docId);
|
|
1659
|
-
return record ? [record] : [];
|
|
2008
|
+
handleWriteBack(result, docId) {
|
|
2009
|
+
if (result.writeBack === "off") return;
|
|
2010
|
+
const doWriteBack = async () => {
|
|
2011
|
+
try {
|
|
2012
|
+
await this.backend.updateDoc(docId, {
|
|
2013
|
+
replaceData: result.record.data,
|
|
2014
|
+
v: result.record.v
|
|
2015
|
+
});
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
|
|
2018
|
+
if (result.writeBack === "eager") {
|
|
2019
|
+
console.error(msg);
|
|
2020
|
+
} else {
|
|
2021
|
+
console.warn(msg);
|
|
1660
2022
|
}
|
|
1661
|
-
return executeMetaQuery(plan.filters, plan.options);
|
|
1662
2023
|
}
|
|
1663
2024
|
};
|
|
2025
|
+
void doWriteBack();
|
|
1664
2026
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
this.seconds = seconds;
|
|
1674
|
-
this.nanoseconds = nanoseconds;
|
|
1675
|
-
}
|
|
1676
|
-
toDate() {
|
|
1677
|
-
return new Date(this.toMillis());
|
|
2027
|
+
// ---------------------------------------------------------------------------
|
|
2028
|
+
// GraphReader
|
|
2029
|
+
// ---------------------------------------------------------------------------
|
|
2030
|
+
async getNode(uid) {
|
|
2031
|
+
const docId = computeNodeDocId(uid);
|
|
2032
|
+
const record = await this.backend.getDoc(docId);
|
|
2033
|
+
if (!record) return null;
|
|
2034
|
+
return this.applyMigration(record, docId);
|
|
1678
2035
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
2036
|
+
async getEdge(aUid, axbType, bUid) {
|
|
2037
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
2038
|
+
const record = await this.backend.getDoc(docId);
|
|
2039
|
+
if (!record) return null;
|
|
2040
|
+
return this.applyMigration(record, docId);
|
|
1681
2041
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
2042
|
+
async edgeExists(aUid, axbType, bUid) {
|
|
2043
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
2044
|
+
const record = await this.backend.getDoc(docId);
|
|
2045
|
+
return record !== null;
|
|
1684
2046
|
}
|
|
1685
|
-
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
2047
|
+
async findEdges(params) {
|
|
2048
|
+
const plan = buildEdgeQueryPlan(params);
|
|
2049
|
+
let records;
|
|
2050
|
+
if (plan.strategy === "get") {
|
|
2051
|
+
const record = await this.backend.getDoc(plan.docId);
|
|
2052
|
+
records = record ? [record] : [];
|
|
2053
|
+
} else {
|
|
2054
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
2055
|
+
records = await this.backend.query(plan.filters, plan.options);
|
|
2056
|
+
}
|
|
2057
|
+
return this.applyMigrations(records);
|
|
1689
2058
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
2059
|
+
async findNodes(params) {
|
|
2060
|
+
const plan = buildNodeQueryPlan(params);
|
|
2061
|
+
let records;
|
|
2062
|
+
if (plan.strategy === "get") {
|
|
2063
|
+
const record = await this.backend.getDoc(plan.docId);
|
|
2064
|
+
records = record ? [record] : [];
|
|
2065
|
+
} else {
|
|
2066
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
2067
|
+
records = await this.backend.query(plan.filters, plan.options);
|
|
2068
|
+
}
|
|
2069
|
+
return this.applyMigrations(records);
|
|
1692
2070
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
//
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
scope TEXT NOT NULL DEFAULT '',
|
|
1712
|
-
a_type TEXT NOT NULL,
|
|
1713
|
-
a_uid TEXT NOT NULL,
|
|
1714
|
-
axb_type TEXT NOT NULL,
|
|
1715
|
-
b_type TEXT NOT NULL,
|
|
1716
|
-
b_uid TEXT NOT NULL,
|
|
1717
|
-
data TEXT NOT NULL,
|
|
1718
|
-
v INTEGER,
|
|
1719
|
-
created_at INTEGER NOT NULL,
|
|
1720
|
-
updated_at INTEGER NOT NULL,
|
|
1721
|
-
PRIMARY KEY (scope, doc_id)
|
|
1722
|
-
)`,
|
|
1723
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_a_uid`)} ON ${t}(scope, a_uid)`,
|
|
1724
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_b_uid`)} ON ${t}(scope, b_uid)`,
|
|
1725
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_axb_type_b_uid`)} ON ${t}(scope, axb_type, b_uid)`,
|
|
1726
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_a_type`)} ON ${t}(scope, a_type)`,
|
|
1727
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_scope_b_type`)} ON ${t}(scope, b_type)`,
|
|
1728
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${table}_idx_doc_id`)} ON ${t}(doc_id)`
|
|
1729
|
-
];
|
|
1730
|
-
}
|
|
1731
|
-
function quoteIdent(name) {
|
|
1732
|
-
validateTableName(name);
|
|
1733
|
-
return `"${name}"`;
|
|
1734
|
-
}
|
|
1735
|
-
function validateTableName(name) {
|
|
1736
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
1737
|
-
throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
2071
|
+
// ---------------------------------------------------------------------------
|
|
2072
|
+
// GraphWriter
|
|
2073
|
+
// ---------------------------------------------------------------------------
|
|
2074
|
+
async putNode(aType, uid, data) {
|
|
2075
|
+
const registry = this.getRegistryForType(aType);
|
|
2076
|
+
if (registry) {
|
|
2077
|
+
registry.validate(aType, NODE_RELATION, aType, data, this.backend.scopePath);
|
|
2078
|
+
}
|
|
2079
|
+
const backend = this.getBackendForType(aType);
|
|
2080
|
+
const docId = computeNodeDocId(uid);
|
|
2081
|
+
const record = buildWritableNodeRecord3(aType, uid, data);
|
|
2082
|
+
if (registry) {
|
|
2083
|
+
const entry = registry.lookup(aType, NODE_RELATION, aType);
|
|
2084
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
2085
|
+
record.v = entry.schemaVersion;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
await backend.setDoc(docId, record);
|
|
1738
2089
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
2090
|
+
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
2091
|
+
const registry = this.getRegistryForType(aType);
|
|
2092
|
+
if (registry) {
|
|
2093
|
+
registry.validate(aType, axbType, bType, data, this.backend.scopePath);
|
|
2094
|
+
}
|
|
2095
|
+
const backend = this.getBackendForType(aType);
|
|
2096
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
2097
|
+
const record = buildWritableEdgeRecord3(aType, aUid, axbType, bType, bUid, data);
|
|
2098
|
+
if (registry) {
|
|
2099
|
+
const entry = registry.lookup(aType, axbType, bType);
|
|
2100
|
+
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
2101
|
+
record.v = entry.schemaVersion;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
await backend.setDoc(docId, record);
|
|
1746
2105
|
}
|
|
1747
|
-
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1750
|
-
return { expr: 'json_extract("data", ?)', pathParam: `$.${key}` };
|
|
2106
|
+
async updateNode(uid, data) {
|
|
2107
|
+
const docId = computeNodeDocId(uid);
|
|
2108
|
+
await this.backend.updateDoc(docId, { dataFields: data });
|
|
1751
2109
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
2110
|
+
async removeNode(uid) {
|
|
2111
|
+
const docId = computeNodeDocId(uid);
|
|
2112
|
+
await this.backend.deleteDoc(docId);
|
|
1754
2113
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
"Timestamp",
|
|
1759
|
-
"GeoPoint",
|
|
1760
|
-
"VectorValue",
|
|
1761
|
-
"DocumentReference",
|
|
1762
|
-
"FieldValue"
|
|
1763
|
-
]);
|
|
1764
|
-
function isFirestoreSpecialType(value) {
|
|
1765
|
-
const ctorName = value.constructor?.name;
|
|
1766
|
-
if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
|
|
1767
|
-
return null;
|
|
1768
|
-
}
|
|
1769
|
-
function bindValue(value) {
|
|
1770
|
-
if (value === null || value === void 0) return null;
|
|
1771
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
1772
|
-
return value;
|
|
2114
|
+
async removeEdge(aUid, axbType, bUid) {
|
|
2115
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
2116
|
+
await this.backend.deleteDoc(docId);
|
|
1773
2117
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2118
|
+
// ---------------------------------------------------------------------------
|
|
2119
|
+
// Transactions & Batches
|
|
2120
|
+
// ---------------------------------------------------------------------------
|
|
2121
|
+
async runTransaction(fn) {
|
|
2122
|
+
return this.backend.runTransaction(async (txBackend) => {
|
|
2123
|
+
const graphTx = new GraphTransactionImpl(
|
|
2124
|
+
txBackend,
|
|
2125
|
+
this.getCombinedRegistry(),
|
|
2126
|
+
this.scanProtection,
|
|
2127
|
+
this.backend.scopePath,
|
|
2128
|
+
this.globalWriteBack
|
|
1781
2129
|
);
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
}
|
|
1785
|
-
return String(value);
|
|
1786
|
-
}
|
|
1787
|
-
var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
1788
|
-
function validateJsonPathKey(key) {
|
|
1789
|
-
if (key.length === 0) {
|
|
1790
|
-
throw new FiregraphError(
|
|
1791
|
-
"SQLite backend: empty JSON path component is not allowed",
|
|
1792
|
-
"INVALID_QUERY"
|
|
1793
|
-
);
|
|
2130
|
+
return fn(graphTx);
|
|
2131
|
+
});
|
|
1794
2132
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2133
|
+
batch() {
|
|
2134
|
+
return new GraphBatchImpl(
|
|
2135
|
+
this.backend.createBatch(),
|
|
2136
|
+
this.getCombinedRegistry(),
|
|
2137
|
+
this.backend.scopePath
|
|
1799
2138
|
);
|
|
1800
2139
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
params.push(bindValue(filter.value));
|
|
1811
|
-
return `${expr} != ?`;
|
|
1812
|
-
case "<":
|
|
1813
|
-
params.push(bindValue(filter.value));
|
|
1814
|
-
return `${expr} < ?`;
|
|
1815
|
-
case "<=":
|
|
1816
|
-
params.push(bindValue(filter.value));
|
|
1817
|
-
return `${expr} <= ?`;
|
|
1818
|
-
case ">":
|
|
1819
|
-
params.push(bindValue(filter.value));
|
|
1820
|
-
return `${expr} > ?`;
|
|
1821
|
-
case ">=":
|
|
1822
|
-
params.push(bindValue(filter.value));
|
|
1823
|
-
return `${expr} >= ?`;
|
|
1824
|
-
case "in": {
|
|
1825
|
-
const values = asArray(filter.value, "in");
|
|
1826
|
-
const placeholders = values.map(() => "?").join(", ");
|
|
1827
|
-
for (const v of values) params.push(bindValue(v));
|
|
1828
|
-
return `${expr} IN (${placeholders})`;
|
|
1829
|
-
}
|
|
1830
|
-
case "not-in": {
|
|
1831
|
-
const values = asArray(filter.value, "not-in");
|
|
1832
|
-
const placeholders = values.map(() => "?").join(", ");
|
|
1833
|
-
for (const v of values) params.push(bindValue(v));
|
|
1834
|
-
return `${expr} NOT IN (${placeholders})`;
|
|
2140
|
+
// ---------------------------------------------------------------------------
|
|
2141
|
+
// Subgraph
|
|
2142
|
+
// ---------------------------------------------------------------------------
|
|
2143
|
+
subgraph(parentNodeUid, name = "graph") {
|
|
2144
|
+
if (!parentNodeUid || parentNodeUid.includes("/")) {
|
|
2145
|
+
throw new FiregraphError(
|
|
2146
|
+
`Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
|
|
2147
|
+
"INVALID_SUBGRAPH"
|
|
2148
|
+
);
|
|
1835
2149
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2150
|
+
if (name.includes("/")) {
|
|
2151
|
+
throw new FiregraphError(
|
|
2152
|
+
`Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
2153
|
+
"INVALID_SUBGRAPH"
|
|
2154
|
+
);
|
|
1839
2155
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2156
|
+
const childBackend = this.backend.subgraph(parentNodeUid, name);
|
|
2157
|
+
return new _GraphClientImpl(
|
|
2158
|
+
childBackend,
|
|
2159
|
+
{
|
|
2160
|
+
registry: this.getCombinedRegistry(),
|
|
2161
|
+
scanProtection: this.scanProtection,
|
|
2162
|
+
migrationWriteBack: this.globalWriteBack,
|
|
2163
|
+
migrationSandbox: this.migrationSandbox
|
|
2164
|
+
}
|
|
2165
|
+
// Subgraphs do not have meta-backends; meta lives only at the root.
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
// ---------------------------------------------------------------------------
|
|
2169
|
+
// Collection group query
|
|
2170
|
+
// ---------------------------------------------------------------------------
|
|
2171
|
+
async findEdgesGlobal(params, collectionName) {
|
|
2172
|
+
if (!this.backend.findEdgesGlobal) {
|
|
2173
|
+
throw new FiregraphError(
|
|
2174
|
+
"findEdgesGlobal() is not supported by the current storage backend.",
|
|
2175
|
+
"UNSUPPORTED_OPERATION"
|
|
2176
|
+
);
|
|
1845
2177
|
}
|
|
1846
|
-
|
|
2178
|
+
const plan = buildEdgeQueryPlan(params);
|
|
2179
|
+
if (plan.strategy === "get") {
|
|
1847
2180
|
throw new FiregraphError(
|
|
1848
|
-
|
|
2181
|
+
"findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
|
|
1849
2182
|
"INVALID_QUERY"
|
|
1850
2183
|
);
|
|
2184
|
+
}
|
|
2185
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
2186
|
+
const records = await this.backend.findEdgesGlobal(params, collectionName);
|
|
2187
|
+
return this.applyMigrations(records);
|
|
1851
2188
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
return value;
|
|
1858
|
-
}
|
|
1859
|
-
function compileOrderBy(options, params) {
|
|
1860
|
-
if (!options?.orderBy) return "";
|
|
1861
|
-
const { field, direction } = options.orderBy;
|
|
1862
|
-
const { expr, pathParam } = compileFieldRef(field);
|
|
1863
|
-
if (pathParam !== void 0) params.push(pathParam);
|
|
1864
|
-
const dir = direction === "desc" ? "DESC" : "ASC";
|
|
1865
|
-
return ` ORDER BY ${expr} ${dir}`;
|
|
1866
|
-
}
|
|
1867
|
-
function compileLimit(options, params) {
|
|
1868
|
-
if (options?.limit === void 0) return "";
|
|
1869
|
-
params.push(options.limit);
|
|
1870
|
-
return ` LIMIT ?`;
|
|
1871
|
-
}
|
|
1872
|
-
function compileSelect(table, scope, filters, options) {
|
|
1873
|
-
const params = [];
|
|
1874
|
-
const conditions = ['"scope" = ?'];
|
|
1875
|
-
params.push(scope);
|
|
1876
|
-
for (const f of filters) {
|
|
1877
|
-
conditions.push(compileFilter(f, params));
|
|
2189
|
+
// ---------------------------------------------------------------------------
|
|
2190
|
+
// Bulk operations
|
|
2191
|
+
// ---------------------------------------------------------------------------
|
|
2192
|
+
async removeNodeCascade(uid, options) {
|
|
2193
|
+
return this.backend.removeNodeCascade(uid, this, options);
|
|
1878
2194
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
sql += orderClause;
|
|
1882
|
-
sql += compileLimit(options, params);
|
|
1883
|
-
return { sql, params };
|
|
1884
|
-
}
|
|
1885
|
-
function compileSelectGlobal(table, filters, options, scopeNameFilter) {
|
|
1886
|
-
if (filters.length === 0) {
|
|
1887
|
-
throw new FiregraphError(
|
|
1888
|
-
"compileSelectGlobal requires at least one filter \u2014 refusing to issue an unbounded SELECT.",
|
|
1889
|
-
"INVALID_QUERY"
|
|
1890
|
-
);
|
|
2195
|
+
async bulkRemoveEdges(params, options) {
|
|
2196
|
+
return this.backend.bulkRemoveEdges(params, this, options);
|
|
1891
2197
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
params.push(`%/${escapeLike(scopeNameFilter.name)}`);
|
|
2198
|
+
// ---------------------------------------------------------------------------
|
|
2199
|
+
// Dynamic registry methods
|
|
2200
|
+
// ---------------------------------------------------------------------------
|
|
2201
|
+
async defineNodeType(name, jsonSchema, description, options) {
|
|
2202
|
+
if (!this.dynamicConfig) {
|
|
2203
|
+
throw new DynamicRegistryError(
|
|
2204
|
+
'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
2205
|
+
);
|
|
1901
2206
|
}
|
|
2207
|
+
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
2208
|
+
throw new DynamicRegistryError(
|
|
2209
|
+
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
|
|
2213
|
+
throw new DynamicRegistryError(
|
|
2214
|
+
`Cannot define node type "${name}": already defined in the static registry.`
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
const uid = generateDeterministicUid(META_NODE_TYPE, name);
|
|
2218
|
+
const data = { name, jsonSchema };
|
|
2219
|
+
if (description !== void 0) data.description = description;
|
|
2220
|
+
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
2221
|
+
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
2222
|
+
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
2223
|
+
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
2224
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
2225
|
+
if (options?.migrationWriteBack !== void 0)
|
|
2226
|
+
data.migrationWriteBack = options.migrationWriteBack;
|
|
2227
|
+
if (options?.migrations !== void 0) {
|
|
2228
|
+
data.migrations = await this.serializeMigrations(options.migrations);
|
|
2229
|
+
}
|
|
2230
|
+
await this.putNode(META_NODE_TYPE, uid, data);
|
|
1902
2231
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
params.push(`$.${k}`);
|
|
1947
|
-
params.push(bindValue(v));
|
|
2232
|
+
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
2233
|
+
if (!this.dynamicConfig) {
|
|
2234
|
+
throw new DynamicRegistryError(
|
|
2235
|
+
'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
2239
|
+
throw new DynamicRegistryError(
|
|
2240
|
+
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
if (this.staticRegistry) {
|
|
2244
|
+
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
2245
|
+
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
2246
|
+
for (const aType of fromTypes) {
|
|
2247
|
+
for (const bType of toTypes) {
|
|
2248
|
+
if (this.staticRegistry.lookup(aType, name, bType)) {
|
|
2249
|
+
throw new DynamicRegistryError(
|
|
2250
|
+
`Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const uid = generateDeterministicUid(META_EDGE_TYPE, name);
|
|
2257
|
+
const data = {
|
|
2258
|
+
name,
|
|
2259
|
+
from: topology.from,
|
|
2260
|
+
to: topology.to
|
|
2261
|
+
};
|
|
2262
|
+
if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
|
|
2263
|
+
if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
|
|
2264
|
+
if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
|
|
2265
|
+
if (description !== void 0) data.description = description;
|
|
2266
|
+
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
2267
|
+
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
2268
|
+
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
2269
|
+
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
2270
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
2271
|
+
if (options?.migrationWriteBack !== void 0)
|
|
2272
|
+
data.migrationWriteBack = options.migrationWriteBack;
|
|
2273
|
+
if (options?.migrations !== void 0) {
|
|
2274
|
+
data.migrations = await this.serializeMigrations(options.migrations);
|
|
1948
2275
|
}
|
|
2276
|
+
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
1949
2277
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2278
|
+
async reloadRegistry() {
|
|
2279
|
+
if (!this.dynamicConfig) {
|
|
2280
|
+
throw new DynamicRegistryError(
|
|
2281
|
+
'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
const reader = this.createMetaReader();
|
|
2285
|
+
const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
|
|
2286
|
+
if (this.staticRegistry) {
|
|
2287
|
+
this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
|
|
2288
|
+
} else {
|
|
2289
|
+
this.dynamicRegistry = dynamicOnly;
|
|
2290
|
+
}
|
|
1953
2291
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
}
|
|
1962
|
-
function compileDelete(table, scope, docId) {
|
|
1963
|
-
return {
|
|
1964
|
-
sql: `DELETE FROM ${quoteIdent(table)} WHERE "scope" = ? AND "doc_id" = ?`,
|
|
1965
|
-
params: [scope, docId]
|
|
1966
|
-
};
|
|
1967
|
-
}
|
|
1968
|
-
function compileDeleteScopePrefix(table, scopePrefix) {
|
|
1969
|
-
const escaped = escapeLike(scopePrefix);
|
|
1970
|
-
return {
|
|
1971
|
-
sql: `DELETE FROM ${quoteIdent(table)} WHERE "scope" LIKE ? ESCAPE '\\'`,
|
|
1972
|
-
params: [`${escaped}/%`]
|
|
1973
|
-
};
|
|
1974
|
-
}
|
|
1975
|
-
function compileCountScopePrefix(table, scopePrefix) {
|
|
1976
|
-
const escaped = escapeLike(scopePrefix);
|
|
1977
|
-
return {
|
|
1978
|
-
sql: `SELECT COUNT(*) AS n FROM ${quoteIdent(table)} WHERE "scope" LIKE ? ESCAPE '\\'`,
|
|
1979
|
-
params: [`${escaped}/%`]
|
|
1980
|
-
};
|
|
1981
|
-
}
|
|
1982
|
-
function escapeLike(value) {
|
|
1983
|
-
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1984
|
-
}
|
|
1985
|
-
function rowToRecord(row) {
|
|
1986
|
-
const dataString = row.data;
|
|
1987
|
-
const data = dataString ? JSON.parse(dataString) : {};
|
|
1988
|
-
const createdMs = toMillis(row.created_at);
|
|
1989
|
-
const updatedMs = toMillis(row.updated_at);
|
|
1990
|
-
const record = {
|
|
1991
|
-
aType: row.a_type,
|
|
1992
|
-
aUid: row.a_uid,
|
|
1993
|
-
axbType: row.axb_type,
|
|
1994
|
-
bType: row.b_type,
|
|
1995
|
-
bUid: row.b_uid,
|
|
1996
|
-
data,
|
|
1997
|
-
createdAt: GraphTimestampImpl.fromMillis(createdMs),
|
|
1998
|
-
updatedAt: GraphTimestampImpl.fromMillis(updatedMs)
|
|
1999
|
-
};
|
|
2000
|
-
if (row.v !== null && row.v !== void 0) {
|
|
2001
|
-
record.v = Number(row.v);
|
|
2292
|
+
async serializeMigrations(migrations) {
|
|
2293
|
+
const result = migrations.map((m) => {
|
|
2294
|
+
const source = typeof m.up === "function" ? m.up.toString() : m.up;
|
|
2295
|
+
return { fromVersion: m.fromVersion, toVersion: m.toVersion, up: source };
|
|
2296
|
+
});
|
|
2297
|
+
await Promise.all(result.map((m) => precompileSource(m.up, this.migrationSandbox)));
|
|
2298
|
+
return result;
|
|
2002
2299
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2300
|
+
/**
|
|
2301
|
+
* Build a `GraphReader` over the meta-backend. If meta lives in the same
|
|
2302
|
+
* collection as the main backend, `this` is returned directly.
|
|
2303
|
+
*/
|
|
2304
|
+
createMetaReader() {
|
|
2305
|
+
if (!this.metaBackend) return this;
|
|
2306
|
+
const backend = this.metaBackend;
|
|
2307
|
+
const executeMetaQuery = (filters, options) => backend.query(filters, options);
|
|
2308
|
+
return {
|
|
2309
|
+
async getNode(uid) {
|
|
2310
|
+
return backend.getDoc(computeNodeDocId(uid));
|
|
2311
|
+
},
|
|
2312
|
+
async getEdge(aUid, axbType, bUid) {
|
|
2313
|
+
return backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
2314
|
+
},
|
|
2315
|
+
async edgeExists(aUid, axbType, bUid) {
|
|
2316
|
+
const record = await backend.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
2317
|
+
return record !== null;
|
|
2318
|
+
},
|
|
2319
|
+
async findEdges(params) {
|
|
2320
|
+
const plan = buildEdgeQueryPlan(params);
|
|
2321
|
+
if (plan.strategy === "get") {
|
|
2322
|
+
const record = await backend.getDoc(plan.docId);
|
|
2323
|
+
return record ? [record] : [];
|
|
2324
|
+
}
|
|
2325
|
+
return executeMetaQuery(plan.filters, plan.options);
|
|
2326
|
+
},
|
|
2327
|
+
async findNodes(params) {
|
|
2328
|
+
const plan = buildNodeQueryPlan(params);
|
|
2329
|
+
if (plan.strategy === "get") {
|
|
2330
|
+
const record = await backend.getDoc(plan.docId);
|
|
2331
|
+
return record ? [record] : [];
|
|
2332
|
+
}
|
|
2333
|
+
return executeMetaQuery(plan.filters, plan.options);
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
function createGraphClientFromBackend(backend, options, metaBackend) {
|
|
2339
|
+
return new GraphClientImpl(backend, options, metaBackend);
|
|
2010
2340
|
}
|
|
2011
2341
|
|
|
2012
|
-
// src/
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
function minDefined(a, b) {
|
|
2020
|
-
if (a === void 0) return b;
|
|
2021
|
-
if (b === void 0) return a;
|
|
2022
|
-
return Math.min(a, b);
|
|
2023
|
-
}
|
|
2024
|
-
function chunkStatements(statements, maxStatements, maxParams) {
|
|
2025
|
-
const stmtCap = maxStatements && maxStatements > 0 && Number.isFinite(maxStatements) ? Math.floor(maxStatements) : Infinity;
|
|
2026
|
-
const paramCap = maxParams && maxParams > 0 && Number.isFinite(maxParams) ? Math.floor(maxParams) : Infinity;
|
|
2027
|
-
if (stmtCap === Infinity && paramCap === Infinity) {
|
|
2028
|
-
return [statements];
|
|
2029
|
-
}
|
|
2030
|
-
const chunks = [];
|
|
2031
|
-
let current = [];
|
|
2032
|
-
let currentParamCount = 0;
|
|
2033
|
-
for (const stmt of statements) {
|
|
2034
|
-
const stmtParams = stmt.params.length;
|
|
2035
|
-
const wouldExceedStmt = current.length + 1 > stmtCap;
|
|
2036
|
-
const wouldExceedParam = currentParamCount + stmtParams > paramCap;
|
|
2037
|
-
if (current.length > 0 && (wouldExceedStmt || wouldExceedParam)) {
|
|
2038
|
-
chunks.push(current);
|
|
2039
|
-
current = [];
|
|
2040
|
-
currentParamCount = 0;
|
|
2041
|
-
}
|
|
2042
|
-
current.push(stmt);
|
|
2043
|
-
currentParamCount += stmtParams;
|
|
2044
|
-
}
|
|
2045
|
-
if (current.length > 0) chunks.push(current);
|
|
2046
|
-
return chunks;
|
|
2047
|
-
}
|
|
2048
|
-
var SqliteTransactionBackendImpl = class {
|
|
2049
|
-
constructor(tx, tableName, storageScope) {
|
|
2050
|
-
this.tx = tx;
|
|
2051
|
-
this.tableName = tableName;
|
|
2052
|
-
this.storageScope = storageScope;
|
|
2053
|
-
}
|
|
2054
|
-
async getDoc(docId) {
|
|
2055
|
-
const stmt = compileSelectByDocId(this.tableName, this.storageScope, docId);
|
|
2056
|
-
const rows = await this.tx.all(stmt.sql, stmt.params);
|
|
2057
|
-
return rows.length === 0 ? null : rowToRecord(rows[0]);
|
|
2058
|
-
}
|
|
2059
|
-
async query(filters, options) {
|
|
2060
|
-
const stmt = compileSelect(this.tableName, this.storageScope, filters, options);
|
|
2061
|
-
const rows = await this.tx.all(stmt.sql, stmt.params);
|
|
2062
|
-
return rows.map(rowToRecord);
|
|
2342
|
+
// src/cloudflare/client.ts
|
|
2343
|
+
function createDOClient(namespace, rootKey, options = {}) {
|
|
2344
|
+
if (!rootKey || typeof rootKey !== "string") {
|
|
2345
|
+
throw new FiregraphError(
|
|
2346
|
+
`createDOClient: rootKey must be a non-empty string, got ${JSON.stringify(rootKey)}.`,
|
|
2347
|
+
"INVALID_ARGUMENT"
|
|
2348
|
+
);
|
|
2063
2349
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2350
|
+
if (rootKey.includes("/")) {
|
|
2351
|
+
throw new FiregraphError(
|
|
2352
|
+
`createDOClient: rootKey must not contain "/". Got: "${rootKey}".`,
|
|
2353
|
+
"INVALID_ARGUMENT"
|
|
2354
|
+
);
|
|
2067
2355
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
const rows = await this.tx.all(sqlWithReturning, stmt.params);
|
|
2072
|
-
if (rows.length === 0) {
|
|
2356
|
+
let client;
|
|
2357
|
+
const registryAccessor = () => {
|
|
2358
|
+
if (!client) {
|
|
2073
2359
|
throw new FiregraphError(
|
|
2074
|
-
|
|
2075
|
-
"
|
|
2360
|
+
"createDOClient: registryAccessor fired before the client was assigned. This indicates a programming error in the DO backend \u2014 the accessor must only be invoked lazily from `removeNodeCascade`, never synchronously from the `DORPCBackend` constructor.",
|
|
2361
|
+
"INTERNAL"
|
|
2076
2362
|
);
|
|
2077
2363
|
}
|
|
2364
|
+
return client.getRegistrySnapshot();
|
|
2365
|
+
};
|
|
2366
|
+
const siblingOptions = { ...options };
|
|
2367
|
+
const makeSiblingClient = (siblingRootKey) => createDOClient(namespace, siblingRootKey, siblingOptions);
|
|
2368
|
+
const backend = new DORPCBackend(namespace, {
|
|
2369
|
+
scopePath: "",
|
|
2370
|
+
storageKey: rootKey,
|
|
2371
|
+
registryAccessor,
|
|
2372
|
+
makeSiblingClient
|
|
2373
|
+
});
|
|
2374
|
+
let metaBackend;
|
|
2375
|
+
if (options.registryMode?.collection) {
|
|
2376
|
+
const metaKey = options.registryMode.collection;
|
|
2377
|
+
if (metaKey.includes("/")) {
|
|
2378
|
+
throw new FiregraphError(
|
|
2379
|
+
`createDOClient: registryMode.collection must not contain "/". Got: "${metaKey}".`,
|
|
2380
|
+
"INVALID_ARGUMENT"
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
if (metaKey !== rootKey) {
|
|
2384
|
+
metaBackend = new DORPCBackend(namespace, {
|
|
2385
|
+
scopePath: "",
|
|
2386
|
+
storageKey: metaKey,
|
|
2387
|
+
// Meta backend shares the accessor so its own `removeNodeCascade`
|
|
2388
|
+
// (unlikely, but safe) would also see the live registry. Sibling
|
|
2389
|
+
// factory is carried for consistency; there's no user-facing path
|
|
2390
|
+
// that creates a sibling from the meta backend, but it costs
|
|
2391
|
+
// nothing to keep the two backends in sync.
|
|
2392
|
+
registryAccessor,
|
|
2393
|
+
makeSiblingClient
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2078
2396
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
this.tableName = tableName;
|
|
2088
|
-
this.storageScope = storageScope;
|
|
2089
|
-
}
|
|
2090
|
-
statements = [];
|
|
2091
|
-
setDoc(docId, record) {
|
|
2092
|
-
this.statements.push(compileSet(this.tableName, this.storageScope, docId, record, Date.now()));
|
|
2093
|
-
}
|
|
2094
|
-
updateDoc(docId, update) {
|
|
2095
|
-
this.statements.push(
|
|
2096
|
-
compileUpdate(this.tableName, this.storageScope, docId, update, Date.now())
|
|
2397
|
+
client = createGraphClientFromBackend(backend, options, metaBackend);
|
|
2398
|
+
return client;
|
|
2399
|
+
}
|
|
2400
|
+
function createSiblingClient(client, siblingRootKey) {
|
|
2401
|
+
if (!siblingRootKey || typeof siblingRootKey !== "string") {
|
|
2402
|
+
throw new FiregraphError(
|
|
2403
|
+
`createSiblingClient: siblingRootKey must be a non-empty string, got ${JSON.stringify(siblingRootKey)}.`,
|
|
2404
|
+
"INVALID_ARGUMENT"
|
|
2097
2405
|
);
|
|
2098
2406
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2407
|
+
if (siblingRootKey.includes("/")) {
|
|
2408
|
+
throw new FiregraphError(
|
|
2409
|
+
`createSiblingClient: siblingRootKey must not contain "/". Got: "${siblingRootKey}".`,
|
|
2410
|
+
"INVALID_ARGUMENT"
|
|
2411
|
+
);
|
|
2101
2412
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2413
|
+
const impl = client;
|
|
2414
|
+
const backend = typeof impl.getBackend === "function" ? impl.getBackend() : void 0;
|
|
2415
|
+
const maker = backend && backend.makeSiblingClient;
|
|
2416
|
+
if (typeof maker !== "function") {
|
|
2417
|
+
throw new FiregraphError(
|
|
2418
|
+
"createSiblingClient: the provided client is not backed by a DO client produced by `createDOClient`. Sibling construction is only available for DO-backed clients.",
|
|
2419
|
+
"UNSUPPORTED_OPERATION"
|
|
2420
|
+
);
|
|
2106
2421
|
}
|
|
2422
|
+
return maker(siblingRootKey);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// src/cloudflare/do.ts
|
|
2426
|
+
var DEFAULT_OPTIONS = {
|
|
2427
|
+
table: "firegraph",
|
|
2428
|
+
autoMigrate: true
|
|
2107
2429
|
};
|
|
2108
|
-
var
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2430
|
+
var FiregraphDO = class {
|
|
2431
|
+
/** @internal — exposed for subclass access, not part of the public RPC. */
|
|
2432
|
+
ctx;
|
|
2433
|
+
/** @internal — exposed for subclass access; opaque to this class. */
|
|
2434
|
+
env;
|
|
2435
|
+
/** @internal — table name used by every compiled statement. */
|
|
2436
|
+
table;
|
|
2437
|
+
/** @internal — registry consulted by `runSchema` for per-entry indexes. */
|
|
2438
|
+
registry;
|
|
2439
|
+
/** @internal — overrides `DEFAULT_CORE_INDEXES` when set. */
|
|
2440
|
+
coreIndexes;
|
|
2441
|
+
constructor(ctx, env, options = {}) {
|
|
2442
|
+
this.ctx = ctx;
|
|
2443
|
+
this.env = env;
|
|
2444
|
+
const table = options.table ?? DEFAULT_OPTIONS.table;
|
|
2445
|
+
validateDOTableName(table);
|
|
2446
|
+
this.table = table;
|
|
2447
|
+
this.registry = options.registry;
|
|
2448
|
+
this.coreIndexes = options.coreIndexes;
|
|
2449
|
+
const autoMigrate = options.autoMigrate ?? DEFAULT_OPTIONS.autoMigrate;
|
|
2450
|
+
if (autoMigrate) {
|
|
2451
|
+
void this.ctx.blockConcurrencyWhile(async () => {
|
|
2452
|
+
this.runSchema();
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2114
2455
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
//
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2456
|
+
// ---------------------------------------------------------------------------
|
|
2457
|
+
// RPC: reads
|
|
2458
|
+
//
|
|
2459
|
+
// Method names are prefixed `_fg` so user subclasses can add their own RPC
|
|
2460
|
+
// methods without name collisions. The client-side backend in
|
|
2461
|
+
// `src/cloudflare/backend.ts` calls these directly on the DO stub.
|
|
2462
|
+
// ---------------------------------------------------------------------------
|
|
2463
|
+
async _fgGetDoc(docId) {
|
|
2464
|
+
const stmt = compileDOSelectByDocId(this.table, docId);
|
|
2465
|
+
const rows = this.execAll(stmt);
|
|
2466
|
+
return rows.length === 0 ? null : rowToDORecord(rows[0]);
|
|
2125
2467
|
}
|
|
2126
|
-
async
|
|
2127
|
-
const stmt =
|
|
2128
|
-
const rows =
|
|
2129
|
-
return rows.map(
|
|
2468
|
+
async _fgQuery(filters, options) {
|
|
2469
|
+
const stmt = compileDOSelect(this.table, filters, options);
|
|
2470
|
+
const rows = this.execAll(stmt);
|
|
2471
|
+
return rows.map(rowToDORecord);
|
|
2130
2472
|
}
|
|
2131
|
-
//
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2473
|
+
// ---------------------------------------------------------------------------
|
|
2474
|
+
// RPC: writes
|
|
2475
|
+
// ---------------------------------------------------------------------------
|
|
2476
|
+
async _fgSetDoc(docId, record) {
|
|
2477
|
+
const stmt = compileDOSet(this.table, docId, record, Date.now());
|
|
2478
|
+
this.execRun(stmt);
|
|
2135
2479
|
}
|
|
2136
|
-
async
|
|
2137
|
-
const stmt =
|
|
2480
|
+
async _fgUpdateDoc(docId, update) {
|
|
2481
|
+
const stmt = compileDOUpdate(this.table, docId, update, Date.now());
|
|
2138
2482
|
const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
|
|
2139
|
-
const rows =
|
|
2483
|
+
const rows = this.ctx.storage.sql.exec(sqlWithReturning, ...stmt.params).toArray();
|
|
2140
2484
|
if (rows.length === 0) {
|
|
2141
|
-
throw new FiregraphError(
|
|
2142
|
-
`updateDoc: no document found for doc_id=${docId} (scope=${this.storageScope})`,
|
|
2143
|
-
"NOT_FOUND"
|
|
2144
|
-
);
|
|
2485
|
+
throw new FiregraphError(`updateDoc: no document found for doc_id=${docId}`, "NOT_FOUND");
|
|
2145
2486
|
}
|
|
2146
2487
|
}
|
|
2147
|
-
async
|
|
2148
|
-
const stmt =
|
|
2149
|
-
|
|
2488
|
+
async _fgDeleteDoc(docId) {
|
|
2489
|
+
const stmt = compileDODelete(this.table, docId);
|
|
2490
|
+
this.execRun(stmt);
|
|
2150
2491
|
}
|
|
2151
|
-
//
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
)
|
|
2165
|
-
|
|
2492
|
+
// ---------------------------------------------------------------------------
|
|
2493
|
+
// RPC: batch
|
|
2494
|
+
// ---------------------------------------------------------------------------
|
|
2495
|
+
/**
|
|
2496
|
+
* Execute a list of write ops atomically. DO SQLite's `transactionSync`
|
|
2497
|
+
* provides real atomicity — either every statement commits or none do.
|
|
2498
|
+
* No statement-count cap applies (contrast with D1's ~100-statement batch
|
|
2499
|
+
* limit), so the caller can submit as many ops as they like in one call.
|
|
2500
|
+
*/
|
|
2501
|
+
async _fgBatch(ops) {
|
|
2502
|
+
if (ops.length === 0) return;
|
|
2503
|
+
const now = Date.now();
|
|
2504
|
+
const statements = ops.map((op) => {
|
|
2505
|
+
switch (op.kind) {
|
|
2506
|
+
case "set":
|
|
2507
|
+
return compileDOSet(this.table, op.docId, op.record, now);
|
|
2508
|
+
case "update":
|
|
2509
|
+
return compileDOUpdate(this.table, op.docId, op.update, now);
|
|
2510
|
+
case "delete":
|
|
2511
|
+
return compileDODelete(this.table, op.docId);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
this.ctx.storage.transactionSync(() => {
|
|
2515
|
+
for (const stmt of statements) {
|
|
2516
|
+
this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
|
|
2517
|
+
}
|
|
2166
2518
|
});
|
|
2167
2519
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
//
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
"INVALID_SUBGRAPH"
|
|
2183
|
-
);
|
|
2184
|
-
}
|
|
2185
|
-
const newStorageScope = this.storageScope ? `${this.storageScope}/${parentNodeUid}/${name}` : `${parentNodeUid}/${name}`;
|
|
2186
|
-
const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
2187
|
-
return new _SqliteBackendImpl(this.executor, this.collectionPath, newStorageScope, newScope);
|
|
2188
|
-
}
|
|
2189
|
-
// --- Cascade & bulk ---
|
|
2190
|
-
async removeNodeCascade(uid, reader, options) {
|
|
2191
|
-
const [outgoingRaw, incomingRaw] = await Promise.all([
|
|
2192
|
-
reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
|
|
2193
|
-
reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
|
|
2194
|
-
]);
|
|
2520
|
+
// ---------------------------------------------------------------------------
|
|
2521
|
+
// RPC: cascade + bulk (local DO only)
|
|
2522
|
+
//
|
|
2523
|
+
// These cascade *within this DO*. Subgraph DOs (nested under this node) are
|
|
2524
|
+
// not reachable from here — the client-side `DORPCBackend.removeNodeCascade`
|
|
2525
|
+
// consults the registry topology to discover descendant subgraph DOs and
|
|
2526
|
+
// fans out explicit `_fgDestroy` calls to each before invoking this method.
|
|
2527
|
+
// Without that topology the DO has no way to enumerate its children.
|
|
2528
|
+
// ---------------------------------------------------------------------------
|
|
2529
|
+
async _fgRemoveNodeCascade(uid) {
|
|
2530
|
+
const outgoingStmt = compileDOSelect(this.table, [{ field: "aUid", op: "==", value: uid }]);
|
|
2531
|
+
const incomingStmt = compileDOSelect(this.table, [{ field: "bUid", op: "==", value: uid }]);
|
|
2532
|
+
const outgoingRows = this.execAll(outgoingStmt);
|
|
2533
|
+
const incomingRows = this.execAll(incomingStmt);
|
|
2195
2534
|
const seen = /* @__PURE__ */ new Set();
|
|
2196
2535
|
const edgeDocIds = [];
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
const
|
|
2536
|
+
let nodeExists = false;
|
|
2537
|
+
for (const row of [...outgoingRows, ...incomingRows]) {
|
|
2538
|
+
const axbType = row.axb_type;
|
|
2539
|
+
const aUid = row.a_uid;
|
|
2540
|
+
const bUid = row.b_uid;
|
|
2541
|
+
if (axbType === NODE_RELATION && aUid === bUid) {
|
|
2542
|
+
nodeExists = true;
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
2200
2546
|
if (!seen.has(docId)) {
|
|
2201
2547
|
seen.add(docId);
|
|
2202
2548
|
edgeDocIds.push(docId);
|
|
2203
2549
|
}
|
|
2204
2550
|
}
|
|
2205
|
-
const
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
if (shouldDeleteSubgraphs) {
|
|
2209
|
-
const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
|
|
2210
|
-
const countStmt = compileCountScopePrefix(this.collectionPath, prefix);
|
|
2211
|
-
const countRows = await this.executor.all(countStmt.sql, countStmt.params);
|
|
2212
|
-
const first = countRows[0];
|
|
2213
|
-
const n = first?.n;
|
|
2214
|
-
subgraphRowCount = typeof n === "bigint" ? Number(n) : Number(n ?? 0);
|
|
2215
|
-
}
|
|
2216
|
-
const writeStatements = edgeDocIds.map(
|
|
2217
|
-
(id) => compileDelete(this.collectionPath, this.storageScope, id)
|
|
2218
|
-
);
|
|
2219
|
-
writeStatements.push(compileDelete(this.collectionPath, this.storageScope, nodeDocId));
|
|
2220
|
-
if (shouldDeleteSubgraphs) {
|
|
2221
|
-
const prefix = this.storageScope ? `${this.storageScope}/${uid}` : uid;
|
|
2222
|
-
writeStatements.push(compileDeleteScopePrefix(this.collectionPath, prefix));
|
|
2223
|
-
}
|
|
2224
|
-
const {
|
|
2225
|
-
deleted: stmtDeleted,
|
|
2226
|
-
batches,
|
|
2227
|
-
errors
|
|
2228
|
-
} = await this.executeChunkedBatches(writeStatements, options);
|
|
2229
|
-
const allOk = errors.length === 0;
|
|
2230
|
-
const edgesDeleted = allOk ? edgeDocIds.length : 0;
|
|
2231
|
-
const nodeDeleted = allOk;
|
|
2232
|
-
const prefixStatementContribution = shouldDeleteSubgraphs && allOk ? 1 : 0;
|
|
2233
|
-
const deleted = stmtDeleted - prefixStatementContribution + (allOk ? subgraphRowCount : 0);
|
|
2234
|
-
return { deleted, batches, errors, edgesDeleted, nodeDeleted };
|
|
2235
|
-
}
|
|
2236
|
-
async bulkRemoveEdges(params, reader, options) {
|
|
2237
|
-
const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
|
|
2238
|
-
const edges = await reader.findEdges(effectiveParams);
|
|
2239
|
-
const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
2240
|
-
if (docIds.length === 0) {
|
|
2241
|
-
return { deleted: 0, batches: 0, errors: [] };
|
|
2551
|
+
const statements = edgeDocIds.map((id) => compileDODelete(this.table, id));
|
|
2552
|
+
if (nodeExists) {
|
|
2553
|
+
statements.push(compileDODelete(this.table, computeNodeDocId(uid)));
|
|
2242
2554
|
}
|
|
2243
|
-
const statements = docIds.map(
|
|
2244
|
-
(id) => compileDelete(this.collectionPath, this.storageScope, id)
|
|
2245
|
-
);
|
|
2246
|
-
return this.executeChunkedBatches(statements, options);
|
|
2247
|
-
}
|
|
2248
|
-
/**
|
|
2249
|
-
* Submit `statements` to the executor as one or more `batch()` calls,
|
|
2250
|
-
* chunking by `executor.maxBatchSize` (e.g. D1's ~100-statement cap).
|
|
2251
|
-
* Drivers that don't advertise a cap submit everything in one batch,
|
|
2252
|
-
* preserving cross-batch atomicity.
|
|
2253
|
-
*
|
|
2254
|
-
* Each chunk is retried with exponential backoff up to `maxRetries`
|
|
2255
|
-
* (default 3) before being recorded in `errors`. The loop continues past
|
|
2256
|
-
* a permanently failed chunk so the caller still gets partial progress
|
|
2257
|
-
* visibility — to halt on first failure, set `maxRetries: 0` and check
|
|
2258
|
-
* `result.errors.length` after the call.
|
|
2259
|
-
*
|
|
2260
|
-
* Returns `BulkResult`-shaped fields. `deleted` reflects only the
|
|
2261
|
-
* statement count of *successfully committed* batches — a prefix-delete
|
|
2262
|
-
* statement contributes 1 to that total even though it may match many
|
|
2263
|
-
* rows; `removeNodeCascade` patches that up with a pre-counted row total.
|
|
2264
|
-
*
|
|
2265
|
-
* **Atomicity caveat (D1):** when chunking kicks in, atomicity is lost
|
|
2266
|
-
* across chunk boundaries — one chunk may commit while a later one fails.
|
|
2267
|
-
* `removeNodeCascade` is idempotent (deleting the same docs again is a
|
|
2268
|
-
* no-op) so a caller can simply retry on partial failure. `bulkRemoveEdges`
|
|
2269
|
-
* is also idempotent for the same reason. DO SQLite leaves `maxBatchSize`
|
|
2270
|
-
* unset, so everything funnels through one atomic `transactionSync` and
|
|
2271
|
-
* this caveat does not apply.
|
|
2272
|
-
*/
|
|
2273
|
-
async executeChunkedBatches(statements, options) {
|
|
2274
2555
|
if (statements.length === 0) {
|
|
2275
|
-
return {
|
|
2556
|
+
return {
|
|
2557
|
+
deleted: 0,
|
|
2558
|
+
batches: 0,
|
|
2559
|
+
errors: [],
|
|
2560
|
+
edgesDeleted: 0,
|
|
2561
|
+
nodeDeleted: false
|
|
2562
|
+
};
|
|
2276
2563
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
const errors = [];
|
|
2282
|
-
let deleted = 0;
|
|
2283
|
-
let batches = 0;
|
|
2284
|
-
const totalBatches = chunks.length;
|
|
2285
|
-
const driverParamCap = this.executor.maxBatchParams;
|
|
2286
|
-
for (let batchIndex = 0; batchIndex < chunks.length; batchIndex++) {
|
|
2287
|
-
const chunk = chunks[batchIndex];
|
|
2288
|
-
const isUnretriableOversize = chunk.length === 1 && driverParamCap !== void 0 && chunk[0].params.length > driverParamCap;
|
|
2289
|
-
let committed = false;
|
|
2290
|
-
let lastError = null;
|
|
2291
|
-
const effectiveRetries = isUnretriableOversize ? 0 : maxRetries;
|
|
2292
|
-
for (let attempt = 0; attempt <= effectiveRetries; attempt++) {
|
|
2293
|
-
try {
|
|
2294
|
-
await this.executor.batch(chunk);
|
|
2295
|
-
committed = true;
|
|
2296
|
-
break;
|
|
2297
|
-
} catch (err) {
|
|
2298
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2299
|
-
if (attempt < effectiveRetries) {
|
|
2300
|
-
const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
|
|
2301
|
-
await sleep(delay);
|
|
2302
|
-
}
|
|
2564
|
+
try {
|
|
2565
|
+
this.ctx.storage.transactionSync(() => {
|
|
2566
|
+
for (const stmt of statements) {
|
|
2567
|
+
this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
|
|
2303
2568
|
}
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
deleted
|
|
2307
|
-
batches
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
}
|
|
2569
|
+
});
|
|
2570
|
+
return {
|
|
2571
|
+
deleted: statements.length,
|
|
2572
|
+
batches: 1,
|
|
2573
|
+
errors: [],
|
|
2574
|
+
edgesDeleted: edgeDocIds.length,
|
|
2575
|
+
nodeDeleted: nodeExists
|
|
2576
|
+
};
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2579
|
+
return {
|
|
2580
|
+
deleted: 0,
|
|
2581
|
+
batches: 0,
|
|
2582
|
+
errors: [{ batchIndex: 0, error, operationCount: statements.length }],
|
|
2583
|
+
edgesDeleted: 0,
|
|
2584
|
+
nodeDeleted: false
|
|
2585
|
+
};
|
|
2322
2586
|
}
|
|
2323
|
-
return { deleted, batches, errors };
|
|
2324
2587
|
}
|
|
2325
|
-
|
|
2326
|
-
async findEdgesGlobal(params, collectionName) {
|
|
2588
|
+
async _fgBulkRemoveEdges(params, _options) {
|
|
2327
2589
|
const plan = buildEdgeQueryPlan(params);
|
|
2590
|
+
let docIds;
|
|
2328
2591
|
if (plan.strategy === "get") {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2592
|
+
const existsStmt = compileDOSelectByDocId(this.table, plan.docId);
|
|
2593
|
+
const rows = this.execAll(existsStmt);
|
|
2594
|
+
docIds = rows.length > 0 ? [plan.docId] : [];
|
|
2595
|
+
} else {
|
|
2596
|
+
const selectStmt = compileDOSelect(this.table, plan.filters, plan.options);
|
|
2597
|
+
const rows = this.execAll(selectStmt);
|
|
2598
|
+
docIds = rows.map(
|
|
2599
|
+
(row) => computeEdgeDocId(row.a_uid, row.axb_type, row.b_uid)
|
|
2332
2600
|
);
|
|
2333
2601
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
};
|
|
2339
|
-
const stmt = compileSelectGlobal(
|
|
2340
|
-
this.collectionPath,
|
|
2341
|
-
plan.filters,
|
|
2342
|
-
plan.options,
|
|
2343
|
-
scopeNameFilter
|
|
2344
|
-
);
|
|
2345
|
-
const rows = await this.executor.all(stmt.sql, stmt.params);
|
|
2346
|
-
return rows.map(rowToRecord);
|
|
2347
|
-
}
|
|
2348
|
-
};
|
|
2349
|
-
function createSqliteBackend(executor, tableName, options = {}) {
|
|
2350
|
-
const storageScope = options.storageScope ?? "";
|
|
2351
|
-
const scopePath = options.scopePath ?? "";
|
|
2352
|
-
return new SqliteBackendImpl(executor, tableName, storageScope, scopePath);
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
// src/do-sqlite.ts
|
|
2356
|
-
var DOSqliteExecutor = class {
|
|
2357
|
-
constructor(storage) {
|
|
2358
|
-
this.storage = storage;
|
|
2359
|
-
}
|
|
2360
|
-
async all(sql, params) {
|
|
2361
|
-
return this.storage.sql.exec(sql, ...params).toArray();
|
|
2362
|
-
}
|
|
2363
|
-
async run(sql, params) {
|
|
2364
|
-
this.storage.sql.exec(sql, ...params).toArray();
|
|
2365
|
-
}
|
|
2366
|
-
async batch(statements) {
|
|
2367
|
-
if (statements.length === 0) return;
|
|
2368
|
-
this.storage.transactionSync(() => {
|
|
2369
|
-
for (const s of statements) {
|
|
2370
|
-
this.storage.sql.exec(s.sql, ...s.params).toArray();
|
|
2371
|
-
}
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
async transaction(fn) {
|
|
2375
|
-
this.storage.sql.exec("BEGIN IMMEDIATE").toArray();
|
|
2602
|
+
if (docIds.length === 0) {
|
|
2603
|
+
return { deleted: 0, batches: 0, errors: [] };
|
|
2604
|
+
}
|
|
2605
|
+
const deleteStmts = docIds.map((id) => compileDODelete(this.table, id));
|
|
2376
2606
|
try {
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
this.storage.sql.exec(sql, ...params).toArray();
|
|
2607
|
+
this.ctx.storage.transactionSync(() => {
|
|
2608
|
+
for (const stmt of deleteStmts) {
|
|
2609
|
+
this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
|
|
2381
2610
|
}
|
|
2382
|
-
};
|
|
2383
|
-
|
|
2384
|
-
this.storage.sql.exec("COMMIT").toArray();
|
|
2385
|
-
return result;
|
|
2611
|
+
});
|
|
2612
|
+
return { deleted: deleteStmts.length, batches: 1, errors: [] };
|
|
2386
2613
|
} catch (err) {
|
|
2387
|
-
|
|
2388
|
-
|
|
2614
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2615
|
+
return {
|
|
2616
|
+
deleted: 0,
|
|
2617
|
+
batches: 0,
|
|
2618
|
+
errors: [{ batchIndex: 0, error, operationCount: deleteStmts.length }]
|
|
2619
|
+
};
|
|
2389
2620
|
}
|
|
2390
2621
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
if (options.autoMigrate !== false) {
|
|
2414
|
-
ensureSchema(storage, metaTable);
|
|
2622
|
+
// ---------------------------------------------------------------------------
|
|
2623
|
+
// RPC: admin
|
|
2624
|
+
// ---------------------------------------------------------------------------
|
|
2625
|
+
/**
|
|
2626
|
+
* Wipe every row. Called by the client when tearing down a subgraph DO as
|
|
2627
|
+
* part of cascade — the DO itself can't be destroyed (DO IDs persist
|
|
2628
|
+
* forever), but its storage can be emptied.
|
|
2629
|
+
*/
|
|
2630
|
+
async _fgDestroy() {
|
|
2631
|
+
const stmt = compileDODeleteAll(this.table);
|
|
2632
|
+
this.execRun(stmt);
|
|
2633
|
+
}
|
|
2634
|
+
// ---------------------------------------------------------------------------
|
|
2635
|
+
// Internals
|
|
2636
|
+
// ---------------------------------------------------------------------------
|
|
2637
|
+
runSchema() {
|
|
2638
|
+
const statements = buildDOSchemaStatements(this.table, {
|
|
2639
|
+
coreIndexes: this.coreIndexes,
|
|
2640
|
+
registry: this.registry
|
|
2641
|
+
});
|
|
2642
|
+
for (const sql of statements) {
|
|
2643
|
+
this.ctx.storage.sql.exec(sql).toArray();
|
|
2415
2644
|
}
|
|
2416
|
-
metaBackend = createSqliteBackend(executor, metaTable);
|
|
2417
2645
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2646
|
+
execAll(stmt) {
|
|
2647
|
+
return this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
|
|
2648
|
+
}
|
|
2649
|
+
execRun(stmt) {
|
|
2650
|
+
this.ctx.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2420
2653
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2421
2654
|
0 && (module.exports = {
|
|
2422
|
-
|
|
2655
|
+
DORPCBackend,
|
|
2656
|
+
FiregraphDO,
|
|
2657
|
+
buildDOSchemaStatements,
|
|
2658
|
+
createDOClient,
|
|
2659
|
+
createSiblingClient
|
|
2423
2660
|
});
|
|
2424
|
-
//# sourceMappingURL=
|
|
2661
|
+
//# sourceMappingURL=index.cjs.map
|