@typicalday/firegraph 0.2.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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Firestore } from '@google-cloud/firestore';
2
- import { G as GraphClientOptions, D as DynamicRegistryConfig, a as DynamicGraphClient, b as GraphClient, R as RegistryEntry, c as DiscoveryResult, d as GraphRegistry, e as GraphReader, f as GraphRecord, F as FindEdgesParams, Q as QueryPlan, g as FindNodesParams, T as TraversalBuilder, h as QueryFilter } from './index-wSlVH5Nv.cjs';
3
- export { B as BulkBatchError, i as BulkOptions, j as BulkProgress, k as BulkResult, C as CascadeResult, l as CodegenOptions, m as DefineTypeOptions, n as DiscoveredEntity, E as EdgeTopology, o as EdgeTypeData, p as FiregraphConfig, q as GraphBatch, r as GraphTransaction, s as GraphWriter, H as HopDefinition, t as HopResult, N as NodeTypeData, u as QueryMode, v as QueryOptions, S as ScanProtection, w as StoredGraphRecord, x as TraversalOptions, y as TraversalResult, V as ViewContext, z as ViewDefaultsConfig, A as ViewResolverConfig, W as WhereClause, I as defineConfig, J as generateTypes, K as resolveView } from './index-wSlVH5Nv.cjs';
2
+ import { G as GraphClientOptions, D as DynamicRegistryConfig, a as DynamicGraphClient, b as GraphClient, R as RegistryEntry, c as DiscoveryResult, d as GraphRegistry, e as GraphReader, f as GraphRecord, F as FindEdgesParams, Q as QueryPlan, g as FindNodesParams, T as TraversalBuilder, h as QueryFilter } from './index-CQkofEC_.cjs';
3
+ export { B as BulkBatchError, i as BulkOptions, j as BulkProgress, k as BulkResult, C as CascadeResult, l as CodegenOptions, m as DefineTypeOptions, n as DiscoveredEntity, E as EdgeTopology, o as EdgeTypeData, p as FiregraphConfig, q as GraphBatch, r as GraphTransaction, s as GraphWriter, H as HopDefinition, t as HopResult, N as NodeTypeData, u as QueryMode, v as QueryOptions, S as ScanProtection, w as StoredGraphRecord, x as TraversalOptions, y as TraversalResult, V as ViewContext, z as ViewDefaultsConfig, A as ViewResolverConfig, W as WhereClause, I as defineConfig, J as generateTypes, K as resolveView } from './index-CQkofEC_.cjs';
4
4
  export { E as EntityViewConfig, a as EntityViewMeta, V as ViewComponentClass, b as ViewMeta, c as ViewRegistry, d as ViewRegistryInput, e as defineViews } from './views-DL60k0cf.cjs';
5
5
  export { Q as QueryClient, a as QueryClientError, b as QueryClientErrorCode, c as QueryClientOptions } from './client-Bk2Cm6xv.cjs';
6
6
 
@@ -71,7 +71,18 @@ declare function buildEdgeRecord(aType: string, aUid: string, axbType: string, b
71
71
  declare function buildEdgeQueryPlan(params: FindEdgesParams): QueryPlan;
72
72
  declare function buildNodeQueryPlan(params: FindNodesParams): QueryPlan;
73
73
 
74
- declare function createTraversal(reader: GraphReader, startUid: string): TraversalBuilder;
74
+ /**
75
+ * Create a traversal builder for multi-hop graph traversal.
76
+ *
77
+ * Accepts either a `GraphReader` (backwards compatible) or a `GraphClient`.
78
+ * When a `GraphClient` is provided, cross-graph traversal via `targetGraph`
79
+ * is supported — the traversal can follow edges into subgraphs.
80
+ *
81
+ * @param reader - A `GraphClient` or `GraphReader` to execute queries against
82
+ * @param startUid - UID of the starting node
83
+ * @param registry - Optional registry for automatic `targetGraph` resolution
84
+ */
85
+ declare function createTraversal(reader: GraphClient | GraphReader, startUid: string, registry?: GraphRegistry): TraversalBuilder;
75
86
 
76
87
  declare class FiregraphError extends Error {
77
88
  readonly code: string;
@@ -187,6 +198,52 @@ declare function matchScope(scopePath: string, pattern: string): boolean;
187
198
  */
188
199
  declare function matchScopeAny(scopePath: string, patterns: string[]): boolean;
189
200
 
201
+ /**
202
+ * Cross-graph edge resolution utilities.
203
+ *
204
+ * Provides path-scanning resolution for determining whether an edge's source
205
+ * (aUid) is an ancestor node by checking if the UID appears in the Firestore
206
+ * collection path.
207
+ *
208
+ * Firestore paths have a rigid alternating structure:
209
+ * collection / docId / collection / docId / collection
210
+ *
211
+ * Given a path like `graph/A/workspace/B/context`, segments at even indices
212
+ * are collection names and odd indices are document IDs. When we find a UID
213
+ * at an odd index, the collection containing that document is the path up to
214
+ * (and including) the preceding even-index segment.
215
+ */
216
+ /**
217
+ * Parse a Firestore collection path and determine the collection path
218
+ * where a given UID's document lives, if that UID is an ancestor in the path.
219
+ *
220
+ * @param collectionPath - The full Firestore collection path of the current client
221
+ * @param uid - The UID to search for in the path
222
+ * @returns The collection path containing the UID, or `null` if not found in the path
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * // Path: graph/A/workspace/B/context
227
+ * resolveAncestorCollection('graph/A/workspace/B/context', 'A')
228
+ * // → 'graph'
229
+ *
230
+ * resolveAncestorCollection('graph/A/workspace/B/context', 'B')
231
+ * // → 'graph/A/workspace'
232
+ *
233
+ * resolveAncestorCollection('graph/A/workspace/B/context', 'unknown')
234
+ * // → null
235
+ * ```
236
+ */
237
+ declare function resolveAncestorCollection(collectionPath: string, uid: string): string | null;
238
+ /**
239
+ * Check whether a UID belongs to an ancestor node by scanning the collection path.
240
+ *
241
+ * @param collectionPath - The full Firestore collection path of the current client
242
+ * @param uid - The UID to check
243
+ * @returns `true` if the UID appears as a document segment in the path
244
+ */
245
+ declare function isAncestorUid(collectionPath: string, uid: string): boolean;
246
+
190
247
  /**
191
248
  * JSON Schema validation and introspection utilities.
192
249
  *
@@ -226,7 +283,7 @@ interface FirestoreIndexField {
226
283
  }
227
284
  interface FirestoreIndex {
228
285
  collectionGroup: string;
229
- queryScope: 'COLLECTION';
286
+ queryScope: 'COLLECTION' | 'COLLECTION_GROUP';
230
287
  fields: FirestoreIndexField[];
231
288
  }
232
289
  interface FirestoreIndexConfig {
@@ -241,10 +298,17 @@ interface FirestoreIndexConfig {
241
298
  * patterns on node data fields:
242
299
  * (aType, axbType, data.{field})
243
300
  *
301
+ * When registry entries with `targetGraph` are provided, also generates
302
+ * collection group indexes for `findEdgesGlobal()` queries. The collection
303
+ * group name defaults to `'graph'` (the standard subgraph name) but can be
304
+ * overridden per `targetGraph` value.
305
+ *
244
306
  * @param collection - Firestore collection name (e.g. 'graph')
245
307
  * @param entities - Optional discovery result for per-entity data field indexes
308
+ * @param registryEntries - Optional registry entries; when any have `targetGraph`,
309
+ * collection group indexes are generated for the distinct subgraph names
246
310
  */
247
- declare function generateIndexConfig(collection: string, entities?: DiscoveryResult): FirestoreIndexConfig;
311
+ declare function generateIndexConfig(collection: string, entities?: DiscoveryResult, registryEntries?: ReadonlyArray<RegistryEntry>): FirestoreIndexConfig;
248
312
 
249
313
  /**
250
314
  * Result of analyzing a query for collection scan risk.
@@ -272,4 +336,4 @@ declare function analyzeQuerySafety(filters: QueryFilter[]): QuerySafetyResult;
272
336
  */
273
337
  declare const DEFAULT_QUERY_LIMIT = 500;
274
338
 
275
- export { BOOTSTRAP_ENTRIES, DEFAULT_QUERY_LIMIT, type DiscoverResult, DiscoveryError, DiscoveryResult, type DiscoveryWarning, DynamicGraphClient, DynamicRegistryConfig, DynamicRegistryError, EDGE_TYPE_SCHEMA, EdgeNotFoundError, type FieldMeta, FindEdgesParams, FindNodesParams, FiregraphError, type FirestoreIndex, type FirestoreIndexConfig, type FirestoreIndexField, GraphClient, GraphClientOptions, GraphReader, GraphRecord, GraphRegistry, InvalidQueryError, META_EDGE_TYPE, META_NODE_TYPE, NODE_TYPE_SCHEMA, NodeNotFoundError, QueryFilter, QueryPlan, QuerySafetyError, type QuerySafetyResult, RegistryEntry, RegistryScopeError, RegistryViolationError, TraversalBuilder, TraversalError, ValidationError, analyzeQuerySafety, buildEdgeQueryPlan, buildEdgeRecord, buildNodeQueryPlan, buildNodeRecord, compileSchema, computeEdgeDocId, computeNodeDocId, createBootstrapRegistry, createGraphClient, createRegistry, createRegistryFromGraph, createTraversal, discoverEntities, generateDeterministicUid, generateId, generateIndexConfig, jsonSchemaToFieldMeta, matchScope, matchScopeAny };
339
+ export { BOOTSTRAP_ENTRIES, DEFAULT_QUERY_LIMIT, type DiscoverResult, DiscoveryError, DiscoveryResult, type DiscoveryWarning, DynamicGraphClient, DynamicRegistryConfig, DynamicRegistryError, EDGE_TYPE_SCHEMA, EdgeNotFoundError, type FieldMeta, FindEdgesParams, FindNodesParams, FiregraphError, type FirestoreIndex, type FirestoreIndexConfig, type FirestoreIndexField, GraphClient, GraphClientOptions, GraphReader, GraphRecord, GraphRegistry, InvalidQueryError, META_EDGE_TYPE, META_NODE_TYPE, NODE_TYPE_SCHEMA, NodeNotFoundError, QueryFilter, QueryPlan, QuerySafetyError, type QuerySafetyResult, RegistryEntry, RegistryScopeError, RegistryViolationError, TraversalBuilder, TraversalError, ValidationError, analyzeQuerySafety, buildEdgeQueryPlan, buildEdgeRecord, buildNodeQueryPlan, buildNodeRecord, compileSchema, computeEdgeDocId, computeNodeDocId, createBootstrapRegistry, createGraphClient, createRegistry, createRegistryFromGraph, createTraversal, discoverEntities, generateDeterministicUid, generateId, generateIndexConfig, isAncestorUid, jsonSchemaToFieldMeta, matchScope, matchScopeAny, resolveAncestorCollection };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Firestore } from '@google-cloud/firestore';
2
- import { G as GraphClientOptions, D as DynamicRegistryConfig, a as DynamicGraphClient, b as GraphClient, R as RegistryEntry, c as DiscoveryResult, d as GraphRegistry, e as GraphReader, f as GraphRecord, F as FindEdgesParams, Q as QueryPlan, g as FindNodesParams, T as TraversalBuilder, h as QueryFilter } from './index-wSlVH5Nv.js';
3
- export { B as BulkBatchError, i as BulkOptions, j as BulkProgress, k as BulkResult, C as CascadeResult, l as CodegenOptions, m as DefineTypeOptions, n as DiscoveredEntity, E as EdgeTopology, o as EdgeTypeData, p as FiregraphConfig, q as GraphBatch, r as GraphTransaction, s as GraphWriter, H as HopDefinition, t as HopResult, N as NodeTypeData, u as QueryMode, v as QueryOptions, S as ScanProtection, w as StoredGraphRecord, x as TraversalOptions, y as TraversalResult, V as ViewContext, z as ViewDefaultsConfig, A as ViewResolverConfig, W as WhereClause, I as defineConfig, J as generateTypes, K as resolveView } from './index-wSlVH5Nv.js';
2
+ import { G as GraphClientOptions, D as DynamicRegistryConfig, a as DynamicGraphClient, b as GraphClient, R as RegistryEntry, c as DiscoveryResult, d as GraphRegistry, e as GraphReader, f as GraphRecord, F as FindEdgesParams, Q as QueryPlan, g as FindNodesParams, T as TraversalBuilder, h as QueryFilter } from './index-CQkofEC_.js';
3
+ export { B as BulkBatchError, i as BulkOptions, j as BulkProgress, k as BulkResult, C as CascadeResult, l as CodegenOptions, m as DefineTypeOptions, n as DiscoveredEntity, E as EdgeTopology, o as EdgeTypeData, p as FiregraphConfig, q as GraphBatch, r as GraphTransaction, s as GraphWriter, H as HopDefinition, t as HopResult, N as NodeTypeData, u as QueryMode, v as QueryOptions, S as ScanProtection, w as StoredGraphRecord, x as TraversalOptions, y as TraversalResult, V as ViewContext, z as ViewDefaultsConfig, A as ViewResolverConfig, W as WhereClause, I as defineConfig, J as generateTypes, K as resolveView } from './index-CQkofEC_.js';
4
4
  export { E as EntityViewConfig, a as EntityViewMeta, V as ViewComponentClass, b as ViewMeta, c as ViewRegistry, d as ViewRegistryInput, e as defineViews } from './views-DL60k0cf.js';
5
5
  export { Q as QueryClient, a as QueryClientError, b as QueryClientErrorCode, c as QueryClientOptions } from './client-Bk2Cm6xv.js';
6
6
 
@@ -71,7 +71,18 @@ declare function buildEdgeRecord(aType: string, aUid: string, axbType: string, b
71
71
  declare function buildEdgeQueryPlan(params: FindEdgesParams): QueryPlan;
72
72
  declare function buildNodeQueryPlan(params: FindNodesParams): QueryPlan;
73
73
 
74
- declare function createTraversal(reader: GraphReader, startUid: string): TraversalBuilder;
74
+ /**
75
+ * Create a traversal builder for multi-hop graph traversal.
76
+ *
77
+ * Accepts either a `GraphReader` (backwards compatible) or a `GraphClient`.
78
+ * When a `GraphClient` is provided, cross-graph traversal via `targetGraph`
79
+ * is supported — the traversal can follow edges into subgraphs.
80
+ *
81
+ * @param reader - A `GraphClient` or `GraphReader` to execute queries against
82
+ * @param startUid - UID of the starting node
83
+ * @param registry - Optional registry for automatic `targetGraph` resolution
84
+ */
85
+ declare function createTraversal(reader: GraphClient | GraphReader, startUid: string, registry?: GraphRegistry): TraversalBuilder;
75
86
 
76
87
  declare class FiregraphError extends Error {
77
88
  readonly code: string;
@@ -187,6 +198,52 @@ declare function matchScope(scopePath: string, pattern: string): boolean;
187
198
  */
188
199
  declare function matchScopeAny(scopePath: string, patterns: string[]): boolean;
189
200
 
201
+ /**
202
+ * Cross-graph edge resolution utilities.
203
+ *
204
+ * Provides path-scanning resolution for determining whether an edge's source
205
+ * (aUid) is an ancestor node by checking if the UID appears in the Firestore
206
+ * collection path.
207
+ *
208
+ * Firestore paths have a rigid alternating structure:
209
+ * collection / docId / collection / docId / collection
210
+ *
211
+ * Given a path like `graph/A/workspace/B/context`, segments at even indices
212
+ * are collection names and odd indices are document IDs. When we find a UID
213
+ * at an odd index, the collection containing that document is the path up to
214
+ * (and including) the preceding even-index segment.
215
+ */
216
+ /**
217
+ * Parse a Firestore collection path and determine the collection path
218
+ * where a given UID's document lives, if that UID is an ancestor in the path.
219
+ *
220
+ * @param collectionPath - The full Firestore collection path of the current client
221
+ * @param uid - The UID to search for in the path
222
+ * @returns The collection path containing the UID, or `null` if not found in the path
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * // Path: graph/A/workspace/B/context
227
+ * resolveAncestorCollection('graph/A/workspace/B/context', 'A')
228
+ * // → 'graph'
229
+ *
230
+ * resolveAncestorCollection('graph/A/workspace/B/context', 'B')
231
+ * // → 'graph/A/workspace'
232
+ *
233
+ * resolveAncestorCollection('graph/A/workspace/B/context', 'unknown')
234
+ * // → null
235
+ * ```
236
+ */
237
+ declare function resolveAncestorCollection(collectionPath: string, uid: string): string | null;
238
+ /**
239
+ * Check whether a UID belongs to an ancestor node by scanning the collection path.
240
+ *
241
+ * @param collectionPath - The full Firestore collection path of the current client
242
+ * @param uid - The UID to check
243
+ * @returns `true` if the UID appears as a document segment in the path
244
+ */
245
+ declare function isAncestorUid(collectionPath: string, uid: string): boolean;
246
+
190
247
  /**
191
248
  * JSON Schema validation and introspection utilities.
192
249
  *
@@ -226,7 +283,7 @@ interface FirestoreIndexField {
226
283
  }
227
284
  interface FirestoreIndex {
228
285
  collectionGroup: string;
229
- queryScope: 'COLLECTION';
286
+ queryScope: 'COLLECTION' | 'COLLECTION_GROUP';
230
287
  fields: FirestoreIndexField[];
231
288
  }
232
289
  interface FirestoreIndexConfig {
@@ -241,10 +298,17 @@ interface FirestoreIndexConfig {
241
298
  * patterns on node data fields:
242
299
  * (aType, axbType, data.{field})
243
300
  *
301
+ * When registry entries with `targetGraph` are provided, also generates
302
+ * collection group indexes for `findEdgesGlobal()` queries. The collection
303
+ * group name defaults to `'graph'` (the standard subgraph name) but can be
304
+ * overridden per `targetGraph` value.
305
+ *
244
306
  * @param collection - Firestore collection name (e.g. 'graph')
245
307
  * @param entities - Optional discovery result for per-entity data field indexes
308
+ * @param registryEntries - Optional registry entries; when any have `targetGraph`,
309
+ * collection group indexes are generated for the distinct subgraph names
246
310
  */
247
- declare function generateIndexConfig(collection: string, entities?: DiscoveryResult): FirestoreIndexConfig;
311
+ declare function generateIndexConfig(collection: string, entities?: DiscoveryResult, registryEntries?: ReadonlyArray<RegistryEntry>): FirestoreIndexConfig;
248
312
 
249
313
  /**
250
314
  * Result of analyzing a query for collection scan risk.
@@ -272,4 +336,4 @@ declare function analyzeQuerySafety(filters: QueryFilter[]): QuerySafetyResult;
272
336
  */
273
337
  declare const DEFAULT_QUERY_LIMIT = 500;
274
338
 
275
- export { BOOTSTRAP_ENTRIES, DEFAULT_QUERY_LIMIT, type DiscoverResult, DiscoveryError, DiscoveryResult, type DiscoveryWarning, DynamicGraphClient, DynamicRegistryConfig, DynamicRegistryError, EDGE_TYPE_SCHEMA, EdgeNotFoundError, type FieldMeta, FindEdgesParams, FindNodesParams, FiregraphError, type FirestoreIndex, type FirestoreIndexConfig, type FirestoreIndexField, GraphClient, GraphClientOptions, GraphReader, GraphRecord, GraphRegistry, InvalidQueryError, META_EDGE_TYPE, META_NODE_TYPE, NODE_TYPE_SCHEMA, NodeNotFoundError, QueryFilter, QueryPlan, QuerySafetyError, type QuerySafetyResult, RegistryEntry, RegistryScopeError, RegistryViolationError, TraversalBuilder, TraversalError, ValidationError, analyzeQuerySafety, buildEdgeQueryPlan, buildEdgeRecord, buildNodeQueryPlan, buildNodeRecord, compileSchema, computeEdgeDocId, computeNodeDocId, createBootstrapRegistry, createGraphClient, createRegistry, createRegistryFromGraph, createTraversal, discoverEntities, generateDeterministicUid, generateId, generateIndexConfig, jsonSchemaToFieldMeta, matchScope, matchScopeAny };
339
+ export { BOOTSTRAP_ENTRIES, DEFAULT_QUERY_LIMIT, type DiscoverResult, DiscoveryError, DiscoveryResult, type DiscoveryWarning, DynamicGraphClient, DynamicRegistryConfig, DynamicRegistryError, EDGE_TYPE_SCHEMA, EdgeNotFoundError, type FieldMeta, FindEdgesParams, FindNodesParams, FiregraphError, type FirestoreIndex, type FirestoreIndexConfig, type FirestoreIndexField, GraphClient, GraphClientOptions, GraphReader, GraphRecord, GraphRegistry, InvalidQueryError, META_EDGE_TYPE, META_NODE_TYPE, NODE_TYPE_SCHEMA, NodeNotFoundError, QueryFilter, QueryPlan, QuerySafetyError, type QuerySafetyResult, RegistryEntry, RegistryScopeError, RegistryViolationError, TraversalBuilder, TraversalError, ValidationError, analyzeQuerySafety, buildEdgeQueryPlan, buildEdgeRecord, buildNodeQueryPlan, buildNodeRecord, compileSchema, computeEdgeDocId, computeNodeDocId, createBootstrapRegistry, createGraphClient, createRegistry, createRegistryFromGraph, createTraversal, discoverEntities, generateDeterministicUid, generateId, generateIndexConfig, isAncestorUid, jsonSchemaToFieldMeta, matchScope, matchScopeAny, resolveAncestorCollection };
package/dist/index.js CHANGED
@@ -783,14 +783,35 @@ function createRegistry(input) {
783
783
  }
784
784
  const entryList = Object.freeze([...entries]);
785
785
  for (const entry of entries) {
786
+ if (entry.targetGraph && entry.targetGraph.includes("/")) {
787
+ throw new ValidationError(
788
+ `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
789
+ );
790
+ }
786
791
  const key = tripleKey(entry.aType, entry.axbType, entry.bType);
787
792
  const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
788
793
  map.set(key, { entry, validate: validator });
789
794
  }
795
+ const axbIndex = /* @__PURE__ */ new Map();
796
+ const axbBuild = /* @__PURE__ */ new Map();
797
+ for (const entry of entries) {
798
+ const existing = axbBuild.get(entry.axbType);
799
+ if (existing) {
800
+ existing.push(entry);
801
+ } else {
802
+ axbBuild.set(entry.axbType, [entry]);
803
+ }
804
+ }
805
+ for (const [key, arr] of axbBuild) {
806
+ axbIndex.set(key, Object.freeze(arr));
807
+ }
790
808
  return {
791
809
  lookup(aType, axbType, bType) {
792
810
  return map.get(tripleKey(aType, axbType, bType))?.entry;
793
811
  },
812
+ lookupByAxbType(axbType) {
813
+ return axbIndex.get(axbType) ?? [];
814
+ },
794
815
  validate(aType, axbType, bType, data, scopePath) {
795
816
  const rec = map.get(tripleKey(aType, axbType, bType));
796
817
  if (!rec) {
@@ -837,6 +858,12 @@ function discoveryToEntries(discovery) {
837
858
  if (!topology) continue;
838
859
  const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
839
860
  const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
861
+ const resolvedTargetGraph = entity.targetGraph ?? topology.targetGraph;
862
+ if (resolvedTargetGraph && resolvedTargetGraph.includes("/")) {
863
+ throw new ValidationError(
864
+ `Edge "${axbType}" has invalid targetGraph "${resolvedTargetGraph}" \u2014 must be a single segment (no "/")`
865
+ );
866
+ }
840
867
  for (const aType of fromTypes) {
841
868
  for (const bType of toTypes) {
842
869
  entries.push({
@@ -848,7 +875,8 @@ function discoveryToEntries(discovery) {
848
875
  inverseLabel: topology.inverseLabel,
849
876
  titleField: entity.titleField,
850
877
  subtitleField: entity.subtitleField,
851
- allowedIn: entity.allowedIn
878
+ allowedIn: entity.allowedIn,
879
+ targetGraph: resolvedTargetGraph
852
880
  });
853
881
  }
854
882
  }
@@ -898,7 +926,8 @@ var EDGE_TYPE_SCHEMA = {
898
926
  subtitleField: { type: "string" },
899
927
  viewTemplate: { type: "string" },
900
928
  viewCss: { type: "string" },
901
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
929
+ allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
930
+ targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
902
931
  },
903
932
  additionalProperties: false
904
933
  };
@@ -959,7 +988,8 @@ async function createRegistryFromGraph(reader) {
959
988
  inverseLabel: data.inverseLabel,
960
989
  titleField: data.titleField,
961
990
  subtitleField: data.subtitleField,
962
- allowedIn: data.allowedIn
991
+ allowedIn: data.allowedIn,
992
+ targetGraph: data.targetGraph
963
993
  });
964
994
  }
965
995
  }
@@ -1213,6 +1243,33 @@ var GraphClientImpl = class _GraphClientImpl {
1213
1243
  );
1214
1244
  }
1215
1245
  // ---------------------------------------------------------------------------
1246
+ // Collection group query
1247
+ // ---------------------------------------------------------------------------
1248
+ async findEdgesGlobal(params, collectionName) {
1249
+ const name = collectionName ?? this.adapter.collectionPath.split("/").pop();
1250
+ const plan = buildEdgeQueryPlan(params);
1251
+ if (plan.strategy === "get") {
1252
+ throw new FiregraphError(
1253
+ "findEdgesGlobal() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
1254
+ "INVALID_QUERY"
1255
+ );
1256
+ }
1257
+ this.checkQuerySafety(plan.filters, params.allowCollectionScan);
1258
+ const collectionGroupRef = this.db.collectionGroup(name);
1259
+ let q = collectionGroupRef;
1260
+ for (const f of plan.filters) {
1261
+ q = q.where(f.field, f.op, f.value);
1262
+ }
1263
+ if (plan.options?.orderBy) {
1264
+ q = q.orderBy(plan.options.orderBy.field, plan.options.orderBy.direction ?? "asc");
1265
+ }
1266
+ if (plan.options?.limit !== void 0) {
1267
+ q = q.limit(plan.options.limit);
1268
+ }
1269
+ const snap = await q.get();
1270
+ return snap.docs.map((doc) => doc.data());
1271
+ }
1272
+ // ---------------------------------------------------------------------------
1216
1273
  // Bulk operations
1217
1274
  // ---------------------------------------------------------------------------
1218
1275
  async removeNodeCascade(uid, options) {
@@ -1264,6 +1321,7 @@ var GraphClientImpl = class _GraphClientImpl {
1264
1321
  };
1265
1322
  if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
1266
1323
  if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
1324
+ if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
1267
1325
  if (description !== void 0) data.description = description;
1268
1326
  if (options?.titleField !== void 0) data.titleField = options.titleField;
1269
1327
  if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
@@ -1338,6 +1396,10 @@ function generateId() {
1338
1396
  var DEFAULT_LIMIT = 10;
1339
1397
  var DEFAULT_MAX_READS = 100;
1340
1398
  var DEFAULT_CONCURRENCY = 5;
1399
+ var _crossGraphWarned = false;
1400
+ function isGraphClient(reader) {
1401
+ return "subgraph" in reader && typeof reader.subgraph === "function";
1402
+ }
1341
1403
  var Semaphore = class {
1342
1404
  constructor(slots) {
1343
1405
  this.slots = slots;
@@ -1363,9 +1425,10 @@ var Semaphore = class {
1363
1425
  }
1364
1426
  };
1365
1427
  var TraversalBuilderImpl = class {
1366
- constructor(reader, startUid) {
1428
+ constructor(reader, startUid, registry) {
1367
1429
  this.reader = reader;
1368
1430
  this.startUid = startUid;
1431
+ this.registry = registry;
1369
1432
  }
1370
1433
  hops = [];
1371
1434
  follow(axbType, options) {
@@ -1382,11 +1445,13 @@ var TraversalBuilderImpl = class {
1382
1445
  const semaphore = new Semaphore(concurrency);
1383
1446
  let totalReads = 0;
1384
1447
  let truncated = false;
1385
- let sourceUids = [this.startUid];
1448
+ let sources = [
1449
+ { uid: this.startUid, reader: this.reader }
1450
+ ];
1386
1451
  const hopResults = [];
1387
1452
  for (let depth = 0; depth < this.hops.length; depth++) {
1388
1453
  const hop = this.hops[depth];
1389
- if (sourceUids.length === 0) {
1454
+ if (sources.length === 0) {
1390
1455
  hopResults.push({
1391
1456
  axbType: hop.axbType,
1392
1457
  depth,
@@ -1397,9 +1462,12 @@ var TraversalBuilderImpl = class {
1397
1462
  continue;
1398
1463
  }
1399
1464
  const hopEdges = [];
1400
- const sourceCount = sourceUids.length;
1465
+ const sourceCount = sources.length;
1401
1466
  let hopTruncated = false;
1402
- const tasks = sourceUids.map((uid) => async () => {
1467
+ const resolvedTargetGraph = this.resolveTargetGraph(hop);
1468
+ const direction = hop.direction ?? "forward";
1469
+ const isCrossGraph = direction === "forward" && !!resolvedTargetGraph;
1470
+ const tasks = sources.map(({ uid, reader: sourceReader }) => async () => {
1403
1471
  if (totalReads >= maxReads) {
1404
1472
  hopTruncated = true;
1405
1473
  return;
@@ -1411,19 +1479,18 @@ var TraversalBuilderImpl = class {
1411
1479
  return;
1412
1480
  }
1413
1481
  totalReads++;
1414
- const direction2 = hop.direction ?? "forward";
1415
1482
  const params = { axbType: hop.axbType };
1416
- if (direction2 === "forward") {
1483
+ if (direction === "forward") {
1417
1484
  params.aUid = uid;
1418
1485
  if (hop.bType) params.bType = hop.bType;
1419
1486
  } else {
1420
1487
  params.bUid = uid;
1421
1488
  if (hop.aType) params.aType = hop.aType;
1422
1489
  }
1423
- if (direction2 === "forward" && hop.aType) {
1490
+ if (direction === "forward" && hop.aType) {
1424
1491
  params.aType = hop.aType;
1425
1492
  }
1426
- if (direction2 === "reverse" && hop.bType) {
1493
+ if (direction === "reverse" && hop.bType) {
1427
1494
  params.bType = hop.bType;
1428
1495
  }
1429
1496
  if (hop.orderBy) params.orderBy = hop.orderBy;
@@ -1433,31 +1500,58 @@ var TraversalBuilderImpl = class {
1433
1500
  } else {
1434
1501
  params.limit = limit;
1435
1502
  }
1436
- let edges = await this.reader.findEdges(params);
1503
+ let hopReader;
1504
+ let nextReader;
1505
+ if (isCrossGraph) {
1506
+ if (isGraphClient(this.reader)) {
1507
+ hopReader = this.reader.subgraph(uid, resolvedTargetGraph);
1508
+ nextReader = hopReader;
1509
+ } else {
1510
+ hopReader = sourceReader;
1511
+ nextReader = sourceReader;
1512
+ if (!_crossGraphWarned) {
1513
+ _crossGraphWarned = true;
1514
+ console.warn(
1515
+ `[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.`
1516
+ );
1517
+ }
1518
+ }
1519
+ } else {
1520
+ hopReader = sourceReader;
1521
+ nextReader = sourceReader;
1522
+ }
1523
+ let edges2 = await hopReader.findEdges(params);
1437
1524
  if (hop.filter) {
1438
- edges = edges.filter(hop.filter);
1439
- edges = edges.slice(0, limit);
1525
+ edges2 = edges2.filter(hop.filter);
1526
+ edges2 = edges2.slice(0, limit);
1527
+ }
1528
+ for (const edge of edges2) {
1529
+ hopEdges.push({ edge, reader: nextReader });
1440
1530
  }
1441
- hopEdges.push(...edges);
1442
1531
  } finally {
1443
1532
  semaphore.release();
1444
1533
  }
1445
1534
  });
1446
1535
  await Promise.all(tasks.map((task) => task()));
1536
+ const edges = hopEdges.map((h) => h.edge);
1447
1537
  hopResults.push({
1448
1538
  axbType: hop.axbType,
1449
1539
  depth,
1450
- edges: returnIntermediates ? [...hopEdges] : hopEdges,
1540
+ edges: returnIntermediates ? [...edges] : edges,
1451
1541
  sourceCount,
1452
1542
  truncated: hopTruncated
1453
1543
  });
1454
1544
  if (hopTruncated) {
1455
1545
  truncated = true;
1456
1546
  }
1457
- const direction = hop.direction ?? "forward";
1458
- sourceUids = [...new Set(
1459
- hopEdges.map((e) => direction === "forward" ? e.bUid : e.aUid)
1460
- )];
1547
+ const seen = /* @__PURE__ */ new Map();
1548
+ for (const { edge, reader: edgeReader } of hopEdges) {
1549
+ const nextUid = direction === "forward" ? edge.bUid : edge.aUid;
1550
+ if (!seen.has(nextUid)) {
1551
+ seen.set(nextUid, edgeReader);
1552
+ }
1553
+ }
1554
+ sources = [...seen.entries()].map(([uid, reader]) => ({ uid, reader }));
1461
1555
  }
1462
1556
  const lastHop = hopResults[hopResults.length - 1];
1463
1557
  return {
@@ -1467,9 +1561,25 @@ var TraversalBuilderImpl = class {
1467
1561
  truncated
1468
1562
  };
1469
1563
  }
1564
+ /**
1565
+ * Resolve the targetGraph for a hop. Priority:
1566
+ * 1. Explicit `hop.targetGraph` (user override)
1567
+ * 2. Registry `targetGraph` for the axbType (if registry available)
1568
+ * 3. undefined (no cross-graph)
1569
+ */
1570
+ resolveTargetGraph(hop) {
1571
+ if (hop.targetGraph) return hop.targetGraph;
1572
+ if (this.registry) {
1573
+ const entries = this.registry.lookupByAxbType(hop.axbType);
1574
+ for (const entry of entries) {
1575
+ if (entry.targetGraph) return entry.targetGraph;
1576
+ }
1577
+ }
1578
+ return void 0;
1579
+ }
1470
1580
  };
1471
- function createTraversal(reader, startUid) {
1472
- return new TraversalBuilderImpl(reader, startUid);
1581
+ function createTraversal(reader, startUid, registry) {
1582
+ return new TraversalBuilderImpl(reader, startUid, registry);
1473
1583
  }
1474
1584
 
1475
1585
  // src/views.ts
@@ -1717,7 +1827,8 @@ function loadEdgeEntity(dir, name) {
1717
1827
  viewDefaults: meta?.viewDefaults,
1718
1828
  viewsPath,
1719
1829
  sampleData,
1720
- allowedIn: meta?.allowedIn
1830
+ allowedIn: meta?.allowedIn,
1831
+ targetGraph: topology.targetGraph ?? meta?.targetGraph
1721
1832
  };
1722
1833
  }
1723
1834
  function getSubdirectories(dir) {
@@ -1760,6 +1871,20 @@ function discoverEntities(entitiesDir) {
1760
1871
  };
1761
1872
  }
1762
1873
 
1874
+ // src/cross-graph.ts
1875
+ function resolveAncestorCollection(collectionPath, uid) {
1876
+ const segments = collectionPath.split("/");
1877
+ for (let i = 1; i < segments.length; i += 2) {
1878
+ if (segments[i] === uid) {
1879
+ return segments.slice(0, i).join("/");
1880
+ }
1881
+ }
1882
+ return null;
1883
+ }
1884
+ function isAncestorUid(collectionPath, uid) {
1885
+ return resolveAncestorCollection(collectionPath, uid) !== null;
1886
+ }
1887
+
1763
1888
  // src/indexes.ts
1764
1889
  function baseIndexes(collection) {
1765
1890
  return [
@@ -1802,7 +1927,43 @@ function extractSchemaFields(schema) {
1802
1927
  if (s.type !== "object" || !s.properties) return [];
1803
1928
  return Object.keys(s.properties);
1804
1929
  }
1805
- function generateIndexConfig(collection, entities) {
1930
+ function collectionGroupIndexes(collectionName) {
1931
+ return [
1932
+ {
1933
+ collectionGroup: collectionName,
1934
+ queryScope: "COLLECTION_GROUP",
1935
+ fields: [
1936
+ { fieldPath: "aUid", order: "ASCENDING" },
1937
+ { fieldPath: "axbType", order: "ASCENDING" }
1938
+ ]
1939
+ },
1940
+ {
1941
+ collectionGroup: collectionName,
1942
+ queryScope: "COLLECTION_GROUP",
1943
+ fields: [
1944
+ { fieldPath: "axbType", order: "ASCENDING" },
1945
+ { fieldPath: "bUid", order: "ASCENDING" }
1946
+ ]
1947
+ },
1948
+ {
1949
+ collectionGroup: collectionName,
1950
+ queryScope: "COLLECTION_GROUP",
1951
+ fields: [
1952
+ { fieldPath: "aType", order: "ASCENDING" },
1953
+ { fieldPath: "axbType", order: "ASCENDING" }
1954
+ ]
1955
+ },
1956
+ {
1957
+ collectionGroup: collectionName,
1958
+ queryScope: "COLLECTION_GROUP",
1959
+ fields: [
1960
+ { fieldPath: "axbType", order: "ASCENDING" },
1961
+ { fieldPath: "bType", order: "ASCENDING" }
1962
+ ]
1963
+ }
1964
+ ];
1965
+ }
1966
+ function generateIndexConfig(collection, entities, registryEntries) {
1806
1967
  const indexes = baseIndexes(collection);
1807
1968
  if (entities) {
1808
1969
  for (const [, entity] of entities.nodes) {
@@ -1834,6 +1995,17 @@ function generateIndexConfig(collection, entities) {
1834
1995
  }
1835
1996
  }
1836
1997
  }
1998
+ if (registryEntries) {
1999
+ const targetGraphNames = /* @__PURE__ */ new Set();
2000
+ for (const entry of registryEntries) {
2001
+ if (entry.targetGraph) {
2002
+ targetGraphNames.add(entry.targetGraph);
2003
+ }
2004
+ }
2005
+ for (const name of targetGraphNames) {
2006
+ indexes.push(...collectionGroupIndexes(name));
2007
+ }
2008
+ }
1837
2009
  return { indexes, fieldOverrides: [] };
1838
2010
  }
1839
2011
  export {
@@ -1876,9 +2048,11 @@ export {
1876
2048
  generateId,
1877
2049
  generateIndexConfig,
1878
2050
  generateTypes,
2051
+ isAncestorUid,
1879
2052
  jsonSchemaToFieldMeta,
1880
2053
  matchScope,
1881
2054
  matchScopeAny,
2055
+ resolveAncestorCollection,
1882
2056
  resolveView
1883
2057
  };
1884
2058
  //# sourceMappingURL=index.js.map