@typicalday/firegraph 0.1.0 → 0.3.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 +253 -6
- package/bin/firegraph.mjs +47 -0
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/editor/server/index.mjs +326 -38
- package/dist/{index-CG3R68Hu.d.cts → index-CQkofEC_.d.cts} +122 -2
- package/dist/{index-CG3R68Hu.d.ts → index-CQkofEC_.d.ts} +122 -2
- package/dist/index.cjs +557 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +157 -4
- package/dist/index.d.ts +157 -4
- package/dist/index.js +548 -59
- package/dist/index.js.map +1 -1
- package/package.json +25 -23
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BOOTSTRAP_ENTRIES: () => BOOTSTRAP_ENTRIES,
|
|
34
|
+
DEFAULT_QUERY_LIMIT: () => DEFAULT_QUERY_LIMIT,
|
|
34
35
|
DiscoveryError: () => DiscoveryError,
|
|
35
36
|
DynamicRegistryError: () => DynamicRegistryError,
|
|
36
37
|
EDGE_TYPE_SCHEMA: () => EDGE_TYPE_SCHEMA,
|
|
@@ -43,9 +44,12 @@ __export(index_exports, {
|
|
|
43
44
|
NodeNotFoundError: () => NodeNotFoundError,
|
|
44
45
|
QueryClient: () => QueryClient,
|
|
45
46
|
QueryClientError: () => QueryClientError,
|
|
47
|
+
QuerySafetyError: () => QuerySafetyError,
|
|
48
|
+
RegistryScopeError: () => RegistryScopeError,
|
|
46
49
|
RegistryViolationError: () => RegistryViolationError,
|
|
47
50
|
TraversalError: () => TraversalError,
|
|
48
51
|
ValidationError: () => ValidationError,
|
|
52
|
+
analyzeQuerySafety: () => analyzeQuerySafety,
|
|
49
53
|
buildEdgeQueryPlan: () => buildEdgeQueryPlan,
|
|
50
54
|
buildEdgeRecord: () => buildEdgeRecord,
|
|
51
55
|
buildNodeQueryPlan: () => buildNodeQueryPlan,
|
|
@@ -63,8 +67,13 @@ __export(index_exports, {
|
|
|
63
67
|
discoverEntities: () => discoverEntities,
|
|
64
68
|
generateDeterministicUid: () => generateDeterministicUid,
|
|
65
69
|
generateId: () => generateId,
|
|
70
|
+
generateIndexConfig: () => generateIndexConfig,
|
|
66
71
|
generateTypes: () => generateTypes,
|
|
72
|
+
isAncestorUid: () => isAncestorUid,
|
|
67
73
|
jsonSchemaToFieldMeta: () => jsonSchemaToFieldMeta,
|
|
74
|
+
matchScope: () => matchScope,
|
|
75
|
+
matchScopeAny: () => matchScopeAny,
|
|
76
|
+
resolveAncestorCollection: () => resolveAncestorCollection,
|
|
68
77
|
resolveView: () => resolveView
|
|
69
78
|
});
|
|
70
79
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -77,6 +86,16 @@ var import_node_crypto = require("crypto");
|
|
|
77
86
|
|
|
78
87
|
// src/internal/constants.ts
|
|
79
88
|
var NODE_RELATION = "is";
|
|
89
|
+
var DEFAULT_QUERY_LIMIT = 500;
|
|
90
|
+
var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
|
|
91
|
+
"aType",
|
|
92
|
+
"aUid",
|
|
93
|
+
"axbType",
|
|
94
|
+
"bType",
|
|
95
|
+
"bUid",
|
|
96
|
+
"createdAt",
|
|
97
|
+
"updatedAt"
|
|
98
|
+
]);
|
|
80
99
|
var SHARD_SEPARATOR = ":";
|
|
81
100
|
|
|
82
101
|
// src/docid.ts
|
|
@@ -173,6 +192,21 @@ var DynamicRegistryError = class extends FiregraphError {
|
|
|
173
192
|
this.name = "DynamicRegistryError";
|
|
174
193
|
}
|
|
175
194
|
};
|
|
195
|
+
var QuerySafetyError = class extends FiregraphError {
|
|
196
|
+
constructor(message) {
|
|
197
|
+
super(message, "QUERY_SAFETY");
|
|
198
|
+
this.name = "QuerySafetyError";
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var RegistryScopeError = class extends FiregraphError {
|
|
202
|
+
constructor(aType, axbType, bType, scopePath, allowedIn) {
|
|
203
|
+
super(
|
|
204
|
+
`Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope "${scopePath || "root"}". Allowed in: [${allowedIn.join(", ")}]`,
|
|
205
|
+
"REGISTRY_SCOPE"
|
|
206
|
+
);
|
|
207
|
+
this.name = "RegistryScopeError";
|
|
208
|
+
}
|
|
209
|
+
};
|
|
176
210
|
|
|
177
211
|
// src/query.ts
|
|
178
212
|
function buildEdgeQueryPlan(params) {
|
|
@@ -186,27 +220,32 @@ function buildEdgeQueryPlan(params) {
|
|
|
186
220
|
if (axbType) filters.push({ field: "axbType", op: "==", value: axbType });
|
|
187
221
|
if (bType) filters.push({ field: "bType", op: "==", value: bType });
|
|
188
222
|
if (bUid) filters.push({ field: "bUid", op: "==", value: bUid });
|
|
189
|
-
const builtinFields = ["aType", "aUid", "axbType", "bType", "bUid", "createdAt", "updatedAt"];
|
|
190
223
|
if (params.where) {
|
|
191
224
|
for (const clause of params.where) {
|
|
192
|
-
const field =
|
|
225
|
+
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
193
226
|
filters.push({ field, op: clause.op, value: clause.value });
|
|
194
227
|
}
|
|
195
228
|
}
|
|
196
229
|
if (filters.length === 0) {
|
|
197
230
|
throw new InvalidQueryError("findEdges requires at least one filter parameter");
|
|
198
231
|
}
|
|
199
|
-
const
|
|
200
|
-
return { strategy: "query", filters, options };
|
|
232
|
+
const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
|
|
233
|
+
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
201
234
|
}
|
|
202
235
|
function buildNodeQueryPlan(params) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
236
|
+
const { aType, limit, orderBy } = params;
|
|
237
|
+
const filters = [
|
|
238
|
+
{ field: "aType", op: "==", value: aType },
|
|
239
|
+
{ field: "axbType", op: "==", value: NODE_RELATION }
|
|
240
|
+
];
|
|
241
|
+
if (params.where) {
|
|
242
|
+
for (const clause of params.where) {
|
|
243
|
+
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
244
|
+
filters.push({ field, op: clause.op, value: clause.value });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const effectiveLimit = limit === void 0 ? DEFAULT_QUERY_LIMIT : limit || void 0;
|
|
248
|
+
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
210
249
|
}
|
|
211
250
|
|
|
212
251
|
// src/internal/firestore-adapter.ts
|
|
@@ -359,10 +398,62 @@ function createPipelineQueryAdapter(db, collectionPath) {
|
|
|
359
398
|
|
|
360
399
|
// src/transaction.ts
|
|
361
400
|
var import_firestore2 = require("@google-cloud/firestore");
|
|
401
|
+
|
|
402
|
+
// src/query-safety.ts
|
|
403
|
+
var SAFE_INDEX_PATTERNS = [
|
|
404
|
+
/* @__PURE__ */ new Set(["aUid", "axbType"]),
|
|
405
|
+
/* @__PURE__ */ new Set(["axbType", "bUid"]),
|
|
406
|
+
/* @__PURE__ */ new Set(["aType", "axbType"]),
|
|
407
|
+
/* @__PURE__ */ new Set(["axbType", "bType"])
|
|
408
|
+
];
|
|
409
|
+
function analyzeQuerySafety(filters) {
|
|
410
|
+
const builtinFieldsPresent = /* @__PURE__ */ new Set();
|
|
411
|
+
let hasDataFilters = false;
|
|
412
|
+
for (const f of filters) {
|
|
413
|
+
if (BUILTIN_FIELDS.has(f.field)) {
|
|
414
|
+
builtinFieldsPresent.add(f.field);
|
|
415
|
+
} else {
|
|
416
|
+
hasDataFilters = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const pattern of SAFE_INDEX_PATTERNS) {
|
|
420
|
+
let matched = true;
|
|
421
|
+
for (const field of pattern) {
|
|
422
|
+
if (!builtinFieldsPresent.has(field)) {
|
|
423
|
+
matched = false;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (matched) {
|
|
428
|
+
return { safe: true };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const presentFields = [...builtinFieldsPresent];
|
|
432
|
+
if (presentFields.length === 0 && hasDataFilters) {
|
|
433
|
+
return {
|
|
434
|
+
safe: false,
|
|
435
|
+
reason: "Query filters only use data.* fields with no builtin field constraints. This requires a full collection scan. Add aType, aUid, axbType, bType, or bUid filters, or set allowCollectionScan: true."
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (hasDataFilters) {
|
|
439
|
+
return {
|
|
440
|
+
safe: false,
|
|
441
|
+
reason: `Query filters on [${presentFields.join(", ")}] do not match any indexed pattern. data.* filters without an indexed base require a full collection scan. Safe patterns: (aUid + axbType), (axbType + bUid), (aType + axbType), (axbType + bType). Set allowCollectionScan: true to override.`
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
safe: false,
|
|
446
|
+
reason: `Query filters on [${presentFields.join(", ")}] do not match any indexed pattern. This may cause a full collection scan on Firestore Enterprise. Safe patterns: (aUid + axbType), (axbType + bUid), (aType + axbType), (axbType + bType). Set allowCollectionScan: true to override.`
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/transaction.ts
|
|
362
451
|
var GraphTransactionImpl = class {
|
|
363
|
-
constructor(adapter, registry) {
|
|
452
|
+
constructor(adapter, registry, scanProtection = "error", scopePath = "") {
|
|
364
453
|
this.adapter = adapter;
|
|
365
454
|
this.registry = registry;
|
|
455
|
+
this.scanProtection = scanProtection;
|
|
456
|
+
this.scopePath = scopePath;
|
|
366
457
|
}
|
|
367
458
|
async getNode(uid) {
|
|
368
459
|
const docId = computeNodeDocId(uid);
|
|
@@ -376,12 +467,22 @@ var GraphTransactionImpl = class {
|
|
|
376
467
|
const record = await this.getEdge(aUid, axbType, bUid);
|
|
377
468
|
return record !== null;
|
|
378
469
|
}
|
|
470
|
+
checkQuerySafety(filters, allowCollectionScan) {
|
|
471
|
+
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
472
|
+
const result = analyzeQuerySafety(filters);
|
|
473
|
+
if (result.safe) return;
|
|
474
|
+
if (this.scanProtection === "error") {
|
|
475
|
+
throw new QuerySafetyError(result.reason);
|
|
476
|
+
}
|
|
477
|
+
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
478
|
+
}
|
|
379
479
|
async findEdges(params) {
|
|
380
480
|
const plan = buildEdgeQueryPlan(params);
|
|
381
481
|
if (plan.strategy === "get") {
|
|
382
482
|
const record = await this.adapter.getDoc(plan.docId);
|
|
383
483
|
return record ? [record] : [];
|
|
384
484
|
}
|
|
485
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
385
486
|
return this.adapter.query(plan.filters, plan.options);
|
|
386
487
|
}
|
|
387
488
|
async findNodes(params) {
|
|
@@ -390,11 +491,12 @@ var GraphTransactionImpl = class {
|
|
|
390
491
|
const record = await this.adapter.getDoc(plan.docId);
|
|
391
492
|
return record ? [record] : [];
|
|
392
493
|
}
|
|
494
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
393
495
|
return this.adapter.query(plan.filters, plan.options);
|
|
394
496
|
}
|
|
395
497
|
async putNode(aType, uid, data) {
|
|
396
498
|
if (this.registry) {
|
|
397
|
-
this.registry.validate(aType, NODE_RELATION, aType, data);
|
|
499
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
398
500
|
}
|
|
399
501
|
const docId = computeNodeDocId(uid);
|
|
400
502
|
const record = buildNodeRecord(aType, uid, data);
|
|
@@ -402,7 +504,7 @@ var GraphTransactionImpl = class {
|
|
|
402
504
|
}
|
|
403
505
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
404
506
|
if (this.registry) {
|
|
405
|
-
this.registry.validate(aType, axbType, bType, data);
|
|
507
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
406
508
|
}
|
|
407
509
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
408
510
|
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
@@ -428,13 +530,14 @@ var GraphTransactionImpl = class {
|
|
|
428
530
|
// src/batch.ts
|
|
429
531
|
var import_firestore3 = require("@google-cloud/firestore");
|
|
430
532
|
var GraphBatchImpl = class {
|
|
431
|
-
constructor(adapter, registry) {
|
|
533
|
+
constructor(adapter, registry, scopePath = "") {
|
|
432
534
|
this.adapter = adapter;
|
|
433
535
|
this.registry = registry;
|
|
536
|
+
this.scopePath = scopePath;
|
|
434
537
|
}
|
|
435
538
|
async putNode(aType, uid, data) {
|
|
436
539
|
if (this.registry) {
|
|
437
|
-
this.registry.validate(aType, NODE_RELATION, aType, data);
|
|
540
|
+
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
438
541
|
}
|
|
439
542
|
const docId = computeNodeDocId(uid);
|
|
440
543
|
const record = buildNodeRecord(aType, uid, data);
|
|
@@ -442,7 +545,7 @@ var GraphBatchImpl = class {
|
|
|
442
545
|
}
|
|
443
546
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
444
547
|
if (this.registry) {
|
|
445
|
-
this.registry.validate(aType, axbType, bType, data);
|
|
548
|
+
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
446
549
|
}
|
|
447
550
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
448
551
|
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
@@ -534,14 +637,39 @@ async function bulkDeleteDocIds(db, collectionPath, docIds, options) {
|
|
|
534
637
|
return { deleted, batches: completedBatches, errors };
|
|
535
638
|
}
|
|
536
639
|
async function bulkRemoveEdges(db, collectionPath, reader, params, options) {
|
|
537
|
-
const
|
|
640
|
+
const effectiveParams = params.limit !== void 0 ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true } : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };
|
|
641
|
+
const edges = await reader.findEdges(effectiveParams);
|
|
538
642
|
const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
539
643
|
return bulkDeleteDocIds(db, collectionPath, docIds, options);
|
|
540
644
|
}
|
|
645
|
+
async function deleteSubcollectionsRecursive(db, collectionPath, docId, options) {
|
|
646
|
+
const docRef = db.collection(collectionPath).doc(docId);
|
|
647
|
+
const subcollections = await docRef.listCollections();
|
|
648
|
+
if (subcollections.length === 0) return { deleted: 0, errors: [] };
|
|
649
|
+
let totalDeleted = 0;
|
|
650
|
+
const allErrors = [];
|
|
651
|
+
const subOptions = options ? { batchSize: options.batchSize, maxRetries: options.maxRetries } : void 0;
|
|
652
|
+
for (const subCollRef of subcollections) {
|
|
653
|
+
const subCollPath = subCollRef.path;
|
|
654
|
+
const snapshot = await subCollRef.select().get();
|
|
655
|
+
const subDocIds = snapshot.docs.map((d) => d.id);
|
|
656
|
+
for (const subDocId of subDocIds) {
|
|
657
|
+
const subResult = await deleteSubcollectionsRecursive(db, subCollPath, subDocId, subOptions);
|
|
658
|
+
totalDeleted += subResult.deleted;
|
|
659
|
+
allErrors.push(...subResult.errors);
|
|
660
|
+
}
|
|
661
|
+
if (subDocIds.length > 0) {
|
|
662
|
+
const result = await bulkDeleteDocIds(db, subCollPath, subDocIds, subOptions);
|
|
663
|
+
totalDeleted += result.deleted;
|
|
664
|
+
allErrors.push(...result.errors);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return { deleted: totalDeleted, errors: allErrors };
|
|
668
|
+
}
|
|
541
669
|
async function removeNodeCascade(db, collectionPath, reader, uid, options) {
|
|
542
670
|
const [outgoingRaw, incomingRaw] = await Promise.all([
|
|
543
|
-
reader.findEdges({ aUid: uid }),
|
|
544
|
-
reader.findEdges({ bUid: uid })
|
|
671
|
+
reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),
|
|
672
|
+
reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 })
|
|
545
673
|
]);
|
|
546
674
|
const outgoing = outgoingRaw.filter((e) => e.axbType !== NODE_RELATION);
|
|
547
675
|
const incoming = incomingRaw.filter((e) => e.axbType !== NODE_RELATION);
|
|
@@ -554,8 +682,18 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
|
|
|
554
682
|
allEdges.push(edge);
|
|
555
683
|
}
|
|
556
684
|
}
|
|
557
|
-
const
|
|
685
|
+
const shouldDeleteSubcollections = options?.deleteSubcollections !== false;
|
|
558
686
|
const nodeDocId = computeNodeDocId(uid);
|
|
687
|
+
let subcollectionResult = { deleted: 0, errors: [] };
|
|
688
|
+
if (shouldDeleteSubcollections) {
|
|
689
|
+
subcollectionResult = await deleteSubcollectionsRecursive(
|
|
690
|
+
db,
|
|
691
|
+
collectionPath,
|
|
692
|
+
nodeDocId,
|
|
693
|
+
options
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
const edgeDocIds = allEdges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
|
|
559
697
|
const allDocIds = [...edgeDocIds, nodeDocId];
|
|
560
698
|
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
561
699
|
const result = await bulkDeleteDocIds(db, collectionPath, allDocIds, {
|
|
@@ -565,9 +703,12 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
|
|
|
565
703
|
const totalChunks = Math.ceil(allDocIds.length / batchSize);
|
|
566
704
|
const nodeChunkIndex = totalChunks - 1;
|
|
567
705
|
const nodeDeleted = !result.errors.some((e) => e.batchIndex === nodeChunkIndex);
|
|
706
|
+
const topLevelEdgesDeleted = nodeDeleted ? result.deleted - 1 : result.deleted;
|
|
568
707
|
return {
|
|
569
|
-
|
|
570
|
-
|
|
708
|
+
deleted: result.deleted + subcollectionResult.deleted,
|
|
709
|
+
batches: result.batches,
|
|
710
|
+
errors: [...result.errors, ...subcollectionResult.errors],
|
|
711
|
+
edgesDeleted: topLevelEdgesDeleted,
|
|
571
712
|
nodeDeleted
|
|
572
713
|
};
|
|
573
714
|
}
|
|
@@ -667,6 +808,39 @@ function propertyToFieldMeta(name, prop, required) {
|
|
|
667
808
|
return { name, type: "unknown", required, description: prop.description };
|
|
668
809
|
}
|
|
669
810
|
|
|
811
|
+
// src/scope.ts
|
|
812
|
+
function matchScope(scopePath, pattern) {
|
|
813
|
+
if (pattern === "root") return scopePath === "";
|
|
814
|
+
if (pattern === "**") return true;
|
|
815
|
+
const pathSegments = scopePath === "" ? [] : scopePath.split("/");
|
|
816
|
+
const patternSegments = pattern.split("/");
|
|
817
|
+
return matchSegments(pathSegments, 0, patternSegments, 0);
|
|
818
|
+
}
|
|
819
|
+
function matchScopeAny(scopePath, patterns) {
|
|
820
|
+
if (!patterns || patterns.length === 0) return true;
|
|
821
|
+
return patterns.some((p) => matchScope(scopePath, p));
|
|
822
|
+
}
|
|
823
|
+
function matchSegments(path, pi, pattern, qi) {
|
|
824
|
+
if (pi === path.length && qi === pattern.length) return true;
|
|
825
|
+
if (qi === pattern.length) return false;
|
|
826
|
+
const seg = pattern[qi];
|
|
827
|
+
if (seg === "**") {
|
|
828
|
+
if (qi === pattern.length - 1) return true;
|
|
829
|
+
for (let skip = 0; skip <= path.length - pi; skip++) {
|
|
830
|
+
if (matchSegments(path, pi + skip, pattern, qi + 1)) return true;
|
|
831
|
+
}
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
if (pi === path.length) return false;
|
|
835
|
+
if (seg === "*") {
|
|
836
|
+
return matchSegments(path, pi + 1, pattern, qi + 1);
|
|
837
|
+
}
|
|
838
|
+
if (path[pi] === seg) {
|
|
839
|
+
return matchSegments(path, pi + 1, pattern, qi + 1);
|
|
840
|
+
}
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
|
|
670
844
|
// src/registry.ts
|
|
671
845
|
function tripleKey(aType, axbType, bType) {
|
|
672
846
|
return `${aType}:${axbType}:${bType}`;
|
|
@@ -681,19 +855,45 @@ function createRegistry(input) {
|
|
|
681
855
|
}
|
|
682
856
|
const entryList = Object.freeze([...entries]);
|
|
683
857
|
for (const entry of entries) {
|
|
858
|
+
if (entry.targetGraph && entry.targetGraph.includes("/")) {
|
|
859
|
+
throw new ValidationError(
|
|
860
|
+
`Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
684
863
|
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
685
864
|
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
686
865
|
map.set(key, { entry, validate: validator });
|
|
687
866
|
}
|
|
867
|
+
const axbIndex = /* @__PURE__ */ new Map();
|
|
868
|
+
const axbBuild = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const entry of entries) {
|
|
870
|
+
const existing = axbBuild.get(entry.axbType);
|
|
871
|
+
if (existing) {
|
|
872
|
+
existing.push(entry);
|
|
873
|
+
} else {
|
|
874
|
+
axbBuild.set(entry.axbType, [entry]);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
for (const [key, arr] of axbBuild) {
|
|
878
|
+
axbIndex.set(key, Object.freeze(arr));
|
|
879
|
+
}
|
|
688
880
|
return {
|
|
689
881
|
lookup(aType, axbType, bType) {
|
|
690
882
|
return map.get(tripleKey(aType, axbType, bType))?.entry;
|
|
691
883
|
},
|
|
692
|
-
|
|
884
|
+
lookupByAxbType(axbType) {
|
|
885
|
+
return axbIndex.get(axbType) ?? [];
|
|
886
|
+
},
|
|
887
|
+
validate(aType, axbType, bType, data, scopePath) {
|
|
693
888
|
const rec = map.get(tripleKey(aType, axbType, bType));
|
|
694
889
|
if (!rec) {
|
|
695
890
|
throw new RegistryViolationError(aType, axbType, bType);
|
|
696
891
|
}
|
|
892
|
+
if (scopePath !== void 0 && rec.entry.allowedIn && rec.entry.allowedIn.length > 0) {
|
|
893
|
+
if (!matchScopeAny(scopePath, rec.entry.allowedIn)) {
|
|
894
|
+
throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
697
897
|
if (rec.validate) {
|
|
698
898
|
try {
|
|
699
899
|
rec.validate(data);
|
|
@@ -721,7 +921,8 @@ function discoveryToEntries(discovery) {
|
|
|
721
921
|
jsonSchema: entity.schema,
|
|
722
922
|
description: entity.description,
|
|
723
923
|
titleField: entity.titleField,
|
|
724
|
-
subtitleField: entity.subtitleField
|
|
924
|
+
subtitleField: entity.subtitleField,
|
|
925
|
+
allowedIn: entity.allowedIn
|
|
725
926
|
});
|
|
726
927
|
}
|
|
727
928
|
for (const [axbType, entity] of discovery.edges) {
|
|
@@ -729,6 +930,12 @@ function discoveryToEntries(discovery) {
|
|
|
729
930
|
if (!topology) continue;
|
|
730
931
|
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
731
932
|
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
933
|
+
const resolvedTargetGraph = entity.targetGraph ?? topology.targetGraph;
|
|
934
|
+
if (resolvedTargetGraph && resolvedTargetGraph.includes("/")) {
|
|
935
|
+
throw new ValidationError(
|
|
936
|
+
`Edge "${axbType}" has invalid targetGraph "${resolvedTargetGraph}" \u2014 must be a single segment (no "/")`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
732
939
|
for (const aType of fromTypes) {
|
|
733
940
|
for (const bType of toTypes) {
|
|
734
941
|
entries.push({
|
|
@@ -739,7 +946,9 @@ function discoveryToEntries(discovery) {
|
|
|
739
946
|
description: entity.description,
|
|
740
947
|
inverseLabel: topology.inverseLabel,
|
|
741
948
|
titleField: entity.titleField,
|
|
742
|
-
subtitleField: entity.subtitleField
|
|
949
|
+
subtitleField: entity.subtitleField,
|
|
950
|
+
allowedIn: entity.allowedIn,
|
|
951
|
+
targetGraph: resolvedTargetGraph
|
|
743
952
|
});
|
|
744
953
|
}
|
|
745
954
|
}
|
|
@@ -760,7 +969,8 @@ var NODE_TYPE_SCHEMA = {
|
|
|
760
969
|
titleField: { type: "string" },
|
|
761
970
|
subtitleField: { type: "string" },
|
|
762
971
|
viewTemplate: { type: "string" },
|
|
763
|
-
viewCss: { type: "string" }
|
|
972
|
+
viewCss: { type: "string" },
|
|
973
|
+
allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
|
|
764
974
|
},
|
|
765
975
|
additionalProperties: false
|
|
766
976
|
};
|
|
@@ -787,7 +997,9 @@ var EDGE_TYPE_SCHEMA = {
|
|
|
787
997
|
titleField: { type: "string" },
|
|
788
998
|
subtitleField: { type: "string" },
|
|
789
999
|
viewTemplate: { type: "string" },
|
|
790
|
-
viewCss: { type: "string" }
|
|
1000
|
+
viewCss: { type: "string" },
|
|
1001
|
+
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
1002
|
+
targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
|
|
791
1003
|
},
|
|
792
1004
|
additionalProperties: false
|
|
793
1005
|
};
|
|
@@ -829,7 +1041,8 @@ async function createRegistryFromGraph(reader) {
|
|
|
829
1041
|
jsonSchema: data.jsonSchema,
|
|
830
1042
|
description: data.description,
|
|
831
1043
|
titleField: data.titleField,
|
|
832
|
-
subtitleField: data.subtitleField
|
|
1044
|
+
subtitleField: data.subtitleField,
|
|
1045
|
+
allowedIn: data.allowedIn
|
|
833
1046
|
});
|
|
834
1047
|
}
|
|
835
1048
|
for (const record of edgeTypes) {
|
|
@@ -846,7 +1059,9 @@ async function createRegistryFromGraph(reader) {
|
|
|
846
1059
|
description: data.description,
|
|
847
1060
|
inverseLabel: data.inverseLabel,
|
|
848
1061
|
titleField: data.titleField,
|
|
849
|
-
subtitleField: data.subtitleField
|
|
1062
|
+
subtitleField: data.subtitleField,
|
|
1063
|
+
allowedIn: data.allowedIn,
|
|
1064
|
+
targetGraph: data.targetGraph
|
|
850
1065
|
});
|
|
851
1066
|
}
|
|
852
1067
|
}
|
|
@@ -857,9 +1072,10 @@ async function createRegistryFromGraph(reader) {
|
|
|
857
1072
|
// src/client.ts
|
|
858
1073
|
var _standardModeWarned = false;
|
|
859
1074
|
var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
|
|
860
|
-
var GraphClientImpl = class {
|
|
861
|
-
constructor(db, collectionPath, options) {
|
|
1075
|
+
var GraphClientImpl = class _GraphClientImpl {
|
|
1076
|
+
constructor(db, collectionPath, options, scopePath = "") {
|
|
862
1077
|
this.db = db;
|
|
1078
|
+
this.scopePath = scopePath;
|
|
863
1079
|
this.adapter = createFirestoreAdapter(db, collectionPath);
|
|
864
1080
|
if (options?.registry && options?.registryMode) {
|
|
865
1081
|
throw new DynamicRegistryError(
|
|
@@ -889,6 +1105,7 @@ var GraphClientImpl = class {
|
|
|
889
1105
|
"[firegraph] Standard query mode enabled. This is NOT recommended for production:\n - Enterprise Firestore: data.* filters cause full collection scans (high billing)\n - Standard Firestore: data.* filters without composite indexes will fail\n See: https://github.com/typicalday/firegraph#query-modes"
|
|
890
1106
|
);
|
|
891
1107
|
}
|
|
1108
|
+
this.scanProtection = options?.scanProtection ?? "error";
|
|
892
1109
|
if (this.queryMode === "pipeline") {
|
|
893
1110
|
this.pipelineAdapter = createPipelineQueryAdapter(db, collectionPath);
|
|
894
1111
|
if (this.metaAdapter) {
|
|
@@ -902,6 +1119,7 @@ var GraphClientImpl = class {
|
|
|
902
1119
|
adapter;
|
|
903
1120
|
pipelineAdapter;
|
|
904
1121
|
queryMode;
|
|
1122
|
+
scanProtection;
|
|
905
1123
|
// Static mode
|
|
906
1124
|
staticRegistry;
|
|
907
1125
|
// Dynamic mode
|
|
@@ -910,6 +1128,8 @@ var GraphClientImpl = class {
|
|
|
910
1128
|
dynamicRegistry;
|
|
911
1129
|
metaAdapter;
|
|
912
1130
|
metaPipelineAdapter;
|
|
1131
|
+
// Subgraph scope tracking
|
|
1132
|
+
scopePath;
|
|
913
1133
|
// ---------------------------------------------------------------------------
|
|
914
1134
|
// Registry routing
|
|
915
1135
|
// ---------------------------------------------------------------------------
|
|
@@ -963,6 +1183,19 @@ var GraphClientImpl = class {
|
|
|
963
1183
|
}
|
|
964
1184
|
return this.adapter.query(filters, options);
|
|
965
1185
|
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Check whether a query's filter set is safe (matches a known index pattern).
|
|
1188
|
+
* Throws QuerySafetyError or logs a warning depending on scanProtection config.
|
|
1189
|
+
*/
|
|
1190
|
+
checkQuerySafety(filters, allowCollectionScan) {
|
|
1191
|
+
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
1192
|
+
const result = analyzeQuerySafety(filters);
|
|
1193
|
+
if (result.safe) return;
|
|
1194
|
+
if (this.scanProtection === "error") {
|
|
1195
|
+
throw new QuerySafetyError(result.reason);
|
|
1196
|
+
}
|
|
1197
|
+
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1198
|
+
}
|
|
966
1199
|
// ---------------------------------------------------------------------------
|
|
967
1200
|
// GraphReader
|
|
968
1201
|
// ---------------------------------------------------------------------------
|
|
@@ -984,6 +1217,7 @@ var GraphClientImpl = class {
|
|
|
984
1217
|
const record = await this.adapter.getDoc(plan.docId);
|
|
985
1218
|
return record ? [record] : [];
|
|
986
1219
|
}
|
|
1220
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
987
1221
|
return this.executeQuery(plan.filters, plan.options);
|
|
988
1222
|
}
|
|
989
1223
|
async findNodes(params) {
|
|
@@ -992,6 +1226,7 @@ var GraphClientImpl = class {
|
|
|
992
1226
|
const record = await this.adapter.getDoc(plan.docId);
|
|
993
1227
|
return record ? [record] : [];
|
|
994
1228
|
}
|
|
1229
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
995
1230
|
return this.executeQuery(plan.filters, plan.options);
|
|
996
1231
|
}
|
|
997
1232
|
// ---------------------------------------------------------------------------
|
|
@@ -1000,7 +1235,7 @@ var GraphClientImpl = class {
|
|
|
1000
1235
|
async putNode(aType, uid, data) {
|
|
1001
1236
|
const registry = this.getRegistryForType(aType);
|
|
1002
1237
|
if (registry) {
|
|
1003
|
-
registry.validate(aType, NODE_RELATION, aType, data);
|
|
1238
|
+
registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
1004
1239
|
}
|
|
1005
1240
|
const adapter = this.getAdapterForType(aType);
|
|
1006
1241
|
const docId = computeNodeDocId(uid);
|
|
@@ -1010,7 +1245,7 @@ var GraphClientImpl = class {
|
|
|
1010
1245
|
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
1011
1246
|
const registry = this.getRegistryForType(aType);
|
|
1012
1247
|
if (registry) {
|
|
1013
|
-
registry.validate(aType, axbType, bType, data);
|
|
1248
|
+
registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
1014
1249
|
}
|
|
1015
1250
|
const adapter = this.getAdapterForType(aType);
|
|
1016
1251
|
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
@@ -1042,13 +1277,69 @@ var GraphClientImpl = class {
|
|
|
1042
1277
|
this.adapter.collectionPath,
|
|
1043
1278
|
firestoreTx
|
|
1044
1279
|
);
|
|
1045
|
-
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry());
|
|
1280
|
+
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath);
|
|
1046
1281
|
return fn(graphTx);
|
|
1047
1282
|
});
|
|
1048
1283
|
}
|
|
1049
1284
|
batch() {
|
|
1050
1285
|
const adapter = createBatchAdapter(this.db, this.adapter.collectionPath);
|
|
1051
|
-
return new GraphBatchImpl(adapter, this.getCombinedRegistry());
|
|
1286
|
+
return new GraphBatchImpl(adapter, this.getCombinedRegistry(), this.scopePath);
|
|
1287
|
+
}
|
|
1288
|
+
// ---------------------------------------------------------------------------
|
|
1289
|
+
// Subgraph
|
|
1290
|
+
// ---------------------------------------------------------------------------
|
|
1291
|
+
subgraph(parentNodeUid, name = "graph") {
|
|
1292
|
+
if (!parentNodeUid || parentNodeUid.includes("/")) {
|
|
1293
|
+
throw new FiregraphError(
|
|
1294
|
+
`Invalid parentNodeUid for subgraph: "${parentNodeUid}". Must be a non-empty string without "/".`,
|
|
1295
|
+
"INVALID_SUBGRAPH"
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
if (name.includes("/")) {
|
|
1299
|
+
throw new FiregraphError(
|
|
1300
|
+
`Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
1301
|
+
"INVALID_SUBGRAPH"
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
const subCollectionPath = `${this.adapter.collectionPath}/${parentNodeUid}/${name}`;
|
|
1305
|
+
const newScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
1306
|
+
return new _GraphClientImpl(
|
|
1307
|
+
this.db,
|
|
1308
|
+
subCollectionPath,
|
|
1309
|
+
{
|
|
1310
|
+
registry: this.getCombinedRegistry(),
|
|
1311
|
+
queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
|
|
1312
|
+
scanProtection: this.scanProtection
|
|
1313
|
+
},
|
|
1314
|
+
newScopePath
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
// ---------------------------------------------------------------------------
|
|
1318
|
+
// Collection group query
|
|
1319
|
+
// ---------------------------------------------------------------------------
|
|
1320
|
+
async findEdgesGlobal(params, collectionName) {
|
|
1321
|
+
const name = collectionName ?? this.adapter.collectionPath.split("/").pop();
|
|
1322
|
+
const plan = buildEdgeQueryPlan(params);
|
|
1323
|
+
if (plan.strategy === "get") {
|
|
1324
|
+
throw new FiregraphError(
|
|
1325
|
+
"findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
|
|
1326
|
+
"INVALID_QUERY"
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1330
|
+
const collectionGroupRef = this.db.collectionGroup(name);
|
|
1331
|
+
let q = collectionGroupRef;
|
|
1332
|
+
for (const f of plan.filters) {
|
|
1333
|
+
q = q.where(f.field, f.op, f.value);
|
|
1334
|
+
}
|
|
1335
|
+
if (plan.options?.orderBy) {
|
|
1336
|
+
q = q.orderBy(plan.options.orderBy.field, plan.options.orderBy.direction ?? "asc");
|
|
1337
|
+
}
|
|
1338
|
+
if (plan.options?.limit !== void 0) {
|
|
1339
|
+
q = q.limit(plan.options.limit);
|
|
1340
|
+
}
|
|
1341
|
+
const snap = await q.get();
|
|
1342
|
+
return snap.docs.map((doc) => doc.data());
|
|
1052
1343
|
}
|
|
1053
1344
|
// ---------------------------------------------------------------------------
|
|
1054
1345
|
// Bulk operations
|
|
@@ -1080,6 +1371,7 @@ var GraphClientImpl = class {
|
|
|
1080
1371
|
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
1081
1372
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1082
1373
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1374
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
1083
1375
|
await this.putNode(META_NODE_TYPE, uid, data);
|
|
1084
1376
|
}
|
|
1085
1377
|
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
@@ -1101,11 +1393,13 @@ var GraphClientImpl = class {
|
|
|
1101
1393
|
};
|
|
1102
1394
|
if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
|
|
1103
1395
|
if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
|
|
1396
|
+
if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
|
|
1104
1397
|
if (description !== void 0) data.description = description;
|
|
1105
1398
|
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
1106
1399
|
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
1107
1400
|
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
1108
1401
|
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
1402
|
+
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
1109
1403
|
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
1110
1404
|
}
|
|
1111
1405
|
async reloadRegistry() {
|
|
@@ -1174,6 +1468,10 @@ function generateId() {
|
|
|
1174
1468
|
var DEFAULT_LIMIT = 10;
|
|
1175
1469
|
var DEFAULT_MAX_READS = 100;
|
|
1176
1470
|
var DEFAULT_CONCURRENCY = 5;
|
|
1471
|
+
var _crossGraphWarned = false;
|
|
1472
|
+
function isGraphClient(reader) {
|
|
1473
|
+
return "subgraph" in reader && typeof reader.subgraph === "function";
|
|
1474
|
+
}
|
|
1177
1475
|
var Semaphore = class {
|
|
1178
1476
|
constructor(slots) {
|
|
1179
1477
|
this.slots = slots;
|
|
@@ -1199,9 +1497,10 @@ var Semaphore = class {
|
|
|
1199
1497
|
}
|
|
1200
1498
|
};
|
|
1201
1499
|
var TraversalBuilderImpl = class {
|
|
1202
|
-
constructor(reader, startUid) {
|
|
1500
|
+
constructor(reader, startUid, registry) {
|
|
1203
1501
|
this.reader = reader;
|
|
1204
1502
|
this.startUid = startUid;
|
|
1503
|
+
this.registry = registry;
|
|
1205
1504
|
}
|
|
1206
1505
|
hops = [];
|
|
1207
1506
|
follow(axbType, options) {
|
|
@@ -1218,11 +1517,13 @@ var TraversalBuilderImpl = class {
|
|
|
1218
1517
|
const semaphore = new Semaphore(concurrency);
|
|
1219
1518
|
let totalReads = 0;
|
|
1220
1519
|
let truncated = false;
|
|
1221
|
-
let
|
|
1520
|
+
let sources = [
|
|
1521
|
+
{ uid: this.startUid, reader: this.reader }
|
|
1522
|
+
];
|
|
1222
1523
|
const hopResults = [];
|
|
1223
1524
|
for (let depth = 0; depth < this.hops.length; depth++) {
|
|
1224
1525
|
const hop = this.hops[depth];
|
|
1225
|
-
if (
|
|
1526
|
+
if (sources.length === 0) {
|
|
1226
1527
|
hopResults.push({
|
|
1227
1528
|
axbType: hop.axbType,
|
|
1228
1529
|
depth,
|
|
@@ -1233,9 +1534,12 @@ var TraversalBuilderImpl = class {
|
|
|
1233
1534
|
continue;
|
|
1234
1535
|
}
|
|
1235
1536
|
const hopEdges = [];
|
|
1236
|
-
const sourceCount =
|
|
1537
|
+
const sourceCount = sources.length;
|
|
1237
1538
|
let hopTruncated = false;
|
|
1238
|
-
const
|
|
1539
|
+
const resolvedTargetGraph = this.resolveTargetGraph(hop);
|
|
1540
|
+
const direction = hop.direction ?? "forward";
|
|
1541
|
+
const isCrossGraph = direction === "forward" && !!resolvedTargetGraph;
|
|
1542
|
+
const tasks = sources.map(({ uid, reader: sourceReader }) => async () => {
|
|
1239
1543
|
if (totalReads >= maxReads) {
|
|
1240
1544
|
hopTruncated = true;
|
|
1241
1545
|
return;
|
|
@@ -1247,51 +1551,79 @@ var TraversalBuilderImpl = class {
|
|
|
1247
1551
|
return;
|
|
1248
1552
|
}
|
|
1249
1553
|
totalReads++;
|
|
1250
|
-
const direction2 = hop.direction ?? "forward";
|
|
1251
1554
|
const params = { axbType: hop.axbType };
|
|
1252
|
-
if (
|
|
1555
|
+
if (direction === "forward") {
|
|
1253
1556
|
params.aUid = uid;
|
|
1254
1557
|
if (hop.bType) params.bType = hop.bType;
|
|
1255
1558
|
} else {
|
|
1256
1559
|
params.bUid = uid;
|
|
1257
1560
|
if (hop.aType) params.aType = hop.aType;
|
|
1258
1561
|
}
|
|
1259
|
-
if (
|
|
1562
|
+
if (direction === "forward" && hop.aType) {
|
|
1260
1563
|
params.aType = hop.aType;
|
|
1261
1564
|
}
|
|
1262
|
-
if (
|
|
1565
|
+
if (direction === "reverse" && hop.bType) {
|
|
1263
1566
|
params.bType = hop.bType;
|
|
1264
1567
|
}
|
|
1265
1568
|
if (hop.orderBy) params.orderBy = hop.orderBy;
|
|
1266
1569
|
const limit = hop.limit ?? DEFAULT_LIMIT;
|
|
1267
|
-
if (
|
|
1570
|
+
if (hop.filter) {
|
|
1571
|
+
params.limit = 0;
|
|
1572
|
+
} else {
|
|
1268
1573
|
params.limit = limit;
|
|
1269
1574
|
}
|
|
1270
|
-
let
|
|
1575
|
+
let hopReader;
|
|
1576
|
+
let nextReader;
|
|
1577
|
+
if (isCrossGraph) {
|
|
1578
|
+
if (isGraphClient(this.reader)) {
|
|
1579
|
+
hopReader = this.reader.subgraph(uid, resolvedTargetGraph);
|
|
1580
|
+
nextReader = hopReader;
|
|
1581
|
+
} else {
|
|
1582
|
+
hopReader = sourceReader;
|
|
1583
|
+
nextReader = sourceReader;
|
|
1584
|
+
if (!_crossGraphWarned) {
|
|
1585
|
+
_crossGraphWarned = true;
|
|
1586
|
+
console.warn(
|
|
1587
|
+
`[firegraph] Traversal hop "${hop.axbType}" has targetGraph "${resolvedTargetGraph}" but the reader does not support subgraph(). Cross-graph hop will query the current collection instead. Pass a GraphClient to createTraversal() to enable cross-graph traversal.`
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
} else {
|
|
1592
|
+
hopReader = sourceReader;
|
|
1593
|
+
nextReader = sourceReader;
|
|
1594
|
+
}
|
|
1595
|
+
let edges2 = await hopReader.findEdges(params);
|
|
1271
1596
|
if (hop.filter) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1597
|
+
edges2 = edges2.filter(hop.filter);
|
|
1598
|
+
edges2 = edges2.slice(0, limit);
|
|
1599
|
+
}
|
|
1600
|
+
for (const edge of edges2) {
|
|
1601
|
+
hopEdges.push({ edge, reader: nextReader });
|
|
1274
1602
|
}
|
|
1275
|
-
hopEdges.push(...edges);
|
|
1276
1603
|
} finally {
|
|
1277
1604
|
semaphore.release();
|
|
1278
1605
|
}
|
|
1279
1606
|
});
|
|
1280
1607
|
await Promise.all(tasks.map((task) => task()));
|
|
1608
|
+
const edges = hopEdges.map((h) => h.edge);
|
|
1281
1609
|
hopResults.push({
|
|
1282
1610
|
axbType: hop.axbType,
|
|
1283
1611
|
depth,
|
|
1284
|
-
edges: returnIntermediates ? [...
|
|
1612
|
+
edges: returnIntermediates ? [...edges] : edges,
|
|
1285
1613
|
sourceCount,
|
|
1286
1614
|
truncated: hopTruncated
|
|
1287
1615
|
});
|
|
1288
1616
|
if (hopTruncated) {
|
|
1289
1617
|
truncated = true;
|
|
1290
1618
|
}
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1619
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1620
|
+
for (const { edge, reader: edgeReader } of hopEdges) {
|
|
1621
|
+
const nextUid = direction === "forward" ? edge.bUid : edge.aUid;
|
|
1622
|
+
if (!seen.has(nextUid)) {
|
|
1623
|
+
seen.set(nextUid, edgeReader);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
sources = [...seen.entries()].map(([uid, reader]) => ({ uid, reader }));
|
|
1295
1627
|
}
|
|
1296
1628
|
const lastHop = hopResults[hopResults.length - 1];
|
|
1297
1629
|
return {
|
|
@@ -1301,9 +1633,25 @@ var TraversalBuilderImpl = class {
|
|
|
1301
1633
|
truncated
|
|
1302
1634
|
};
|
|
1303
1635
|
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Resolve the targetGraph for a hop. Priority:
|
|
1638
|
+
* 1. Explicit `hop.targetGraph` (user override)
|
|
1639
|
+
* 2. Registry `targetGraph` for the axbType (if registry available)
|
|
1640
|
+
* 3. undefined (no cross-graph)
|
|
1641
|
+
*/
|
|
1642
|
+
resolveTargetGraph(hop) {
|
|
1643
|
+
if (hop.targetGraph) return hop.targetGraph;
|
|
1644
|
+
if (this.registry) {
|
|
1645
|
+
const entries = this.registry.lookupByAxbType(hop.axbType);
|
|
1646
|
+
for (const entry of entries) {
|
|
1647
|
+
if (entry.targetGraph) return entry.targetGraph;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return void 0;
|
|
1651
|
+
}
|
|
1304
1652
|
};
|
|
1305
|
-
function createTraversal(reader, startUid) {
|
|
1306
|
-
return new TraversalBuilderImpl(reader, startUid);
|
|
1653
|
+
function createTraversal(reader, startUid, registry) {
|
|
1654
|
+
return new TraversalBuilderImpl(reader, startUid, registry);
|
|
1307
1655
|
}
|
|
1308
1656
|
|
|
1309
1657
|
// src/views.ts
|
|
@@ -1515,7 +1863,8 @@ function loadNodeEntity(dir, name) {
|
|
|
1515
1863
|
subtitleField: meta?.subtitleField,
|
|
1516
1864
|
viewDefaults: meta?.viewDefaults,
|
|
1517
1865
|
viewsPath,
|
|
1518
|
-
sampleData
|
|
1866
|
+
sampleData,
|
|
1867
|
+
allowedIn: meta?.allowedIn
|
|
1519
1868
|
};
|
|
1520
1869
|
}
|
|
1521
1870
|
function loadEdgeEntity(dir, name) {
|
|
@@ -1550,7 +1899,9 @@ function loadEdgeEntity(dir, name) {
|
|
|
1550
1899
|
subtitleField: meta?.subtitleField,
|
|
1551
1900
|
viewDefaults: meta?.viewDefaults,
|
|
1552
1901
|
viewsPath,
|
|
1553
|
-
sampleData
|
|
1902
|
+
sampleData,
|
|
1903
|
+
allowedIn: meta?.allowedIn,
|
|
1904
|
+
targetGraph: topology.targetGraph ?? meta?.targetGraph
|
|
1554
1905
|
};
|
|
1555
1906
|
}
|
|
1556
1907
|
function getSubdirectories(dir) {
|
|
@@ -1593,6 +1944,20 @@ function discoverEntities(entitiesDir) {
|
|
|
1593
1944
|
};
|
|
1594
1945
|
}
|
|
1595
1946
|
|
|
1947
|
+
// src/cross-graph.ts
|
|
1948
|
+
function resolveAncestorCollection(collectionPath, uid) {
|
|
1949
|
+
const segments = collectionPath.split("/");
|
|
1950
|
+
for (let i = 1; i < segments.length; i += 2) {
|
|
1951
|
+
if (segments[i] === uid) {
|
|
1952
|
+
return segments.slice(0, i).join("/");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return null;
|
|
1956
|
+
}
|
|
1957
|
+
function isAncestorUid(collectionPath, uid) {
|
|
1958
|
+
return resolveAncestorCollection(collectionPath, uid) !== null;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1596
1961
|
// src/codegen/index.ts
|
|
1597
1962
|
function pascalCase(s) {
|
|
1598
1963
|
return s.replace(
|
|
@@ -1636,6 +2001,130 @@ async function generateTypes(discovery, options = {}) {
|
|
|
1636
2001
|
return chunks.join("\n").trimEnd() + "\n";
|
|
1637
2002
|
}
|
|
1638
2003
|
|
|
2004
|
+
// src/indexes.ts
|
|
2005
|
+
function baseIndexes(collection) {
|
|
2006
|
+
return [
|
|
2007
|
+
{
|
|
2008
|
+
collectionGroup: collection,
|
|
2009
|
+
queryScope: "COLLECTION",
|
|
2010
|
+
fields: [
|
|
2011
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
2012
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2013
|
+
]
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
collectionGroup: collection,
|
|
2017
|
+
queryScope: "COLLECTION",
|
|
2018
|
+
fields: [
|
|
2019
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2020
|
+
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
2021
|
+
]
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
collectionGroup: collection,
|
|
2025
|
+
queryScope: "COLLECTION",
|
|
2026
|
+
fields: [
|
|
2027
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
2028
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2029
|
+
]
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
collectionGroup: collection,
|
|
2033
|
+
queryScope: "COLLECTION",
|
|
2034
|
+
fields: [
|
|
2035
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2036
|
+
{ fieldPath: "bType", order: "ASCENDING" }
|
|
2037
|
+
]
|
|
2038
|
+
}
|
|
2039
|
+
];
|
|
2040
|
+
}
|
|
2041
|
+
function extractSchemaFields(schema) {
|
|
2042
|
+
const s = schema;
|
|
2043
|
+
if (s.type !== "object" || !s.properties) return [];
|
|
2044
|
+
return Object.keys(s.properties);
|
|
2045
|
+
}
|
|
2046
|
+
function collectionGroupIndexes(collectionName) {
|
|
2047
|
+
return [
|
|
2048
|
+
{
|
|
2049
|
+
collectionGroup: collectionName,
|
|
2050
|
+
queryScope: "COLLECTION_GROUP",
|
|
2051
|
+
fields: [
|
|
2052
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
2053
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2054
|
+
]
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
collectionGroup: collectionName,
|
|
2058
|
+
queryScope: "COLLECTION_GROUP",
|
|
2059
|
+
fields: [
|
|
2060
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2061
|
+
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
2062
|
+
]
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
collectionGroup: collectionName,
|
|
2066
|
+
queryScope: "COLLECTION_GROUP",
|
|
2067
|
+
fields: [
|
|
2068
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
2069
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2070
|
+
]
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
collectionGroup: collectionName,
|
|
2074
|
+
queryScope: "COLLECTION_GROUP",
|
|
2075
|
+
fields: [
|
|
2076
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2077
|
+
{ fieldPath: "bType", order: "ASCENDING" }
|
|
2078
|
+
]
|
|
2079
|
+
}
|
|
2080
|
+
];
|
|
2081
|
+
}
|
|
2082
|
+
function generateIndexConfig(collection, entities, registryEntries) {
|
|
2083
|
+
const indexes = baseIndexes(collection);
|
|
2084
|
+
if (entities) {
|
|
2085
|
+
for (const [, entity] of entities.nodes) {
|
|
2086
|
+
const fields = extractSchemaFields(entity.schema);
|
|
2087
|
+
for (const field of fields) {
|
|
2088
|
+
indexes.push({
|
|
2089
|
+
collectionGroup: collection,
|
|
2090
|
+
queryScope: "COLLECTION",
|
|
2091
|
+
fields: [
|
|
2092
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
2093
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2094
|
+
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
2095
|
+
]
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
for (const [, entity] of entities.edges) {
|
|
2100
|
+
const fields = extractSchemaFields(entity.schema);
|
|
2101
|
+
for (const field of fields) {
|
|
2102
|
+
indexes.push({
|
|
2103
|
+
collectionGroup: collection,
|
|
2104
|
+
queryScope: "COLLECTION",
|
|
2105
|
+
fields: [
|
|
2106
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
2107
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2108
|
+
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
2109
|
+
]
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
if (registryEntries) {
|
|
2115
|
+
const targetGraphNames = /* @__PURE__ */ new Set();
|
|
2116
|
+
for (const entry of registryEntries) {
|
|
2117
|
+
if (entry.targetGraph) {
|
|
2118
|
+
targetGraphNames.add(entry.targetGraph);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
for (const name of targetGraphNames) {
|
|
2122
|
+
indexes.push(...collectionGroupIndexes(name));
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return { indexes, fieldOverrides: [] };
|
|
2126
|
+
}
|
|
2127
|
+
|
|
1639
2128
|
// src/query-client/client.ts
|
|
1640
2129
|
var import_node_http = __toESM(require("http"), 1);
|
|
1641
2130
|
|
|
@@ -1914,6 +2403,7 @@ var QueryClient = class {
|
|
|
1914
2403
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1915
2404
|
0 && (module.exports = {
|
|
1916
2405
|
BOOTSTRAP_ENTRIES,
|
|
2406
|
+
DEFAULT_QUERY_LIMIT,
|
|
1917
2407
|
DiscoveryError,
|
|
1918
2408
|
DynamicRegistryError,
|
|
1919
2409
|
EDGE_TYPE_SCHEMA,
|
|
@@ -1926,9 +2416,12 @@ var QueryClient = class {
|
|
|
1926
2416
|
NodeNotFoundError,
|
|
1927
2417
|
QueryClient,
|
|
1928
2418
|
QueryClientError,
|
|
2419
|
+
QuerySafetyError,
|
|
2420
|
+
RegistryScopeError,
|
|
1929
2421
|
RegistryViolationError,
|
|
1930
2422
|
TraversalError,
|
|
1931
2423
|
ValidationError,
|
|
2424
|
+
analyzeQuerySafety,
|
|
1932
2425
|
buildEdgeQueryPlan,
|
|
1933
2426
|
buildEdgeRecord,
|
|
1934
2427
|
buildNodeQueryPlan,
|
|
@@ -1946,8 +2439,13 @@ var QueryClient = class {
|
|
|
1946
2439
|
discoverEntities,
|
|
1947
2440
|
generateDeterministicUid,
|
|
1948
2441
|
generateId,
|
|
2442
|
+
generateIndexConfig,
|
|
1949
2443
|
generateTypes,
|
|
2444
|
+
isAncestorUid,
|
|
1950
2445
|
jsonSchemaToFieldMeta,
|
|
2446
|
+
matchScope,
|
|
2447
|
+
matchScopeAny,
|
|
2448
|
+
resolveAncestorCollection,
|
|
1951
2449
|
resolveView
|
|
1952
2450
|
});
|
|
1953
2451
|
//# sourceMappingURL=index.cjs.map
|