@typicalday/firegraph 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -73
- package/dist/backend-DuvHGgK1.d.cts +1897 -0
- package/dist/backend-DuvHGgK1.d.ts +1897 -0
- package/dist/backend.cjs +222 -3
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +25 -5
- package/dist/backend.d.ts +25 -5
- package/dist/backend.js +197 -4
- package/dist/backend.js.map +1 -1
- package/dist/chunk-2DHMNTV6.js +16 -0
- package/dist/chunk-2DHMNTV6.js.map +1 -0
- package/dist/chunk-4MMQ5W74.js +288 -0
- package/dist/chunk-4MMQ5W74.js.map +1 -0
- package/dist/chunk-D4J7Z4FE.js +67 -0
- package/dist/chunk-D4J7Z4FE.js.map +1 -0
- package/dist/chunk-N5HFDWQX.js +23 -0
- package/dist/chunk-N5HFDWQX.js.map +1 -0
- package/dist/chunk-PAD7WFFU.js +573 -0
- package/dist/chunk-PAD7WFFU.js.map +1 -0
- package/dist/{chunk-AWW4MUJ5.js → chunk-TK64DNVK.js} +12 -1
- package/dist/chunk-TK64DNVK.js.map +1 -0
- package/dist/{chunk-HONQY4HF.js → chunk-WRTFC5NG.js} +362 -17
- package/dist/chunk-WRTFC5NG.js.map +1 -0
- package/dist/client-BKi3vk0Q.d.ts +34 -0
- package/dist/client-BrsaXtDV.d.cts +34 -0
- package/dist/cloudflare/index.cjs +930 -3
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +213 -12
- package/dist/cloudflare/index.d.ts +213 -12
- package/dist/cloudflare/index.js +562 -281
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/errors-BRc3I_eH.d.cts +73 -0
- package/dist/errors-BRc3I_eH.d.ts +73 -0
- package/dist/firestore-enterprise/index.cjs +3877 -0
- package/dist/firestore-enterprise/index.cjs.map +1 -0
- package/dist/firestore-enterprise/index.d.cts +141 -0
- package/dist/firestore-enterprise/index.d.ts +141 -0
- package/dist/firestore-enterprise/index.js +985 -0
- package/dist/firestore-enterprise/index.js.map +1 -0
- package/dist/firestore-standard/index.cjs +3117 -0
- package/dist/firestore-standard/index.cjs.map +1 -0
- package/dist/firestore-standard/index.d.cts +49 -0
- package/dist/firestore-standard/index.d.ts +49 -0
- package/dist/firestore-standard/index.js +283 -0
- package/dist/firestore-standard/index.js.map +1 -0
- package/dist/index.cjs +590 -550
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -37
- package/dist/index.d.ts +9 -37
- package/dist/index.js +178 -555
- package/dist/index.js.map +1 -1
- package/dist/{registry-Fi074zVa.d.ts → registry-Bc7h6WTM.d.cts} +1 -1
- package/dist/{registry-B1qsVL0E.d.cts → registry-C2KUPVZj.d.ts} +1 -1
- package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.cts} +1 -56
- package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.ts} +1 -56
- package/dist/sqlite/index.cjs +3631 -0
- package/dist/sqlite/index.cjs.map +1 -0
- package/dist/sqlite/index.d.cts +111 -0
- package/dist/sqlite/index.d.ts +111 -0
- package/dist/sqlite/index.js +1164 -0
- package/dist/sqlite/index.js.map +1 -0
- package/package.json +33 -3
- package/dist/backend-BsR0lnFL.d.ts +0 -200
- package/dist/backend-Ct-fLlkG.d.cts +0 -200
- package/dist/chunk-AWW4MUJ5.js.map +0 -1
- package/dist/chunk-HONQY4HF.js.map +0 -1
- package/dist/types-DxYLy8Ol.d.cts +0 -770
- package/dist/types-DxYLy8Ol.d.ts +0 -770
package/dist/backend.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { C as
|
|
2
|
-
import { S as StorageBackend } from './backend-
|
|
3
|
-
export {
|
|
4
|
-
|
|
1
|
+
export { C as CapabilityNotSupportedError, a as CrossBackendTransactionError } from './errors-BRc3I_eH.cjs';
|
|
2
|
+
import { S as StorageBackend, B as BackendCapabilities } from './backend-DuvHGgK1.cjs';
|
|
3
|
+
export { a as BatchBackend, b as BulkUpdatePatch, D as DELETE_FIELD, c as DataPathOp, d as DmlExtension, E as ExpandParams, e as ExpandResult, J as JoinExtension, T as TransactionBackend, U as UpdatePayload, W as WritableRecord, f as WriteMode, g as createCapabilities, h as deleteField, i as flattenPatch, j as intersectCapabilities, k as isDeleteSentinel } from './backend-DuvHGgK1.cjs';
|
|
4
|
+
export { S as StorageScopeSegment, a as appendStorageScope, i as isAncestorScopeUid, p as parseStorageScope, r as resolveAncestorScope } from './scope-path-CROFZGr9.cjs';
|
|
5
5
|
import '@google-cloud/firestore';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -95,6 +95,26 @@ interface RoutingBackendOptions {
|
|
|
95
95
|
* consulted.
|
|
96
96
|
*/
|
|
97
97
|
route: (ctx: RoutingContext) => StorageBackend | null | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Capability sets for any backend `route()` may return. The root routing
|
|
100
|
+
* wrapper's `capabilities` becomes the intersection of `base.capabilities`
|
|
101
|
+
* and every set passed here, satisfying invariant 5 from
|
|
102
|
+
* `.claude/backend-capabilities.md` ("a graph mounted across multiple
|
|
103
|
+
* backends declares the intersection of child capability sets").
|
|
104
|
+
*
|
|
105
|
+
* Capability declarations are required by invariant 3 to be **static** at
|
|
106
|
+
* construction. Because `route()` is consulted dynamically, the wrapper
|
|
107
|
+
* cannot discover routed children's caps after the fact — callers
|
|
108
|
+
* intersecting across backends must enumerate the participants up front.
|
|
109
|
+
*
|
|
110
|
+
* When `undefined` or empty, the routing wrapper mirrors
|
|
111
|
+
* `base.capabilities`. That matches the common single-backend routing
|
|
112
|
+
* use case (route one subgraph name to a peer of the same backend type)
|
|
113
|
+
* without forcing every caller to declare an explicit list. Mixed-backend
|
|
114
|
+
* callers should always populate this — the cap surface won't lie about
|
|
115
|
+
* what's safe across hops.
|
|
116
|
+
*/
|
|
117
|
+
routedCapabilities?: ReadonlyArray<BackendCapabilities>;
|
|
98
118
|
}
|
|
99
119
|
/**
|
|
100
120
|
* Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be
|
|
@@ -119,4 +139,4 @@ interface RoutingBackendOptions {
|
|
|
119
139
|
*/
|
|
120
140
|
declare function createRoutingBackend(base: StorageBackend, options: RoutingBackendOptions): StorageBackend;
|
|
121
141
|
|
|
122
|
-
export { type RoutingBackendOptions, type RoutingContext, StorageBackend, createRoutingBackend };
|
|
142
|
+
export { BackendCapabilities, type RoutingBackendOptions, type RoutingContext, StorageBackend, createRoutingBackend };
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { C as
|
|
2
|
-
import { S as StorageBackend } from './backend-
|
|
3
|
-
export {
|
|
4
|
-
|
|
1
|
+
export { C as CapabilityNotSupportedError, a as CrossBackendTransactionError } from './errors-BRc3I_eH.js';
|
|
2
|
+
import { S as StorageBackend, B as BackendCapabilities } from './backend-DuvHGgK1.js';
|
|
3
|
+
export { a as BatchBackend, b as BulkUpdatePatch, D as DELETE_FIELD, c as DataPathOp, d as DmlExtension, E as ExpandParams, e as ExpandResult, J as JoinExtension, T as TransactionBackend, U as UpdatePayload, W as WritableRecord, f as WriteMode, g as createCapabilities, h as deleteField, i as flattenPatch, j as intersectCapabilities, k as isDeleteSentinel } from './backend-DuvHGgK1.js';
|
|
4
|
+
export { S as StorageScopeSegment, a as appendStorageScope, i as isAncestorScopeUid, p as parseStorageScope, r as resolveAncestorScope } from './scope-path-CROFZGr9.js';
|
|
5
5
|
import '@google-cloud/firestore';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -95,6 +95,26 @@ interface RoutingBackendOptions {
|
|
|
95
95
|
* consulted.
|
|
96
96
|
*/
|
|
97
97
|
route: (ctx: RoutingContext) => StorageBackend | null | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Capability sets for any backend `route()` may return. The root routing
|
|
100
|
+
* wrapper's `capabilities` becomes the intersection of `base.capabilities`
|
|
101
|
+
* and every set passed here, satisfying invariant 5 from
|
|
102
|
+
* `.claude/backend-capabilities.md` ("a graph mounted across multiple
|
|
103
|
+
* backends declares the intersection of child capability sets").
|
|
104
|
+
*
|
|
105
|
+
* Capability declarations are required by invariant 3 to be **static** at
|
|
106
|
+
* construction. Because `route()` is consulted dynamically, the wrapper
|
|
107
|
+
* cannot discover routed children's caps after the fact — callers
|
|
108
|
+
* intersecting across backends must enumerate the participants up front.
|
|
109
|
+
*
|
|
110
|
+
* When `undefined` or empty, the routing wrapper mirrors
|
|
111
|
+
* `base.capabilities`. That matches the common single-backend routing
|
|
112
|
+
* use case (route one subgraph name to a peer of the same backend type)
|
|
113
|
+
* without forcing every caller to declare an explicit list. Mixed-backend
|
|
114
|
+
* callers should always populate this — the cap surface won't lie about
|
|
115
|
+
* what's safe across hops.
|
|
116
|
+
*/
|
|
117
|
+
routedCapabilities?: ReadonlyArray<BackendCapabilities>;
|
|
98
118
|
}
|
|
99
119
|
/**
|
|
100
120
|
* Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be
|
|
@@ -119,4 +139,4 @@ interface RoutingBackendOptions {
|
|
|
119
139
|
*/
|
|
120
140
|
declare function createRoutingBackend(base: StorageBackend, options: RoutingBackendOptions): StorageBackend;
|
|
121
141
|
|
|
122
|
-
export { type RoutingBackendOptions, type RoutingContext, StorageBackend, createRoutingBackend };
|
|
142
|
+
export { BackendCapabilities, type RoutingBackendOptions, type RoutingContext, StorageBackend, createRoutingBackend };
|
package/dist/backend.js
CHANGED
|
@@ -5,13 +5,18 @@ import {
|
|
|
5
5
|
resolveAncestorScope
|
|
6
6
|
} from "./chunk-TYYPRVIE.js";
|
|
7
7
|
import {
|
|
8
|
+
createCapabilities,
|
|
9
|
+
intersectCapabilities
|
|
10
|
+
} from "./chunk-N5HFDWQX.js";
|
|
11
|
+
import {
|
|
12
|
+
CapabilityNotSupportedError,
|
|
8
13
|
CrossBackendTransactionError,
|
|
9
14
|
DELETE_FIELD,
|
|
10
15
|
FiregraphError,
|
|
11
16
|
deleteField,
|
|
12
17
|
flattenPatch,
|
|
13
18
|
isDeleteSentinel
|
|
14
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-TK64DNVK.js";
|
|
15
20
|
import "./chunk-EQJUUVFG.js";
|
|
16
21
|
|
|
17
22
|
// src/internal/routing-backend.ts
|
|
@@ -30,16 +35,70 @@ function assertValidSubgraphArgs(parentNodeUid, name) {
|
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
var RoutingStorageBackend = class _RoutingStorageBackend {
|
|
33
|
-
constructor(base, options, storageScope, logicalScopePath) {
|
|
38
|
+
constructor(base, options, storageScope, logicalScopePath, capabilities) {
|
|
34
39
|
this.base = base;
|
|
35
40
|
this.options = options;
|
|
36
41
|
this.collectionPath = base.collectionPath;
|
|
37
42
|
this.scopePath = logicalScopePath;
|
|
38
43
|
this.storageScope = storageScope;
|
|
44
|
+
if (capabilities) {
|
|
45
|
+
this.capabilities = capabilities;
|
|
46
|
+
} else if (options.routedCapabilities && options.routedCapabilities.length > 0) {
|
|
47
|
+
this.capabilities = intersectCapabilities([base.capabilities, ...options.routedCapabilities]);
|
|
48
|
+
} else {
|
|
49
|
+
this.capabilities = base.capabilities;
|
|
50
|
+
}
|
|
39
51
|
if (base.findEdgesGlobal) {
|
|
40
52
|
this.findEdgesGlobal = (params, collectionName) => base.findEdgesGlobal(params, collectionName);
|
|
41
53
|
}
|
|
54
|
+
if (base.aggregate && this.capabilities.has("query.aggregate")) {
|
|
55
|
+
this.aggregate = (spec, filters) => base.aggregate(spec, filters);
|
|
56
|
+
}
|
|
57
|
+
if (base.bulkDelete && this.capabilities.has("query.dml")) {
|
|
58
|
+
this.bulkDelete = (filters, options2) => base.bulkDelete(filters, options2);
|
|
59
|
+
}
|
|
60
|
+
if (base.bulkUpdate && this.capabilities.has("query.dml")) {
|
|
61
|
+
this.bulkUpdate = (filters, patch, options2) => base.bulkUpdate(filters, patch, options2);
|
|
62
|
+
}
|
|
63
|
+
if (base.expand && this.capabilities.has("query.join")) {
|
|
64
|
+
this.expand = (params) => base.expand(params);
|
|
65
|
+
}
|
|
66
|
+
if (base.runEngineTraversal && this.capabilities.has("traversal.serverSide")) {
|
|
67
|
+
this.runEngineTraversal = (params) => base.runEngineTraversal(params);
|
|
68
|
+
}
|
|
69
|
+
if (base.findEdgesProjected && this.capabilities.has("query.select")) {
|
|
70
|
+
this.findEdgesProjected = (select, filters, options2) => base.findEdgesProjected(select, filters, options2);
|
|
71
|
+
}
|
|
72
|
+
if (base.findNearest && this.capabilities.has("search.vector")) {
|
|
73
|
+
this.findNearest = (params) => base.findNearest(params);
|
|
74
|
+
}
|
|
75
|
+
if (base.fullTextSearch && this.capabilities.has("search.fullText")) {
|
|
76
|
+
this.fullTextSearch = (params) => base.fullTextSearch(params);
|
|
77
|
+
}
|
|
78
|
+
if (base.geoSearch && this.capabilities.has("search.geo")) {
|
|
79
|
+
this.geoSearch = (params) => base.geoSearch(params);
|
|
80
|
+
}
|
|
42
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Effective capability set for this wrapper.
|
|
84
|
+
*
|
|
85
|
+
* - **Root wrapper** (`createRoutingBackend(...)` direct return): if the
|
|
86
|
+
* caller supplied `options.routedCapabilities`, the cap set is the
|
|
87
|
+
* intersection of `base.capabilities` and every set in that list. If
|
|
88
|
+
* not, the cap set mirrors `base.capabilities` (suitable when routes
|
|
89
|
+
* target peers of the same backend type — no capability differential
|
|
90
|
+
* to honour).
|
|
91
|
+
* - **Child wrapper** (returned from `subgraph()`): the cap set mirrors
|
|
92
|
+
* the *wrapped* backend (either `base.subgraph(...)` or the backend
|
|
93
|
+
* returned by `route()`). Each child handle reflects what's safe to
|
|
94
|
+
* call against the specific backend it targets — invariant 3 holds
|
|
95
|
+
* per-instance.
|
|
96
|
+
*
|
|
97
|
+
* This satisfies invariant 5 (intersection across mixed-backend graphs)
|
|
98
|
+
* when callers opt in, and falls back to a non-lying mirror when they
|
|
99
|
+
* don't.
|
|
100
|
+
*/
|
|
101
|
+
capabilities;
|
|
43
102
|
collectionPath;
|
|
44
103
|
/**
|
|
45
104
|
* Logical (names-only) scope path for *this* wrapper. Tracked
|
|
@@ -66,6 +125,125 @@ var RoutingStorageBackend = class _RoutingStorageBackend {
|
|
|
66
125
|
* shape in the `StorageBackend` interface.
|
|
67
126
|
*/
|
|
68
127
|
findEdgesGlobal;
|
|
128
|
+
/**
|
|
129
|
+
* Same conditional-install pattern as `findEdgesGlobal`. The router's
|
|
130
|
+
* declared capability set is mirrored from the base (or intersected with
|
|
131
|
+
* the user's `routedCapabilities`) — if `query.aggregate` is in that
|
|
132
|
+
* set, the underlying method must be present, otherwise `client.aggregate()`
|
|
133
|
+
* would resolve `UNSUPPORTED_OPERATION` despite the cap claim. This
|
|
134
|
+
* ensures the "declared capability ⇒ method exists" invariant holds
|
|
135
|
+
* through routing wrappers (Phase 4 audit C1).
|
|
136
|
+
*/
|
|
137
|
+
aggregate;
|
|
138
|
+
/**
|
|
139
|
+
* DML pass-throughs. Same conditional-install pattern as `aggregate`:
|
|
140
|
+
* gated on BOTH the base method's existence AND `this.capabilities`
|
|
141
|
+
* advertising `query.dml`. If `routedCapabilities` intersected
|
|
142
|
+
* `query.dml` away (e.g. one routed peer is Firestore Standard which
|
|
143
|
+
* has no pipeline-DML support), the methods are *not* installed even
|
|
144
|
+
* though `base.bulkDelete` exists — otherwise the router would silently
|
|
145
|
+
* outperform what the declared cap set promises across hops. This
|
|
146
|
+
* preserves the "declared capability ⇒ method exists" invariant in
|
|
147
|
+
* both directions (Phase 5).
|
|
148
|
+
*/
|
|
149
|
+
bulkDelete;
|
|
150
|
+
bulkUpdate;
|
|
151
|
+
/**
|
|
152
|
+
* Multi-source fan-out pass-through. Same conditional-install pattern as
|
|
153
|
+
* `aggregate` and the bulk-DML methods: gated on BOTH the base method's
|
|
154
|
+
* existence AND `this.capabilities` advertising `query.join`. If
|
|
155
|
+
* `routedCapabilities` intersected `query.join` away (e.g. one routed peer
|
|
156
|
+
* is Firestore Standard which has no pipeline-join support), the method is
|
|
157
|
+
* not installed even though `base.expand` exists. This preserves the
|
|
158
|
+
* "declared capability ⇒ method exists" invariant in both directions.
|
|
159
|
+
*
|
|
160
|
+
* Like `aggregate` and bulk DML, `expand` runs against the base backend
|
|
161
|
+
* only — it cannot fan out across routed children, since each routed
|
|
162
|
+
* subgraph is a separate physical store. Cross-graph hops (which resolve
|
|
163
|
+
* to per-source subgraph readers) are therefore never dispatched through
|
|
164
|
+
* `expand` by `traverse.ts`; the same constraint applies here, naturally.
|
|
165
|
+
*/
|
|
166
|
+
expand;
|
|
167
|
+
/**
|
|
168
|
+
* Engine-level multi-hop traversal pass-through. Same conditional-install
|
|
169
|
+
* pattern as `aggregate`, bulk DML, and `expand`: gated on BOTH the base
|
|
170
|
+
* method's existence AND `this.capabilities` advertising
|
|
171
|
+
* `traversal.serverSide`. If `routedCapabilities` intersected
|
|
172
|
+
* `traversal.serverSide` away (e.g. one routed peer is a SQLite-shaped
|
|
173
|
+
* backend that has no nested-pipeline path), the method is not installed
|
|
174
|
+
* even though `base.runEngineTraversal` exists. This preserves the
|
|
175
|
+
* "declared capability ⇒ method exists" invariant in both directions.
|
|
176
|
+
*
|
|
177
|
+
* Like the other extensions, engine traversal runs against the base
|
|
178
|
+
* backend only — a routed child's own `runEngineTraversal` is reached
|
|
179
|
+
* through `.subgraph().runEngineTraversal()` against the routed handle.
|
|
180
|
+
* Cross-graph hops never reach this method anyway: the traversal
|
|
181
|
+
* compiler in `firestore-traverse-compiler.ts` rejects specs whose
|
|
182
|
+
* hops carry `targetGraph`, falling back to the per-hop loop. Routed
|
|
183
|
+
* children are physically distinct backends, so even an "in-graph"
|
|
184
|
+
* traversal across a routed-child boundary is structurally a
|
|
185
|
+
* cross-backend hop and never compiles for engine dispatch.
|
|
186
|
+
*/
|
|
187
|
+
runEngineTraversal;
|
|
188
|
+
/**
|
|
189
|
+
* Server-side projection pass-through. Same conditional-install pattern as
|
|
190
|
+
* `aggregate`, bulk DML, and `expand`: gated on BOTH the base method's
|
|
191
|
+
* existence AND `this.capabilities` advertising `query.select`. If
|
|
192
|
+
* `routedCapabilities` intersected `query.select` away (e.g. one routed
|
|
193
|
+
* peer doesn't implement projection), the method is not installed even
|
|
194
|
+
* though `base.findEdgesProjected` exists. This preserves the "declared
|
|
195
|
+
* capability ⇒ method exists" invariant in both directions.
|
|
196
|
+
*
|
|
197
|
+
* Like `aggregate` and `expand`, projection runs against the base backend
|
|
198
|
+
* only — a routed child's own projection is reached through
|
|
199
|
+
* `.subgraph().findEdgesProjected()` against the routed handle.
|
|
200
|
+
*/
|
|
201
|
+
findEdgesProjected;
|
|
202
|
+
/**
|
|
203
|
+
* Vector / nearest-neighbour pass-through. Same conditional-install
|
|
204
|
+
* pattern as `aggregate`, bulk DML, `expand`, and `findEdgesProjected`:
|
|
205
|
+
* gated on BOTH the base method's existence AND `this.capabilities`
|
|
206
|
+
* advertising `search.vector`. If `routedCapabilities` intersected
|
|
207
|
+
* `search.vector` away (e.g. one routed peer is a SQLite-shaped backend
|
|
208
|
+
* that has no native ANN index), the method is not installed even
|
|
209
|
+
* though `base.findNearest` exists. This preserves the "declared
|
|
210
|
+
* capability ⇒ method exists" invariant in both directions.
|
|
211
|
+
*
|
|
212
|
+
* Like the other extensions, vector search runs against the base
|
|
213
|
+
* backend only — a routed child's own `findNearest` is reached through
|
|
214
|
+
* `.subgraph().findNearest()` against the routed handle.
|
|
215
|
+
*/
|
|
216
|
+
findNearest;
|
|
217
|
+
/**
|
|
218
|
+
* Full-text search pass-through. Same conditional-install pattern as
|
|
219
|
+
* `findNearest`: gated on BOTH the base method's existence AND
|
|
220
|
+
* `this.capabilities` advertising `search.fullText`. If
|
|
221
|
+
* `routedCapabilities` intersected `search.fullText` away (e.g. one
|
|
222
|
+
* routed peer is Firestore Standard or a SQLite-shaped backend that
|
|
223
|
+
* has no native FTS index), the method is not installed even though
|
|
224
|
+
* `base.fullTextSearch` exists. This preserves the "declared
|
|
225
|
+
* capability ⇒ method exists" invariant in both directions.
|
|
226
|
+
*
|
|
227
|
+
* Like the other extensions, FTS runs against the base backend only —
|
|
228
|
+
* a routed child's own `fullTextSearch` is reached through
|
|
229
|
+
* `.subgraph().fullTextSearch()` against the routed handle.
|
|
230
|
+
*/
|
|
231
|
+
fullTextSearch;
|
|
232
|
+
/**
|
|
233
|
+
* Geospatial distance pass-through. Same conditional-install pattern
|
|
234
|
+
* as `fullTextSearch`: gated on BOTH the base method's existence AND
|
|
235
|
+
* `this.capabilities` advertising `search.geo`. If `routedCapabilities`
|
|
236
|
+
* intersected `search.geo` away (e.g. one routed peer is Firestore
|
|
237
|
+
* Standard or a SQLite-shaped backend that has no native geo index),
|
|
238
|
+
* the method is not installed even though `base.geoSearch` exists.
|
|
239
|
+
* This preserves the "declared capability ⇒ method exists" invariant
|
|
240
|
+
* in both directions.
|
|
241
|
+
*
|
|
242
|
+
* Like the other extensions, geo search runs against the base backend
|
|
243
|
+
* only — a routed child's own `geoSearch` is reached through
|
|
244
|
+
* `.subgraph().geoSearch()` against the routed handle.
|
|
245
|
+
*/
|
|
246
|
+
geoSearch;
|
|
69
247
|
// --- Pass-through reads ---
|
|
70
248
|
getDoc(docId) {
|
|
71
249
|
return this.base.getDoc(docId);
|
|
@@ -102,10 +280,22 @@ var RoutingStorageBackend = class _RoutingStorageBackend {
|
|
|
102
280
|
storageScope: childStorageScope
|
|
103
281
|
});
|
|
104
282
|
if (routed) {
|
|
105
|
-
return new _RoutingStorageBackend(
|
|
283
|
+
return new _RoutingStorageBackend(
|
|
284
|
+
routed,
|
|
285
|
+
this.options,
|
|
286
|
+
childStorageScope,
|
|
287
|
+
childScopePath,
|
|
288
|
+
routed.capabilities
|
|
289
|
+
);
|
|
106
290
|
}
|
|
107
291
|
const childBase = this.base.subgraph(parentNodeUid, name);
|
|
108
|
-
return new _RoutingStorageBackend(
|
|
292
|
+
return new _RoutingStorageBackend(
|
|
293
|
+
childBase,
|
|
294
|
+
this.options,
|
|
295
|
+
childStorageScope,
|
|
296
|
+
childScopePath,
|
|
297
|
+
childBase.capabilities
|
|
298
|
+
);
|
|
109
299
|
}
|
|
110
300
|
// --- Bulk operations: delegate, but cascade is base-scope only ---
|
|
111
301
|
removeNodeCascade(uid, reader, options) {
|
|
@@ -131,12 +321,15 @@ function createRoutingBackend(base, options) {
|
|
|
131
321
|
return new RoutingStorageBackend(base, options, "", base.scopePath);
|
|
132
322
|
}
|
|
133
323
|
export {
|
|
324
|
+
CapabilityNotSupportedError,
|
|
134
325
|
CrossBackendTransactionError,
|
|
135
326
|
DELETE_FIELD,
|
|
136
327
|
appendStorageScope,
|
|
328
|
+
createCapabilities,
|
|
137
329
|
createRoutingBackend,
|
|
138
330
|
deleteField,
|
|
139
331
|
flattenPatch,
|
|
332
|
+
intersectCapabilities,
|
|
140
333
|
isAncestorScopeUid,
|
|
141
334
|
isDeleteSentinel,
|
|
142
335
|
parseStorageScope,
|
package/dist/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/routing-backend.ts"],"sourcesContent":["/**\n * Routing `StorageBackend` wrapper.\n *\n * `createRoutingBackend(base, { route })` returns a `StorageBackend` that\n * behaves identically to `base` except for `subgraph(parentUid, name)`:\n * each such call consults the caller-supplied `route` function, and if it\n * returns a non-null `StorageBackend`, that backend is used for the child\n * scope.\n *\n * This is the single seam firegraph ships for splitting a logical graph\n * across multiple physical storage backends — e.g. fanning particular\n * subgraph names out to their own Durable Objects to stay under the 10 GB\n * per-DO limit. The routing policy itself, the RPC protocol, and any\n * live-scope index are left to the caller; firegraph only owns the\n * composition primitive and the invariants that come with it.\n *\n * ## Contract — nested routing\n *\n * Whether `route()` returns a routed backend OR `null` (pass-through), the\n * child returned by `subgraph()` is **always** itself wrapped by the same\n * router. Without that self-wrap, a call chain like\n *\n * ```ts\n * router.subgraph(A, 'memories').subgraph(B, 'context')\n * ```\n *\n * would route the first hop correctly but bypass the router on the second\n * hop (since the routed backend's own `.subgraph()` doesn't know about the\n * caller's policy). Keeping routing active through grandchildren is the\n * load-bearing behaviour; `'continues routing on grandchildren …'` in the\n * unit tests locks it in.\n *\n * ## Contract — `route` is synchronous\n *\n * `.subgraph()` is synchronous in firegraph's public API. Making the\n * routing callback async would require rippling Promises through every\n * client-factory call site. Consequence: `route` can only consult data it\n * already has in hand (DO bindings, naming rules, in-memory caches). If\n * you need \"does this DO exist?\" checks, do them lazily — the first read\n * against the returned backend will surface the failure naturally.\n *\n * ## Contract — cross-backend atomicity is not silently degraded\n *\n * The wrapper's `runTransaction` and `createBatch` delegate to `base` —\n * they run entirely on the base backend. `TransactionBackend` and\n * `BatchBackend` deliberately have no `subgraph()` method, so user code\n * physically cannot open a routed child from inside a transaction\n * callback. Any attempt to bypass that (via `as any` / unchecked casts)\n * should surface as `CrossBackendTransactionError` so app code can catch\n * it cleanly — the error type is part of the public surface.\n *\n * ## Contract — `findEdgesGlobal` is base-scope only\n *\n * When delegated, `findEdgesGlobal` runs against the base backend only.\n * It does **not** fan out to routed children — firegraph has no\n * enumeration index for which routed backends exist. Callers who need\n * cross-shard collection-group queries must maintain their own scope\n * directory and query it directly. This keeps the common case (local\n * analytics inside one DO) fast.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type {\n BulkOptions,\n BulkResult,\n CascadeResult,\n FindEdgesParams,\n GraphReader,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n WriteMode,\n} from './backend.js';\n\n/**\n * Context passed to a routing callback when `subgraph(parentUid, name)` is\n * called on a routed backend. All four strings describe the *child* scope\n * the caller is requesting, so the router can key its decision off whichever\n * representation is most convenient:\n *\n * - `parentUid` / `subgraphName` — the arguments just passed to `subgraph()`.\n * - `scopePath` — logical, names-only chain (`'memories'`, `'memories/context'`).\n * This is what `allowedIn` patterns match against.\n * - `storageScope` — the materialized-path form (`'A/memories'`,\n * `'A/memories/B/context'`), suitable for use as a DO name or shard key\n * because it's globally unique within a root graph.\n */\nexport interface RoutingContext {\n parentUid: string;\n subgraphName: string;\n scopePath: string;\n storageScope: string;\n}\n\nexport interface RoutingBackendOptions {\n /**\n * Decide whether a `subgraph(parentUid, name)` call should route to a\n * different backend. Return the target backend to route; return `null`\n * (or `undefined`) to fall through to the wrapped base backend.\n *\n * The returned backend is itself wrapped by the same router so that\n * nested `.subgraph()` calls on the returned child continue to be\n * consulted.\n */\n route: (ctx: RoutingContext) => StorageBackend | null | undefined;\n}\n\nfunction assertValidSubgraphArgs(parentNodeUid: string, name: string): void {\n if (!parentNodeUid || parentNodeUid.includes('/')) {\n throw new FiregraphError(\n `Invalid parentNodeUid for subgraph: \"${parentNodeUid}\". ` +\n 'Must be a non-empty string without \"/\".',\n 'INVALID_SUBGRAPH',\n );\n }\n if (!name || name.includes('/')) {\n throw new FiregraphError(\n `Subgraph name must not contain \"/\" and must be non-empty: got \"${name}\". ` +\n 'Use chained .subgraph() calls for nested subgraphs.',\n 'INVALID_SUBGRAPH',\n );\n }\n}\n\nclass RoutingStorageBackend implements StorageBackend {\n readonly collectionPath: string;\n /**\n * Logical (names-only) scope path for *this* wrapper. Tracked\n * independently of `base.scopePath` because a routed backend returned by\n * `options.route()` typically represents its own physical root and has\n * no knowledge of the caller's logical chain. The wrapper is the\n * authoritative source of the logical scope for routing decisions and\n * for satisfying the `StorageBackend.scopePath` contract surfaced to\n * client code.\n */\n readonly scopePath: string;\n /**\n * Materialized-path form of `scopePath` — interleaved `<uid>/<name>`\n * pairs. Not a property on the underlying `StorageBackend` interface\n * (Firestore doesn't produce one), so we track it ourselves from\n * `.subgraph()` arguments. Root routers start with `''`.\n */\n private readonly storageScope: string;\n /**\n * Conditionally installed in the constructor — only present when the\n * wrapped base backend supports it. Declared as an optional instance\n * property (rather than a prototype method) so `typeof router.findEdgesGlobal\n * === 'function'` reflects the base's capability, matching the optional\n * shape in the `StorageBackend` interface.\n */\n findEdgesGlobal?: StorageBackend['findEdgesGlobal'];\n\n constructor(\n private readonly base: StorageBackend,\n private readonly options: RoutingBackendOptions,\n storageScope: string,\n logicalScopePath: string,\n ) {\n this.collectionPath = base.collectionPath;\n this.scopePath = logicalScopePath;\n this.storageScope = storageScope;\n if (base.findEdgesGlobal) {\n // We deliberately do *not* fan out across routed children: we have no\n // enumeration index for which backends exist. Callers needing\n // cross-shard collection-group queries must maintain their own index.\n this.findEdgesGlobal = (params, collectionName) =>\n base.findEdgesGlobal!(params, collectionName);\n }\n }\n\n // --- Pass-through reads ---\n\n getDoc(docId: string): Promise<StoredGraphRecord | null> {\n return this.base.getDoc(docId);\n }\n\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n return this.base.query(filters, options);\n }\n\n // --- Pass-through writes ---\n\n setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void> {\n return this.base.setDoc(docId, record, mode);\n }\n\n updateDoc(docId: string, update: UpdatePayload): Promise<void> {\n return this.base.updateDoc(docId, update);\n }\n\n deleteDoc(docId: string): Promise<void> {\n return this.base.deleteDoc(docId);\n }\n\n // --- Transactions / batches run against the base backend only ---\n\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T> {\n // Transactions cannot span base + routed backends (different DBs /\n // DOs / Firestore projects). `TransactionBackend` has no `subgraph()`\n // method, so the user physically cannot open a routed child from\n // inside the callback — the compiler rejects it. At runtime, all\n // reads/writes are confined to the base backend.\n return this.base.runTransaction(fn);\n }\n\n createBatch(): BatchBackend {\n // Same constraint as transactions: `BatchBackend` has no `subgraph()`\n // so all buffered ops target the base backend. The router itself\n // doesn't need to guard anything here.\n return this.base.createBatch();\n }\n\n // --- Subgraphs: the only method that actually routes ---\n\n subgraph(parentNodeUid: string, name: string): StorageBackend {\n assertValidSubgraphArgs(parentNodeUid, name);\n\n const childScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;\n const childStorageScope = this.storageScope\n ? `${this.storageScope}/${parentNodeUid}/${name}`\n : `${parentNodeUid}/${name}`;\n\n const routed = this.options.route({\n parentUid: parentNodeUid,\n subgraphName: name,\n scopePath: childScopePath,\n storageScope: childStorageScope,\n });\n\n if (routed) {\n // The user returned a different backend. We still wrap it so that\n // further `.subgraph()` calls on the returned child continue to\n // consult the router. The routed backend's own `scopePath` / storage\n // layout is its business — for routing purposes we carry *our*\n // logical view forward (`childScopePath`) so grandchildren see a\n // correct context regardless of what `routed.scopePath` happens to\n // be (typically `''` for a freshly-minted per-DO backend).\n return new RoutingStorageBackend(routed, this.options, childStorageScope, childScopePath);\n }\n\n // No route — delegate to the base backend and keep routing in effect\n // for grandchildren.\n const childBase = this.base.subgraph(parentNodeUid, name);\n return new RoutingStorageBackend(childBase, this.options, childStorageScope, childScopePath);\n }\n\n // --- Bulk operations: delegate, but cascade is base-scope only ---\n\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult> {\n // `removeNodeCascade` on the base backend cannot see rows that live\n // in routed child backends — each routed backend is a different\n // physical store. Callers with routed subgraphs under `uid` are\n // responsible for cascading those themselves (see routing.md).\n return this.base.removeNodeCascade(uid, reader, options);\n }\n\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult> {\n return this.base.bulkRemoveEdges(params, reader, options);\n }\n\n // --- Collection-group queries are base-scope only ---\n //\n // `findEdgesGlobal` is installed in the constructor *only* when the base\n // backend supports it, so `typeof router.findEdgesGlobal === 'function'`\n // reflects the base's capability — matching the optional shape declared\n // on `StorageBackend`.\n}\n\n/**\n * Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be\n * routed to a different backend based on a user-supplied callback.\n *\n * See the module docstring for the atomicity rules. In short: transactions\n * and batches opened on a routing backend run entirely on the *base*\n * backend — they cannot span routed children, by design.\n *\n * @example\n * ```ts\n * // `base` is any StorageBackend — e.g. a Firestore-backed one, an\n * // in-process SQLite backend, or the DO backend from firegraph/cloudflare.\n * const routed = createRoutingBackend(base, {\n * route: ({ subgraphName, storageScope }) => {\n * if (subgraphName !== 'memories') return null;\n * return createMyMemoriesBackend(storageScope); // caller-owned\n * },\n * });\n * const client = createGraphClientFromBackend(routed, { registry });\n * ```\n */\nexport function createRoutingBackend(\n base: StorageBackend,\n options: RoutingBackendOptions,\n): StorageBackend {\n if (typeof options?.route !== 'function') {\n throw new FiregraphError(\n 'createRoutingBackend: `options.route` must be a function.',\n 'INVALID_ARGUMENT',\n );\n }\n return new RoutingStorageBackend(base, options, '', base.scopePath);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/routing-backend.ts"],"sourcesContent":["/**\n * Routing `StorageBackend` wrapper.\n *\n * `createRoutingBackend(base, { route })` returns a `StorageBackend` that\n * behaves identically to `base` except for `subgraph(parentUid, name)`:\n * each such call consults the caller-supplied `route` function, and if it\n * returns a non-null `StorageBackend`, that backend is used for the child\n * scope.\n *\n * This is the single seam firegraph ships for splitting a logical graph\n * across multiple physical storage backends — e.g. fanning particular\n * subgraph names out to their own Durable Objects to stay under the 10 GB\n * per-DO limit. The routing policy itself, the RPC protocol, and any\n * live-scope index are left to the caller; firegraph only owns the\n * composition primitive and the invariants that come with it.\n *\n * ## Contract — nested routing\n *\n * Whether `route()` returns a routed backend OR `null` (pass-through), the\n * child returned by `subgraph()` is **always** itself wrapped by the same\n * router. Without that self-wrap, a call chain like\n *\n * ```ts\n * router.subgraph(A, 'memories').subgraph(B, 'context')\n * ```\n *\n * would route the first hop correctly but bypass the router on the second\n * hop (since the routed backend's own `.subgraph()` doesn't know about the\n * caller's policy). Keeping routing active through grandchildren is the\n * load-bearing behaviour; `'continues routing on grandchildren …'` in the\n * unit tests locks it in.\n *\n * ## Contract — `route` is synchronous\n *\n * `.subgraph()` is synchronous in firegraph's public API. Making the\n * routing callback async would require rippling Promises through every\n * client-factory call site. Consequence: `route` can only consult data it\n * already has in hand (DO bindings, naming rules, in-memory caches). If\n * you need \"does this DO exist?\" checks, do them lazily — the first read\n * against the returned backend will surface the failure naturally.\n *\n * ## Contract — cross-backend atomicity is not silently degraded\n *\n * The wrapper's `runTransaction` and `createBatch` delegate to `base` —\n * they run entirely on the base backend. `TransactionBackend` and\n * `BatchBackend` deliberately have no `subgraph()` method, so user code\n * physically cannot open a routed child from inside a transaction\n * callback. Any attempt to bypass that (via `as any` / unchecked casts)\n * should surface as `CrossBackendTransactionError` so app code can catch\n * it cleanly — the error type is part of the public surface.\n *\n * ## Contract — `findEdgesGlobal` is base-scope only\n *\n * When delegated, `findEdgesGlobal` runs against the base backend only.\n * It does **not** fan out to routed children — firegraph has no\n * enumeration index for which routed backends exist. Callers who need\n * cross-shard collection-group queries must maintain their own scope\n * directory and query it directly. This keeps the common case (local\n * analytics inside one DO) fast.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type {\n AggregateSpec,\n BulkOptions,\n BulkResult,\n BulkUpdatePatch,\n CascadeResult,\n EngineTraversalParams,\n EngineTraversalResult,\n ExpandParams,\n ExpandResult,\n FindEdgesParams,\n FindNearestParams,\n FullTextSearchParams,\n GeoSearchParams,\n GraphReader,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type {\n BackendCapabilities,\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n WriteMode,\n} from './backend.js';\nimport { intersectCapabilities } from './backend.js';\n\n/**\n * Context passed to a routing callback when `subgraph(parentUid, name)` is\n * called on a routed backend. All four strings describe the *child* scope\n * the caller is requesting, so the router can key its decision off whichever\n * representation is most convenient:\n *\n * - `parentUid` / `subgraphName` — the arguments just passed to `subgraph()`.\n * - `scopePath` — logical, names-only chain (`'memories'`, `'memories/context'`).\n * This is what `allowedIn` patterns match against.\n * - `storageScope` — the materialized-path form (`'A/memories'`,\n * `'A/memories/B/context'`), suitable for use as a DO name or shard key\n * because it's globally unique within a root graph.\n */\nexport interface RoutingContext {\n parentUid: string;\n subgraphName: string;\n scopePath: string;\n storageScope: string;\n}\n\nexport interface RoutingBackendOptions {\n /**\n * Decide whether a `subgraph(parentUid, name)` call should route to a\n * different backend. Return the target backend to route; return `null`\n * (or `undefined`) to fall through to the wrapped base backend.\n *\n * The returned backend is itself wrapped by the same router so that\n * nested `.subgraph()` calls on the returned child continue to be\n * consulted.\n */\n route: (ctx: RoutingContext) => StorageBackend | null | undefined;\n /**\n * Capability sets for any backend `route()` may return. The root routing\n * wrapper's `capabilities` becomes the intersection of `base.capabilities`\n * and every set passed here, satisfying invariant 5 from\n * `.claude/backend-capabilities.md` (\"a graph mounted across multiple\n * backends declares the intersection of child capability sets\").\n *\n * Capability declarations are required by invariant 3 to be **static** at\n * construction. Because `route()` is consulted dynamically, the wrapper\n * cannot discover routed children's caps after the fact — callers\n * intersecting across backends must enumerate the participants up front.\n *\n * When `undefined` or empty, the routing wrapper mirrors\n * `base.capabilities`. That matches the common single-backend routing\n * use case (route one subgraph name to a peer of the same backend type)\n * without forcing every caller to declare an explicit list. Mixed-backend\n * callers should always populate this — the cap surface won't lie about\n * what's safe across hops.\n */\n routedCapabilities?: ReadonlyArray<BackendCapabilities>;\n}\n\nfunction assertValidSubgraphArgs(parentNodeUid: string, name: string): void {\n if (!parentNodeUid || parentNodeUid.includes('/')) {\n throw new FiregraphError(\n `Invalid parentNodeUid for subgraph: \"${parentNodeUid}\". ` +\n 'Must be a non-empty string without \"/\".',\n 'INVALID_SUBGRAPH',\n );\n }\n if (!name || name.includes('/')) {\n throw new FiregraphError(\n `Subgraph name must not contain \"/\" and must be non-empty: got \"${name}\". ` +\n 'Use chained .subgraph() calls for nested subgraphs.',\n 'INVALID_SUBGRAPH',\n );\n }\n}\n\nclass RoutingStorageBackend implements StorageBackend {\n /**\n * Effective capability set for this wrapper.\n *\n * - **Root wrapper** (`createRoutingBackend(...)` direct return): if the\n * caller supplied `options.routedCapabilities`, the cap set is the\n * intersection of `base.capabilities` and every set in that list. If\n * not, the cap set mirrors `base.capabilities` (suitable when routes\n * target peers of the same backend type — no capability differential\n * to honour).\n * - **Child wrapper** (returned from `subgraph()`): the cap set mirrors\n * the *wrapped* backend (either `base.subgraph(...)` or the backend\n * returned by `route()`). Each child handle reflects what's safe to\n * call against the specific backend it targets — invariant 3 holds\n * per-instance.\n *\n * This satisfies invariant 5 (intersection across mixed-backend graphs)\n * when callers opt in, and falls back to a non-lying mirror when they\n * don't.\n */\n readonly capabilities: BackendCapabilities;\n readonly collectionPath: string;\n /**\n * Logical (names-only) scope path for *this* wrapper. Tracked\n * independently of `base.scopePath` because a routed backend returned by\n * `options.route()` typically represents its own physical root and has\n * no knowledge of the caller's logical chain. The wrapper is the\n * authoritative source of the logical scope for routing decisions and\n * for satisfying the `StorageBackend.scopePath` contract surfaced to\n * client code.\n */\n readonly scopePath: string;\n /**\n * Materialized-path form of `scopePath` — interleaved `<uid>/<name>`\n * pairs. Not a property on the underlying `StorageBackend` interface\n * (Firestore doesn't produce one), so we track it ourselves from\n * `.subgraph()` arguments. Root routers start with `''`.\n */\n private readonly storageScope: string;\n /**\n * Conditionally installed in the constructor — only present when the\n * wrapped base backend supports it. Declared as an optional instance\n * property (rather than a prototype method) so `typeof router.findEdgesGlobal\n * === 'function'` reflects the base's capability, matching the optional\n * shape in the `StorageBackend` interface.\n */\n findEdgesGlobal?: StorageBackend['findEdgesGlobal'];\n /**\n * Same conditional-install pattern as `findEdgesGlobal`. The router's\n * declared capability set is mirrored from the base (or intersected with\n * the user's `routedCapabilities`) — if `query.aggregate` is in that\n * set, the underlying method must be present, otherwise `client.aggregate()`\n * would resolve `UNSUPPORTED_OPERATION` despite the cap claim. This\n * ensures the \"declared capability ⇒ method exists\" invariant holds\n * through routing wrappers (Phase 4 audit C1).\n */\n aggregate?: StorageBackend['aggregate'];\n /**\n * DML pass-throughs. Same conditional-install pattern as `aggregate`:\n * gated on BOTH the base method's existence AND `this.capabilities`\n * advertising `query.dml`. If `routedCapabilities` intersected\n * `query.dml` away (e.g. one routed peer is Firestore Standard which\n * has no pipeline-DML support), the methods are *not* installed even\n * though `base.bulkDelete` exists — otherwise the router would silently\n * outperform what the declared cap set promises across hops. This\n * preserves the \"declared capability ⇒ method exists\" invariant in\n * both directions (Phase 5).\n */\n bulkDelete?: StorageBackend['bulkDelete'];\n bulkUpdate?: StorageBackend['bulkUpdate'];\n /**\n * Multi-source fan-out pass-through. Same conditional-install pattern as\n * `aggregate` and the bulk-DML methods: gated on BOTH the base method's\n * existence AND `this.capabilities` advertising `query.join`. If\n * `routedCapabilities` intersected `query.join` away (e.g. one routed peer\n * is Firestore Standard which has no pipeline-join support), the method is\n * not installed even though `base.expand` exists. This preserves the\n * \"declared capability ⇒ method exists\" invariant in both directions.\n *\n * Like `aggregate` and bulk DML, `expand` runs against the base backend\n * only — it cannot fan out across routed children, since each routed\n * subgraph is a separate physical store. Cross-graph hops (which resolve\n * to per-source subgraph readers) are therefore never dispatched through\n * `expand` by `traverse.ts`; the same constraint applies here, naturally.\n */\n expand?: StorageBackend['expand'];\n /**\n * Engine-level multi-hop traversal pass-through. Same conditional-install\n * pattern as `aggregate`, bulk DML, and `expand`: gated on BOTH the base\n * method's existence AND `this.capabilities` advertising\n * `traversal.serverSide`. If `routedCapabilities` intersected\n * `traversal.serverSide` away (e.g. one routed peer is a SQLite-shaped\n * backend that has no nested-pipeline path), the method is not installed\n * even though `base.runEngineTraversal` exists. This preserves the\n * \"declared capability ⇒ method exists\" invariant in both directions.\n *\n * Like the other extensions, engine traversal runs against the base\n * backend only — a routed child's own `runEngineTraversal` is reached\n * through `.subgraph().runEngineTraversal()` against the routed handle.\n * Cross-graph hops never reach this method anyway: the traversal\n * compiler in `firestore-traverse-compiler.ts` rejects specs whose\n * hops carry `targetGraph`, falling back to the per-hop loop. Routed\n * children are physically distinct backends, so even an \"in-graph\"\n * traversal across a routed-child boundary is structurally a\n * cross-backend hop and never compiles for engine dispatch.\n */\n runEngineTraversal?: StorageBackend['runEngineTraversal'];\n /**\n * Server-side projection pass-through. Same conditional-install pattern as\n * `aggregate`, bulk DML, and `expand`: gated on BOTH the base method's\n * existence AND `this.capabilities` advertising `query.select`. If\n * `routedCapabilities` intersected `query.select` away (e.g. one routed\n * peer doesn't implement projection), the method is not installed even\n * though `base.findEdgesProjected` exists. This preserves the \"declared\n * capability ⇒ method exists\" invariant in both directions.\n *\n * Like `aggregate` and `expand`, projection runs against the base backend\n * only — a routed child's own projection is reached through\n * `.subgraph().findEdgesProjected()` against the routed handle.\n */\n findEdgesProjected?: StorageBackend['findEdgesProjected'];\n /**\n * Vector / nearest-neighbour pass-through. Same conditional-install\n * pattern as `aggregate`, bulk DML, `expand`, and `findEdgesProjected`:\n * gated on BOTH the base method's existence AND `this.capabilities`\n * advertising `search.vector`. If `routedCapabilities` intersected\n * `search.vector` away (e.g. one routed peer is a SQLite-shaped backend\n * that has no native ANN index), the method is not installed even\n * though `base.findNearest` exists. This preserves the \"declared\n * capability ⇒ method exists\" invariant in both directions.\n *\n * Like the other extensions, vector search runs against the base\n * backend only — a routed child's own `findNearest` is reached through\n * `.subgraph().findNearest()` against the routed handle.\n */\n findNearest?: StorageBackend['findNearest'];\n /**\n * Full-text search pass-through. Same conditional-install pattern as\n * `findNearest`: gated on BOTH the base method's existence AND\n * `this.capabilities` advertising `search.fullText`. If\n * `routedCapabilities` intersected `search.fullText` away (e.g. one\n * routed peer is Firestore Standard or a SQLite-shaped backend that\n * has no native FTS index), the method is not installed even though\n * `base.fullTextSearch` exists. This preserves the \"declared\n * capability ⇒ method exists\" invariant in both directions.\n *\n * Like the other extensions, FTS runs against the base backend only —\n * a routed child's own `fullTextSearch` is reached through\n * `.subgraph().fullTextSearch()` against the routed handle.\n */\n fullTextSearch?: StorageBackend['fullTextSearch'];\n /**\n * Geospatial distance pass-through. Same conditional-install pattern\n * as `fullTextSearch`: gated on BOTH the base method's existence AND\n * `this.capabilities` advertising `search.geo`. If `routedCapabilities`\n * intersected `search.geo` away (e.g. one routed peer is Firestore\n * Standard or a SQLite-shaped backend that has no native geo index),\n * the method is not installed even though `base.geoSearch` exists.\n * This preserves the \"declared capability ⇒ method exists\" invariant\n * in both directions.\n *\n * Like the other extensions, geo search runs against the base backend\n * only — a routed child's own `geoSearch` is reached through\n * `.subgraph().geoSearch()` against the routed handle.\n */\n geoSearch?: StorageBackend['geoSearch'];\n\n constructor(\n private readonly base: StorageBackend,\n private readonly options: RoutingBackendOptions,\n storageScope: string,\n logicalScopePath: string,\n /**\n * Explicit cap set for this wrapper. Passed by `subgraph()` so child\n * wrappers mirror the routed child's caps. `createRoutingBackend`\n * leaves it `undefined` so the constructor computes the root-level\n * intersection from `options.routedCapabilities`.\n */\n capabilities?: BackendCapabilities,\n ) {\n this.collectionPath = base.collectionPath;\n this.scopePath = logicalScopePath;\n this.storageScope = storageScope;\n if (capabilities) {\n this.capabilities = capabilities;\n } else if (options.routedCapabilities && options.routedCapabilities.length > 0) {\n this.capabilities = intersectCapabilities([base.capabilities, ...options.routedCapabilities]);\n } else {\n this.capabilities = base.capabilities;\n }\n if (base.findEdgesGlobal) {\n // We deliberately do *not* fan out across routed children: we have no\n // enumeration index for which backends exist. Callers needing\n // cross-shard collection-group queries must maintain their own index.\n this.findEdgesGlobal = (params, collectionName) =>\n base.findEdgesGlobal!(params, collectionName);\n }\n if (base.aggregate && this.capabilities.has('query.aggregate')) {\n // Aggregates are scoped to the base backend — same rationale as\n // `findEdgesGlobal`. A routed child has its own backend with its own\n // `aggregate` method that the user reaches via `.subgraph().aggregate()`;\n // this router-level pass-through covers the base scope only.\n //\n // The cap check matters when `routedCapabilities` intersected\n // `query.aggregate` away: even if `base.aggregate` exists, the router's\n // declared cap set says \"no aggregate\", and installing the method\n // would violate the \"declared capability ⇒ method exists\" invariant\n // in the inverse direction (declared-absent yet runtime-present).\n // The post-Phase-4 audit (M-C) calls this out explicitly.\n this.aggregate = (spec: AggregateSpec, filters: QueryFilter[]) =>\n base.aggregate!(spec, filters);\n }\n if (base.bulkDelete && this.capabilities.has('query.dml')) {\n // Same scope rationale as `aggregate`: bulk DML runs against the base\n // backend only. A routed child's own DML support is reached through\n // `.subgraph().bulkDelete()` against the routed handle.\n this.bulkDelete = (filters: QueryFilter[], options?: BulkOptions) =>\n base.bulkDelete!(filters, options);\n }\n if (base.bulkUpdate && this.capabilities.has('query.dml')) {\n this.bulkUpdate = (filters: QueryFilter[], patch: BulkUpdatePatch, options?: BulkOptions) =>\n base.bulkUpdate!(filters, patch, options);\n }\n if (base.expand && this.capabilities.has('query.join')) {\n // Same scope rationale as `aggregate` and bulk DML: `expand` runs\n // against the base backend only. A routed child's own `expand` is\n // reached through `.subgraph().expand()` against the routed handle.\n this.expand = (params: ExpandParams): Promise<ExpandResult> => base.expand!(params);\n }\n if (base.runEngineTraversal && this.capabilities.has('traversal.serverSide')) {\n // Same scope rationale as the other extensions: engine traversal\n // runs against the base backend only. A routed child's own\n // `runEngineTraversal` is reached through\n // `.subgraph().runEngineTraversal()` against the routed handle.\n // Cross-routed-backend traversal is structurally impossible —\n // the compiler bails on cross-graph hops and the per-hop loop\n // re-resolves the reader at each carry-forward step.\n this.runEngineTraversal = (params: EngineTraversalParams): Promise<EngineTraversalResult> =>\n base.runEngineTraversal!(params);\n }\n if (base.findEdgesProjected && this.capabilities.has('query.select')) {\n // Same scope rationale as `aggregate`, bulk DML, and `expand`:\n // server-side projection runs against the base backend only. A routed\n // child's own projection is reached through\n // `.subgraph().findEdgesProjected()` against the routed handle.\n this.findEdgesProjected = (\n select: ReadonlyArray<string>,\n filters: QueryFilter[],\n options?: QueryOptions,\n ) => base.findEdgesProjected!(select, filters, options);\n }\n if (base.findNearest && this.capabilities.has('search.vector')) {\n // Same scope rationale as the other extensions: vector search runs\n // against the base backend only. A routed child's own `findNearest`\n // is reached through `.subgraph().findNearest()` against the routed\n // handle.\n this.findNearest = (params: FindNearestParams) => base.findNearest!(params);\n }\n if (base.fullTextSearch && this.capabilities.has('search.fullText')) {\n // Same scope rationale as the other extensions: FTS runs against\n // the base backend only. A routed child's own `fullTextSearch` is\n // reached through `.subgraph().fullTextSearch()` against the routed\n // handle.\n this.fullTextSearch = (params: FullTextSearchParams) => base.fullTextSearch!(params);\n }\n if (base.geoSearch && this.capabilities.has('search.geo')) {\n // Same scope rationale as the other extensions: geo search runs\n // against the base backend only. A routed child's own `geoSearch`\n // is reached through `.subgraph().geoSearch()` against the routed\n // handle.\n this.geoSearch = (params: GeoSearchParams) => base.geoSearch!(params);\n }\n }\n\n // --- Pass-through reads ---\n\n getDoc(docId: string): Promise<StoredGraphRecord | null> {\n return this.base.getDoc(docId);\n }\n\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n return this.base.query(filters, options);\n }\n\n // --- Pass-through writes ---\n\n setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void> {\n return this.base.setDoc(docId, record, mode);\n }\n\n updateDoc(docId: string, update: UpdatePayload): Promise<void> {\n return this.base.updateDoc(docId, update);\n }\n\n deleteDoc(docId: string): Promise<void> {\n return this.base.deleteDoc(docId);\n }\n\n // --- Transactions / batches run against the base backend only ---\n\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T> {\n // Transactions cannot span base + routed backends (different DBs /\n // DOs / Firestore projects). `TransactionBackend` has no `subgraph()`\n // method, so the user physically cannot open a routed child from\n // inside the callback — the compiler rejects it. At runtime, all\n // reads/writes are confined to the base backend.\n return this.base.runTransaction(fn);\n }\n\n createBatch(): BatchBackend {\n // Same constraint as transactions: `BatchBackend` has no `subgraph()`\n // so all buffered ops target the base backend. The router itself\n // doesn't need to guard anything here.\n return this.base.createBatch();\n }\n\n // --- Subgraphs: the only method that actually routes ---\n\n subgraph(parentNodeUid: string, name: string): StorageBackend {\n assertValidSubgraphArgs(parentNodeUid, name);\n\n const childScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;\n const childStorageScope = this.storageScope\n ? `${this.storageScope}/${parentNodeUid}/${name}`\n : `${parentNodeUid}/${name}`;\n\n const routed = this.options.route({\n parentUid: parentNodeUid,\n subgraphName: name,\n scopePath: childScopePath,\n storageScope: childStorageScope,\n });\n\n if (routed) {\n // The user returned a different backend. We still wrap it so that\n // further `.subgraph()` calls on the returned child continue to\n // consult the router. The routed backend's own `scopePath` / storage\n // layout is its business — for routing purposes we carry *our*\n // logical view forward (`childScopePath`) so grandchildren see a\n // correct context regardless of what `routed.scopePath` happens to\n // be (typically `''` for a freshly-minted per-DO backend). The child\n // wrapper mirrors the routed backend's own cap set: invariant 3\n // (static caps per instance) holds, and the user's view of the\n // child handle reflects what that backend actually supports.\n return new RoutingStorageBackend(\n routed,\n this.options,\n childStorageScope,\n childScopePath,\n routed.capabilities,\n );\n }\n\n // No route — delegate to the base backend and keep routing in effect\n // for grandchildren. Child wrapper mirrors the base subgraph's caps\n // (typically identical to `base.capabilities` itself, but we ask the\n // child explicitly so a backend that narrows caps in subgraphs is\n // honoured).\n const childBase = this.base.subgraph(parentNodeUid, name);\n return new RoutingStorageBackend(\n childBase,\n this.options,\n childStorageScope,\n childScopePath,\n childBase.capabilities,\n );\n }\n\n // --- Bulk operations: delegate, but cascade is base-scope only ---\n\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult> {\n // `removeNodeCascade` on the base backend cannot see rows that live\n // in routed child backends — each routed backend is a different\n // physical store. Callers with routed subgraphs under `uid` are\n // responsible for cascading those themselves (see routing.md).\n return this.base.removeNodeCascade(uid, reader, options);\n }\n\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult> {\n return this.base.bulkRemoveEdges(params, reader, options);\n }\n\n // --- Collection-group queries are base-scope only ---\n //\n // `findEdgesGlobal` is installed in the constructor *only* when the base\n // backend supports it, so `typeof router.findEdgesGlobal === 'function'`\n // reflects the base's capability — matching the optional shape declared\n // on `StorageBackend`.\n}\n\n/**\n * Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be\n * routed to a different backend based on a user-supplied callback.\n *\n * See the module docstring for the atomicity rules. In short: transactions\n * and batches opened on a routing backend run entirely on the *base*\n * backend — they cannot span routed children, by design.\n *\n * @example\n * ```ts\n * // `base` is any StorageBackend — e.g. a Firestore-backed one, an\n * // in-process SQLite backend, or the DO backend from firegraph/cloudflare.\n * const routed = createRoutingBackend(base, {\n * route: ({ subgraphName, storageScope }) => {\n * if (subgraphName !== 'memories') return null;\n * return createMyMemoriesBackend(storageScope); // caller-owned\n * },\n * });\n * const client = createGraphClientFromBackend(routed, { registry });\n * ```\n */\nexport function createRoutingBackend(\n base: StorageBackend,\n options: RoutingBackendOptions,\n): StorageBackend {\n if (typeof options?.route !== 'function') {\n throw new FiregraphError(\n 'createRoutingBackend: `options.route` must be a function.',\n 'INVALID_ARGUMENT',\n );\n }\n return new RoutingStorageBackend(base, options, '', base.scopePath);\n}\n"],"mappings":"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","names":["options"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/default-indexes.ts
|
|
2
|
+
var DEFAULT_CORE_INDEXES = Object.freeze([
|
|
3
|
+
{ fields: ["aUid"] },
|
|
4
|
+
{ fields: ["bUid"] },
|
|
5
|
+
{ fields: ["aType"] },
|
|
6
|
+
{ fields: ["bType"] },
|
|
7
|
+
{ fields: ["aUid", "axbType"] },
|
|
8
|
+
{ fields: ["axbType", "bUid"] },
|
|
9
|
+
{ fields: ["aType", "axbType"] },
|
|
10
|
+
{ fields: ["axbType", "bType"] }
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
DEFAULT_CORE_INDEXES
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=chunk-2DHMNTV6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/default-indexes.ts"],"sourcesContent":["/**\n * Default core index preset.\n *\n * This set covers the query patterns firegraph's query planner emits for\n * built-in operations — `findNodes`, `findEdges`, cascade delete, traversal,\n * and the DO/SQLite path compilers. Apps that need additional indexes\n * (descending timestamps, `data.*` filters, composite fields unique to\n * their query shapes) declare them on `RegistryEntry.indexes` or override\n * this preset wholesale via the backend-specific `coreIndexes` option —\n * `FiregraphDOOptions.coreIndexes` for the DO backend,\n * `BuildSchemaOptions.coreIndexes` for the legacy SQLite backend, and\n * `GenerateIndexOptions.coreIndexes` for the Firestore CLI generator.\n *\n * ## Ownership model\n *\n * This list is firegraph's *recommendation* — not non-negotiable policy.\n * Consumers can:\n *\n * 1. Accept the preset as-is (default).\n * 2. Extend it: `coreIndexes: [...DEFAULT_CORE_INDEXES, ...more]`.\n * 3. Replace it entirely with a tailored set.\n * 4. Disable it (`coreIndexes: []`) and take full responsibility for\n * index coverage — only do this if you're provisioning a complete\n * custom set.\n *\n * ## Per-backend emission\n *\n * The Firestore generator skips single-field entries (Firestore implicitly\n * indexes every field) and emits one composite index per multi-field spec.\n * The SQLite-flavored generators (DO, legacy) emit every spec as-is.\n *\n * ## Why these specific indexes\n *\n * - `aUid` / `bUid` — required for `_fgRemoveNodeCascade`, which scans by\n * each UID side independently. A composite `(aUid, axbType)` also\n * satisfies `aUid`-alone via leading-column prefix, but the single-field\n * form is cheaper for the common case.\n * - `aType` / `bType` — `findNodes({ aType })` and cross-type enumeration.\n * - `(aUid, axbType)` — forward edge lookup (`findEdges({ aUid, axbType })`)\n * and the `get` strategy fallback when only two of three triple fields\n * are present.\n * - `(axbType, bUid)` — reverse edge traversal.\n * - `(aType, axbType)` — type-scoped edge scans (e.g., `findEdges({ aType, axbType })`).\n * - `(axbType, bType)` — scope edges of one relation to a target type.\n */\n\nimport type { IndexSpec } from './types.js';\n\nexport const DEFAULT_CORE_INDEXES: ReadonlyArray<IndexSpec> = Object.freeze([\n { fields: ['aUid'] },\n { fields: ['bUid'] },\n { fields: ['aType'] },\n { fields: ['bType'] },\n { fields: ['aUid', 'axbType'] },\n { fields: ['axbType', 'bUid'] },\n { fields: ['aType', 'axbType'] },\n { fields: ['axbType', 'bType'] },\n]);\n"],"mappings":";AAgDO,IAAM,uBAAiD,OAAO,OAAO;AAAA,EAC1E,EAAE,QAAQ,CAAC,MAAM,EAAE;AAAA,EACnB,EAAE,QAAQ,CAAC,MAAM,EAAE;AAAA,EACnB,EAAE,QAAQ,CAAC,OAAO,EAAE;AAAA,EACpB,EAAE,QAAQ,CAAC,OAAO,EAAE;AAAA,EACpB,EAAE,QAAQ,CAAC,QAAQ,SAAS,EAAE;AAAA,EAC9B,EAAE,QAAQ,CAAC,WAAW,MAAM,EAAE;AAAA,EAC9B,EAAE,QAAQ,CAAC,SAAS,SAAS,EAAE;AAAA,EAC/B,EAAE,QAAQ,CAAC,WAAW,OAAO,EAAE;AACjC,CAAC;","names":[]}
|