@typicalday/firegraph 0.13.0 → 0.14.1

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.
Files changed (35) hide show
  1. package/README.md +57 -1
  2. package/dist/backend.cjs +2 -3
  3. package/dist/backend.cjs.map +1 -1
  4. package/dist/backend.js +1 -1
  5. package/dist/{chunk-WRTFC5NG.js → chunk-3AHHXMWX.js} +2 -2
  6. package/dist/{chunk-PAD7WFFU.js → chunk-DJI3VXXA.js} +36 -10
  7. package/dist/chunk-DJI3VXXA.js.map +1 -0
  8. package/dist/{chunk-4MMQ5W74.js → chunk-NNBSUOOF.js} +7 -6
  9. package/dist/chunk-NNBSUOOF.js.map +1 -0
  10. package/dist/{chunk-TK64DNVK.js → chunk-SIHE4UY4.js} +3 -4
  11. package/dist/chunk-SIHE4UY4.js.map +1 -0
  12. package/dist/cloudflare/index.cjs +7 -7
  13. package/dist/cloudflare/index.cjs.map +1 -1
  14. package/dist/cloudflare/index.js +3 -3
  15. package/dist/firestore-enterprise/index.cjs +57 -48
  16. package/dist/firestore-enterprise/index.cjs.map +1 -1
  17. package/dist/firestore-enterprise/index.d.cts +41 -11
  18. package/dist/firestore-enterprise/index.d.ts +41 -11
  19. package/dist/firestore-enterprise/index.js +31 -42
  20. package/dist/firestore-enterprise/index.js.map +1 -1
  21. package/dist/firestore-standard/index.cjs +34 -37
  22. package/dist/firestore-standard/index.cjs.map +1 -1
  23. package/dist/firestore-standard/index.js +9 -34
  24. package/dist/firestore-standard/index.js.map +1 -1
  25. package/dist/index.cjs +2 -3
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/sqlite/index.cjs +7 -7
  29. package/dist/sqlite/index.cjs.map +1 -1
  30. package/dist/sqlite/index.js +3 -3
  31. package/package.json +1 -1
  32. package/dist/chunk-4MMQ5W74.js.map +0 -1
  33. package/dist/chunk-PAD7WFFU.js.map +0 -1
  34. package/dist/chunk-TK64DNVK.js.map +0 -1
  35. /package/dist/{chunk-WRTFC5NG.js.map → chunk-3AHHXMWX.js.map} +0 -0
@@ -92,10 +92,15 @@ interface FirestoreEnterpriseOptions {
92
92
  * (search, aggregate, etc., once implemented) always use pipelines
93
93
  * regardless of this option.
94
94
  *
95
- * The emulator does not execute pipeline queries, so this option is
96
- * forced to `'classic'` whenever `FIRESTORE_EMULATOR_HOST` is set, with
97
- * a one-time `console.warn` if the caller explicitly asked for pipeline
98
- * mode.
95
+ * The legacy / standard-edition Firestore emulator does not execute
96
+ * pipeline queries, so this option is coerced to `'classic'` whenever
97
+ * `FIRESTORE_EMULATOR_HOST` is set AND the caller has NOT opted into
98
+ * the enterprise-edition emulator via `FIRESTORE_EMULATOR_EDITION=enterprise`
99
+ * (case-insensitive). When that env var is set, the requested mode is
100
+ * honored end-to-end — pipelines work in firebase-tools v15.14+ with
101
+ * `--database-edition enterprise`. A one-time `console.warn` fires when
102
+ * the emulator forces the fallback after the caller explicitly asked for
103
+ * pipeline mode.
99
104
  */
100
105
  defaultQueryMode?: FirestoreEnterpriseQueryMode;
101
106
  /**
@@ -127,15 +132,40 @@ interface FirestoreEnterpriseOptions {
127
132
  /** Internal: the logical scope path inherited from a parent subgraph. */
128
133
  scopePath?: string;
129
134
  }
135
+ /**
136
+ * The runtime shape returned from `createFirestoreEnterpriseBackend`.
137
+ *
138
+ * Extends `StorageBackend<FirestoreEnterpriseCapability>` with the
139
+ * `queryMode` field. The field always reflects the *effective* mode after
140
+ * emulator coercion, not the mode the caller requested. The public
141
+ * `GraphClient` surface does not expose the backend, so to introspect
142
+ * `queryMode` keep a reference to the backend before passing it to
143
+ * `createGraphClient`:
144
+ *
145
+ * ```ts
146
+ * const backend = createFirestoreEnterpriseBackend(db, 'firegraph', { defaultQueryMode: 'pipeline' });
147
+ * console.log(backend.queryMode);
148
+ * const client = createGraphClient(backend);
149
+ * ```
150
+ */
151
+ interface FirestoreEnterpriseBackend extends StorageBackend<FirestoreEnterpriseCapability> {
152
+ readonly queryMode: FirestoreEnterpriseQueryMode;
153
+ }
130
154
  /**
131
155
  * Create a Firestore Enterprise-edition `StorageBackend`.
132
156
  *
133
- * Pipeline mode is the default. When `FIRESTORE_EMULATOR_HOST` is set the
134
- * effective mode is forced to `'classic'` because the emulator does not
135
- * execute pipelines; if the caller explicitly asked for pipeline mode in
136
- * that environment, a one-time `console.warn` surfaces the override so
137
- * the deployment mismatch is visible without breaking the test run.
157
+ * Pipeline mode is the default. The legacy / standard-edition Firestore
158
+ * emulator does not execute pipelines, so the effective mode is coerced
159
+ * to `'classic'` whenever `FIRESTORE_EMULATOR_HOST` is set AND the caller
160
+ * has NOT opted into the enterprise-edition emulator via
161
+ * `FIRESTORE_EMULATOR_EDITION=enterprise` (case-insensitive). The opt-in
162
+ * env var mirrors firebase-tools v15.14+'s `--database-edition enterprise`
163
+ * (and `firestore.edition` in `firebase.json`); when set, pipelines work
164
+ * end-to-end against the local emulator without patching firegraph. If
165
+ * the caller explicitly asked for pipeline mode in a non-enterprise
166
+ * emulator, a one-time `console.warn` surfaces the override so the
167
+ * deployment mismatch is visible without breaking the test run.
138
168
  */
139
- declare function createFirestoreEnterpriseBackend(db: Firestore, collectionPath: string, options?: FirestoreEnterpriseOptions): StorageBackend<FirestoreEnterpriseCapability>;
169
+ declare function createFirestoreEnterpriseBackend(db: Firestore, collectionPath: string, options?: FirestoreEnterpriseOptions): FirestoreEnterpriseBackend;
140
170
 
141
- export { type FirestoreEnterpriseCapability, type FirestoreEnterpriseOptions, type FirestoreEnterpriseQueryMode, createFirestoreEnterpriseBackend };
171
+ export { type FirestoreEnterpriseBackend, type FirestoreEnterpriseCapability, type FirestoreEnterpriseOptions, type FirestoreEnterpriseQueryMode, createFirestoreEnterpriseBackend };
@@ -92,10 +92,15 @@ interface FirestoreEnterpriseOptions {
92
92
  * (search, aggregate, etc., once implemented) always use pipelines
93
93
  * regardless of this option.
94
94
  *
95
- * The emulator does not execute pipeline queries, so this option is
96
- * forced to `'classic'` whenever `FIRESTORE_EMULATOR_HOST` is set, with
97
- * a one-time `console.warn` if the caller explicitly asked for pipeline
98
- * mode.
95
+ * The legacy / standard-edition Firestore emulator does not execute
96
+ * pipeline queries, so this option is coerced to `'classic'` whenever
97
+ * `FIRESTORE_EMULATOR_HOST` is set AND the caller has NOT opted into
98
+ * the enterprise-edition emulator via `FIRESTORE_EMULATOR_EDITION=enterprise`
99
+ * (case-insensitive). When that env var is set, the requested mode is
100
+ * honored end-to-end — pipelines work in firebase-tools v15.14+ with
101
+ * `--database-edition enterprise`. A one-time `console.warn` fires when
102
+ * the emulator forces the fallback after the caller explicitly asked for
103
+ * pipeline mode.
99
104
  */
100
105
  defaultQueryMode?: FirestoreEnterpriseQueryMode;
101
106
  /**
@@ -127,15 +132,40 @@ interface FirestoreEnterpriseOptions {
127
132
  /** Internal: the logical scope path inherited from a parent subgraph. */
128
133
  scopePath?: string;
129
134
  }
135
+ /**
136
+ * The runtime shape returned from `createFirestoreEnterpriseBackend`.
137
+ *
138
+ * Extends `StorageBackend<FirestoreEnterpriseCapability>` with the
139
+ * `queryMode` field. The field always reflects the *effective* mode after
140
+ * emulator coercion, not the mode the caller requested. The public
141
+ * `GraphClient` surface does not expose the backend, so to introspect
142
+ * `queryMode` keep a reference to the backend before passing it to
143
+ * `createGraphClient`:
144
+ *
145
+ * ```ts
146
+ * const backend = createFirestoreEnterpriseBackend(db, 'firegraph', { defaultQueryMode: 'pipeline' });
147
+ * console.log(backend.queryMode);
148
+ * const client = createGraphClient(backend);
149
+ * ```
150
+ */
151
+ interface FirestoreEnterpriseBackend extends StorageBackend<FirestoreEnterpriseCapability> {
152
+ readonly queryMode: FirestoreEnterpriseQueryMode;
153
+ }
130
154
  /**
131
155
  * Create a Firestore Enterprise-edition `StorageBackend`.
132
156
  *
133
- * Pipeline mode is the default. When `FIRESTORE_EMULATOR_HOST` is set the
134
- * effective mode is forced to `'classic'` because the emulator does not
135
- * execute pipelines; if the caller explicitly asked for pipeline mode in
136
- * that environment, a one-time `console.warn` surfaces the override so
137
- * the deployment mismatch is visible without breaking the test run.
157
+ * Pipeline mode is the default. The legacy / standard-edition Firestore
158
+ * emulator does not execute pipelines, so the effective mode is coerced
159
+ * to `'classic'` whenever `FIRESTORE_EMULATOR_HOST` is set AND the caller
160
+ * has NOT opted into the enterprise-edition emulator via
161
+ * `FIRESTORE_EMULATOR_EDITION=enterprise` (case-insensitive). The opt-in
162
+ * env var mirrors firebase-tools v15.14+'s `--database-edition enterprise`
163
+ * (and `firestore.edition` in `firebase.json`); when set, pipelines work
164
+ * end-to-end against the local emulator without patching firegraph. If
165
+ * the caller explicitly asked for pipeline mode in a non-enterprise
166
+ * emulator, a one-time `console.warn` surfaces the override so the
167
+ * deployment mismatch is visible without breaking the test run.
138
168
  */
139
- declare function createFirestoreEnterpriseBackend(db: Firestore, collectionPath: string, options?: FirestoreEnterpriseOptions): StorageBackend<FirestoreEnterpriseCapability>;
169
+ declare function createFirestoreEnterpriseBackend(db: Firestore, collectionPath: string, options?: FirestoreEnterpriseOptions): FirestoreEnterpriseBackend;
140
170
 
141
- export { type FirestoreEnterpriseCapability, type FirestoreEnterpriseOptions, type FirestoreEnterpriseQueryMode, createFirestoreEnterpriseBackend };
171
+ export { type FirestoreEnterpriseBackend, type FirestoreEnterpriseCapability, type FirestoreEnterpriseOptions, type FirestoreEnterpriseQueryMode, createFirestoreEnterpriseBackend };
@@ -2,6 +2,7 @@ import {
2
2
  compileEngineTraversal
3
3
  } from "../chunk-D4J7Z4FE.js";
4
4
  import {
5
+ buildFirestoreUpdateArgs,
5
6
  bulkRemoveEdges,
6
7
  createBatchAdapter,
7
8
  createFirestoreAdapter,
@@ -11,10 +12,8 @@ import {
11
12
  runFirestoreClassicExpand,
12
13
  runFirestoreFindEdgesProjected,
13
14
  runFirestoreFindNearest
14
- } from "../chunk-PAD7WFFU.js";
15
- import {
16
- deserializeFirestoreTypes
17
- } from "../chunk-C2QMD7RY.js";
15
+ } from "../chunk-DJI3VXXA.js";
16
+ import "../chunk-C2QMD7RY.js";
18
17
  import {
19
18
  createCapabilities
20
19
  } from "../chunk-N5HFDWQX.js";
@@ -27,13 +26,11 @@ import {
27
26
  createMergedRegistry,
28
27
  createRegistry,
29
28
  generateId
30
- } from "../chunk-WRTFC5NG.js";
29
+ } from "../chunk-3AHHXMWX.js";
31
30
  import {
32
31
  FiregraphError,
33
- assertSafePath,
34
- assertUpdatePayloadExclusive,
35
32
  flattenPatch
36
- } from "../chunk-TK64DNVK.js";
33
+ } from "../chunk-SIHE4UY4.js";
37
34
  import "../chunk-EQJUUVFG.js";
38
35
 
39
36
  // src/firestore-enterprise/backend.ts
@@ -50,6 +47,13 @@ async function getFirestoreSurface() {
50
47
  }
51
48
  return { P: _Pipelines, Ts: _Timestamp };
52
49
  }
50
+ var UNESCAPED_FIELD_NAME_RE = /^[_A-Za-z][_A-Za-z0-9]*$/;
51
+ function buildDataPathAlias(segments) {
52
+ const encoded = segments.map(
53
+ (seg) => UNESCAPED_FIELD_NAME_RE.test(seg) ? seg : "`" + seg.replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`"
54
+ );
55
+ return ["data", ...encoded].join(".");
56
+ }
53
57
  function buildFilterExpression(P, filter) {
54
58
  const { field: fieldName, op, value } = filter;
55
59
  switch (op) {
@@ -127,7 +131,7 @@ async function runFirestorePipelineUpdate(db, collectionPath, filters, patch, _o
127
131
  }
128
132
  const { P, Ts } = await getFirestoreSurface();
129
133
  const transforms = ops.map((op) => {
130
- const alias = `data.${op.path.join(".")}`;
134
+ const alias = buildDataPathAlias(op.path);
131
135
  return P.constant(op.value).as(alias);
132
136
  });
133
137
  transforms.push(P.constant(Ts.now()).as("updatedAt"));
@@ -472,10 +476,13 @@ function decodeTree(rootRows, hops) {
472
476
  }
473
477
  async function runFirestoreEngineTraversal(db, collectionPath, params) {
474
478
  if (process.env.FIRESTORE_EMULATOR_HOST) {
475
- throw new FiregraphError(
476
- "engine traversal requires Pipelines \u2014 not supported on the Firestore emulator",
477
- "UNSUPPORTED_OPERATION"
478
- );
479
+ const emulatorEdition = (process.env.FIRESTORE_EMULATOR_EDITION ?? "").trim().toLowerCase();
480
+ if (emulatorEdition !== "enterprise") {
481
+ throw new FiregraphError(
482
+ "engine traversal requires Pipelines \u2014 not supported on the default Firestore emulator. Run firebase-tools v15.14+ with `--database-edition enterprise` and set `FIRESTORE_EMULATOR_EDITION=enterprise` to opt in.",
483
+ "UNSUPPORTED_OPERATION"
484
+ );
485
+ }
479
486
  }
480
487
  const compiled = compileEngineTraversal(params);
481
488
  if (!compiled.eligible) {
@@ -582,28 +589,6 @@ var ENTERPRISE_BASE_CAPS = /* @__PURE__ */ new Set([
582
589
  var _emulatorFallbackWarned = false;
583
590
  var _classicInProductionWarned = false;
584
591
  var _previewDmlWarned = false;
585
- function dottedDataPath(op) {
586
- assertSafePath(op.path);
587
- return `data.${op.path.join(".")}`;
588
- }
589
- function buildFirestoreUpdate(update, db) {
590
- assertUpdatePayloadExclusive(update);
591
- const out = {
592
- updatedAt: FieldValue.serverTimestamp()
593
- };
594
- if (update.replaceData) {
595
- out.data = deserializeFirestoreTypes(update.replaceData, db);
596
- } else if (update.dataOps) {
597
- for (const op of update.dataOps) {
598
- const key = dottedDataPath(op);
599
- out[key] = op.delete ? FieldValue.delete() : op.value;
600
- }
601
- }
602
- if (update.v !== void 0) {
603
- out.v = update.v;
604
- }
605
- return out;
606
- }
607
592
  function stampWritableRecord(record) {
608
593
  const now = FieldValue.serverTimestamp();
609
594
  const out = {
@@ -638,7 +623,7 @@ var FirestoreEnterpriseTransactionBackend = class {
638
623
  );
639
624
  }
640
625
  async updateDoc(docId, update) {
641
- this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
626
+ this.adapter.updateDoc(docId, buildFirestoreUpdateArgs(update, this.db));
642
627
  }
643
628
  async deleteDoc(docId) {
644
629
  this.adapter.deleteDoc(docId);
@@ -657,7 +642,7 @@ var FirestoreEnterpriseBatchBackend = class {
657
642
  );
658
643
  }
659
644
  updateDoc(docId, update) {
660
- this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
645
+ this.adapter.updateDoc(docId, buildFirestoreUpdateArgs(update, this.db));
661
646
  }
662
647
  deleteDoc(docId) {
663
648
  this.adapter.deleteDoc(docId);
@@ -669,8 +654,8 @@ var FirestoreEnterpriseBatchBackend = class {
669
654
  var FirestoreEnterpriseBackendImpl = class _FirestoreEnterpriseBackendImpl {
670
655
  constructor(db, collectionPath, queryMode, scopePath, previewDml) {
671
656
  this.db = db;
672
- this.queryMode = queryMode;
673
657
  this.previewDml = previewDml;
658
+ this.queryMode = queryMode;
674
659
  this.collectionPath = collectionPath;
675
660
  this.scopePath = scopePath;
676
661
  this.adapter = createFirestoreAdapter(db, collectionPath);
@@ -683,6 +668,7 @@ var FirestoreEnterpriseBackendImpl = class _FirestoreEnterpriseBackendImpl {
683
668
  capabilities;
684
669
  collectionPath;
685
670
  scopePath;
671
+ queryMode;
686
672
  adapter;
687
673
  pipelineAdapter;
688
674
  // --- Reads ---
@@ -704,7 +690,7 @@ var FirestoreEnterpriseBackendImpl = class _FirestoreEnterpriseBackendImpl {
704
690
  );
705
691
  }
706
692
  updateDoc(docId, update) {
707
- return this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
693
+ return this.adapter.updateDoc(docId, buildFirestoreUpdateArgs(update, this.db));
708
694
  }
709
695
  deleteDoc(docId) {
710
696
  return this.adapter.deleteDoc(docId);
@@ -944,11 +930,14 @@ var FirestoreEnterpriseBackendImpl = class _FirestoreEnterpriseBackendImpl {
944
930
  function createFirestoreEnterpriseBackend(db, collectionPath, options = {}) {
945
931
  const requestedMode = options.defaultQueryMode ?? "pipeline";
946
932
  const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
947
- const effectiveMode = isEmulator && requestedMode === "pipeline" ? "classic" : requestedMode;
948
- if (isEmulator && requestedMode === "pipeline" && effectiveMode === "classic" && !_emulatorFallbackWarned) {
933
+ const emulatorEdition = (process.env.FIRESTORE_EMULATOR_EDITION ?? "").trim().toLowerCase();
934
+ const emulatorIsEnterprise = emulatorEdition === "enterprise";
935
+ const emulatorForcesClassic = isEmulator && !emulatorIsEnterprise;
936
+ const effectiveMode = emulatorForcesClassic && requestedMode === "pipeline" ? "classic" : requestedMode;
937
+ if (emulatorForcesClassic && requestedMode === "pipeline" && effectiveMode === "classic" && !_emulatorFallbackWarned) {
949
938
  _emulatorFallbackWarned = true;
950
939
  console.warn(
951
- "[firegraph] Firestore Enterprise pipeline mode is unavailable in the emulator; falling back to classic Query API for this run. Set `defaultQueryMode: 'classic'` to silence this warning."
940
+ "[firegraph] Firestore Enterprise pipeline mode is unavailable in the default emulator; falling back to classic Query API for this run. If you are running firebase-tools v15.14+ with the enterprise-edition emulator (`firebase emulators:start --database-edition enterprise`), set `FIRESTORE_EMULATOR_EDITION=enterprise` in your environment to honor pipeline mode. Otherwise set `defaultQueryMode: 'classic'` to silence this warning."
952
941
  );
953
942
  }
954
943
  if (!isEmulator && requestedMode === "classic" && !_classicInProductionWarned) {