@typicalday/firegraph 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -17
- package/dist/{backend-CvImIwTY.d.cts → backend-CE3pM9-T.d.ts} +32 -2
- package/dist/{backend-BpYLdwCW.d.cts → backend-DNzv8KSR.d.cts} +33 -19
- package/dist/{backend-BpYLdwCW.d.ts → backend-DNzv8KSR.d.ts} +33 -19
- package/dist/{backend-YH5HtawN.d.ts → backend-EjFfw9yO.d.cts} +32 -2
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +2 -2
- package/dist/backend.d.ts +2 -2
- package/dist/backend.js +1 -1
- package/dist/{chunk-FODIMIWY.js → chunk-5JBNLH5W.js} +17 -6
- package/dist/chunk-5JBNLH5W.js.map +1 -0
- package/dist/{chunk-5HIRYV2S.js → chunk-6IO74NKD.js} +12 -10
- package/dist/{chunk-5HIRYV2S.js.map → chunk-6IO74NKD.js.map} +1 -1
- package/dist/{chunk-ULRDQ6HZ.js → chunk-NZVSLWNY.js} +6 -1
- package/dist/chunk-NZVSLWNY.js.map +1 -0
- package/dist/{chunk-N5HFDWQX.js → chunk-PWIO46RT.js} +1 -1
- package/dist/{chunk-N5HFDWQX.js.map → chunk-PWIO46RT.js.map} +1 -1
- package/dist/{client-B5o39X79.d.ts → client-CNAwJayO.d.ts} +1 -1
- package/dist/{client-BGHwxwPg.d.cts → client-CaXH5D5C.d.cts} +1 -1
- package/dist/cloudflare/index.cjs +11 -9
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +3 -3
- package/dist/cloudflare/index.d.ts +3 -3
- package/dist/cloudflare/index.js +3 -3
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/firestore-enterprise/index.cjs +11 -9
- package/dist/firestore-enterprise/index.cjs.map +1 -1
- package/dist/firestore-enterprise/index.d.cts +3 -3
- package/dist/firestore-enterprise/index.d.ts +3 -3
- package/dist/firestore-enterprise/index.js +2 -2
- package/dist/firestore-standard/index.cjs +11 -9
- package/dist/firestore-standard/index.cjs.map +1 -1
- package/dist/firestore-standard/index.d.cts +3 -3
- package/dist/firestore-standard/index.d.ts +3 -3
- package/dist/firestore-standard/index.js +2 -2
- package/dist/index.cjs +11 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{registry-tKTb5Kx1.d.ts → registry-By1i-zge.d.ts} +1 -1
- package/dist/{registry-BGh7Jqpb.d.cts → registry-CNToyEra.d.cts} +1 -1
- package/dist/sqlite/index.cjs +24 -12
- package/dist/sqlite/index.cjs.map +1 -1
- package/dist/sqlite/index.d.cts +4 -4
- package/dist/sqlite/index.d.ts +4 -4
- package/dist/sqlite/index.js +4 -4
- package/dist/sqlite/local.cjs +484 -47
- package/dist/sqlite/local.cjs.map +1 -1
- package/dist/sqlite/local.d.cts +31 -5
- package/dist/sqlite/local.d.ts +31 -5
- package/dist/sqlite/local.js +439 -4
- package/dist/sqlite/local.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-FODIMIWY.js.map +0 -1
- package/dist/chunk-ULRDQ6HZ.js.map +0 -1
|
@@ -418,6 +418,9 @@ function compileFilter(filter, params) {
|
|
|
418
418
|
);
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
|
+
function compileFilterConditions(filters, params) {
|
|
422
|
+
return filters.map((f) => compileFilter(f, params));
|
|
423
|
+
}
|
|
421
424
|
function asArray(value, op) {
|
|
422
425
|
if (!Array.isArray(value) || value.length === 0) {
|
|
423
426
|
throw new FiregraphError(`Operator "${op}" requires a non-empty array value`, "INVALID_QUERY");
|
|
@@ -840,9 +843,11 @@ function rowTimestampToMillis(value) {
|
|
|
840
843
|
|
|
841
844
|
export {
|
|
842
845
|
GraphTimestampImpl,
|
|
846
|
+
validateJsonPathKey,
|
|
843
847
|
buildSchemaStatements,
|
|
844
848
|
quoteIdent2 as quoteIdent,
|
|
845
849
|
validateTableName,
|
|
850
|
+
compileFilterConditions,
|
|
846
851
|
compileSelect,
|
|
847
852
|
compileExpand,
|
|
848
853
|
compileExpandHydrate,
|
|
@@ -859,4 +864,4 @@ export {
|
|
|
859
864
|
rowToRecord,
|
|
860
865
|
rowTimestampToMillis
|
|
861
866
|
};
|
|
862
|
-
//# sourceMappingURL=chunk-
|
|
867
|
+
//# sourceMappingURL=chunk-NZVSLWNY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/sqlite-index-ddl.ts","../src/internal/sqlite-schema.ts","../src/timestamp.ts","../src/internal/sqlite-data-ops.ts","../src/internal/sqlite-payload-guard.ts","../src/internal/sqlite-sql.ts"],"sourcesContent":["/**\n * Translator from `IndexSpec` to SQLite `CREATE INDEX` DDL.\n *\n * Shared by every SQLite-shaped backend (the table-per-graph edition in\n * `src/sqlite/` and the Cloudflare DO edition) via the common schema module\n * (`src/internal/sqlite-schema.ts`). Both use the same scope-free row shape,\n * so the only knob is the `fieldToColumn` mapping.\n *\n * ## JSON path expression indexes\n *\n * Data-field specs (`data.foo`, `data.nested.bar`) compile to\n * `json_extract(\"data\", '$.foo')` expression indexes. The JSON path\n * literal is inlined — not parametrized — so the SQLite query planner can\n * match the index against the expression emitted by the query compiler\n * (which also inlines the literal after this PR). Path components are\n * validated against a safe identifier pattern so inlining is not an\n * injection risk.\n *\n * ## Index naming\n *\n * Names are `{table}_idx_{hash}` where `hash` is a short FNV-1a of a\n * canonicalized spec. This keeps names stable across runs (so\n * `CREATE INDEX IF NOT EXISTS` is idempotent) and prevents collisions\n * between similar specs. The hash includes the field list, per-field\n * direction, and the `where` predicate.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type { IndexFieldSpec, IndexSpec } from '../types.js';\n\n/**\n * Valid SQLite identifier pattern — used for table and column names.\n * Mirrors the validation in `sqlite-schema.ts` / `cloudflare/schema.ts` so\n * this module doesn't need to import one over the other.\n */\nconst IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Safe JSON path component. Must match `JSON_PATH_KEY_RE` in the SQLite\n * query compilers — an index is only useful if the query emits an\n * identical `json_extract` expression.\n */\nconst JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\nfunction quoteIdent(name: string): string {\n if (!IDENT_RE.test(name)) {\n throw new FiregraphError(\n `Invalid SQL identifier in index DDL: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,\n 'INVALID_INDEX',\n );\n }\n return `\"${name}\"`;\n}\n\n/**\n * FNV-1a 32-bit hash, returned as 8-char hex. Non-cryptographic;\n * used only to produce short, stable index names.\n */\nfunction fnv1a32(str: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\nfunction normalizeFields(\n fields: Array<string | IndexFieldSpec>,\n): Array<{ path: string; desc: boolean }> {\n return fields.map((f) => {\n if (typeof f === 'string') return { path: f, desc: false };\n if (!f.path || typeof f.path !== 'string') {\n throw new FiregraphError(\n `IndexSpec field must be a string or { path: string, desc?: boolean }; got ${JSON.stringify(f)}`,\n 'INVALID_INDEX',\n );\n }\n return { path: f.path, desc: !!f.desc };\n });\n}\n\nfunction specFingerprint(spec: IndexSpec): string {\n // Canonical form: JSON of normalized fields + where. The `lead` key is\n // kept (always empty now) so fingerprints — and therefore index names —\n // stay stable across the removal of the legacy scope leading column.\n const normalized = {\n lead: [] as string[],\n fields: normalizeFields(spec.fields),\n where: spec.where ?? '',\n };\n return fnv1a32(JSON.stringify(normalized));\n}\n\n/**\n * Compile one field path to its SQLite column expression.\n *\n * - Firegraph top-level fields (`aType`, `createdAt`, …) → mapped column.\n * - `data.foo` / `data.foo.bar` → `json_extract(\"data\", '$.foo.bar')`.\n * - `data` alone → `json_extract(\"data\", '$')`.\n */\nfunction compileFieldExpr(path: string, fieldToColumn: Record<string, string>): string {\n const col = fieldToColumn[path];\n if (col) return quoteIdent(col);\n\n if (path === 'data') {\n return `json_extract(\"data\", '$')`;\n }\n if (path.startsWith('data.')) {\n const suffix = path.slice(5);\n const parts = suffix.split('.');\n for (const part of parts) {\n if (!JSON_PATH_KEY_RE.test(part)) {\n throw new FiregraphError(\n `IndexSpec data path \"${path}\" has invalid component \"${part}\". ` +\n `Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,\n 'INVALID_INDEX',\n );\n }\n }\n // Inline the path literal (no parameter). Validated components above\n // are safe to embed — no quote or escape characters.\n return `json_extract(\"data\", '$.${suffix}')`;\n }\n\n throw new FiregraphError(\n `IndexSpec field \"${path}\" is not a known firegraph field. ` +\n `Use a top-level field (aType, aUid, axbType, bType, bUid, createdAt, updatedAt, v) ` +\n `or a dotted data path like 'data.status'.`,\n 'INVALID_INDEX',\n );\n}\n\nexport interface SqliteIndexDDLOptions {\n /** Target table. */\n table: string;\n /** Map from firegraph field name to SQLite column name. */\n fieldToColumn: Record<string, string>;\n}\n\n/**\n * Emit the `CREATE INDEX IF NOT EXISTS` DDL for one `IndexSpec`.\n *\n * Returns a single SQL string. Name is deterministic (same spec → same\n * name across runs), so re-running the bootstrap is idempotent.\n */\nexport function buildIndexDDL(spec: IndexSpec, options: SqliteIndexDDLOptions): string {\n const { table, fieldToColumn } = options;\n\n if (!spec.fields || spec.fields.length === 0) {\n throw new FiregraphError('IndexSpec.fields must be a non-empty array', 'INVALID_INDEX');\n }\n\n const normalized = normalizeFields(spec.fields);\n const hash = specFingerprint(spec);\n const indexName = `${table}_idx_${hash}`;\n\n const cols: string[] = [];\n for (const f of normalized) {\n const expr = compileFieldExpr(f.path, fieldToColumn);\n cols.push(f.desc ? `${expr} DESC` : expr);\n }\n\n let ddl = `CREATE INDEX IF NOT EXISTS ${quoteIdent(indexName)} ON ${quoteIdent(table)}(${cols.join(', ')})`;\n\n if (spec.where) {\n // The predicate is inlined verbatim. It comes from library/app\n // configuration — never from user data — so we don't attempt to\n // parse, rewrite, or validate it. Callers authoring partial indexes\n // are responsible for writing a valid SQLite WHERE clause.\n ddl += ` WHERE ${spec.where}`;\n }\n\n return ddl;\n}\n\n/**\n * Deduplicate index specs by their deterministic fingerprint. Same spec\n * declared twice (e.g., by core preset + registry entry) collapses to a\n * single DDL statement.\n */\nexport function dedupeIndexSpecs(specs: ReadonlyArray<IndexSpec>): IndexSpec[] {\n const seen = new Set<string>();\n const out: IndexSpec[] = [];\n for (const spec of specs) {\n const fp = specFingerprint(spec);\n if (seen.has(fp)) continue;\n seen.add(fp);\n out.push(spec);\n }\n return out;\n}\n","/**\n * SQLite schema for firegraph triples.\n *\n * Single-table design — both nodes (self-loops with `axbType = 'is'`) and\n * edges share one row shape. Each table holds exactly one graph's triples:\n * subgraph isolation is physical (one table per graph, or one Durable\n * Object per graph on Cloudflare), so there is no `scope` discriminator\n * column. The table a row lives in *is* its scope.\n *\n * `data` is a JSON string. Built-in fields are projected to typed columns so\n * the query planner can use indexes without going through `json_extract`.\n *\n * ## Indexes\n *\n * Index specs come from the core preset (overridable via\n * `BuildSchemaOptions.coreIndexes`) plus per-entry `indexes` declared on\n * registry entries. Specs are deduplicated by canonical fingerprint before\n * emission.\n */\n\nimport { DEFAULT_CORE_INDEXES } from '../default-indexes.js';\nimport type { GraphRegistry, IndexSpec } from '../types.js';\nimport { buildIndexDDL, dedupeIndexSpecs } from './sqlite-index-ddl.js';\n\nexport const SQLITE_COLUMNS = [\n 'doc_id',\n 'a_type',\n 'a_uid',\n 'axb_type',\n 'b_type',\n 'b_uid',\n 'data',\n 'v',\n 'created_at',\n 'updated_at',\n] as const;\n\nexport type SqliteColumn = (typeof SQLITE_COLUMNS)[number];\n\n/**\n * Map firegraph field names (as they appear in `QueryFilter.field` and the\n * record envelope) to SQLite column names.\n */\nexport const FIELD_TO_COLUMN: Record<string, SqliteColumn> = {\n aType: 'a_type',\n aUid: 'a_uid',\n axbType: 'axb_type',\n bType: 'b_type',\n bUid: 'b_uid',\n v: 'v',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n};\n\n/**\n * Options controlling DDL emission for `buildSchemaStatements`.\n */\nexport interface BuildSchemaOptions {\n /**\n * Replaces the built-in core preset. Defaults to `DEFAULT_CORE_INDEXES`.\n * Pass `[]` to disable core indexes entirely.\n */\n coreIndexes?: IndexSpec[];\n /**\n * Registry contributing per-triple `indexes` declarations.\n */\n registry?: GraphRegistry;\n}\n\n/**\n * Build the DDL statements that create one graph's triple table and its\n * indexes. Returned as separate statements because some drivers (D1, DO\n * SQLite's `exec()`) require one statement per call.\n *\n * The CREATE TABLE statement is always first; index statements follow in\n * deterministic order. Same specs across runs produce the same statements,\n * so `CREATE … IF NOT EXISTS` is idempotent.\n */\nexport function buildSchemaStatements(table: string, options: BuildSchemaOptions = {}): string[] {\n const t = quoteIdent(table);\n const statements: string[] = [\n `CREATE TABLE IF NOT EXISTS ${t} (\n doc_id TEXT NOT NULL PRIMARY KEY,\n a_type TEXT NOT NULL,\n a_uid TEXT NOT NULL,\n axb_type TEXT NOT NULL,\n b_type TEXT NOT NULL,\n b_uid TEXT NOT NULL,\n data TEXT NOT NULL,\n v INTEGER,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )`,\n ];\n\n const core = options.coreIndexes ?? [...DEFAULT_CORE_INDEXES];\n const fromRegistry = options.registry?.entries().flatMap((e) => e.indexes ?? []) ?? [];\n\n const deduped = dedupeIndexSpecs([...core, ...fromRegistry]);\n for (const spec of deduped) {\n statements.push(buildIndexDDL(spec, { table, fieldToColumn: FIELD_TO_COLUMN }));\n }\n return statements;\n}\n\n/**\n * Quote a SQL identifier with double quotes, escaping any embedded quotes.\n *\n * Identifier names (table, column, index) come from configuration and\n * static code in this module — never from user data — but quoting still\n * protects against accidental keyword collisions.\n */\nexport function quoteIdent(name: string): string {\n validateTableName(name);\n return `\"${name}\"`;\n}\n\n/**\n * Validate a SQLite identifier (table name, column name) against the\n * allowed character set. Exposed so factory functions can fail fast on\n * an invalid `options.table` rather than waiting until first SQL.\n */\nexport function validateTableName(name: string): void {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);\n }\n}\n\n/**\n * Quote a SQL column-alias label. Unlike `quoteIdent` (which validates the\n * input as a SQL identifier and is used for table/column names), this helper\n * accepts arbitrary text — projection aliases are pure labels we read back\n * out of the result row, never executed as identifiers, so they can carry\n * dots (e.g. `data.detail.region`) and other characters that\n * `validateTableName` rejects.\n *\n * Embedded double quotes are escaped per the SQL standard (`\"` → `\"\"`),\n * which is sufficient to prevent the alias text from terminating the quoted\n * label early. This is the only injection vector for an alias — even if\n * the input contained `\";--`, double-quote escaping would render it\n * `\"\"\";--` inside `\"...\"`, harmless.\n *\n * Used by `compileFindEdgesProjected` for the caller-supplied projection\n * field name; the underlying SQL expression (`json_extract(...)`, column\n * reference) still goes through the strict compiler with no caller input.\n */\nexport function quoteColumnAlias(label: string): string {\n return `\"${label.replace(/\"/g, '\"\"')}\"`;\n}\n","/**\n * Backend-agnostic timestamp.\n *\n * Structurally compatible with `@google-cloud/firestore`'s `Timestamp` so\n * that records returned by either the Firestore or SQLite backend can be\n * consumed through the same `StoredGraphRecord` shape.\n *\n * Firestore's native `Timestamp` already satisfies this interface, so\n * existing Firestore consumers see no behavior change. The SQLite backend\n * returns instances of `GraphTimestampImpl` which also satisfies it.\n */\n\nexport interface GraphTimestamp {\n readonly seconds: number;\n readonly nanoseconds: number;\n toDate(): Date;\n toMillis(): number;\n}\n\n/**\n * Concrete `GraphTimestamp` implementation used by non-Firestore backends.\n * Mirrors the surface of Firestore's `Timestamp` enough for typical use.\n */\nexport class GraphTimestampImpl implements GraphTimestamp {\n constructor(\n public readonly seconds: number,\n public readonly nanoseconds: number,\n ) {}\n\n toDate(): Date {\n return new Date(this.toMillis());\n }\n\n toMillis(): number {\n return this.seconds * 1000 + Math.floor(this.nanoseconds / 1e6);\n }\n\n toJSON(): { seconds: number; nanoseconds: number } {\n return { seconds: this.seconds, nanoseconds: this.nanoseconds };\n }\n\n static fromMillis(ms: number): GraphTimestampImpl {\n const seconds = Math.floor(ms / 1000);\n const nanoseconds = (ms - seconds * 1000) * 1e6;\n return new GraphTimestampImpl(seconds, nanoseconds);\n }\n\n static now(): GraphTimestampImpl {\n return GraphTimestampImpl.fromMillis(Date.now());\n }\n}\n\n/**\n * Sentinel returned by `StorageBackend.serverTimestamp()` when the backend\n * has no native server-time concept and just wants a placeholder that the\n * adapter resolves to a concrete time at write commit. SQLite backends\n * substitute the wall-clock millis at the moment of `setDoc`/`updateDoc`.\n */\nexport const SERVER_TIMESTAMP_SENTINEL = Symbol.for('firegraph.serverTimestamp');\nexport type ServerTimestampSentinel = typeof SERVER_TIMESTAMP_SENTINEL;\n\nexport function isServerTimestampSentinel(value: unknown): value is ServerTimestampSentinel {\n return value === SERVER_TIMESTAMP_SENTINEL;\n}\n","/**\n * Shared `dataOps` SQL compilation helpers used by both SQLite-style backends\n * (`internal/sqlite-sql.ts` for the shared-table backend and `cloudflare/sql.ts`\n * for the per-DO backend).\n *\n * The two backends differ in identifier quoting and scope handling, but the\n * `data` column lives in JSON in both, the deep-merge / replace contract is\n * identical, and the `json_set` / `json_remove` expression they emit for a\n * `DataPathOp[]` is byte-for-byte the same. Lifting the helpers here keeps\n * that shape in one place — the comment in `cloudflare/sql.ts` used to read\n * \"keep them in sync\"; this module is what they keep in sync against.\n *\n * The helpers take a `backendLabel` parameter so error messages still\n * distinguish `\"SQLite backend\"` (shared-table) from `\"DO SQLite backend\"`\n * (per-Durable-Object). Identifier quoting is the caller's job — the helpers\n * here only emit JSON-path expressions against an opaque `base` argument,\n * never bare column names.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type { DataPathOp } from './write-plan.js';\n\n/**\n * Constructor names of Firestore special types that don't survive a plain\n * `JSON.stringify` round-trip — they have non-enumerable accessors (e.g.\n * `Timestamp.seconds`) or class identity that JSON loses. Detection is by\n * `constructor.name` to keep this module dependency-free (importing\n * `@google-cloud/firestore` here would pollute the Cloudflare Workers bundle —\n * see tests/unit/bundle-pollution.test.ts).\n */\nexport const FIRESTORE_TYPE_NAMES = new Set([\n 'Timestamp',\n 'GeoPoint',\n 'VectorValue',\n 'DocumentReference',\n 'FieldValue',\n]);\n\nexport function isFirestoreSpecialType(value: object): string | null {\n const ctorName = (value as { constructor?: { name?: string } }).constructor?.name;\n if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;\n return null;\n}\n\n/**\n * Identifiers accepted in `data.<key>` paths and `dataOps` path segments.\n * The pattern (`/^[A-Za-z_][A-Za-z0-9_-]*$/`) covers code-style identifiers\n * (camel, snake, kebab). Silently quoting exotic keys would require symmetric\n * quoting at every read/write call site; any drift produces silent data\n * corruption. Failing loudly at compile time is safer — users with exotic\n * keys can use `replaceNode` / `replaceEdge` (full-data overwrite) instead.\n */\nexport const JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\nexport function validateJsonPathKey(key: string, backendLabel: string): void {\n if (key.length === 0) {\n throw new FiregraphError(\n `${backendLabel}: empty JSON path component is not allowed`,\n 'INVALID_QUERY',\n );\n }\n if (!JSON_PATH_KEY_RE.test(key)) {\n throw new FiregraphError(\n `${backendLabel}: data field path component \"${key}\" is not a safe JSON-path identifier. ` +\n `Allowed pattern: /^[A-Za-z_][A-Za-z0-9_-]*$/. Use replaceNode/replaceEdge (full-data overwrite) ` +\n `for keys with reserved characters (whitespace, dots, brackets, quotes, etc.).`,\n 'INVALID_QUERY',\n );\n }\n}\n\n/**\n * Build a SQLite JSON path (`$.\"a\".\"b\".\"c\"`) from `DataPathOp` segments.\n *\n * Each segment is wrapped as a double-quoted JSON-path label via\n * `JSON.stringify`, which quotes the key and backslash-escapes any embedded\n * double-quotes or backslashes — exactly the escaping SQLite's JSON path\n * parser accepts for quoted labels (verified on SQLite 3.53). Quoting every\n * segment means digit-leading keys (`4f9Kq_2bN`), hyphens, dots, brackets,\n * and whitespace all address the literal key rather than being reparsed as\n * path syntax. Dots stay inside the quotes, so `{ 'a.b': 1 }` writes the\n * single key `\"a.b\"` instead of a nested `a → b`.\n *\n * The result is always bound as a SQL parameter (never interpolated), so\n * there is no injection surface — the quoting here is purely about producing\n * a path string SQLite parses as the intended literal key.\n */\nexport function buildJsonPath(segments: readonly string[]): string {\n return '$' + segments.map((seg) => '.' + JSON.stringify(seg)).join('');\n}\n\n/**\n * Bind a value as a JSON-serializable string for `json(?)` placeholders in\n * the compiled `json_set` expression. `assertJsonSafePayload` already runs\n * eagerly at the write boundary, so the Firestore-special-type rejection\n * here is defense-in-depth — left in place per the team's preference for\n * symmetric guards across the SQLite compilers.\n */\nexport function jsonBind(value: unknown, backendLabel: string): string {\n if (value === undefined) return 'null';\n if (value !== null && typeof value === 'object') {\n const firestoreType = isFirestoreSpecialType(value);\n if (firestoreType) {\n throw new FiregraphError(\n `${backendLabel} cannot persist a Firestore ${firestoreType} value. ` +\n `Convert to a primitive before writing (e.g. \\`ts.toMillis()\\` for Timestamp).`,\n 'INVALID_ARGUMENT',\n );\n }\n }\n return JSON.stringify(value);\n}\n\n/**\n * Build the SQL expression that applies a list of `DataPathOp`s onto an\n * existing JSON column reference (e.g. `\"data\"` or `COALESCE(\"data\", '{}')`).\n *\n * Returns the full expression (already parenthesised where needed) and pushes\n * the bound parameters onto `params` in left-to-right order. Returns `null`\n * when there are no ops at all — the caller picks a fallback expression.\n *\n * Strategy:\n * 1. `json_remove(<base>, '$.a.b', '$.c', …)` strips delete-ops.\n * 2. `json_set(<#1>, '$.x.y', json(?), '$.z', json(?), …)` writes value-ops.\n * `json(?)` ensures non-string values bind as JSON (objects, arrays,\n * numbers, booleans, null).\n */\nexport function compileDataOpsExpr(\n ops: readonly DataPathOp[],\n base: string,\n params: unknown[],\n backendLabel: string,\n): string | null {\n if (ops.length === 0) return null;\n\n const deletes: DataPathOp[] = [];\n const sets: DataPathOp[] = [];\n for (const op of ops) (op.delete ? deletes : sets).push(op);\n\n let expr = base;\n\n if (deletes.length > 0) {\n const placeholders = deletes.map(() => '?').join(', ');\n expr = `json_remove(${expr}, ${placeholders})`;\n for (const op of deletes) {\n params.push(buildJsonPath(op.path));\n }\n }\n\n if (sets.length > 0) {\n const pieces = sets.map(() => '?, json(?)').join(', ');\n expr = `json_set(${expr}, ${pieces})`;\n for (const op of sets) {\n params.push(buildJsonPath(op.path));\n params.push(jsonBind(op.value, backendLabel));\n }\n }\n\n return expr;\n}\n","/**\n * Shared eager-validation helper for SQLite-style backends\n * (`internal/sqlite-sql.ts` and `cloudflare/sql.ts`).\n *\n * Both backends serialise `record.data` (and `update.replaceData`) as a raw\n * JSON blob via `JSON.stringify`. Two classes of value silently corrupt that\n * representation and the cross-backend contract:\n *\n * 1. **Firestore special types** (`Timestamp`, `GeoPoint`, `VectorValue`,\n * `DocumentReference`, `FieldValue`). They have non-enumerable accessors\n * or rely on class identity that JSON drops, so they round-trip as `{}`\n * or garbage. Callers must convert to primitives before writing.\n * 2. **`DELETE_FIELD` sentinel.** A `Symbol` is invisible to\n * `JSON.stringify`. If a caller embeds the sentinel in a `replaceNode`\n * payload or in a fresh-insert (no existing row), the field would\n * silently disappear instead of erroring loudly the way it does for the\n * `dataOps` path — so we reject it eagerly here.\n * 3. **Tagged serialization payloads** (`__firegraph_ser__`). These are the\n * sandbox migration boundary marshalling form. They are valid inside\n * Firestore (the Firestore backend re-hydrates them via\n * `deserializeFirestoreTypes`), but on SQLite they would persist as\n * opaque tagged objects that no downstream reader knows how to interpret.\n * Reject them at the boundary so the failure is loud.\n *\n * The Firestore backend does NOT call this — it accepts those types natively\n * and `deserializeFirestoreTypes` rebuilds tagged values into real Firestore\n * objects on its own write path.\n *\n * Detection avoids `instanceof` so this module stays free of\n * `@google-cloud/firestore`. Constructor-name + duck-type matches the\n * approach used by `bindValue`/`jsonBind` elsewhere in the SQLite compilers.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport { SERIALIZATION_TAG } from './serialization-tag.js';\nimport { isDeleteSentinel } from './write-plan.js';\n\nconst FIRESTORE_TYPE_NAMES = new Set([\n 'Timestamp',\n 'GeoPoint',\n 'VectorValue',\n 'DocumentReference',\n 'FieldValue',\n]);\n\n/**\n * Walk `data` and throw on any value that the SQLite-style raw-JSON\n * persistence path can't faithfully serialise. `label` distinguishes the\n * caller in error messages (e.g. `'shared-table SQLite'` vs `'DO SQLite'`).\n *\n * Plain objects recurse. Arrays recurse element-wise. Primitives, `null`,\n * and `undefined` are accepted (mirroring how `flattenPatch` treats them\n * during the merge path).\n */\nexport function assertJsonSafePayload(data: unknown, label: string): void {\n walk(data, [], label);\n}\n\nfunction walk(node: unknown, path: readonly string[], label: string): void {\n if (node === null || node === undefined) return;\n if (isDeleteSentinel(node)) {\n throw new FiregraphError(\n `${label} backend cannot persist a deleteField() sentinel inside a ` +\n `full-data payload (replaceNode/replaceEdge or first-insert). The ` +\n `sentinel is only valid inside an updateNode/updateEdge dataOps patch. ` +\n `Path: ${formatPath(path)}.`,\n 'INVALID_ARGUMENT',\n );\n }\n const t = typeof node;\n if (t === 'symbol' || t === 'function') {\n throw new FiregraphError(\n `${label} backend cannot persist a value of type ${t}. ` +\n `JSON.stringify drops it silently. Path: ${formatPath(path)}.`,\n 'INVALID_ARGUMENT',\n );\n }\n if (t === 'bigint') {\n throw new FiregraphError(\n `${label} backend cannot persist a value of type bigint. ` +\n `JSON.stringify cannot serialize this type (throws TypeError). ` +\n `Path: ${formatPath(path)}.`,\n 'INVALID_ARGUMENT',\n );\n }\n if (t !== 'object') return;\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n walk(node[i], [...path, String(i)], label);\n }\n return;\n }\n // Reject any object carrying the firegraph serialization tag — both legit\n // tagged Firestore-type payloads (the migration-sandbox output that round-\n // trips through Firestore) and bogus user data that happens to put a\n // literal `__firegraph_ser__` key on a plain object. SQLite has no\n // Timestamp class to rebuild the tag into, and silently writing the\n // envelope would produce an unreadable column.\n const obj = node as Record<string, unknown>;\n if (Object.prototype.hasOwnProperty.call(obj, SERIALIZATION_TAG)) {\n const tagValue = obj[SERIALIZATION_TAG];\n throw new FiregraphError(\n `${label} backend cannot persist an object with a \\`${SERIALIZATION_TAG}\\` ` +\n `key (value: ${formatTagValue(tagValue)}). Recognised tags are valid only on ` +\n `the Firestore backend (migration-sandbox output); a literal ` +\n `\\`${SERIALIZATION_TAG}\\` field in user data is reserved and not allowed. ` +\n `Path: ${formatPath(path)}.`,\n 'INVALID_ARGUMENT',\n );\n }\n // Class instances: reject Firestore special types loudly; reject every\n // other class instance generically (Map, Set, Date are caller's\n // responsibility to convert — Date is allowed in filter binds via\n // `bindValue` but not as a stored payload value because JSON.stringify\n // produces a string, not a real Date).\n const proto = Object.getPrototypeOf(node);\n if (proto !== null && proto !== Object.prototype) {\n const ctor = (node as { constructor?: { name?: string } }).constructor;\n const ctorName = ctor && typeof ctor.name === 'string' ? ctor.name : '<anonymous>';\n if (FIRESTORE_TYPE_NAMES.has(ctorName)) {\n throw new FiregraphError(\n `${label} backend cannot persist a Firestore ${ctorName} value. ` +\n `Convert to a primitive before writing (e.g. \\`ts.toMillis()\\` for ` +\n `Timestamp, \\`{lat,lng}\\` for GeoPoint). Path: ${formatPath(path)}.`,\n 'INVALID_ARGUMENT',\n );\n }\n // Accept Date as an alias for its epoch-ms — it round-trips as an ISO\n // string via JSON.stringify, which the caller chose; not our place to\n // reject. Same for Buffer / typed arrays — they'll JSON-serialize as\n // best they can. Reject only opaque exotic instances that JSON drops.\n if (node instanceof Date) return;\n throw new FiregraphError(\n `${label} backend cannot persist a class instance of type ${ctorName}. ` +\n `Only plain objects, arrays, and primitives round-trip safely through ` +\n `JSON storage. Path: ${formatPath(path)}.`,\n 'INVALID_ARGUMENT',\n );\n }\n for (const key of Object.keys(obj)) {\n walk(obj[key], [...path, key], label);\n }\n}\n\nfunction formatPath(path: readonly string[]): string {\n return path.length === 0 ? '<root>' : path.map((p) => JSON.stringify(p)).join(' > ');\n}\n\nfunction formatTagValue(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value === 'string') return JSON.stringify(value);\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value);\n }\n return typeof value;\n}\n","/**\n * SQL compilation for SQLite-shaped firegraph backends.\n *\n * One table holds exactly one graph's triples — there is no `scope` column\n * and no scope discriminator on any statement. Subgraph isolation is\n * physical: the shared SQLite backend (`src/sqlite/`) maps each graph to\n * its own table, and the Cloudflare DO backend (`src/cloudflare/`) maps\n * each graph to its own Durable Object. Every compiler in this module is\n * parameterized by the target table name.\n *\n * Filter compilation, JSON-path validation, and value binding are shared so\n * the query planner (`src/query.ts`) emits the same `QueryFilter[]` shape\n * regardless of backend.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type { GraphTimestamp } from '../timestamp.js';\nimport { GraphTimestampImpl } from '../timestamp.js';\nimport type {\n AggregateSpec,\n ExpandParams,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type { UpdatePayload, WritableRecord, WriteMode } from './backend.js';\nimport { NODE_RELATION } from './constants.js';\nimport {\n compileDataOpsExpr,\n isFirestoreSpecialType,\n validateJsonPathKey,\n} from './sqlite-data-ops.js';\nimport { assertJsonSafePayload } from './sqlite-payload-guard.js';\nimport { FIELD_TO_COLUMN, quoteColumnAlias, quoteIdent } from './sqlite-schema.js';\nimport { assertUpdatePayloadExclusive, flattenPatch } from './write-plan.js';\n\nconst BACKEND_LABEL = 'SQLite';\nconst BACKEND_ERR_LABEL = 'SQLite backend';\n\nexport interface CompiledStatement {\n sql: string;\n params: unknown[];\n}\n\n// ---------------------------------------------------------------------------\n// Filter compilation\n// ---------------------------------------------------------------------------\n\n/**\n * Translate a firegraph filter field to either a column reference or a\n * `json_extract(\"data\", '$.<path>')` expression. Built-in fields go\n * straight to their column; `data.<key>[.<key>…]` and bare `data` are\n * projected through `json_extract` with the JSON path **inlined as a\n * string literal** — not parametrized.\n *\n * Inlining matters: SQLite's query planner matches an expression index\n * (`CREATE INDEX … ON tbl(json_extract(\"data\", '$.status'))`) against\n * *textually identical* expressions in the WHERE clause. `json_extract(\n * \"data\", ?)` parametrizes the path and would never hit the index, even\n * though it evaluates to the same value. Inlining here makes the\n * expression literal in the SQL, which is what the index builder in\n * `sqlite-index-ddl.ts` also emits.\n *\n * Inlining is safe: each path component is validated against\n * `JSON_PATH_KEY_RE` (`/^[A-Za-z_][A-Za-z0-9_-]*$/`) before it reaches\n * this function — the pattern excludes every character SQLite would\n * treat as syntax (quote, backslash, dot, bracket, whitespace), so\n * string concatenation can't produce injection.\n */\nfunction compileFieldRef(field: string): { expr: string } {\n const column = FIELD_TO_COLUMN[field];\n if (column) {\n return { expr: quoteIdent(column) };\n }\n if (field.startsWith('data.')) {\n const suffix = field.slice(5);\n for (const part of suffix.split('.')) {\n validateJsonPathKey(part, BACKEND_ERR_LABEL);\n }\n return { expr: `json_extract(\"data\", '$.${suffix}')` };\n }\n if (field === 'data') {\n return { expr: `json_extract(\"data\", '$')` };\n }\n throw new FiregraphError(`SQLite backend cannot resolve filter field: ${field}`, 'INVALID_QUERY');\n}\n\n/**\n * Coerce a JS filter/update value into a SQLite-bindable primitive. Firestore\n * special types are rejected loudly because `JSON.stringify` would emit\n * garbage that silently fails to match any stored row. Callers should\n * project to a primitive (e.g. `ts.toMillis()`) before passing in.\n */\nfunction bindValue(value: unknown): unknown {\n if (value === null || value === undefined) return null;\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'bigint' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n if (value instanceof Date) return value.getTime();\n if (typeof value === 'object') {\n const firestoreType = isFirestoreSpecialType(value);\n if (firestoreType) {\n throw new FiregraphError(\n `SQLite backend cannot bind a Firestore ${firestoreType} value — JSON serialization ` +\n `would silently drop fields and the resulting bind would never match a stored row. ` +\n `Convert to a primitive (e.g. \\`ts.toMillis()\\` for Timestamp) before filtering or updating.`,\n 'INVALID_QUERY',\n );\n }\n return JSON.stringify(value);\n }\n return String(value);\n}\n\nfunction compileFilter(filter: QueryFilter, params: unknown[]): string {\n const { expr } = compileFieldRef(filter.field);\n\n switch (filter.op) {\n case '==':\n params.push(bindValue(filter.value));\n return `${expr} = ?`;\n case '!=':\n params.push(bindValue(filter.value));\n return `${expr} != ?`;\n case '<':\n params.push(bindValue(filter.value));\n return `${expr} < ?`;\n case '<=':\n params.push(bindValue(filter.value));\n return `${expr} <= ?`;\n case '>':\n params.push(bindValue(filter.value));\n return `${expr} > ?`;\n case '>=':\n params.push(bindValue(filter.value));\n return `${expr} >= ?`;\n case 'in': {\n const values = asArray(filter.value, 'in');\n const placeholders = values.map(() => '?').join(', ');\n for (const v of values) params.push(bindValue(v));\n return `${expr} IN (${placeholders})`;\n }\n case 'not-in': {\n const values = asArray(filter.value, 'not-in');\n const placeholders = values.map(() => '?').join(', ');\n for (const v of values) params.push(bindValue(v));\n return `${expr} NOT IN (${placeholders})`;\n }\n case 'array-contains': {\n params.push(bindValue(filter.value));\n return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value = ?)`;\n }\n case 'array-contains-any': {\n const values = asArray(filter.value, 'array-contains-any');\n const placeholders = values.map(() => '?').join(', ');\n for (const v of values) params.push(bindValue(v));\n return `EXISTS (SELECT 1 FROM json_each(${expr}) WHERE value IN (${placeholders}))`;\n }\n default:\n throw new FiregraphError(\n `SQLite backend does not support filter operator: ${String(filter.op)}`,\n 'INVALID_QUERY',\n );\n }\n}\n\n/**\n * Compile a filter list into SQL condition fragments, pushing bound values\n * onto `params` in order. This is the same code path `compileSelect` runs,\n * exported so the local-SQLite search layer (`src/internal/sqlite-search.ts`)\n * compiles vector-search `where` / identifying filters with identical\n * operator support and `data.*` path handling.\n */\nexport function compileFilterConditions(filters: QueryFilter[], params: unknown[]): string[] {\n return filters.map((f) => compileFilter(f, params));\n}\n\nfunction asArray(value: unknown, op: string): unknown[] {\n if (!Array.isArray(value) || value.length === 0) {\n throw new FiregraphError(`Operator \"${op}\" requires a non-empty array value`, 'INVALID_QUERY');\n }\n return value;\n}\n\nfunction compileOrderBy(options: QueryOptions | undefined, _params: unknown[]): string {\n if (!options?.orderBy) return '';\n const { field, direction } = options.orderBy;\n const { expr } = compileFieldRef(field);\n const dir = direction === 'desc' ? 'DESC' : 'ASC';\n return ` ORDER BY ${expr} ${dir}`;\n}\n\nfunction compileLimit(options: QueryOptions | undefined, params: unknown[]): string {\n if (options?.limit === undefined) return '';\n params.push(options.limit);\n return ` LIMIT ?`;\n}\n\n// ---------------------------------------------------------------------------\n// Statement compilation\n// ---------------------------------------------------------------------------\n\n/**\n * SELECT rows matching `filters`. No scope predicate — every row in the\n * table belongs to the same graph.\n */\nexport function compileSelect(\n table: string,\n filters: QueryFilter[],\n options?: QueryOptions,\n): CompiledStatement {\n const params: unknown[] = [];\n const conditions: string[] = [];\n\n for (const f of filters) {\n conditions.push(compileFilter(f, params));\n }\n\n const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';\n let sql = `SELECT * FROM ${quoteIdent(table)}${where}`;\n sql += compileOrderBy(options, params);\n sql += compileLimit(options, params);\n\n return { sql, params };\n}\n\n/**\n * Compile an `expand()` fan-out into one SELECT statement.\n *\n * Forward direction:\n *\n * SELECT * FROM <table>\n * WHERE \"axb_type\" = ? AND \"a_uid\" IN (?, ?, …)\n * [AND \"a_type\" = ?] [AND \"b_type\" = ?]\n * [ORDER BY …]\n * [LIMIT ?]\n *\n * Reverse swaps the IN-predicate to `\"b_uid\"`. Per-source LIMIT enforcement\n * is approximated by `sources.length * limitPerSource` (see\n * `ExpandParams.limitPerSource`); window-function partitioning is out of\n * scope for the round-trip-collapse goal.\n *\n * Empty `params.sources` is rejected at the compiler — `IN ()` is invalid\n * SQL. Backends short-circuit empty input before reaching here.\n */\nexport function compileExpand(table: string, params: ExpandParams): CompiledStatement {\n if (params.sources.length === 0) {\n throw new FiregraphError(\n 'compileExpand requires a non-empty sources list — empty IN () is invalid SQL.',\n 'INVALID_QUERY',\n );\n }\n const direction = params.direction ?? 'forward';\n // Resolve every column reference through `compileFieldRef` so the\n // emitted SQL uses the on-disk snake_case names (`a_uid`, `axb_type`,\n // `b_uid`, …) defined by `FIELD_TO_COLUMN` in `sqlite-schema.ts`.\n const aUidCol = compileFieldRef('aUid').expr;\n const bUidCol = compileFieldRef('bUid').expr;\n const aTypeCol = compileFieldRef('aType').expr;\n const bTypeCol = compileFieldRef('bType').expr;\n const axbTypeCol = compileFieldRef('axbType').expr;\n const sourceColumn = direction === 'forward' ? aUidCol : bUidCol;\n\n const sqlParams: unknown[] = [params.axbType];\n const conditions: string[] = [`${axbTypeCol} = ?`];\n\n const placeholders = params.sources.map(() => '?').join(', ');\n conditions.push(`${sourceColumn} IN (${placeholders})`);\n for (const uid of params.sources) sqlParams.push(uid);\n\n if (params.aType !== undefined) {\n conditions.push(`${aTypeCol} = ?`);\n sqlParams.push(params.aType);\n }\n if (params.bType !== undefined) {\n conditions.push(`${bTypeCol} = ?`);\n sqlParams.push(params.bType);\n }\n\n // Self-loop guard for the corner case where the caller asks for the\n // node-relation as the hop type — without it, every node row would\n // come back as a degenerate \"edge\" from itself to itself.\n if (params.axbType === NODE_RELATION) {\n conditions.push(`${aUidCol} != ${bUidCol}`);\n }\n\n let sql = `SELECT * FROM ${quoteIdent(table)} WHERE ${conditions.join(' AND ')}`;\n\n if (params.orderBy) {\n sql += compileOrderBy({ orderBy: params.orderBy }, sqlParams);\n }\n if (params.limitPerSource !== undefined) {\n const totalLimit = params.sources.length * params.limitPerSource;\n sql += ` LIMIT ?`;\n sqlParams.push(totalLimit);\n }\n return { sql, params: sqlParams };\n}\n\n/**\n * Hydration-pass for `expand({ hydrate: true })`. Pulls every node row\n * whose `bUid` is in the supplied target list (one statement). The caller\n * stitches alignment in JS via a `Map<bUid, row>`.\n */\nexport function compileExpandHydrate(table: string, targetUids: string[]): CompiledStatement {\n if (targetUids.length === 0) {\n throw new FiregraphError(\n 'compileExpandHydrate requires a non-empty target list — empty IN () is invalid SQL.',\n 'INVALID_QUERY',\n );\n }\n const placeholders = targetUids.map(() => '?').join(', ');\n const sqlParams: unknown[] = [NODE_RELATION];\n for (const uid of targetUids) sqlParams.push(uid);\n\n // Resolve column refs via `compileFieldRef` — see `compileExpand`\n // for the schema-rename rationale.\n const aUidCol = compileFieldRef('aUid').expr;\n const bUidCol = compileFieldRef('bUid').expr;\n const axbTypeCol = compileFieldRef('axbType').expr;\n\n return {\n sql:\n `SELECT * FROM ${quoteIdent(table)} ` +\n `WHERE ${axbTypeCol} = ? AND ${aUidCol} = ${bUidCol} AND ${bUidCol} IN (${placeholders})`,\n params: sqlParams,\n };\n}\n\n/**\n * SELECT a single row by doc_id. `doc_id` is the PK so this is an O(1)\n * index lookup.\n */\nexport function compileSelectByDocId(table: string, docId: string): CompiledStatement {\n return {\n sql: `SELECT * FROM ${quoteIdent(table)} WHERE \"doc_id\" = ? LIMIT 1`,\n params: [docId],\n };\n}\n\n/**\n * Discriminator for one projected column. The decoder uses this to recover\n * the JS-shape of the requested field.\n */\nexport type ProjectedColumnKind =\n | 'builtin-text'\n | 'builtin-int'\n | 'builtin-timestamp'\n | 'data'\n | 'json';\n\n/** Per-column metadata returned alongside the compiled projection statement. */\nexport interface ProjectedColumnSpec {\n /** Original caller-supplied field name. Used as the alias in the SQL\n * projection list AND as the key in the returned JS row. */\n field: string;\n /** Kind discriminator — see `ProjectedColumnKind`. */\n kind: ProjectedColumnKind;\n /**\n * For `kind === 'json'` only: alias of the paired `json_type` companion\n * column. Uses a positional sentinel (`__fg_t_<idx>`) keyed by the\n * field's position in the unique projection list rather than the\n * historical `<field>__t` suffix, which would collide if the caller\n * projected both `'foo'` and `'foo__t'` (both legal user input).\n */\n typeAlias?: string;\n}\n\n/**\n * Normalize a projection field name to the canonical form `compileFieldRef`\n * understands: built-ins stay as-is, `data` and `data.*` stay as-is, and a\n * bare `name` is rewritten to `data.name`.\n */\nfunction normalizeProjectionField(field: string): string {\n if (field in FIELD_TO_COLUMN) return field;\n if (field === 'data' || field.startsWith('data.')) return field;\n return `data.${field}`;\n}\n\n/**\n * Compile a `findEdgesProjected({ select })` call into a single SELECT.\n *\n * Shape:\n *\n * SELECT\n * <expr-1> AS \"<field-1>\", [json_type(...) AS \"__fg_t_<idx>\",]\n * ...\n * FROM <table>\n * [WHERE <filters>]\n * [ORDER BY ...]\n * [LIMIT ?]\n *\n * For `data.*` projections the compiler also emits a paired `json_type`\n * column so the decoder can recover JSON-encoded objects/arrays without a\n * second round trip. Built-in field projections are passthrough columns.\n * The companion alias uses a positional sentinel `__fg_t_<idx>` rather\n * than `<field>__t` to avoid colliding with a user-projected field\n * literally named `<x>__t`.\n *\n * Empty `select` is rejected at the compiler — the client wrapper enforces\n * this too. Duplicates collapse at compile time, preserving first-occurrence\n * order.\n */\nexport function compileFindEdgesProjected(\n table: string,\n select: ReadonlyArray<string>,\n filters: QueryFilter[],\n options?: QueryOptions,\n): { stmt: CompiledStatement; columns: ProjectedColumnSpec[] } {\n if (select.length === 0) {\n throw new FiregraphError(\n 'compileFindEdgesProjected requires a non-empty select list — ' +\n 'an empty projection has no SQL representation distinct from `findEdges`.',\n 'INVALID_QUERY',\n );\n }\n\n const seen = new Set<string>();\n const uniqueFields: string[] = [];\n for (const f of select) {\n if (!seen.has(f)) {\n seen.add(f);\n uniqueFields.push(f);\n }\n }\n\n const projections: string[] = [];\n const columns: ProjectedColumnSpec[] = [];\n for (let idx = 0; idx < uniqueFields.length; idx++) {\n const field = uniqueFields[idx]!;\n const canonical = normalizeProjectionField(field);\n const { expr } = compileFieldRef(canonical);\n // Alias is the caller-supplied field name verbatim — relaxed quoting\n // accepts dotted paths like `data.detail.region`. The decoder reads\n // back via `row[c.field]`, so the alias must equal the original\n // field string.\n const alias = quoteColumnAlias(field);\n projections.push(`${expr} AS ${alias}`);\n\n let kind: ProjectedColumnKind;\n let typeAliasName: string | undefined;\n if (canonical === 'data') {\n kind = 'data';\n } else if (canonical.startsWith('data.')) {\n kind = 'json';\n // Positional sentinel — `<field>__t` would collide if the caller\n // projected both `'foo'` and `'foo__t'` (both legal user input).\n typeAliasName = `__fg_t_${idx}`;\n const typeAlias = quoteColumnAlias(typeAliasName);\n projections.push(`json_type(\"data\", '$.${canonical.slice(5)}') AS ${typeAlias}`);\n } else {\n if (canonical === 'v') kind = 'builtin-int';\n else if (canonical === 'createdAt' || canonical === 'updatedAt') kind = 'builtin-timestamp';\n else kind = 'builtin-text';\n }\n columns.push({ field, kind, typeAlias: typeAliasName });\n }\n\n const params: unknown[] = [];\n const conditions: string[] = [];\n for (const f of filters) {\n conditions.push(compileFilter(f, params));\n }\n\n const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';\n let sql = `SELECT ${projections.join(', ')} FROM ${quoteIdent(table)}${where}`;\n sql += compileOrderBy(options, params);\n sql += compileLimit(options, params);\n\n return { stmt: { sql, params }, columns };\n}\n\n/**\n * Decode one SQL row into the projected JS shape: built-in TEXT/INTEGER\n * columns pass through with light coercion, timestamps wrap in\n * `GraphTimestampImpl`, and `data.*` JSON projections use the paired\n * `json_type` column to recover JSON-encoded objects/arrays.\n */\nexport function decodeProjectedRow(\n row: Record<string, unknown>,\n columns: ProjectedColumnSpec[],\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const c of columns) {\n const raw = row[c.field];\n switch (c.kind) {\n case 'builtin-text':\n out[c.field] = raw === null || raw === undefined ? null : String(raw);\n break;\n case 'builtin-int':\n if (raw === null || raw === undefined) {\n out[c.field] = null;\n } else if (typeof raw === 'bigint') {\n out[c.field] = Number(raw);\n } else if (typeof raw === 'number') {\n out[c.field] = raw;\n } else {\n out[c.field] = Number(raw);\n }\n break;\n case 'builtin-timestamp': {\n const ms = rowTimestampToMillis(raw);\n out[c.field] = GraphTimestampImpl.fromMillis(ms) as unknown as GraphTimestamp;\n break;\n }\n case 'data':\n if (raw === null || raw === undefined || raw === '') {\n out[c.field] = {};\n } else {\n out[c.field] = JSON.parse(raw as string);\n }\n break;\n case 'json': {\n // Read the paired `json_type` companion column via the positional\n // sentinel recorded at compile time — see `ProjectedColumnSpec.typeAlias`.\n const t = row[c.typeAlias!] as string | null | undefined;\n if (raw === null || raw === undefined) {\n out[c.field] = null;\n } else if (t === 'object' || t === 'array') {\n out[c.field] = typeof raw === 'string' ? JSON.parse(raw) : raw;\n } else if (t === 'integer' && typeof raw === 'bigint') {\n out[c.field] = Number(raw);\n } else {\n out[c.field] = raw;\n }\n break;\n }\n }\n }\n return out;\n}\n\n/**\n * Compile an aggregate query.\n *\n * SUM/AVG/MIN/MAX cast the JSON-extracted value through\n * `CAST(... AS REAL)` for numeric semantics; without the cast,\n * comparisons would be lexicographic on the underlying string storage.\n *\n * The returned tuple includes the alias list so the JS-side caller can\n * rehydrate the result columns in spec order without reflecting on the\n * raw row keys (which the SQL layer doesn't guarantee a stable order for).\n */\nexport function compileAggregate(\n table: string,\n spec: AggregateSpec,\n filters: QueryFilter[],\n): { stmt: CompiledStatement; aliases: string[] } {\n const aliases = Object.keys(spec);\n if (aliases.length === 0) {\n throw new FiregraphError(\n 'aggregate() requires at least one aggregation in the `aggregates` map.',\n 'INVALID_QUERY',\n );\n }\n\n const projections: string[] = [];\n for (const alias of aliases) {\n const { op, field } = spec[alias];\n // Aliases are inlined into the SQL (SQL aliases can't be bound\n // parameters). Validate against the same JSON-path-key charset rule\n // used everywhere else so caller-supplied aliases can't inject SQL.\n validateJsonPathKey(alias, BACKEND_ERR_LABEL);\n if (op === 'count') {\n // Reject a stray field — see `AggregateField` JSDoc for rationale.\n if (field !== undefined) {\n throw new FiregraphError(\n `Aggregate '${alias}' op 'count' must not specify a field — ` +\n `count operates on rows, not a column expression.`,\n 'INVALID_QUERY',\n );\n }\n projections.push(`COUNT(*) AS ${quoteIdent(alias)}`);\n continue;\n }\n if (!field) {\n throw new FiregraphError(\n `Aggregate '${alias}' op '${op}' requires a field.`,\n 'INVALID_QUERY',\n );\n }\n const { expr } = compileFieldRef(field);\n const numeric = `CAST(${expr} AS REAL)`;\n if (op === 'sum') projections.push(`SUM(${numeric}) AS ${quoteIdent(alias)}`);\n else if (op === 'avg') projections.push(`AVG(${numeric}) AS ${quoteIdent(alias)}`);\n else if (op === 'min') projections.push(`MIN(${numeric}) AS ${quoteIdent(alias)}`);\n else if (op === 'max') projections.push(`MAX(${numeric}) AS ${quoteIdent(alias)}`);\n else\n throw new FiregraphError(\n `SQLite backend does not support aggregate op: ${String(op)}`,\n 'INVALID_QUERY',\n );\n }\n\n const params: unknown[] = [];\n const conditions: string[] = [];\n for (const f of filters) {\n conditions.push(compileFilter(f, params));\n }\n const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';\n const sql = `SELECT ${projections.join(', ')} FROM ${quoteIdent(table)}${where}`;\n return { stmt: { sql, params }, aliases };\n}\n\n/**\n * Compile a `setDoc(record, mode)` call into a single statement.\n *\n * `mode === 'replace'` issues `INSERT OR REPLACE` (full row replacement).\n * `mode === 'merge'` issues `INSERT … ON CONFLICT(doc_id) DO UPDATE SET …`,\n * deep-merging the incoming `data` into the existing JSON via the chained\n * `json_set` / `json_remove` expression produced by `compileDataOpsExpr`.\n * Sibling keys at every depth survive; arrays are terminal (replaced).\n *\n * `created_at` is re-stamped on every put for both modes (matches the\n * cross-backend contract today).\n */\nexport function compileSet(\n table: string,\n docId: string,\n record: WritableRecord,\n nowMillis: number,\n mode: WriteMode,\n): CompiledStatement {\n // Eager validation so the first-insert path can't silently corrupt\n // Firestore special types or drop a DELETE_FIELD sentinel that\n // JSON.stringify would erase.\n assertJsonSafePayload(record.data, BACKEND_LABEL);\n if (mode === 'replace') {\n const sql = `INSERT OR REPLACE INTO ${quoteIdent(table)} (\n doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n const params: unknown[] = [\n docId,\n record.aType,\n record.aUid,\n record.axbType,\n record.bType,\n record.bUid,\n JSON.stringify(record.data ?? {}),\n record.v ?? null,\n nowMillis,\n nowMillis,\n ];\n return { sql, params };\n }\n\n const insertParams: unknown[] = [\n docId,\n record.aType,\n record.aUid,\n record.axbType,\n record.bType,\n record.bUid,\n JSON.stringify(record.data ?? {}),\n record.v ?? null,\n nowMillis,\n nowMillis,\n ];\n\n const ops = flattenPatch(record.data ?? {});\n const updateParams: unknown[] = [];\n const dataExpr =\n compileDataOpsExpr(ops, `COALESCE(\"data\", '{}')`, updateParams, BACKEND_ERR_LABEL) ??\n `COALESCE(\"data\", '{}')`;\n\n // COALESCE preserves pre-existing `v` when the incoming record has none,\n // matching Firestore's \"undefined leaves the stored field alone\" merge\n // semantic for the version stamp.\n const sql = `INSERT INTO ${quoteIdent(table)} (\n doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(doc_id) DO UPDATE SET\n \"a_type\" = excluded.\"a_type\",\n \"a_uid\" = excluded.\"a_uid\",\n \"axb_type\" = excluded.\"axb_type\",\n \"b_type\" = excluded.\"b_type\",\n \"b_uid\" = excluded.\"b_uid\",\n \"data\" = ${dataExpr},\n \"v\" = COALESCE(excluded.\"v\", \"v\"),\n \"created_at\" = excluded.\"created_at\",\n \"updated_at\" = excluded.\"updated_at\"`;\n\n return { sql, params: [...insertParams, ...updateParams] };\n}\n\n/**\n * Compile an `UpdatePayload` into a single UPDATE statement.\n *\n * - `replaceData` overwrites the whole `data` JSON.\n * - `dataOps` applies a deep-path patch via chained `json_remove` /\n * `json_set` — sibling keys at every depth survive; arrays are terminal;\n * delete-ops use `json_remove`.\n * - `v` is set when provided.\n * - `updated_at` is always stamped.\n */\nexport function compileUpdate(\n table: string,\n docId: string,\n update: UpdatePayload,\n nowMillis: number,\n): CompiledStatement {\n assertUpdatePayloadExclusive(update);\n const setClauses: string[] = [];\n const params: unknown[] = [];\n\n if (update.replaceData) {\n assertJsonSafePayload(update.replaceData, BACKEND_LABEL);\n setClauses.push(`\"data\" = ?`);\n params.push(JSON.stringify(update.replaceData));\n } else if (update.dataOps && update.dataOps.length > 0) {\n for (const op of update.dataOps) {\n if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);\n }\n const expr = compileDataOpsExpr(\n update.dataOps,\n `COALESCE(\"data\", '{}')`,\n params,\n BACKEND_ERR_LABEL,\n );\n if (expr !== null) {\n setClauses.push(`\"data\" = ${expr}`);\n }\n }\n\n if (update.v !== undefined) {\n setClauses.push(`\"v\" = ?`);\n params.push(update.v);\n }\n\n setClauses.push(`\"updated_at\" = ?`);\n params.push(nowMillis);\n\n params.push(docId);\n\n return {\n sql: `UPDATE ${quoteIdent(table)} SET ${setClauses.join(', ')} WHERE \"doc_id\" = ?`,\n params,\n };\n}\n\nexport function compileDelete(table: string, docId: string): CompiledStatement {\n return {\n sql: `DELETE FROM ${quoteIdent(table)} WHERE \"doc_id\" = ?`,\n params: [docId],\n };\n}\n\n/**\n * Compile a server-side bulk DELETE.\n *\n * The compiler accepts an empty filter list (would emit\n * `DELETE FROM <table>`); callers that must never wipe a whole graph\n * reject that shape at their own boundary (the DO RPC layer does, as\n * defense-in-depth against a misconfigured stub).\n */\nexport function compileBulkDelete(table: string, filters: QueryFilter[]): CompiledStatement {\n const params: unknown[] = [];\n const conditions: string[] = [];\n for (const f of filters) {\n conditions.push(compileFilter(f, params));\n }\n const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';\n return {\n sql: `DELETE FROM ${quoteIdent(table)}${where}`,\n params,\n };\n}\n\n/**\n * Compile a server-side bulk UPDATE.\n *\n * The `patch.data` payload is deep-merged into each matching row's `data`\n * field via the same `flattenPatch` → `compileDataOpsExpr` pipeline that\n * `compileUpdate` (single-row) uses. Identifying columns are read-only.\n *\n * Empty patches are rejected — an empty patch would only rewrite\n * `updated_at`, which is almost certainly a caller bug.\n */\nexport function compileBulkUpdate(\n table: string,\n filters: QueryFilter[],\n patchData: Record<string, unknown>,\n nowMillis: number,\n): CompiledStatement {\n const dataOps = flattenPatch(patchData);\n if (dataOps.length === 0) {\n throw new FiregraphError(\n 'bulkUpdate() patch.data must contain at least one leaf — an empty patch ' +\n 'would only rewrite `updated_at`, which is almost certainly a bug. ' +\n 'Use `setDoc` with merge mode if you want to stamp without editing data.',\n 'INVALID_QUERY',\n );\n }\n for (const op of dataOps) {\n if (!op.delete) assertJsonSafePayload(op.value, BACKEND_LABEL);\n }\n const setParams: unknown[] = [];\n const expr = compileDataOpsExpr(dataOps, `COALESCE(\"data\", '{}')`, setParams, BACKEND_ERR_LABEL);\n if (expr === null) {\n throw new FiregraphError(\n 'bulkUpdate() patch produced no SQL operations — internal invariant violated.',\n 'INVALID_ARGUMENT',\n );\n }\n const setClauses: string[] = [`\"data\" = ${expr}`, `\"updated_at\" = ?`];\n setParams.push(nowMillis);\n\n const whereParams: unknown[] = [];\n const conditions: string[] = [];\n for (const f of filters) {\n conditions.push(compileFilter(f, whereParams));\n }\n const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';\n\n return {\n sql: `UPDATE ${quoteIdent(table)} SET ${setClauses.join(', ')}${where}`,\n params: [...setParams, ...whereParams],\n };\n}\n\n/**\n * DELETE every row in the table. Used when tearing down an entire graph —\n * the caller discovers the set of graphs to wipe (registry topology for the\n * DO backend, the graph catalog for the shared SQLite backend) and clears\n * each one.\n */\nexport function compileDeleteAll(table: string): CompiledStatement {\n return {\n sql: `DELETE FROM ${quoteIdent(table)}`,\n params: [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Row -> record\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a SQLite row into a full `StoredGraphRecord`, wrapping the\n * millisecond timestamp columns in `GraphTimestampImpl`.\n */\nexport function rowToRecord(row: Record<string, unknown>): StoredGraphRecord {\n const dataString = row.data as string | null;\n const data = dataString ? (JSON.parse(dataString) as Record<string, unknown>) : {};\n\n const createdMs = rowTimestampToMillis(row.created_at);\n const updatedMs = rowTimestampToMillis(row.updated_at);\n\n const record: Record<string, unknown> = {\n aType: row.a_type as string,\n aUid: row.a_uid as string,\n axbType: row.axb_type as string,\n bType: row.b_type as string,\n bUid: row.b_uid as string,\n data,\n createdAt: GraphTimestampImpl.fromMillis(createdMs) as unknown as GraphTimestamp,\n updatedAt: GraphTimestampImpl.fromMillis(updatedMs) as unknown as GraphTimestamp,\n };\n\n if (row.v !== null && row.v !== undefined) {\n record.v = Number(row.v);\n }\n return record as unknown as StoredGraphRecord;\n}\n\n/**\n * Coerce a timestamp column value to a plain millis number. The schema types\n * `created_at` / `updated_at` as `INTEGER NOT NULL` so the column should\n * always arrive as a number (or possibly a bigint on some SQLite bindings),\n * but a string row value from SQLite (e.g. BigInt.toString fallback) is also\n * accepted. Anything else indicates a corrupt row — throw loudly rather than\n * silently returning 0, which would quietly mask the bug on every read.\n */\nexport function rowTimestampToMillis(value: unknown): number {\n if (typeof value === 'number') return value;\n if (typeof value === 'bigint') return Number(value);\n if (typeof value === 'string') {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n }\n throw new FiregraphError(\n `SQLite row has non-numeric timestamp column: ${typeof value} (${String(value)})`,\n 'INVALID_QUERY',\n );\n}\n"],"mappings":"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","names":["quoteIdent","JSON_PATH_KEY_RE","FIRESTORE_TYPE_NAMES","quoteIdent","sql"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/backend.ts"],"sourcesContent":["/**\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"],"mappings":";AA0DO,SAAS,mBACd,MACwB;AACxB,SAAO;AAAA,IACL,KAAK,CAAC,eAAoC,KAAK,IAAI,UAAe;AAAA,IAClE,QAAQ,MAAM,KAAK,OAAO;AAAA,EAC5B;AACF;AAOO,SAAS,sBACd,OACqB;AACrB,MAAI,MAAM,WAAW,EAAG,QAAO,mBAAmB,oBAAI,IAAgB,CAAC;AACvE,QAAM,OAAO,MAAM,IAAI,CAAC,MAAM,IAAI,IAAgB,EAAE,OAAO,CAAC,CAAC;AAC7D,QAAM,CAAC,OAAO,GAAG,IAAI,IAAI;AACzB,QAAM,eAAe,oBAAI,IAAgB;AACzC,aAAW,KAAK,OAAO;AACrB,QAAI,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAG,cAAa,IAAI,CAAC;AAAA,EACrD;AACA,SAAO,mBAAmB,YAAY;AACxC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/backend.ts"],"sourcesContent":["/**\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 — backends\n * without the cap throw `UNSUPPORTED_OPERATION` from the client wrapper.\n * In-tree: both Firestore editions (native ANN via `Query.findNearest`)\n * and the local better-sqlite3 backend (`firegraph/sqlite-local`, a\n * brute-force UDF-scored scan — exact, not approximate). The D1 and\n * Cloudflare DO editions stay without the cap: no UDF registration\n * surface, and a JS-side k-NN sweep over `findEdges()` would scale\n * catastrophically.\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 — backends\n * without the cap throw `UNSUPPORTED_OPERATION` from the client\n * wrapper. In-tree: Firestore Enterprise (via Pipeline\n * `search({ query: documentMatches(...) })`) and the local\n * better-sqlite3 backend (`firegraph/sqlite-local`, via a\n * trigger-synced FTS5 index ranked by `bm25()`). Firestore Standard\n * never gets it (Enterprise-only product feature); D1 and the\n * Cloudflare DO edition don't ship FTS5 trigger infrastructure.\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"],"mappings":";AA0DO,SAAS,mBACd,MACwB;AACxB,SAAO;AAAA,IACL,KAAK,CAAC,eAAoC,KAAK,IAAI,UAAe;AAAA,IAClE,QAAQ,MAAM,KAAK,OAAO;AAAA,EAC5B;AACF;AAOO,SAAS,sBACd,OACqB;AACrB,MAAI,MAAM,WAAW,EAAG,QAAO,mBAAmB,oBAAI,IAAgB,CAAC;AACvE,QAAM,OAAO,MAAM,IAAI,CAAC,MAAM,IAAI,IAAgB,EAAE,OAAO,CAAC,CAAC;AAC7D,QAAM,CAAC,OAAO,GAAG,IAAI,IAAI;AACzB,QAAM,eAAe,oBAAI,IAAgB;AACzC,aAAW,KAAK,OAAO;AACrB,QAAI,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAG,cAAa,IAAI,CAAC;AAAA,EACrD;AACA,SAAO,mBAAmB,YAAY;AACxC;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as Capability, S as StorageBackend, G as GraphClientOptions, m as DynamicRegistryConfig, n as DynamicGraphClient, o as GraphClient } from './backend-
|
|
1
|
+
import { C as Capability, S as StorageBackend, G as GraphClientOptions, m as DynamicRegistryConfig, n as DynamicGraphClient, o as GraphClient } from './backend-DNzv8KSR.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Create a `GraphClient` backed by a `StorageBackend`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as Capability, S as StorageBackend, G as GraphClientOptions, m as DynamicRegistryConfig, n as DynamicGraphClient, o as GraphClient } from './backend-
|
|
1
|
+
import { C as Capability, S as StorageBackend, G as GraphClientOptions, m as DynamicRegistryConfig, n as DynamicGraphClient, o as GraphClient } from './backend-DNzv8KSR.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Create a `GraphClient` backed by a `StorageBackend`.
|
|
@@ -3254,7 +3254,7 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
3254
3254
|
async findNearest(params) {
|
|
3255
3255
|
if (!this.backend.findNearest) {
|
|
3256
3256
|
throw new FiregraphError(
|
|
3257
|
-
"findNearest() is not supported by the current storage backend. Vector search requires a backend that declares `search.vector` (currently Firestore Standard and
|
|
3257
|
+
"findNearest() is not supported by the current storage backend. Vector search requires a backend that declares `search.vector` (currently Firestore Standard, Firestore Enterprise, and the local better-sqlite3 backend). There is no client-side fallback because emulating ANN on top of the generic backend surface does not scale beyond toy datasets.",
|
|
3258
3258
|
"UNSUPPORTED_OPERATION"
|
|
3259
3259
|
);
|
|
3260
3260
|
}
|
|
@@ -3270,13 +3270,15 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
3270
3270
|
* Native full-text search (capability `search.fullText`).
|
|
3271
3271
|
*
|
|
3272
3272
|
* Returns the top-N records by relevance, ordered by the search
|
|
3273
|
-
* index's score.
|
|
3274
|
-
*
|
|
3275
|
-
*
|
|
3276
|
-
*
|
|
3277
|
-
*
|
|
3278
|
-
*
|
|
3279
|
-
*
|
|
3273
|
+
* index's score. Firestore Enterprise declares this capability (via
|
|
3274
|
+
* the Pipelines `search({ query: documentMatches(...) })` stage over
|
|
3275
|
+
* Enterprise's FTS index), as does the local better-sqlite3 backend
|
|
3276
|
+
* (`firegraph/sqlite-local`, via a trigger-synced FTS5 index ranked
|
|
3277
|
+
* by `bm25()`). Standard does not declare the cap (FTS is an
|
|
3278
|
+
* Enterprise-only product feature, not a typed-API gap); D1 and the
|
|
3279
|
+
* Cloudflare DO edition have no FTS trigger infrastructure. Backends
|
|
3280
|
+
* without `search.fullText` throw `UNSUPPORTED_OPERATION` from this
|
|
3281
|
+
* wrapper.
|
|
3280
3282
|
*
|
|
3281
3283
|
* Scan-protection mirrors `findNearest`: a search with no
|
|
3282
3284
|
* identifying filters (`aType` / `axbType` / `bType`) walks every
|
|
@@ -3292,7 +3294,7 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
3292
3294
|
async fullTextSearch(params) {
|
|
3293
3295
|
if (!this.backend.fullTextSearch) {
|
|
3294
3296
|
throw new FiregraphError(
|
|
3295
|
-
"fullTextSearch() is not supported by the current storage backend. Full-text search requires a backend that declares `search.fullText` (currently Firestore Enterprise
|
|
3297
|
+
"fullTextSearch() is not supported by the current storage backend. Full-text search requires a backend that declares `search.fullText` (currently Firestore Enterprise and the local better-sqlite3 backend). There is no client-side fallback because emulating FTS over the generic backend surface would not scale beyond toy datasets.",
|
|
3296
3298
|
"UNSUPPORTED_OPERATION"
|
|
3297
3299
|
);
|
|
3298
3300
|
}
|