@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.cjs CHANGED
@@ -69,9 +69,11 @@ __export(index_exports, {
69
69
  generateId: () => generateId,
70
70
  generateIndexConfig: () => generateIndexConfig,
71
71
  generateTypes: () => generateTypes,
72
+ isAncestorUid: () => isAncestorUid,
72
73
  jsonSchemaToFieldMeta: () => jsonSchemaToFieldMeta,
73
74
  matchScope: () => matchScope,
74
75
  matchScopeAny: () => matchScopeAny,
76
+ resolveAncestorCollection: () => resolveAncestorCollection,
75
77
  resolveView: () => resolveView
76
78
  });
77
79
  module.exports = __toCommonJS(index_exports);
@@ -853,14 +855,35 @@ function createRegistry(input) {
853
855
  }
854
856
  const entryList = Object.freeze([...entries]);
855
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
+ }
856
863
  const key = tripleKey(entry.aType, entry.axbType, entry.bType);
857
864
  const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
858
865
  map.set(key, { entry, validate: validator });
859
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
+ }
860
880
  return {
861
881
  lookup(aType, axbType, bType) {
862
882
  return map.get(tripleKey(aType, axbType, bType))?.entry;
863
883
  },
884
+ lookupByAxbType(axbType) {
885
+ return axbIndex.get(axbType) ?? [];
886
+ },
864
887
  validate(aType, axbType, bType, data, scopePath) {
865
888
  const rec = map.get(tripleKey(aType, axbType, bType));
866
889
  if (!rec) {
@@ -907,6 +930,12 @@ function discoveryToEntries(discovery) {
907
930
  if (!topology) continue;
908
931
  const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
909
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
+ }
910
939
  for (const aType of fromTypes) {
911
940
  for (const bType of toTypes) {
912
941
  entries.push({
@@ -918,7 +947,8 @@ function discoveryToEntries(discovery) {
918
947
  inverseLabel: topology.inverseLabel,
919
948
  titleField: entity.titleField,
920
949
  subtitleField: entity.subtitleField,
921
- allowedIn: entity.allowedIn
950
+ allowedIn: entity.allowedIn,
951
+ targetGraph: resolvedTargetGraph
922
952
  });
923
953
  }
924
954
  }
@@ -968,7 +998,8 @@ var EDGE_TYPE_SCHEMA = {
968
998
  subtitleField: { type: "string" },
969
999
  viewTemplate: { type: "string" },
970
1000
  viewCss: { type: "string" },
971
- allowedIn: { type: "array", items: { type: "string", minLength: 1 } }
1001
+ allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
1002
+ targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" }
972
1003
  },
973
1004
  additionalProperties: false
974
1005
  };
@@ -1029,7 +1060,8 @@ async function createRegistryFromGraph(reader) {
1029
1060
  inverseLabel: data.inverseLabel,
1030
1061
  titleField: data.titleField,
1031
1062
  subtitleField: data.subtitleField,
1032
- allowedIn: data.allowedIn
1063
+ allowedIn: data.allowedIn,
1064
+ targetGraph: data.targetGraph
1033
1065
  });
1034
1066
  }
1035
1067
  }
@@ -1283,6 +1315,33 @@ var GraphClientImpl = class _GraphClientImpl {
1283
1315
  );
1284
1316
  }
1285
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());
1343
+ }
1344
+ // ---------------------------------------------------------------------------
1286
1345
  // Bulk operations
1287
1346
  // ---------------------------------------------------------------------------
1288
1347
  async removeNodeCascade(uid, options) {
@@ -1334,6 +1393,7 @@ var GraphClientImpl = class _GraphClientImpl {
1334
1393
  };
1335
1394
  if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
1336
1395
  if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
1396
+ if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
1337
1397
  if (description !== void 0) data.description = description;
1338
1398
  if (options?.titleField !== void 0) data.titleField = options.titleField;
1339
1399
  if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
@@ -1408,6 +1468,10 @@ function generateId() {
1408
1468
  var DEFAULT_LIMIT = 10;
1409
1469
  var DEFAULT_MAX_READS = 100;
1410
1470
  var DEFAULT_CONCURRENCY = 5;
1471
+ var _crossGraphWarned = false;
1472
+ function isGraphClient(reader) {
1473
+ return "subgraph" in reader && typeof reader.subgraph === "function";
1474
+ }
1411
1475
  var Semaphore = class {
1412
1476
  constructor(slots) {
1413
1477
  this.slots = slots;
@@ -1433,9 +1497,10 @@ var Semaphore = class {
1433
1497
  }
1434
1498
  };
1435
1499
  var TraversalBuilderImpl = class {
1436
- constructor(reader, startUid) {
1500
+ constructor(reader, startUid, registry) {
1437
1501
  this.reader = reader;
1438
1502
  this.startUid = startUid;
1503
+ this.registry = registry;
1439
1504
  }
1440
1505
  hops = [];
1441
1506
  follow(axbType, options) {
@@ -1452,11 +1517,13 @@ var TraversalBuilderImpl = class {
1452
1517
  const semaphore = new Semaphore(concurrency);
1453
1518
  let totalReads = 0;
1454
1519
  let truncated = false;
1455
- let sourceUids = [this.startUid];
1520
+ let sources = [
1521
+ { uid: this.startUid, reader: this.reader }
1522
+ ];
1456
1523
  const hopResults = [];
1457
1524
  for (let depth = 0; depth < this.hops.length; depth++) {
1458
1525
  const hop = this.hops[depth];
1459
- if (sourceUids.length === 0) {
1526
+ if (sources.length === 0) {
1460
1527
  hopResults.push({
1461
1528
  axbType: hop.axbType,
1462
1529
  depth,
@@ -1467,9 +1534,12 @@ var TraversalBuilderImpl = class {
1467
1534
  continue;
1468
1535
  }
1469
1536
  const hopEdges = [];
1470
- const sourceCount = sourceUids.length;
1537
+ const sourceCount = sources.length;
1471
1538
  let hopTruncated = false;
1472
- 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 () => {
1473
1543
  if (totalReads >= maxReads) {
1474
1544
  hopTruncated = true;
1475
1545
  return;
@@ -1481,19 +1551,18 @@ var TraversalBuilderImpl = class {
1481
1551
  return;
1482
1552
  }
1483
1553
  totalReads++;
1484
- const direction2 = hop.direction ?? "forward";
1485
1554
  const params = { axbType: hop.axbType };
1486
- if (direction2 === "forward") {
1555
+ if (direction === "forward") {
1487
1556
  params.aUid = uid;
1488
1557
  if (hop.bType) params.bType = hop.bType;
1489
1558
  } else {
1490
1559
  params.bUid = uid;
1491
1560
  if (hop.aType) params.aType = hop.aType;
1492
1561
  }
1493
- if (direction2 === "forward" && hop.aType) {
1562
+ if (direction === "forward" && hop.aType) {
1494
1563
  params.aType = hop.aType;
1495
1564
  }
1496
- if (direction2 === "reverse" && hop.bType) {
1565
+ if (direction === "reverse" && hop.bType) {
1497
1566
  params.bType = hop.bType;
1498
1567
  }
1499
1568
  if (hop.orderBy) params.orderBy = hop.orderBy;
@@ -1503,31 +1572,58 @@ var TraversalBuilderImpl = class {
1503
1572
  } else {
1504
1573
  params.limit = limit;
1505
1574
  }
1506
- 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);
1507
1596
  if (hop.filter) {
1508
- edges = edges.filter(hop.filter);
1509
- 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 });
1510
1602
  }
1511
- hopEdges.push(...edges);
1512
1603
  } finally {
1513
1604
  semaphore.release();
1514
1605
  }
1515
1606
  });
1516
1607
  await Promise.all(tasks.map((task) => task()));
1608
+ const edges = hopEdges.map((h) => h.edge);
1517
1609
  hopResults.push({
1518
1610
  axbType: hop.axbType,
1519
1611
  depth,
1520
- edges: returnIntermediates ? [...hopEdges] : hopEdges,
1612
+ edges: returnIntermediates ? [...edges] : edges,
1521
1613
  sourceCount,
1522
1614
  truncated: hopTruncated
1523
1615
  });
1524
1616
  if (hopTruncated) {
1525
1617
  truncated = true;
1526
1618
  }
1527
- const direction = hop.direction ?? "forward";
1528
- sourceUids = [...new Set(
1529
- hopEdges.map((e) => direction === "forward" ? e.bUid : e.aUid)
1530
- )];
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 }));
1531
1627
  }
1532
1628
  const lastHop = hopResults[hopResults.length - 1];
1533
1629
  return {
@@ -1537,9 +1633,25 @@ var TraversalBuilderImpl = class {
1537
1633
  truncated
1538
1634
  };
1539
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
+ }
1540
1652
  };
1541
- function createTraversal(reader, startUid) {
1542
- return new TraversalBuilderImpl(reader, startUid);
1653
+ function createTraversal(reader, startUid, registry) {
1654
+ return new TraversalBuilderImpl(reader, startUid, registry);
1543
1655
  }
1544
1656
 
1545
1657
  // src/views.ts
@@ -1788,7 +1900,8 @@ function loadEdgeEntity(dir, name) {
1788
1900
  viewDefaults: meta?.viewDefaults,
1789
1901
  viewsPath,
1790
1902
  sampleData,
1791
- allowedIn: meta?.allowedIn
1903
+ allowedIn: meta?.allowedIn,
1904
+ targetGraph: topology.targetGraph ?? meta?.targetGraph
1792
1905
  };
1793
1906
  }
1794
1907
  function getSubdirectories(dir) {
@@ -1831,6 +1944,20 @@ function discoverEntities(entitiesDir) {
1831
1944
  };
1832
1945
  }
1833
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
+
1834
1961
  // src/codegen/index.ts
1835
1962
  function pascalCase(s) {
1836
1963
  return s.replace(
@@ -1916,7 +2043,43 @@ function extractSchemaFields(schema) {
1916
2043
  if (s.type !== "object" || !s.properties) return [];
1917
2044
  return Object.keys(s.properties);
1918
2045
  }
1919
- function generateIndexConfig(collection, entities) {
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) {
1920
2083
  const indexes = baseIndexes(collection);
1921
2084
  if (entities) {
1922
2085
  for (const [, entity] of entities.nodes) {
@@ -1948,6 +2111,17 @@ function generateIndexConfig(collection, entities) {
1948
2111
  }
1949
2112
  }
1950
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
+ }
1951
2125
  return { indexes, fieldOverrides: [] };
1952
2126
  }
1953
2127
 
@@ -2267,9 +2441,11 @@ var QueryClient = class {
2267
2441
  generateId,
2268
2442
  generateIndexConfig,
2269
2443
  generateTypes,
2444
+ isAncestorUid,
2270
2445
  jsonSchemaToFieldMeta,
2271
2446
  matchScope,
2272
2447
  matchScopeAny,
2448
+ resolveAncestorCollection,
2273
2449
  resolveView
2274
2450
  });
2275
2451
  //# sourceMappingURL=index.cjs.map