@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/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 = builtinFields.includes(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.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 options = limit !== void 0 || orderBy ? { limit, orderBy } : void 0;
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
- return {
204
- strategy: "query",
205
- filters: [
206
- { field: "aType", op: "==", value: params.aType },
207
- { field: "axbType", op: "==", value: NODE_RELATION }
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 edges = await reader.findEdges(params);
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 edgeDocIds = allEdges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));
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
- ...result,
570
- edgesDeleted: nodeDeleted ? result.deleted - 1 : result.deleted,
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
- validate(aType, axbType, bType, data) {
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 sourceUids = [this.startUid];
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 (sourceUids.length === 0) {
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 = sourceUids.length;
1537
+ const sourceCount = sources.length;
1237
1538
  let hopTruncated = false;
1238
- const tasks = sourceUids.map((uid) => async () => {
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 (direction2 === "forward") {
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 (direction2 === "forward" && hop.aType) {
1562
+ if (direction === "forward" && hop.aType) {
1260
1563
  params.aType = hop.aType;
1261
1564
  }
1262
- if (direction2 === "reverse" && hop.bType) {
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 (!hop.filter) {
1570
+ if (hop.filter) {
1571
+ params.limit = 0;
1572
+ } else {
1268
1573
  params.limit = limit;
1269
1574
  }
1270
- let edges = await this.reader.findEdges(params);
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
- edges = edges.filter(hop.filter);
1273
- edges = edges.slice(0, limit);
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 ? [...hopEdges] : hopEdges,
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 direction = hop.direction ?? "forward";
1292
- sourceUids = [...new Set(
1293
- hopEdges.map((e) => direction === "forward" ? e.bUid : e.aUid)
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