@typicalday/firegraph 0.11.2 → 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 +355 -78
- package/dist/backend-DuvHGgK1.d.cts +1897 -0
- package/dist/backend-DuvHGgK1.d.ts +1897 -0
- package/dist/backend.cjs +365 -5
- 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 +209 -7
- 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-5753Y42M.js → chunk-C2QMD7RY.js} +6 -10
- package/dist/chunk-C2QMD7RY.js.map +1 -0
- package/dist/chunk-D4J7Z4FE.js +67 -0
- package/dist/chunk-D4J7Z4FE.js.map +1 -0
- package/dist/chunk-EQJUUVFG.js +14 -0
- package/dist/chunk-EQJUUVFG.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-TK64DNVK.js +256 -0
- package/dist/chunk-TK64DNVK.js.map +1 -0
- package/dist/{chunk-NJSOD64C.js → chunk-WRTFC5NG.js} +438 -30
- 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 +1386 -74
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +217 -13
- package/dist/cloudflare/index.d.ts +217 -13
- package/dist/cloudflare/index.js +639 -180
- 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 +809 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -100
- package/dist/index.d.ts +24 -100
- package/dist/index.js +184 -531
- package/dist/index.js.map +1 -1
- package/dist/registry-Bc7h6WTM.d.cts +64 -0
- package/dist/registry-C2KUPVZj.d.ts +64 -0
- package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.cts} +1 -56
- package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.ts} +1 -56
- package/dist/{serialization-ZZ7RSDRX.js → serialization-OE2PFZMY.js} +6 -4
- 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-U-MLShlg.d.ts +0 -97
- package/dist/backend-np4gEVhB.d.cts +0 -97
- package/dist/chunk-5753Y42M.js.map +0 -1
- package/dist/chunk-NJSOD64C.js.map +0 -1
- package/dist/chunk-R7CRGYY4.js +0 -94
- package/dist/chunk-R7CRGYY4.js.map +0 -1
- package/dist/types-BGWxcpI_.d.cts +0 -736
- package/dist/types-BGWxcpI_.d.ts +0 -736
- /package/dist/{serialization-ZZ7RSDRX.js.map → serialization-OE2PFZMY.js.map} +0 -0
package/dist/backend.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/backend.ts","../src/errors.ts","../src/internal/routing-backend.ts","../src/scope-path.ts"],"sourcesContent":["/**\n * Public backend surface — the stable set of types and primitives for code\n * that wants to wrap, substitute, or compose `StorageBackend`s.\n *\n * Most firegraph users only touch `GraphClient`; this module is for the\n * narrower set of users who write their own storage drivers (e.g. an RPC\n * executor that tunnels a `StorageBackend` into a Durable Object) or who\n * need to route `subgraph()` calls across multiple physical backends\n * (see `createRoutingBackend`).\n *\n * Entry point: `firegraph/backend`.\n */\n\nexport { CrossBackendTransactionError } from './errors.js';\nexport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './internal/backend.js';\nexport type { RoutingBackendOptions, RoutingContext } from './internal/routing-backend.js';\nexport { createRoutingBackend } from './internal/routing-backend.js';\nexport type { StorageScopeSegment } from './scope-path.js';\nexport {\n appendStorageScope,\n isAncestorScopeUid,\n parseStorageScope,\n resolveAncestorScope,\n} from './scope-path.js';\n","export class FiregraphError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'FiregraphError';\n }\n}\n\nexport class NodeNotFoundError extends FiregraphError {\n constructor(uid: string) {\n super(`Node not found: ${uid}`, 'NODE_NOT_FOUND');\n this.name = 'NodeNotFoundError';\n }\n}\n\nexport class EdgeNotFoundError extends FiregraphError {\n constructor(aUid: string, axbType: string, bUid: string) {\n super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, 'EDGE_NOT_FOUND');\n this.name = 'EdgeNotFoundError';\n }\n}\n\nexport class ValidationError extends FiregraphError {\n constructor(\n message: string,\n public readonly details?: unknown,\n ) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class RegistryViolationError extends FiregraphError {\n constructor(aType: string, axbType: string, bType: string) {\n super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, 'REGISTRY_VIOLATION');\n this.name = 'RegistryViolationError';\n }\n}\n\nexport class InvalidQueryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'INVALID_QUERY');\n this.name = 'InvalidQueryError';\n }\n}\n\nexport class TraversalError extends FiregraphError {\n constructor(message: string) {\n super(message, 'TRAVERSAL_ERROR');\n this.name = 'TraversalError';\n }\n}\n\nexport class DynamicRegistryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'DYNAMIC_REGISTRY_ERROR');\n this.name = 'DynamicRegistryError';\n }\n}\n\nexport class QuerySafetyError extends FiregraphError {\n constructor(message: string) {\n super(message, 'QUERY_SAFETY');\n this.name = 'QuerySafetyError';\n }\n}\n\nexport class RegistryScopeError extends FiregraphError {\n constructor(\n aType: string,\n axbType: string,\n bType: string,\n scopePath: string,\n allowedIn: string[],\n ) {\n super(\n `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope \"${scopePath || 'root'}\". ` +\n `Allowed in: [${allowedIn.join(', ')}]`,\n 'REGISTRY_SCOPE',\n );\n this.name = 'RegistryScopeError';\n }\n}\n\nexport class MigrationError extends FiregraphError {\n constructor(message: string) {\n super(message, 'MIGRATION_ERROR');\n this.name = 'MigrationError';\n }\n}\n\n/**\n * Thrown when a caller tries to perform an operation that would require\n * atomicity across two physical storage backends — e.g. opening a routed\n * subgraph client from inside a transaction callback. Cross-backend\n * atomicity cannot be honoured by real-world storage engines (Firestore,\n * SQLite drivers over D1/DO/better-sqlite3, etc.), so firegraph surfaces\n * this as a typed error instead of silently confining the write to the\n * base backend.\n *\n * Normally `TransactionBackend` and `BatchBackend` don't expose `subgraph()`\n * at the type level, so this error is unreachable through well-typed code.\n * It exists as a public catchable type for app code that needs to tolerate\n * this case deliberately (e.g. dynamic code paths that bypass the type\n * system) and as future-proofing if the interface ever grows a way to\n * request a sub-scope inside a transaction.\n */\nexport class CrossBackendTransactionError extends FiregraphError {\n constructor(message: string) {\n super(message, 'CROSS_BACKEND_TRANSACTION');\n this.name = 'CrossBackendTransactionError';\n }\n}\n","/**\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} 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): Promise<void> {\n return this.base.setDoc(docId, record);\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","/**\n * Storage-scope path utilities — materialized-path parsing helpers for the\n * SQLite backend's `storageScope` string and for any custom backend that\n * adopts the same encoding (e.g. a cross-DO routing layer that uses\n * `storageScope` as a Durable Object name).\n *\n * **Storage-scope** (as produced by `SqliteBackendImpl`) interleaves parent\n * UIDs with subgraph names:\n *\n * ```\n * '' // root\n * 'A/memories' // g.subgraph(A, 'memories')\n * 'A/memories/B/context' // .subgraph(B, 'context') on the above\n * ```\n *\n * The structure is the same as a Firestore collection path with the\n * collection/doc segments reordered: each pair is `<uid>/<name>`, where\n * `<uid>` is a node UID in the parent scope and `<name>` is the subgraph\n * name. Use these helpers to decode that structure when building cross-\n * backend routers (see `createRoutingBackend`).\n *\n * For Firestore paths (which begin with a collection segment), use\n * `resolveAncestorCollection` / `isAncestorUid` from `./cross-graph.js`.\n */\n\n/**\n * One segment of a materialized-path storage-scope — a `(uid, name)` pair\n * produced by one `subgraph(uid, name)` call.\n */\nexport interface StorageScopeSegment {\n /** Parent node UID at the enclosing scope. */\n uid: string;\n /** Subgraph name chosen by the caller (e.g. `'memories'`). */\n name: string;\n}\n\n/**\n * Parse a materialized-path storage-scope into its `(uid, name)` pairs.\n *\n * Returns `[]` for the root (`''`). Throws `Error('INVALID_SCOPE_PATH')`\n * when the string has an odd number of segments (a corrupt path — every\n * level contributes exactly two segments) or when any segment is empty.\n *\n * @example\n * ```ts\n * parseStorageScope(''); // []\n * parseStorageScope('A/memories'); // [{ uid: 'A', name: 'memories' }]\n * parseStorageScope('A/memories/B/context'); // [{ uid: 'A', name: 'memories' }, { uid: 'B', name: 'context' }]\n * ```\n */\nexport function parseStorageScope(scope: string): StorageScopeSegment[] {\n if (scope === '') return [];\n const parts = scope.split('/');\n if (parts.length % 2 !== 0) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" has an odd number of segments; ` +\n 'expected interleaved <uid>/<name> pairs.',\n );\n }\n const out: StorageScopeSegment[] = [];\n for (let i = 0; i < parts.length; i += 2) {\n const uid = parts[i];\n const name = parts[i + 1];\n if (!uid || !name) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" contains an empty segment at position ${i}.`,\n );\n }\n out.push({ uid, name });\n }\n return out;\n}\n\n/**\n * Resolve the ancestor **storage-scope** at which a given UID's node lives,\n * by scanning a materialized-path storage-scope for that UID.\n *\n * Mirrors `resolveAncestorCollection()` from `./cross-graph.js` for\n * Firestore paths, but operates on `storageScope` (no leading collection\n * segment — segments are `<uid>/<name>` pairs).\n *\n * @returns The storage-scope at which the UID's node was added via\n * `subgraph(uid, _)`, or `null` if the UID does not appear at a UID\n * position in the path.\n *\n * @example\n * ```ts\n * // Scope: 'A/memories/B/context'\n * resolveAncestorScope('A/memories/B/context', 'A'); // '' (A was added at root)\n * resolveAncestorScope('A/memories/B/context', 'B'); // 'A/memories'\n * resolveAncestorScope('A/memories/B/context', 'X'); // null\n * ```\n */\nexport function resolveAncestorScope(storageScope: string, uid: string): string | null {\n if (!uid) return null;\n if (storageScope === '') return null;\n const parts = storageScope.split('/');\n // UID positions are even indices (0, 2, 4, …); names are at odd indices.\n for (let i = 0; i < parts.length; i += 2) {\n if (parts[i] === uid) {\n return i === 0 ? '' : parts.slice(0, i).join('/');\n }\n }\n return null;\n}\n\n/**\n * Boolean shorthand for `resolveAncestorScope(scope, uid) !== null`.\n */\nexport function isAncestorScopeUid(storageScope: string, uid: string): boolean {\n return resolveAncestorScope(storageScope, uid) !== null;\n}\n\n/**\n * Join a parent storage-scope with a new `(uid, name)` pair, producing the\n * storage-scope that `backend.subgraph(uid, name)` would use internally.\n *\n * This is the inverse of `parseStorageScope`'s per-segment semantics and is\n * useful when computing DO names / shard keys from the router callback.\n */\nexport function appendStorageScope(parentScope: string, uid: string, name: string): string {\n if (!uid || uid.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: uid must be non-empty and must not contain \"/\": got \"${uid}\".`,\n );\n }\n if (!name || name.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: name must be non-empty and must not contain \"/\": got \"${name}\".`,\n );\n }\n return parentScope ? `${parentScope}/${uid}/${name}` : `${uid}/${name}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/backend.ts","../src/errors.ts","../src/internal/backend.ts","../src/internal/routing-backend.ts","../src/internal/serialization-tag.ts","../src/internal/write-plan.ts","../src/scope-path.ts"],"sourcesContent":["/**\n * Public backend surface — the stable set of types and primitives for code\n * that wants to wrap, substitute, or compose `StorageBackend`s.\n *\n * Most firegraph users only touch `GraphClient`; this module is for the\n * narrower set of users who write their own storage drivers (e.g. an RPC\n * executor that tunnels a `StorageBackend` into a Durable Object) or who\n * need to route `subgraph()` calls across multiple physical backends\n * (see `createRoutingBackend`).\n *\n * Entry point: `firegraph/backend`.\n */\n\nexport { CapabilityNotSupportedError, CrossBackendTransactionError } from './errors.js';\nexport type {\n BackendCapabilities,\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n WriteMode,\n} from './internal/backend.js';\nexport { createCapabilities, intersectCapabilities } from './internal/backend.js';\nexport type { RoutingBackendOptions, RoutingContext } from './internal/routing-backend.js';\nexport { createRoutingBackend } from './internal/routing-backend.js';\nexport type { DataPathOp } from './internal/write-plan.js';\nexport {\n DELETE_FIELD,\n deleteField,\n flattenPatch,\n isDeleteSentinel,\n} from './internal/write-plan.js';\nexport type { StorageScopeSegment } from './scope-path.js';\nexport {\n appendStorageScope,\n isAncestorScopeUid,\n parseStorageScope,\n resolveAncestorScope,\n} from './scope-path.js';\n// DML types (Phase 5, `query.dml`). Re-exported here so backend authors\n// implementing the optional `StorageBackend.bulkDelete` / `bulkUpdate`\n// signatures can pull `BulkUpdatePatch` from the same entry that surfaces\n// `StorageBackend` itself, instead of having to reach into the root\n// `firegraph` package. `DmlExtension` is the client-surface counterpart;\n// it's bundled here so `intersectCapabilities` consumers building a\n// composed type can name both in one place.\nexport type { BulkUpdatePatch, DmlExtension } from './types.js';\n// Join types (Phase 6, `query.join`). Same rationale as the DML re-exports:\n// backend authors implementing the optional `StorageBackend.expand` signature\n// can pull `ExpandParams` / `ExpandResult` from this entry directly.\n// `JoinExtension` is the client-surface counterpart, bundled here so a\n// composed cap-typed client can name all three in one place.\nexport type { ExpandParams, ExpandResult, JoinExtension } from './types.js';\n","export class FiregraphError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'FiregraphError';\n }\n}\n\nexport class NodeNotFoundError extends FiregraphError {\n constructor(uid: string) {\n super(`Node not found: ${uid}`, 'NODE_NOT_FOUND');\n this.name = 'NodeNotFoundError';\n }\n}\n\nexport class EdgeNotFoundError extends FiregraphError {\n constructor(aUid: string, axbType: string, bUid: string) {\n super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, 'EDGE_NOT_FOUND');\n this.name = 'EdgeNotFoundError';\n }\n}\n\nexport class ValidationError extends FiregraphError {\n constructor(\n message: string,\n public readonly details?: unknown,\n ) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class RegistryViolationError extends FiregraphError {\n constructor(aType: string, axbType: string, bType: string) {\n super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, 'REGISTRY_VIOLATION');\n this.name = 'RegistryViolationError';\n }\n}\n\nexport class InvalidQueryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'INVALID_QUERY');\n this.name = 'InvalidQueryError';\n }\n}\n\nexport class TraversalError extends FiregraphError {\n constructor(message: string) {\n super(message, 'TRAVERSAL_ERROR');\n this.name = 'TraversalError';\n }\n}\n\nexport class DynamicRegistryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'DYNAMIC_REGISTRY_ERROR');\n this.name = 'DynamicRegistryError';\n }\n}\n\nexport class QuerySafetyError extends FiregraphError {\n constructor(message: string) {\n super(message, 'QUERY_SAFETY');\n this.name = 'QuerySafetyError';\n }\n}\n\nexport class RegistryScopeError extends FiregraphError {\n constructor(\n aType: string,\n axbType: string,\n bType: string,\n scopePath: string,\n allowedIn: string[],\n ) {\n super(\n `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope \"${scopePath || 'root'}\". ` +\n `Allowed in: [${allowedIn.join(', ')}]`,\n 'REGISTRY_SCOPE',\n );\n this.name = 'RegistryScopeError';\n }\n}\n\nexport class MigrationError extends FiregraphError {\n constructor(message: string) {\n super(message, 'MIGRATION_ERROR');\n this.name = 'MigrationError';\n }\n}\n\n/**\n * Thrown when a caller tries to perform an operation that would require\n * atomicity across two physical storage backends — e.g. opening a routed\n * subgraph client from inside a transaction callback. Cross-backend\n * atomicity cannot be honoured by real-world storage engines (Firestore,\n * SQLite drivers over D1/DO/better-sqlite3, etc.), so firegraph surfaces\n * this as a typed error instead of silently confining the write to the\n * base backend.\n *\n * Normally `TransactionBackend` and `BatchBackend` don't expose `subgraph()`\n * at the type level, so this error is unreachable through well-typed code.\n * It exists as a public catchable type for app code that needs to tolerate\n * this case deliberately (e.g. dynamic code paths that bypass the type\n * system) and as future-proofing if the interface ever grows a way to\n * request a sub-scope inside a transaction.\n */\nexport class CrossBackendTransactionError extends FiregraphError {\n constructor(message: string) {\n super(message, 'CROSS_BACKEND_TRANSACTION');\n this.name = 'CrossBackendTransactionError';\n }\n}\n\n/**\n * Thrown when a caller invokes a capability-gated operation on a backend\n * that does not declare the required capability. Capability gating is\n * primarily a compile-time concern (see `BackendCapabilities` and the\n * type-level extension surfaces in `GraphClient<C>`), but this runtime\n * error covers the cases where the type system is bypassed — dynamic\n * registries, `as any` casts, or callers explicitly downcasting through\n * the generic-erased `StorageBackend` shape.\n *\n * The error code is `CAPABILITY_NOT_SUPPORTED`. The message names the\n * missing capability and the backend that was asked, so app code can\n * diagnose without inspecting the cap set itself.\n */\nexport class CapabilityNotSupportedError extends FiregraphError {\n constructor(\n public readonly capability: string,\n backendDescription: string,\n ) {\n super(\n `Capability \"${capability}\" is not supported by ${backendDescription}.`,\n 'CAPABILITY_NOT_SUPPORTED',\n );\n this.name = 'CapabilityNotSupportedError';\n }\n}\n","/**\n * Backend abstraction for firegraph.\n *\n * `StorageBackend` is the single interface every storage driver implements.\n * The Firestore backend wraps `@google-cloud/firestore`; the SQLite backend\n * (shared by D1 and Durable Object SQLite) uses a parameterized SQL executor.\n *\n * `GraphClientImpl` and friends depend only on this interface — they have\n * no direct knowledge of Firestore or SQLite.\n */\n\nimport type {\n AggregateSpec,\n BulkOptions,\n BulkResult,\n BulkUpdatePatch,\n Capability,\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 { DataPathOp } from './write-plan.js';\n\n/**\n * Runtime descriptor of which `Capability`s a `StorageBackend` actually\n * implements. Static for the lifetime of a backend instance; declared at\n * construction. The phantom `_phantom` field is a type-level marker\n * (never read at runtime) that lets the type parameter `C` flow through\n * the descriptor for use by `GraphClient<C>` conditional gating.\n *\n * Use `createCapabilities` to construct one. Use `.has(c)` to check\n * membership at runtime; the type system gates extension methods on the\n * client level (see `.claude/backend-capabilities.md`).\n */\nexport interface BackendCapabilities<C extends Capability = Capability> {\n /** Runtime membership check. */\n has(capability: Capability): boolean;\n /** Iterate declared capabilities (diagnostics, error messages). */\n values(): IterableIterator<Capability>;\n /** Type-level marker. Never read at runtime. */\n readonly _phantom?: C;\n}\n\n/**\n * Construct a `BackendCapabilities<C>` from an explicit set. The set is\n * captured by reference; callers should treat it as readonly after passing\n * it in. The runtime cost of `has()` is one Set lookup.\n */\nexport function createCapabilities<C extends Capability>(\n caps: ReadonlySet<C>,\n): BackendCapabilities<C> {\n return {\n has: (capability: Capability): boolean => caps.has(capability as C),\n values: () => caps.values() as IterableIterator<Capability>,\n };\n}\n\n/**\n * Intersect multiple capability sets. Used by `RoutingStorageBackend` to\n * derive the capability set of a composite backend: a routed graph can\n * only honour a capability if every wrapped backend honours it.\n */\nexport function intersectCapabilities(\n parts: ReadonlyArray<BackendCapabilities>,\n): BackendCapabilities {\n if (parts.length === 0) return createCapabilities(new Set<Capability>());\n const sets = parts.map((p) => new Set<Capability>(p.values()));\n const [first, ...rest] = sets;\n const intersection = new Set<Capability>();\n for (const c of first) {\n if (rest.every((s) => s.has(c))) intersection.add(c);\n }\n return createCapabilities(intersection);\n}\n\n/**\n * Per-record write payload — backend-agnostic. Timestamps are not present;\n * the backend supplies them via `serverTimestamp()` placeholders that it\n * itself resolves at commit time.\n */\nexport interface WritableRecord {\n aType: string;\n aUid: string;\n axbType: string;\n bType: string;\n bUid: string;\n data: Record<string, unknown>;\n /** Schema version (set by the writer when registry has migrations). */\n v?: number;\n}\n\n/**\n * Write semantics for `setDoc`.\n *\n * - `'merge'` — the new contract (0.12+). Existing fields not mentioned\n * in the new data survive; nested objects are recursively merged;\n * arrays are replaced as a unit. This is the default for\n * `putNode` / `putEdge`.\n * - `'replace'` — the document is replaced wholesale, dropping any\n * fields not present in the payload. This is the explicit escape\n * hatch surfaced as `replaceNode` / `replaceEdge` and used by\n * migration write-back.\n */\nexport type WriteMode = 'merge' | 'replace';\n\n/**\n * Patch shape for `updateDoc`.\n *\n * - `dataOps`: list of deep-path terminal ops produced by\n * `flattenPatch()` (one op per leaf — arrays / primitives / Firestore\n * special types are terminal). Used by `updateNode` / `updateEdge`.\n * Sibling keys at every depth are preserved.\n * - `replaceData`: full `data` replacement. Used only by the migration\n * write-back path, which has already produced a complete migrated\n * document.\n * - `v`: optional schema-version stamp.\n *\n * `updatedAt` is always set by the backend.\n */\nexport interface UpdatePayload {\n dataOps?: DataPathOp[];\n replaceData?: Record<string, unknown>;\n v?: number;\n}\n\n/**\n * Read/write transaction adapter. Mirrors Firestore's transaction semantics:\n * reads are snapshot-consistent; writes are issued inside the transaction\n * and a rejection from any write aborts the surrounding `runTransaction`.\n *\n * Writes return `Promise<void>` so SQL drivers can surface row-level errors\n * (constraint violations, malformed JSON paths) rather than swallowing them.\n * Firestore implementations can resolve synchronously since the underlying\n * `Transaction.set/update/delete` calls are themselves synchronous buffers.\n */\nexport interface TransactionBackend {\n getDoc(docId: string): Promise<StoredGraphRecord | null>;\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;\n setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void>;\n updateDoc(docId: string, update: UpdatePayload): Promise<void>;\n deleteDoc(docId: string): Promise<void>;\n}\n\n/**\n * Atomic multi-write batch.\n */\nexport interface BatchBackend {\n setDoc(docId: string, record: WritableRecord, mode: WriteMode): void;\n updateDoc(docId: string, update: UpdatePayload): void;\n deleteDoc(docId: string): void;\n commit(): Promise<void>;\n}\n\n/**\n * The single storage abstraction.\n *\n * Each backend instance is scoped to a \"graph location\" — for Firestore\n * that's a collection path; for SQLite it's a (table, scopePath) pair.\n * `subgraph()` returns a child backend bound to a nested location.\n */\nexport interface StorageBackend<C extends Capability = Capability> {\n /** Capabilities this backend instance declares. Static for the lifetime of the backend. */\n readonly capabilities: BackendCapabilities<C>;\n /** Backend-internal location identifier (collection path or table name). */\n readonly collectionPath: string;\n /** Subgraph scope (empty string for root). */\n readonly scopePath: string;\n\n // --- Reads ---\n getDoc(docId: string): Promise<StoredGraphRecord | null>;\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;\n\n // --- Writes ---\n setDoc(docId: string, record: WritableRecord, mode: WriteMode): Promise<void>;\n updateDoc(docId: string, update: UpdatePayload): Promise<void>;\n deleteDoc(docId: string): Promise<void>;\n\n // --- Transactions & batches ---\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T>;\n createBatch(): BatchBackend;\n\n // --- Subgraphs ---\n subgraph(parentNodeUid: string, name: string): StorageBackend;\n\n // --- Cascade & bulk ---\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult>;\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult>;\n\n // --- Cross-collection queries ---\n /**\n * Find edges across all subgraphs sharing a given collection name.\n * Optional — backends that can't support this should throw a clear error.\n */\n findEdgesGlobal?(params: FindEdgesParams, collectionName?: string): Promise<StoredGraphRecord[]>;\n\n // --- Aggregations ---\n /**\n * Run an aggregate query (count/sum/avg/min/max). Present only on backends\n * that declare `query.aggregate`. The map's keys are caller-defined aliases\n * matching `AggregateSpec`; values are the resolved numeric results.\n *\n * Backends that can't satisfy a particular op throw `FiregraphError` with\n * code `UNSUPPORTED_AGGREGATE` (e.g. Firestore Standard rejects min/max).\n */\n aggregate?(spec: AggregateSpec, filters: QueryFilter[]): Promise<Record<string, number>>;\n\n // --- Server-side DML ---\n /**\n * Delete every row matching `filters` in one server-side statement.\n * Present only on backends that declare `query.dml`. The default cascade\n * implementation in `bulk.ts` uses this when available; backends without\n * the cap (e.g. Firestore Standard) fall back to a fetch-then-delete\n * loop driven by `findEdges` + per-row `deleteDoc`.\n *\n * The contract matches `findEdges`: scope predicates are honoured\n * automatically by the backend's own internal scope tracking. Callers\n * supply only the filter list — the same shape produced by\n * `buildEdgeQueryPlan`.\n */\n bulkDelete?(filters: QueryFilter[], options?: BulkOptions): Promise<BulkResult>;\n /**\n * Update every row matching `filters` with `patch` in one server-side\n * statement. The patch is deep-merged into each row's `data` field, the\n * same flatten-then-merge pipeline `updateDoc` uses. Identifying columns\n * (`aType`, `axbType`, `aUid`, `bType`, `bUid`, `v`) are not writable\n * through this path.\n */\n bulkUpdate?(\n filters: QueryFilter[],\n patch: BulkUpdatePatch,\n options?: BulkOptions,\n ): Promise<BulkResult>;\n\n // --- Server-side multi-source fan-out ---\n /**\n * Fan out from `params.sources` over a single edge type in one server-side\n * round trip. Present only on backends that declare `query.join`. The\n * traversal layer (`traverse.ts`) calls `expand` once per hop when the\n * backend declares the cap; otherwise it falls back to the per-source\n * `findEdges` loop.\n *\n * Cross-graph hops are never dispatched through `expand` — each source\n * UID resolves to a distinct subgraph location, which can't be fanned\n * out as a single statement. The traversal layer enforces that\n * boundary; `expand` itself does not need to inspect `targetGraph`.\n */\n expand?(params: ExpandParams): Promise<ExpandResult>;\n\n // --- Engine-level multi-hop traversal ---\n /**\n * Compile a multi-hop traversal spec into one server-side query and\n * dispatch a single round trip. Present only on backends that declare\n * `traversal.serverSide` (Firestore Enterprise today, via nested\n * Pipelines that combine `define`, `addFields`, and\n * `toArrayExpression`).\n *\n * The traversal layer (`traverse.ts`) compiles a `TraversalBuilder`\n * spec into `EngineTraversalParams` only when the spec is eligible\n * (no cross-graph hops, no JS filters, depth ≤ `MAX_PIPELINE_DEPTH`,\n * `Π(limitPerSource_i × N_i) ≤ maxReads`, `limitPerSource` set on\n * every hop). Ineligible specs fall back to the per-hop `expand()`\n * loop without invoking this method.\n *\n * The result collapses the nested-pipeline tree into per-hop edge\n * arrays so the traversal layer can fold the result into the same\n * `HopResult[]` shape it produces from the per-hop loop.\n */\n runEngineTraversal?(params: EngineTraversalParams): Promise<EngineTraversalResult>;\n\n // --- Server-side projection ---\n /**\n * Run a projecting query — return only the listed fields per row. Present\n * only on backends that declare `query.select`. The cap-less fallback is\n * `findEdges` followed by a JS-side projection in user code; firegraph\n * does not auto-fall-back because the wire-payload reduction is the only\n * reason to call this method.\n *\n * `select` is the explicit field list; `filters` and `options` mirror the\n * `query()` shape. The returned rows have one slot per unique entry in\n * `select`. Field-name interpretation is the backend's responsibility:\n * built-in fields resolve to columns / Firestore field names, bare names\n * resolve to `data.<name>`, and dotted paths resolve verbatim. See\n * `FindEdgesProjectedParams` for the user-facing contract.\n *\n * Migrations are not applied to the result — the caller asked for a\n * specific projection shape, and rehydrating a partial record into the\n * migration pipeline would require synthesising every absent field.\n */\n findEdgesProjected?(\n select: ReadonlyArray<string>,\n filters: QueryFilter[],\n options?: QueryOptions,\n ): Promise<Array<Record<string, unknown>>>;\n\n // --- Native vector / nearest-neighbour search ---\n /**\n * Run a vector / nearest-neighbour query. Present only on backends that\n * declare `search.vector`. There is no client-side fallback — the\n * SQLite-shaped backends (shared SQLite, Cloudflare DO) genuinely have\n * no native ANN index, and a JS-side k-NN sweep over `findEdges()` would\n * scale catastrophically. Backends without the cap throw\n * `UNSUPPORTED_OPERATION` from the client wrapper.\n *\n * `params` carries the user-facing shape (vector field path, query\n * vector, distance metric, optional threshold and result-field). The\n * client wrapper has already run scan-protection on the identifying\n * / `where` filter list before dispatching.\n *\n * Path normalisation is the backend's responsibility: rewriting bare\n * `vectorField` / `distanceResultField` names to `data.<name>` and\n * rejecting envelope fields (`aType`, `axbType`, `bType`, `aUid`,\n * `bUid`, `v`, etc.) with `INVALID_QUERY` happens inside the\n * backend, not the client wrapper. The two in-tree Firestore-edition\n * backends share `runFirestoreFindNearest` (see\n * `src/internal/firestore-vector.ts`) for this; third-party backends\n * declaring `search.vector` must apply equivalent normalisation\n * before calling their underlying SDK.\n *\n * The backend is also responsible for translating to the underlying\n * SDK call (`Query.findNearest` on Firestore today) and decoding the\n * result snapshot into `StoredGraphRecord[]`.\n *\n * Migrations are not applied to the result. The vector index walks the\n * raw stored shape; rehydrating into the migration pipeline before\n * returning would change the candidate set the index already chose.\n */\n findNearest?(params: FindNearestParams): Promise<StoredGraphRecord[]>;\n\n // --- Native full-text search ---\n /**\n * Run a full-text search query. Present only on backends that declare\n * `search.fullText`. There is no client-side fallback — the only\n * in-tree backend that supports it is Firestore Enterprise (via\n * Pipeline `search({ query: documentMatches(...) })`); Standard and\n * the SQLite-shaped backends throw `UNSUPPORTED_OPERATION` from the\n * client wrapper.\n *\n * The backend is responsible for path normalisation (rewriting\n * bare `fields` entries to `data.<name>`, rejecting envelope fields\n * with `INVALID_QUERY`), translating to the underlying SDK call,\n * and decoding the result into `StoredGraphRecord[]`.\n *\n * Migrations are not applied to the result. The search index walked\n * the raw stored shape; rehydrating into the migration pipeline\n * would change the candidate set the index already scored.\n */\n fullTextSearch?(params: FullTextSearchParams): Promise<StoredGraphRecord[]>;\n\n // --- Native geospatial distance search ---\n /**\n * Run a geospatial distance search. Present only on backends that\n * declare `search.geo`. There is no client-side fallback — only\n * Firestore Enterprise has a native geo index (translated via\n * Pipeline `search({ query: geoDistance(...).lessThanOrEqual(...) })`).\n * Backends without the cap throw `UNSUPPORTED_OPERATION` from the\n * client wrapper.\n *\n * The backend is responsible for `geoField` path normalisation,\n * translating `point` to a Firestore `GeoPoint`, applying the\n * radius cap inside the search query, and (when\n * `orderByDistance` is true / unset) emitting the\n * `geoDistance(...).ascending()` ordering inside the search stage.\n *\n * Migrations are not applied to the result.\n */\n geoSearch?(params: GeoSearchParams): Promise<StoredGraphRecord[]>;\n}\n","/**\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","/**\n * Firegraph serialization tag — split from `src/serialization.ts` so it can\n * be imported from Workers-facing code without dragging in\n * `@google-cloud/firestore`.\n *\n * The full serialization module (with Timestamp/GeoPoint round-tripping)\n * lives one folder up because the sandbox migration pipeline needs it; the\n * write-plan helper only needs to recognise tagged objects to keep them\n * terminal during patch flattening, so it imports just the tag from here.\n */\n\n/** Sentinel key used to tag serialized Firestore types. */\nexport const SERIALIZATION_TAG = '__firegraph_ser__' as const;\n\nconst KNOWN_TYPES = new Set(['Timestamp', 'GeoPoint', 'VectorValue', 'DocumentReference']);\n\n/** Check if a value is a tagged serialized Firestore type. */\nexport function isTaggedValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const tag = (value as Record<string, unknown>)[SERIALIZATION_TAG];\n return typeof tag === 'string' && KNOWN_TYPES.has(tag);\n}\n","/**\n * Write-plan helper — flattens partial-update payloads into a list of\n * deep-path operations every backend can execute identically.\n *\n * Background: firegraph used to ship two write semantics that quietly\n * disagreed about depth.\n * - `putNode`/`putEdge` did a full document replace.\n * - `updateNode`/`updateEdge` did a one-level shallow merge: top-level\n * keys were preserved, but nested objects were replaced wholesale.\n *\n * Both behaviours dropped sibling keys silently. The 0.12 contract is that\n * `put*` and `update*` deep-merge by default (sibling keys at any depth\n * survive); `replace*` is the explicit escape hatch.\n *\n * `flattenPatch` walks a partial-update payload and emits one\n * {@link DataPathOp} per terminal value. Plain objects recurse; arrays,\n * primitives, Firestore special types, and tagged firegraph-serialization\n * objects are terminal (replaced as a unit). `undefined` values are\n * skipped; `null` is preserved as a real `null` write; the\n * {@link DELETE_FIELD} sentinel marks a field for removal.\n *\n * The output is deliberately backend-agnostic. Each backend translates ops\n * into its native dialect:\n * - Firestore: dotted field path → `data.a.b.c` for `update()`.\n * - SQLite / DO SQLite: `json_set(data, '$.a.b.c', ?)` /\n * `json_remove(data, '$.a.b.c')`.\n */\n\nimport { isTaggedValue, SERIALIZATION_TAG } from './serialization-tag.js';\n\n// ---------------------------------------------------------------------------\n// Public sentinel\n// ---------------------------------------------------------------------------\n\n/**\n * Sentinel returned by {@link deleteField}. Treated by all backends as\n * \"remove this field from the stored document\".\n *\n * Equivalent to Firestore's `FieldValue.delete()`, but works for SQLite\n * backends too. Use inside `updateNode`/`updateEdge` payloads.\n */\nexport const DELETE_FIELD: unique symbol = Symbol.for('firegraph.deleteField');\nexport type DeleteSentinel = typeof DELETE_FIELD;\n\n/**\n * Returns the firegraph delete sentinel. Place this anywhere in an\n * `updateNode`/`updateEdge` payload to remove the corresponding field.\n *\n * ```ts\n * await client.updateNode('tour', uid, {\n * attrs: { obsoleteFlag: deleteField() },\n * });\n * ```\n */\nexport function deleteField(): DeleteSentinel {\n return DELETE_FIELD;\n}\n\n/** Type guard for the delete sentinel. */\nexport function isDeleteSentinel(value: unknown): value is DeleteSentinel {\n return value === DELETE_FIELD;\n}\n\n// ---------------------------------------------------------------------------\n// Terminal-detection helpers\n// ---------------------------------------------------------------------------\n\nconst FIRESTORE_TERMINAL_CTOR = new Set([\n 'Timestamp',\n 'GeoPoint',\n 'VectorValue',\n 'DocumentReference',\n 'FieldValue',\n 'NumericIncrementTransform',\n 'ArrayUnionTransform',\n 'ArrayRemoveTransform',\n 'ServerTimestampTransform',\n 'DeleteTransform',\n]);\n\n/**\n * Should this value be written as a single terminal op (no recursion)?\n *\n * Plain JS objects (constructor === Object, or no prototype) are recursed.\n * Everything else — arrays, primitives, class instances, Firestore special\n * types, tagged serialization payloads — is terminal.\n */\nexport function isTerminalValue(value: unknown): boolean {\n if (value === null) return true;\n const t = typeof value;\n if (t !== 'object') return true;\n if (Array.isArray(value)) return true;\n // Tagged serialization payloads carry the SERIALIZATION_TAG sentinel and\n // should be persisted whole — never split into per-field ops.\n if (isTaggedValue(value)) return true;\n const proto = Object.getPrototypeOf(value);\n if (proto === null || proto === Object.prototype) return false;\n // Class instances — Firestore types or anything else exotic.\n const ctor = (value as { constructor?: { name?: string } }).constructor;\n if (ctor && typeof ctor.name === 'string' && FIRESTORE_TERMINAL_CTOR.has(ctor.name)) return true;\n // Unknown class instance: treat as terminal. Recursing into a class\n // instance is almost always wrong (Map, Set, Date, Buffer...).\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Core type\n// ---------------------------------------------------------------------------\n\n/**\n * Single terminal write operation produced by {@link flattenPatch}.\n *\n * `path` is a non-empty array of plain object keys. `value` is the value to\n * write; ignored when `delete` is `true`. Arrays / primitives / Firestore\n * special types appear here as whole terminal values.\n */\nexport interface DataPathOp {\n path: readonly string[];\n value: unknown;\n delete: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Path-segment validation\n// ---------------------------------------------------------------------------\n\n/**\n * Object keys that are safe to embed in SQLite `json_set`/`json_remove`\n * paths. The SQLite backend uses an allowlist regex too — keep these in\n * sync (see `JSON_PATH_KEY_RE` in `internal/sqlite-sql.ts` and\n * `cloudflare/sql.ts`).\n *\n * Allows: ASCII letters, digits, `_`, `-`. Must start with a letter or\n * underscore. This rejects keys containing dots, brackets, quotes, or\n * non-ASCII characters that could break path parsing or be used to\n * inject into the path expression.\n */\nconst SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * Mutual-exclusion guard for {@link UpdatePayload}. The two branches of the\n * shape — `dataOps` (deep-merge) and `replaceData` (full replace) — are\n * structurally incompatible: combining them would tell the backend to\n * simultaneously merge AND wipe, and the three backends disagree on which\n * wins. This helper centralises the runtime check so all three backends\n * trip the same error.\n *\n * Imported as a runtime check from `firestore-backend`, `sqlite-sql`, and\n * `cloudflare/sql`. Backend authors implementing the public `StorageBackend`\n * contract should call it too.\n */\nexport function assertUpdatePayloadExclusive(update: {\n dataOps?: unknown;\n replaceData?: unknown;\n}): void {\n if (update.replaceData !== undefined && update.dataOps !== undefined) {\n throw new Error(\n 'firegraph: UpdatePayload cannot specify both `replaceData` and `dataOps`. ' +\n 'Use one or the other — `replaceData` is the migration-write-back form, ' +\n '`dataOps` is the standard partial-update form.',\n );\n }\n}\n\n/**\n * Reject `DELETE_FIELD` sentinels in payloads where field deletion isn't a\n * meaningful operation: full-document replace (`replaceNode`/`replaceEdge`)\n * and the merge-default put surface (`putNode`/`putEdge`).\n *\n * Why both:\n * - In **replace**, the entire `data` field is overwritten. A delete\n * sentinel in that payload either silently disappears (Firestore drops\n * the Symbol during `.set()` serialization) or produces an empty SQLite\n * `json_remove` no-op, depending on backend. Either way the caller's\n * intent — \"remove field X\" — is lost. Use `updateNode` instead.\n * - In **put** (merge mode), behaviour diverges across backends today:\n * SQLite's flattenPatch emits a real delete op, but Firestore's\n * `.set(..., {merge: true})` silently drops the Symbol. Until that's\n * fixed end-to-end, the safest contract is to reject sentinels at the\n * entry point and steer callers to `updateNode`.\n *\n * The walk mirrors `flattenPatch`: plain objects recurse, everything else\n * is terminal. Tagged serialization payloads short-circuit so we don't\n * recurse into the `__firegraph_ser__` envelope.\n */\nexport function assertNoDeleteSentinels(data: unknown, callerLabel: string): void {\n walkForDeleteSentinels(data, [], { kind: 'root' }, ({ path }) => {\n const where = path.length === 0 ? '<root>' : path.map((p) => JSON.stringify(p)).join(' > ');\n throw new Error(\n `firegraph: ${callerLabel} payload contains a deleteField() sentinel at ${where}. ` +\n `deleteField() is only valid inside updateNode/updateEdge — full-data ` +\n `writes (put*, replace*) cannot delete individual fields. Use updateNode ` +\n `with a deleteField() value, or omit the field from the replace payload.`,\n );\n });\n}\n\ntype SentinelParent = { kind: 'root' } | { kind: 'object' } | { kind: 'array'; index: number };\n\nfunction walkForDeleteSentinels(\n node: unknown,\n path: readonly string[],\n parent: SentinelParent,\n visit: (ctx: { path: readonly string[]; parent: SentinelParent }) => void,\n): void {\n if (node === null || node === undefined) return;\n if (isDeleteSentinel(node)) {\n visit({ path, parent });\n return;\n }\n if (typeof node !== 'object') return;\n if (isTaggedValue(node)) return;\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n walkForDeleteSentinels(node[i], [...path, String(i)], { kind: 'array', index: i }, visit);\n }\n return;\n }\n const proto = Object.getPrototypeOf(node);\n if (proto !== null && proto !== Object.prototype) return;\n const obj = node as Record<string, unknown>;\n for (const key of Object.keys(obj)) {\n walkForDeleteSentinels(obj[key], [...path, key], { kind: 'object' }, visit);\n }\n}\n\n/** Throws if any path segment in the patch is unsafe for SQLite paths. */\nexport function assertSafePath(path: readonly string[]): void {\n for (const seg of path) {\n if (!SAFE_KEY_RE.test(seg)) {\n throw new Error(\n `firegraph: unsafe object key ${JSON.stringify(seg)} at path ${path\n .map((p) => JSON.stringify(p))\n .join(' > ')}. Keys used inside update payloads must match ` +\n `/^[A-Za-z_][A-Za-z0-9_-]*$/ so they can be embedded safely in ` +\n `SQLite JSON paths.`,\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// flattenPatch\n// ---------------------------------------------------------------------------\n\n/**\n * Flatten a partial-update payload into a list of terminal {@link DataPathOp}s.\n *\n * Rules:\n * - Plain objects (no prototype or `Object.prototype`) recurse — each\n * key becomes another path segment.\n * - Arrays are terminal: writing `{tags: ['a']}` overwrites the whole\n * `tags` array. Element-wise array merging is intentionally NOT\n * supported — it's almost never what callers actually want, and\n * Firestore `arrayUnion`/`arrayRemove` give precise semantics when\n * they are.\n * - `undefined` values are skipped (no op generated). Use\n * {@link deleteField} if you actually want to remove a field.\n * - `null` is preserved verbatim — emits a terminal op with `value: null`.\n * - {@link DELETE_FIELD} produces an op with `delete: true`.\n * - Firestore special types and tagged serialization payloads are terminal.\n * - Class instances are terminal.\n *\n * Throws if any object key on the recursion path is unsafe (see\n * {@link assertSafePath}).\n */\nexport function flattenPatch(data: Record<string, unknown>): DataPathOp[] {\n const ops: DataPathOp[] = [];\n walk(data, [], ops);\n return ops;\n}\n\nfunction assertNoDeleteSentinelsInArrayValue(\n arr: readonly unknown[],\n arrayPath: readonly string[],\n): void {\n walkForDeleteSentinels(arr, arrayPath, { kind: 'root' }, ({ parent }) => {\n const arrayPathStr =\n arrayPath.length === 0 ? '<root>' : arrayPath.map((p) => JSON.stringify(p)).join(' > ');\n if (parent.kind === 'array') {\n throw new Error(\n `firegraph: deleteField() sentinel at index ${parent.index} inside an array at ` +\n `path ${arrayPathStr}. Arrays are ` +\n `terminal in update payloads (replaced as a unit), so the sentinel ` +\n `would be silently dropped by JSON serialization. To remove the ` +\n `field entirely, pass deleteField() in place of the whole array.`,\n );\n }\n throw new Error(\n `firegraph: deleteField() sentinel inside an array element at ` +\n `path ${arrayPathStr}. ` +\n `Arrays are terminal in update payloads — the sentinel would ` +\n `be silently dropped by JSON serialization.`,\n );\n });\n}\n\nfunction walk(node: unknown, path: string[], out: DataPathOp[]): void {\n // Caller guarantees the root is a plain object; this branch only\n // matters for recursion.\n if (node === undefined) return;\n if (isDeleteSentinel(node)) {\n if (path.length === 0) {\n throw new Error('firegraph: deleteField() cannot be the entire update payload.');\n }\n assertSafePath(path);\n out.push({ path: [...path], value: undefined, delete: true });\n return;\n }\n if (isTerminalValue(node)) {\n if (path.length === 0) {\n // `null` / array / primitive at the root is illegal — patches must\n // describe per-key changes.\n throw new Error(\n 'firegraph: update payload must be a plain object. Got ' +\n (node === null ? 'null' : Array.isArray(node) ? 'array' : typeof node) +\n '.',\n );\n }\n // A DELETE_FIELD sentinel embedded inside an array (which is terminal\n // and replaced as a unit) would silently disappear: JSON.stringify drops\n // Symbols, and Firestore's serializer does likewise. Reject loudly so\n // the divergence between \"user wrote a delete\" and \"field stayed put\"\n // can't happen.\n if (Array.isArray(node)) {\n assertNoDeleteSentinelsInArrayValue(node, path);\n }\n assertSafePath(path);\n out.push({ path: [...path], value: node, delete: false });\n return;\n }\n // Plain object: recurse into its own enumerable keys.\n const obj = node as Record<string, unknown>;\n const keys = Object.keys(obj);\n if (keys.length === 0) {\n // Empty object at non-root: emit terminal op so an empty object can\n // be written explicitly when the caller really wants one. Skip at\n // the root — no-op patches should produce no ops.\n if (path.length > 0) {\n assertSafePath(path);\n out.push({ path: [...path], value: {}, delete: false });\n }\n return;\n }\n for (const key of keys) {\n if (key === SERIALIZATION_TAG) {\n const where = path.length === 0 ? '<root>' : path.map((p) => JSON.stringify(p)).join(' > ');\n throw new Error(\n `firegraph: update payload contains a literal \\`${SERIALIZATION_TAG}\\` key at ` +\n `${where}. That key is reserved for firegraph's serialization envelope and ` +\n `cannot appear on a plain object in user data. Use a different field name, ` +\n `or pass a recognized tagged value through replaceNode/replaceEdge instead.`,\n );\n }\n walk(obj[key], [...path, key], out);\n }\n}\n","/**\n * Storage-scope path utilities — materialized-path parsing helpers for the\n * SQLite backend's `storageScope` string and for any custom backend that\n * adopts the same encoding (e.g. a cross-DO routing layer that uses\n * `storageScope` as a Durable Object name).\n *\n * **Storage-scope** (as produced by `SqliteBackendImpl`) interleaves parent\n * UIDs with subgraph names:\n *\n * ```\n * '' // root\n * 'A/memories' // g.subgraph(A, 'memories')\n * 'A/memories/B/context' // .subgraph(B, 'context') on the above\n * ```\n *\n * The structure is the same as a Firestore collection path with the\n * collection/doc segments reordered: each pair is `<uid>/<name>`, where\n * `<uid>` is a node UID in the parent scope and `<name>` is the subgraph\n * name. Use these helpers to decode that structure when building cross-\n * backend routers (see `createRoutingBackend`).\n *\n * For Firestore paths (which begin with a collection segment), use\n * `resolveAncestorCollection` / `isAncestorUid` from `./cross-graph.js`.\n */\n\n/**\n * One segment of a materialized-path storage-scope — a `(uid, name)` pair\n * produced by one `subgraph(uid, name)` call.\n */\nexport interface StorageScopeSegment {\n /** Parent node UID at the enclosing scope. */\n uid: string;\n /** Subgraph name chosen by the caller (e.g. `'memories'`). */\n name: string;\n}\n\n/**\n * Parse a materialized-path storage-scope into its `(uid, name)` pairs.\n *\n * Returns `[]` for the root (`''`). Throws `Error('INVALID_SCOPE_PATH')`\n * when the string has an odd number of segments (a corrupt path — every\n * level contributes exactly two segments) or when any segment is empty.\n *\n * @example\n * ```ts\n * parseStorageScope(''); // []\n * parseStorageScope('A/memories'); // [{ uid: 'A', name: 'memories' }]\n * parseStorageScope('A/memories/B/context'); // [{ uid: 'A', name: 'memories' }, { uid: 'B', name: 'context' }]\n * ```\n */\nexport function parseStorageScope(scope: string): StorageScopeSegment[] {\n if (scope === '') return [];\n const parts = scope.split('/');\n if (parts.length % 2 !== 0) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" has an odd number of segments; ` +\n 'expected interleaved <uid>/<name> pairs.',\n );\n }\n const out: StorageScopeSegment[] = [];\n for (let i = 0; i < parts.length; i += 2) {\n const uid = parts[i];\n const name = parts[i + 1];\n if (!uid || !name) {\n throw new Error(\n `INVALID_SCOPE_PATH: storage-scope \"${scope}\" contains an empty segment at position ${i}.`,\n );\n }\n out.push({ uid, name });\n }\n return out;\n}\n\n/**\n * Resolve the ancestor **storage-scope** at which a given UID's node lives,\n * by scanning a materialized-path storage-scope for that UID.\n *\n * Mirrors `resolveAncestorCollection()` from `./cross-graph.js` for\n * Firestore paths, but operates on `storageScope` (no leading collection\n * segment — segments are `<uid>/<name>` pairs).\n *\n * @returns The storage-scope at which the UID's node was added via\n * `subgraph(uid, _)`, or `null` if the UID does not appear at a UID\n * position in the path.\n *\n * @example\n * ```ts\n * // Scope: 'A/memories/B/context'\n * resolveAncestorScope('A/memories/B/context', 'A'); // '' (A was added at root)\n * resolveAncestorScope('A/memories/B/context', 'B'); // 'A/memories'\n * resolveAncestorScope('A/memories/B/context', 'X'); // null\n * ```\n */\nexport function resolveAncestorScope(storageScope: string, uid: string): string | null {\n if (!uid) return null;\n if (storageScope === '') return null;\n const parts = storageScope.split('/');\n // UID positions are even indices (0, 2, 4, …); names are at odd indices.\n for (let i = 0; i < parts.length; i += 2) {\n if (parts[i] === uid) {\n return i === 0 ? '' : parts.slice(0, i).join('/');\n }\n }\n return null;\n}\n\n/**\n * Boolean shorthand for `resolveAncestorScope(scope, uid) !== null`.\n */\nexport function isAncestorScopeUid(storageScope: string, uid: string): boolean {\n return resolveAncestorScope(storageScope, uid) !== null;\n}\n\n/**\n * Join a parent storage-scope with a new `(uid, name)` pair, producing the\n * storage-scope that `backend.subgraph(uid, name)` would use internally.\n *\n * This is the inverse of `parseStorageScope`'s per-segment semantics and is\n * useful when computing DO names / shard keys from the router callback.\n */\nexport function appendStorageScope(parentScope: string, uid: string, name: string): string {\n if (!uid || uid.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: uid must be non-empty and must not contain \"/\": got \"${uid}\".`,\n );\n }\n if (!name || name.includes('/')) {\n throw new Error(\n `INVALID_SCOPE_PATH: name must be non-empty and must not contain \"/\": got \"${name}\".`,\n );\n }\n return parentScope ? `${parentScope}/${uid}/${name}` : `${uid}/${name}`;\n}\n"],"mappings":"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","names":["options"]}
|
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,9 +5,19 @@ 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
|
-
|
|
10
|
-
|
|
14
|
+
DELETE_FIELD,
|
|
15
|
+
FiregraphError,
|
|
16
|
+
deleteField,
|
|
17
|
+
flattenPatch,
|
|
18
|
+
isDeleteSentinel
|
|
19
|
+
} from "./chunk-TK64DNVK.js";
|
|
20
|
+
import "./chunk-EQJUUVFG.js";
|
|
11
21
|
|
|
12
22
|
// src/internal/routing-backend.ts
|
|
13
23
|
function assertValidSubgraphArgs(parentNodeUid, name) {
|
|
@@ -25,16 +35,70 @@ function assertValidSubgraphArgs(parentNodeUid, name) {
|
|
|
25
35
|
}
|
|
26
36
|
}
|
|
27
37
|
var RoutingStorageBackend = class _RoutingStorageBackend {
|
|
28
|
-
constructor(base, options, storageScope, logicalScopePath) {
|
|
38
|
+
constructor(base, options, storageScope, logicalScopePath, capabilities) {
|
|
29
39
|
this.base = base;
|
|
30
40
|
this.options = options;
|
|
31
41
|
this.collectionPath = base.collectionPath;
|
|
32
42
|
this.scopePath = logicalScopePath;
|
|
33
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
|
+
}
|
|
34
51
|
if (base.findEdgesGlobal) {
|
|
35
52
|
this.findEdgesGlobal = (params, collectionName) => base.findEdgesGlobal(params, collectionName);
|
|
36
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
|
+
}
|
|
37
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;
|
|
38
102
|
collectionPath;
|
|
39
103
|
/**
|
|
40
104
|
* Logical (names-only) scope path for *this* wrapper. Tracked
|
|
@@ -61,6 +125,125 @@ var RoutingStorageBackend = class _RoutingStorageBackend {
|
|
|
61
125
|
* shape in the `StorageBackend` interface.
|
|
62
126
|
*/
|
|
63
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;
|
|
64
247
|
// --- Pass-through reads ---
|
|
65
248
|
getDoc(docId) {
|
|
66
249
|
return this.base.getDoc(docId);
|
|
@@ -69,8 +252,8 @@ var RoutingStorageBackend = class _RoutingStorageBackend {
|
|
|
69
252
|
return this.base.query(filters, options);
|
|
70
253
|
}
|
|
71
254
|
// --- Pass-through writes ---
|
|
72
|
-
setDoc(docId, record) {
|
|
73
|
-
return this.base.setDoc(docId, record);
|
|
255
|
+
setDoc(docId, record, mode) {
|
|
256
|
+
return this.base.setDoc(docId, record, mode);
|
|
74
257
|
}
|
|
75
258
|
updateDoc(docId, update) {
|
|
76
259
|
return this.base.updateDoc(docId, update);
|
|
@@ -97,10 +280,22 @@ var RoutingStorageBackend = class _RoutingStorageBackend {
|
|
|
97
280
|
storageScope: childStorageScope
|
|
98
281
|
});
|
|
99
282
|
if (routed) {
|
|
100
|
-
return new _RoutingStorageBackend(
|
|
283
|
+
return new _RoutingStorageBackend(
|
|
284
|
+
routed,
|
|
285
|
+
this.options,
|
|
286
|
+
childStorageScope,
|
|
287
|
+
childScopePath,
|
|
288
|
+
routed.capabilities
|
|
289
|
+
);
|
|
101
290
|
}
|
|
102
291
|
const childBase = this.base.subgraph(parentNodeUid, name);
|
|
103
|
-
return new _RoutingStorageBackend(
|
|
292
|
+
return new _RoutingStorageBackend(
|
|
293
|
+
childBase,
|
|
294
|
+
this.options,
|
|
295
|
+
childStorageScope,
|
|
296
|
+
childScopePath,
|
|
297
|
+
childBase.capabilities
|
|
298
|
+
);
|
|
104
299
|
}
|
|
105
300
|
// --- Bulk operations: delegate, but cascade is base-scope only ---
|
|
106
301
|
removeNodeCascade(uid, reader, options) {
|
|
@@ -126,10 +321,17 @@ function createRoutingBackend(base, options) {
|
|
|
126
321
|
return new RoutingStorageBackend(base, options, "", base.scopePath);
|
|
127
322
|
}
|
|
128
323
|
export {
|
|
324
|
+
CapabilityNotSupportedError,
|
|
129
325
|
CrossBackendTransactionError,
|
|
326
|
+
DELETE_FIELD,
|
|
130
327
|
appendStorageScope,
|
|
328
|
+
createCapabilities,
|
|
131
329
|
createRoutingBackend,
|
|
330
|
+
deleteField,
|
|
331
|
+
flattenPatch,
|
|
332
|
+
intersectCapabilities,
|
|
132
333
|
isAncestorScopeUid,
|
|
334
|
+
isDeleteSentinel,
|
|
133
335
|
parseStorageScope,
|
|
134
336
|
resolveAncestorScope
|
|
135
337
|
};
|