@typicalday/firegraph 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -73
- package/dist/backend-DuvHGgK1.d.cts +1897 -0
- package/dist/backend-DuvHGgK1.d.ts +1897 -0
- package/dist/backend.cjs +222 -3
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +25 -5
- package/dist/backend.d.ts +25 -5
- package/dist/backend.js +197 -4
- package/dist/backend.js.map +1 -1
- package/dist/chunk-2DHMNTV6.js +16 -0
- package/dist/chunk-2DHMNTV6.js.map +1 -0
- package/dist/chunk-4MMQ5W74.js +288 -0
- package/dist/chunk-4MMQ5W74.js.map +1 -0
- package/dist/chunk-D4J7Z4FE.js +67 -0
- package/dist/chunk-D4J7Z4FE.js.map +1 -0
- package/dist/chunk-N5HFDWQX.js +23 -0
- package/dist/chunk-N5HFDWQX.js.map +1 -0
- package/dist/chunk-PAD7WFFU.js +573 -0
- package/dist/chunk-PAD7WFFU.js.map +1 -0
- package/dist/{chunk-AWW4MUJ5.js → chunk-TK64DNVK.js} +12 -1
- package/dist/chunk-TK64DNVK.js.map +1 -0
- package/dist/{chunk-HONQY4HF.js → chunk-WRTFC5NG.js} +362 -17
- package/dist/chunk-WRTFC5NG.js.map +1 -0
- package/dist/client-BKi3vk0Q.d.ts +34 -0
- package/dist/client-BrsaXtDV.d.cts +34 -0
- package/dist/cloudflare/index.cjs +930 -3
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +213 -12
- package/dist/cloudflare/index.d.ts +213 -12
- package/dist/cloudflare/index.js +562 -281
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/errors-BRc3I_eH.d.cts +73 -0
- package/dist/errors-BRc3I_eH.d.ts +73 -0
- package/dist/firestore-enterprise/index.cjs +3877 -0
- package/dist/firestore-enterprise/index.cjs.map +1 -0
- package/dist/firestore-enterprise/index.d.cts +141 -0
- package/dist/firestore-enterprise/index.d.ts +141 -0
- package/dist/firestore-enterprise/index.js +985 -0
- package/dist/firestore-enterprise/index.js.map +1 -0
- package/dist/firestore-standard/index.cjs +3117 -0
- package/dist/firestore-standard/index.cjs.map +1 -0
- package/dist/firestore-standard/index.d.cts +49 -0
- package/dist/firestore-standard/index.d.ts +49 -0
- package/dist/firestore-standard/index.js +283 -0
- package/dist/firestore-standard/index.js.map +1 -0
- package/dist/index.cjs +590 -550
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -37
- package/dist/index.d.ts +9 -37
- package/dist/index.js +178 -555
- package/dist/index.js.map +1 -1
- package/dist/{registry-Fi074zVa.d.ts → registry-Bc7h6WTM.d.cts} +1 -1
- package/dist/{registry-B1qsVL0E.d.cts → registry-C2KUPVZj.d.ts} +1 -1
- package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.cts} +1 -56
- package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.ts} +1 -56
- package/dist/sqlite/index.cjs +3631 -0
- package/dist/sqlite/index.cjs.map +1 -0
- package/dist/sqlite/index.d.cts +111 -0
- package/dist/sqlite/index.d.ts +111 -0
- package/dist/sqlite/index.js +1164 -0
- package/dist/sqlite/index.js.map +1 -0
- package/package.json +33 -3
- package/dist/backend-BsR0lnFL.d.ts +0 -200
- package/dist/backend-Ct-fLlkG.d.cts +0 -200
- package/dist/chunk-AWW4MUJ5.js.map +0 -1
- package/dist/chunk-HONQY4HF.js.map +0 -1
- package/dist/types-DxYLy8Ol.d.cts +0 -770
- package/dist/types-DxYLy8Ol.d.ts +0 -770
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bulk.ts","../src/internal/firestore-aggregate.ts","../src/internal/firestore-classic-adapter.ts","../src/internal/firestore-classic-expand.ts","../src/internal/projection.ts","../src/internal/firestore-projection.ts","../src/internal/firestore-vector.ts"],"sourcesContent":["import type { Firestore } from '@google-cloud/firestore';\n\nimport { computeEdgeDocId, computeNodeDocId } from './docid.js';\nimport { NODE_RELATION } from './internal/constants.js';\nimport type {\n BulkBatchError,\n BulkOptions,\n BulkResult,\n CascadeResult,\n FindEdgesParams,\n GraphReader,\n StoredGraphRecord,\n} from './types.js';\n\nconst MAX_BATCH_SIZE = 500;\nconst DEFAULT_MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 200;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Splits an array into chunks of at most `size` elements.\n */\nfunction chunk<T>(arr: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n}\n\n/**\n * Deletes a list of document IDs in chunked Firestore batches with retries.\n */\nexport async function bulkDeleteDocIds(\n db: Firestore,\n collectionPath: string,\n docIds: string[],\n options?: BulkOptions,\n): Promise<BulkResult> {\n if (docIds.length === 0) {\n return { deleted: 0, batches: 0, errors: [] };\n }\n\n const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);\n const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;\n const onProgress = options?.onProgress;\n\n const chunks = chunk(docIds, batchSize);\n const errors: BulkBatchError[] = [];\n let deleted = 0;\n let completedBatches = 0;\n\n for (let i = 0; i < chunks.length; i++) {\n const ids = chunks[i];\n let committed = false;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const batch = db.batch();\n const collectionRef = db.collection(collectionPath);\n for (const id of ids) {\n batch.delete(collectionRef.doc(id));\n }\n await batch.commit();\n committed = true;\n deleted += ids.length;\n break;\n } catch (err) {\n if (attempt < maxRetries) {\n const delay = BASE_DELAY_MS * Math.pow(2, attempt);\n await sleep(delay);\n } else {\n errors.push({\n batchIndex: i,\n error: err instanceof Error ? err : new Error(String(err)),\n operationCount: ids.length,\n });\n }\n }\n }\n\n if (committed) {\n completedBatches++;\n }\n\n if (onProgress) {\n onProgress({\n completedBatches,\n totalBatches: chunks.length,\n deletedSoFar: deleted,\n });\n }\n }\n\n return { deleted, batches: completedBatches, errors };\n}\n\n/**\n * Finds all edges matching `params`, then deletes them in chunked batches.\n */\nexport async function bulkRemoveEdges(\n db: Firestore,\n collectionPath: string,\n reader: GraphReader,\n params: FindEdgesParams,\n options?: BulkOptions,\n): Promise<BulkResult> {\n // Override default query limit for bulk deletion — we need all matching edges.\n // limit: 0 bypasses DEFAULT_QUERY_LIMIT; an explicit user limit is preserved.\n // allowCollectionScan: true — bulk deletion inherently implies scanning.\n const effectiveParams =\n params.limit !== undefined\n ? { ...params, allowCollectionScan: params.allowCollectionScan ?? true }\n : { ...params, limit: 0, allowCollectionScan: params.allowCollectionScan ?? true };\n const edges = await reader.findEdges(effectiveParams);\n const docIds = edges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));\n return bulkDeleteDocIds(db, collectionPath, docIds, options);\n}\n\n/** Result from recursive subcollection deletion. */\ninterface SubcollectionDeleteResult {\n deleted: number;\n errors: BulkBatchError[];\n}\n\n/**\n * Recursively delete all documents in all subcollections under a given document.\n * Uses `listCollections()` (Admin SDK) to discover subcollections, then for each\n * subcollection: recurse into each document's subcollections first (depth-first),\n * then bulk delete all documents in the subcollection.\n *\n * The `onProgress` callback is intentionally NOT forwarded to subcollection\n * deletes to avoid confusing callers with interleaved progress from different\n * collection depths.\n */\nasync function deleteSubcollectionsRecursive(\n db: Firestore,\n collectionPath: string,\n docId: string,\n options?: BulkOptions,\n): Promise<SubcollectionDeleteResult> {\n const docRef = db.collection(collectionPath).doc(docId);\n const subcollections = await docRef.listCollections();\n\n if (subcollections.length === 0) return { deleted: 0, errors: [] };\n\n let totalDeleted = 0;\n const allErrors: BulkBatchError[] = [];\n\n // Strip onProgress for subcollection deletes — callers should only see\n // top-level progress, not interleaved reports from nested depths.\n const subOptions: BulkOptions | undefined = options\n ? { batchSize: options.batchSize, maxRetries: options.maxRetries }\n : undefined;\n\n for (const subCollRef of subcollections) {\n const subCollPath = subCollRef.path;\n // List all documents in this subcollection\n const snapshot = await subCollRef.select().get();\n const subDocIds = snapshot.docs.map((d) => d.id);\n\n // Depth-first: recurse into each document's subcollections\n for (const subDocId of subDocIds) {\n const subResult = await deleteSubcollectionsRecursive(db, subCollPath, subDocId, subOptions);\n totalDeleted += subResult.deleted;\n allErrors.push(...subResult.errors);\n }\n\n // Now delete all documents in this subcollection\n if (subDocIds.length > 0) {\n const result = await bulkDeleteDocIds(db, subCollPath, subDocIds, subOptions);\n totalDeleted += result.deleted;\n allErrors.push(...result.errors);\n }\n }\n\n return { deleted: totalDeleted, errors: allErrors };\n}\n\n/**\n * Deletes a node and all of its outgoing and incoming edges.\n *\n * Edges are deleted first in chunked batches, then the node document\n * is deleted in the final batch. This is NOT atomic across batches —\n * if a batch fails after retries, remaining batches still execute.\n *\n * By default, subcollections (subgraphs) under the node's document are\n * recursively deleted. Set `options.deleteSubcollections` to `false` to skip.\n */\nexport async function removeNodeCascade(\n db: Firestore,\n collectionPath: string,\n reader: GraphReader,\n uid: string,\n options?: BulkOptions,\n): Promise<CascadeResult> {\n // Find all edges touching this node (outgoing + incoming).\n // Filter out the node's own self-loop record (axbType === 'is').\n // These queries intentionally scan broadly — allowCollectionScan bypasses safety checks.\n // limit: 0 bypasses the DEFAULT_QUERY_LIMIT to ensure we find all edges.\n const [outgoingRaw, incomingRaw] = await Promise.all([\n reader.findEdges({ aUid: uid, allowCollectionScan: true, limit: 0 }),\n reader.findEdges({ bUid: uid, allowCollectionScan: true, limit: 0 }),\n ]);\n const outgoing = outgoingRaw.filter((e) => e.axbType !== NODE_RELATION);\n const incoming = incomingRaw.filter((e) => e.axbType !== NODE_RELATION);\n\n // Deduplicate: a self-referencing edge could appear in both lists.\n const edgeDocIdSet = new Set<string>();\n const allEdges: StoredGraphRecord[] = [];\n for (const edge of [...outgoing, ...incoming]) {\n const docId = computeEdgeDocId(edge.aUid, edge.axbType, edge.bUid);\n if (!edgeDocIdSet.has(docId)) {\n edgeDocIdSet.add(docId);\n allEdges.push(edge);\n }\n }\n\n // Delete subcollections (subgraphs) under this node's document (depth-first).\n const shouldDeleteSubcollections = options?.deleteSubcollections !== false;\n const nodeDocId = computeNodeDocId(uid);\n let subcollectionResult: SubcollectionDeleteResult = { deleted: 0, errors: [] };\n\n if (shouldDeleteSubcollections) {\n subcollectionResult = await deleteSubcollectionsRecursive(\n db,\n collectionPath,\n nodeDocId,\n options,\n );\n }\n\n // Build doc IDs: edges first, then the node last.\n const edgeDocIds = allEdges.map((e) => computeEdgeDocId(e.aUid, e.axbType, e.bUid));\n const allDocIds = [...edgeDocIds, nodeDocId];\n\n // Wrap the progress callback to track overall progress.\n const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);\n const result = await bulkDeleteDocIds(db, collectionPath, allDocIds, {\n ...options,\n batchSize,\n });\n\n // Determine if the node doc was in a failed batch.\n // The node is always in the last doc ID. If the last batch errored, node wasn't deleted.\n const totalChunks = Math.ceil(allDocIds.length / batchSize);\n const nodeChunkIndex = totalChunks - 1;\n const nodeDeleted = !result.errors.some((e) => e.batchIndex === nodeChunkIndex);\n\n // edgesDeleted counts only top-level edges (not subcollection docs).\n // deleted includes everything: top-level edges + node + subcollection docs.\n const topLevelEdgesDeleted = nodeDeleted ? result.deleted - 1 : result.deleted;\n\n return {\n deleted: result.deleted + subcollectionResult.deleted,\n batches: result.batches,\n errors: [...result.errors, ...subcollectionResult.errors],\n edgesDeleted: topLevelEdgesDeleted,\n nodeDeleted,\n };\n}\n","/**\n * Shared classic-API aggregate translation for both Firestore editions.\n *\n * Translates an `AggregateSpec` into a `Query.aggregate()` call using\n * `AggregateField.count() / sum() / average()`. The classic API does not\n * support `min` / `max` — those throw `UNSUPPORTED_AGGREGATE`. Backends\n * declaring `query.aggregate` are required to support at least count/sum/avg;\n * per-op limitations surface as runtime errors with a clear message.\n *\n * Both Standard and Enterprise editions can call `Query.aggregate()` directly\n * (the Enterprise pipeline `aggregate()` stage is a future optimisation that\n * would unlock min/max — Phase 11+). Until then both editions delegate to\n * this single helper, which keeps the per-edition backends thin and avoids\n * cross-subpath imports between them.\n *\n * Field paths follow the same dotted convention used elsewhere in firegraph\n * (`'data.price'`, `'data.profile.score'`). They are passed through to\n * Firestore unchanged — Firestore itself interprets `.` as a field-path\n * separator.\n */\n\nimport { AggregateField, type Query } from '@google-cloud/firestore';\n\nimport { FiregraphError } from '../errors.js';\nimport type { AggregateSpec, QueryFilter } from '../types.js';\n\n/**\n * Apply the firegraph filter list to a base Firestore query. Mirrors the\n * tiny `where()` loop used by the classic adapter; kept local so the\n * aggregate path doesn't depend on adapter internals it doesn't otherwise\n * need.\n */\nexport function applyFiltersToQuery(base: Query, filters: QueryFilter[]): Query {\n let q = base;\n for (const f of filters) {\n q = q.where(f.field, f.op, f.value);\n }\n return q;\n}\n\n/**\n * Run an aggregate query against a base Firestore `Query`. Returns the\n * resolved numeric result keyed by alias.\n *\n * `count` is the only op that ignores `field`; everything else requires it.\n * Missing fields throw `INVALID_QUERY` so the caller sees the bad spec\n * rather than a Firestore-side error from passing `undefined` into\n * `AggregateField.sum`.\n */\nexport async function runFirestoreAggregate(\n base: Query,\n spec: AggregateSpec,\n filters: QueryFilter[],\n { edition }: { edition: 'standard' | 'enterprise' },\n): Promise<Record<string, number>> {\n if (Object.keys(spec).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 filtered = applyFiltersToQuery(base, filters);\n // Firestore's `AggregateField` map values are heterogeneous — `count()`,\n // `sum(field)`, and `average(field)` each return distinct AggregateField\n // subtypes parameterized by their own input type. The library accepts a\n // record whose value union covers all three, so we type the local map\n // accordingly.\n type AnyAggField = ReturnType<typeof AggregateField.count>;\n const aggregations: Record<string, AnyAggField> = {};\n for (const [alias, { op, field }] of Object.entries(spec)) {\n if (op === 'count') {\n // Reject a stray `field` on count: counting rows never uses a column\n // expression, and silently ignoring would mask typos like\n // `{ n: { op: 'count', field: 'data.price' } }` (cribbed from a sum\n // spec). Better to surface the misuse than return a misleading row\n // count.\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 aggregations[alias] = AggregateField.count();\n continue;\n }\n if (!field) {\n throw new FiregraphError(\n `Aggregate '${alias}' op '${op}' requires a field.`,\n 'INVALID_QUERY',\n );\n }\n if (op === 'sum') {\n aggregations[alias] = AggregateField.sum(field) as unknown as AnyAggField;\n } else if (op === 'avg') {\n aggregations[alias] = AggregateField.average(field) as unknown as AnyAggField;\n } else {\n // Both editions currently route through the classic `Query.aggregate`\n // API, which exposes only count/sum/avg. Enterprise *could* expose\n // min/max via pipelines (deferred to a later phase). Until then, both\n // editions reject min/max identically — the message names the edition\n // so the diagnostic is accurate.\n const editionLabel = edition === 'enterprise' ? 'Firestore Enterprise' : 'Firestore Standard';\n throw new FiregraphError(\n `Aggregate op '${op}' is not supported on ${editionLabel}. ` +\n `Both Firestore editions support count/sum/avg via the classic Query API; ` +\n `min/max requires a backend with SQL aggregation (SQLite or DO).`,\n 'UNSUPPORTED_AGGREGATE',\n );\n }\n }\n\n const snap = await filtered.aggregate(aggregations).get();\n const data = snap.data() as Record<string, number | null>;\n const out: Record<string, number> = {};\n for (const alias of Object.keys(spec)) {\n const v = data[alias];\n // Firestore returns `null` for sum/avg over an empty set. Surface that\n // as `0` for sum (well-defined) and `NaN` for avg (avg of empty set is\n // mathematically undefined). `count` is never null.\n if (v === null || v === undefined) {\n const op = spec[alias].op;\n out[alias] = op === 'avg' ? Number.NaN : 0;\n } else {\n out[alias] = v;\n }\n }\n return out;\n}\n","import type { Firestore, Query, Transaction } from '@google-cloud/firestore';\n\nimport type { QueryFilter, QueryOptions, StoredGraphRecord } from '../types.js';\n\nexport interface FirestoreAdapter {\n collectionPath: string;\n getDoc(docId: string): Promise<StoredGraphRecord | null>;\n setDoc(\n docId: string,\n data: Record<string, unknown>,\n options?: { merge?: boolean },\n ): Promise<void>;\n updateDoc(docId: string, data: Record<string, unknown>): Promise<void>;\n deleteDoc(docId: string): Promise<void>;\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;\n}\n\nexport function createFirestoreAdapter(db: Firestore, collectionPath: string): FirestoreAdapter {\n const collectionRef = db.collection(collectionPath);\n\n return {\n collectionPath,\n\n async getDoc(docId: string): Promise<StoredGraphRecord | null> {\n const snap = await collectionRef.doc(docId).get();\n if (!snap.exists) return null;\n return snap.data() as StoredGraphRecord;\n },\n\n async setDoc(\n docId: string,\n data: Record<string, unknown>,\n options?: { merge?: boolean },\n ): Promise<void> {\n if (options?.merge) {\n await collectionRef.doc(docId).set(data, { merge: true });\n } else {\n await collectionRef.doc(docId).set(data);\n }\n },\n\n async updateDoc(docId: string, data: Record<string, unknown>): Promise<void> {\n await collectionRef.doc(docId).update(data);\n },\n\n async deleteDoc(docId: string): Promise<void> {\n await collectionRef.doc(docId).delete();\n },\n\n async query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n let q: Query = collectionRef;\n for (const f of filters) {\n q = q.where(f.field, f.op, f.value);\n }\n if (options?.orderBy) {\n q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? 'asc');\n }\n if (options?.limit !== undefined) {\n q = q.limit(options.limit);\n }\n const snap = await q.get();\n return snap.docs.map((doc) => doc.data() as StoredGraphRecord);\n },\n };\n}\n\nexport interface TransactionAdapter {\n getDoc(docId: string): Promise<StoredGraphRecord | null>;\n setDoc(docId: string, data: Record<string, unknown>, options?: { merge?: boolean }): void;\n updateDoc(docId: string, data: Record<string, unknown>): void;\n deleteDoc(docId: string): void;\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]>;\n}\n\nexport function createTransactionAdapter(\n db: Firestore,\n collectionPath: string,\n tx: Transaction,\n): TransactionAdapter {\n const collectionRef = db.collection(collectionPath);\n\n return {\n async getDoc(docId: string): Promise<StoredGraphRecord | null> {\n const snap = await tx.get(collectionRef.doc(docId));\n if (!snap.exists) return null;\n return snap.data() as StoredGraphRecord;\n },\n\n setDoc(docId: string, data: Record<string, unknown>, options?: { merge?: boolean }): void {\n if (options?.merge) {\n tx.set(collectionRef.doc(docId), data, { merge: true });\n } else {\n tx.set(collectionRef.doc(docId), data);\n }\n },\n\n updateDoc(docId: string, data: Record<string, unknown>): void {\n tx.update(collectionRef.doc(docId), data);\n },\n\n deleteDoc(docId: string): void {\n tx.delete(collectionRef.doc(docId));\n },\n\n async query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n let q: Query = collectionRef;\n for (const f of filters) {\n q = q.where(f.field, f.op, f.value);\n }\n if (options?.orderBy) {\n q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? 'asc');\n }\n if (options?.limit !== undefined) {\n q = q.limit(options.limit);\n }\n const snap = await tx.get(q);\n return snap.docs.map((doc) => doc.data() as StoredGraphRecord);\n },\n };\n}\n\nexport interface BatchAdapter {\n setDoc(docId: string, data: Record<string, unknown>, options?: { merge?: boolean }): void;\n updateDoc(docId: string, data: Record<string, unknown>): void;\n deleteDoc(docId: string): void;\n commit(): Promise<void>;\n}\n\nexport function createBatchAdapter(db: Firestore, collectionPath: string): BatchAdapter {\n const collectionRef = db.collection(collectionPath);\n const batch = db.batch();\n\n return {\n setDoc(docId: string, data: Record<string, unknown>, options?: { merge?: boolean }): void {\n if (options?.merge) {\n batch.set(collectionRef.doc(docId), data, { merge: true });\n } else {\n batch.set(collectionRef.doc(docId), data);\n }\n },\n\n updateDoc(docId: string, data: Record<string, unknown>): void {\n batch.update(collectionRef.doc(docId), data);\n },\n\n deleteDoc(docId: string): void {\n batch.delete(collectionRef.doc(docId));\n },\n\n async commit(): Promise<void> {\n await batch.commit();\n },\n };\n}\n","/**\n * Shared classic-API multi-source fan-out for Firestore.\n *\n * Used by:\n *\n * - `firestore-standard` (always — Standard has no Pipelines path).\n * - `firestore-enterprise` (only when `queryMode === 'classic'`, i.e.\n * under the emulator or when explicitly forced).\n *\n * Strategy: chunk `params.sources` into 30-element groups (the classic\n * `'in'` operator's documented cap), dispatch one `Query.where(field, 'in',\n * chunk)` per chunk in parallel via `Promise.all`, then concat. Optional\n * post-pass sort + total-limit cap so observable behaviour matches the\n * SQL backends' single-statement `WHERE … IN (?,?,…) ORDER BY … LIMIT N`.\n *\n * Why this is still a win over the per-source `findEdges` loop in\n * `traverse.ts`: that loop emits one query per source UID. With 100\n * sources it's 100 round trips. The chunked path is `ceil(100/30) = 4`\n * round trips. The Pipelines `equalAny` path (Enterprise) collapses all\n * 100 into one — see `firestore-expand.ts` — but Standard can't reach\n * that, so chunked classic is the best Standard can do.\n *\n * Hydration follows the same chunking strategy: a single classic query\n * per chunk that fetches every node row whose `aUid` is in the chunk\n * (nodes are stored as self-loops `(uid, 'is', uid)` so `aUid IN chunk`\n * picks them up). Document IDs would also work via per-UID `getDoc`,\n * but that's N round trips for N targets — chunking matches the\n * fan-out path's round-trip count.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type { ExpandParams, ExpandResult, QueryFilter, StoredGraphRecord } from '../types.js';\nimport { NODE_RELATION } from './constants.js';\nimport type { FirestoreAdapter } from './firestore-classic-adapter.js';\n\n/**\n * Maximum elements per Firestore classic `'in'` operator. Documented as 30\n * across the Node Admin SDK, Firestore web SDK, and the security-rules\n * runtime. Exceeding this throws `INVALID_ARGUMENT` on the wire.\n */\nexport const FIRESTORE_CLASSIC_IN_CHUNK_SIZE = 30;\n\n/**\n * Read a (possibly dotted) field from a record. Used for the post-concat\n * sort pass. Mirrors the dotted-path resolution in\n * `firestore-projection.ts` but read-only and without bare-name rewriting\n * — `ExpandParams.orderBy.field` is always a fully-qualified path.\n */\nfunction readField(record: StoredGraphRecord, path: string): unknown {\n if (!path.includes('.')) {\n return (record as unknown as Record<string, unknown>)[path];\n }\n let cursor: unknown = record;\n for (const seg of path.split('.')) {\n if (cursor === null || typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\n/**\n * Compare two field values for a stable, total ordering. Mirrors\n * Firestore's classic-API ordering semantics: numbers numerically,\n * strings lexicographically, booleans (false < true), and nullish goes\n * first. Mixed-type comparisons fall back to `String(...)` so the sort is\n * defined for every input shape — the alternative would be the JS\n * default `<` semantics, which silently coerce and lose stability.\n */\nfunction compareFieldValues(a: unknown, b: unknown): number {\n if (a === b) return 0;\n if (a === undefined || a === null) return -1;\n if (b === undefined || b === null) return 1;\n const ta = typeof a;\n const tb = typeof b;\n if (ta === tb) {\n // `a` and `b` share a primitive type (number, string, boolean, etc.);\n // a direct `<` comparison is safe under the JS comparison rules. Cast\n // through `string` solely to satisfy TS's `unknown < unknown` ban —\n // the runtime behaviour is identical for any pair of same-type\n // primitives.\n return (a as string) < (b as string) ? -1 : 1;\n }\n // Mixed types — fall back to string comparison.\n const sa = String(a);\n const sb = String(b);\n return sa < sb ? -1 : sa > sb ? 1 : 0;\n}\n\n/**\n * Run a classic-API `expand()` against a Firestore collection via the\n * supplied `FirestoreAdapter`. Returns the same `ExpandResult` shape as\n * the SQL backends.\n *\n * Empty `params.sources` short-circuits to an empty result without\n * touching the adapter — the chunking pass would emit zero queries\n * anyway, but the early return makes the contract explicit and matches\n * the `client.expand` wrapper.\n *\n * Self-loop guard: when `params.axbType === NODE_RELATION`, edges where\n * `aUid === bUid` are node rows, not real hops. The SQL backends filter\n * those out via `aUid != bUid`; we mirror the same filter as a\n * post-process pass because Firestore classic queries don't support\n * column-vs-column predicates. The guard is defensive — `traverse.ts`\n * never sends `NODE_RELATION` as `axbType`, but a direct\n * `client.expand({ axbType: 'is' })` call would otherwise return the\n * source nodes themselves.\n */\nexport async function runFirestoreClassicExpand(\n adapter: FirestoreAdapter,\n params: ExpandParams,\n): Promise<ExpandResult> {\n if (params.sources.length === 0) {\n return params.hydrate ? { edges: [], targets: [] } : { edges: [] };\n }\n\n if (params.axbType.length === 0) {\n throw new FiregraphError('expand(): axbType must be a non-empty string.', 'INVALID_QUERY');\n }\n\n const direction = params.direction ?? 'forward';\n const sourceField = direction === 'forward' ? 'aUid' : 'bUid';\n\n const chunks = chunkUids(params.sources, FIRESTORE_CLASSIC_IN_CHUNK_SIZE);\n const totalLimit =\n params.limitPerSource !== undefined ? params.sources.length * params.limitPerSource : undefined;\n\n const buildFilters = (chunk: string[]): QueryFilter[] => {\n const filters: QueryFilter[] = [\n { field: 'axbType', op: '==', value: params.axbType },\n { field: sourceField, op: 'in', value: chunk },\n ];\n if (params.aType !== undefined) {\n filters.push({ field: 'aType', op: '==', value: params.aType });\n }\n if (params.bType !== undefined) {\n filters.push({ field: 'bType', op: '==', value: params.bType });\n }\n return filters;\n };\n\n const buildOptions = (chunk: string[]) => {\n const opts: { orderBy?: ExpandParams['orderBy']; limit?: number } = {};\n if (params.orderBy) opts.orderBy = params.orderBy;\n if (params.limitPerSource !== undefined) {\n // Per-chunk soft cap: chunk.length * limitPerSource. The post-concat\n // slice enforces the global cap.\n opts.limit = chunk.length * params.limitPerSource;\n }\n return opts;\n };\n\n const chunkResults = await Promise.all(\n chunks.map((chunk) => adapter.query(buildFilters(chunk), buildOptions(chunk))),\n );\n let edges: StoredGraphRecord[] = chunkResults.flat();\n\n // Self-loop filter when the caller targeted the node-relation. See JSDoc.\n if (params.axbType === NODE_RELATION) {\n edges = edges.filter((e) => e.aUid !== e.bUid);\n }\n\n // Cross-chunk ordering pass. Each chunk's result is already sorted (we\n // pushed `orderBy` into the per-chunk query), but concat'd order is not\n // globally sorted. Re-sort to honour the contract.\n if (params.orderBy) {\n const sortField = params.orderBy.field;\n const dir = params.orderBy.direction ?? 'asc';\n edges.sort((a, b) => {\n const cmp = compareFieldValues(readField(a, sortField), readField(b, sortField));\n return dir === 'asc' ? cmp : -cmp;\n });\n }\n if (totalLimit !== undefined && edges.length > totalLimit) {\n edges = edges.slice(0, totalLimit);\n }\n\n if (!params.hydrate) return { edges };\n\n // Hydration: fetch every target node by `aUid in chunk` against the\n // node-relation. Nodes are self-loops, so this picks up exactly one row\n // per UID. Chunked the same way as the fan-out queries above.\n const targetUids = edges.map((e) => (direction === 'forward' ? e.bUid : e.aUid));\n const uniqueTargets = [...new Set(targetUids)];\n if (uniqueTargets.length === 0) {\n return { edges, targets: [] };\n }\n const hydrateChunks = chunkUids(uniqueTargets, FIRESTORE_CLASSIC_IN_CHUNK_SIZE);\n const hydrateResults = await Promise.all(\n hydrateChunks.map((chunk) =>\n adapter.query([\n { field: 'axbType', op: '==', value: NODE_RELATION },\n { field: 'aUid', op: 'in', value: chunk },\n ]),\n ),\n );\n const byUid = new Map<string, StoredGraphRecord>();\n for (const row of hydrateResults.flat()) {\n // `bUid === aUid === uid` for node rows by construction.\n byUid.set(row.bUid, row);\n }\n const targets = targetUids.map((uid) => byUid.get(uid) ?? null);\n return { edges, targets };\n}\n\n/** Split a list into fixed-size chunks. Exported for the unit test. */\nexport function chunkUids(uids: readonly string[], chunkSize: number): string[][] {\n if (chunkSize <= 0) {\n throw new FiregraphError(\n `chunkUids: chunkSize must be positive (got ${chunkSize}).`,\n 'INVALID_QUERY',\n );\n }\n const out: string[][] = [];\n for (let i = 0; i < uids.length; i += chunkSize) {\n out.push(uids.slice(i, i + chunkSize) as string[]);\n }\n return out;\n}\n","/**\n * Shared helpers for the `query.select` projection contract.\n *\n * The SQLite-shaped backends (`src/sqlite/sql.ts`,\n * `src/cloudflare/sql.ts`) carry their own `normalizeProjectionField`\n * implementations because they're entangled with `FIELD_TO_COLUMN` /\n * `DO_FIELD_TO_COLUMN`. The Firestore-shaped backends (`firestore-standard`,\n * `firestore-enterprise`) plus the `RoutingStorageBackend` pass-through use\n * the helpers below to keep the projection contract consistent across the\n * three runtimes:\n *\n * - bare names → `data.<name>`\n * - `'data'` and `'data.*'` → as-is\n * - top-level envelope fields (`aType`, `aUid`, `axbType`, `bType`,\n * `bUid`, `createdAt`, `updatedAt`, `v`) → as-is\n *\n * The set diverges from `BUILTIN_FIELDS` in `internal/constants.ts` — that\n * one is the queryable-filter set and does not include `v`. Projection\n * accepts `v` because it is a top-level envelope field that the user may\n * want to read for diagnostics.\n */\n\nconst PROJECTION_BUILTIN_FIELDS: ReadonlySet<string> = new Set([\n 'aType',\n 'aUid',\n 'axbType',\n 'bType',\n 'bUid',\n 'createdAt',\n 'updatedAt',\n 'v',\n]);\n\n/**\n * Rewrite a caller-supplied projection field to the canonical form the\n * Firestore-shaped backends consume. See file header for the rules.\n */\nexport function normalizeFirestoreProjectionField(field: string): string {\n if (PROJECTION_BUILTIN_FIELDS.has(field)) return field;\n if (field === 'data' || field.startsWith('data.')) return field;\n return `data.${field}`;\n}\n\n/**\n * Read a (possibly dotted) path out of a partial document. Used by the\n * Firestore-shaped backends to translate `doc.data()` into the projected JS\n * shape: each row in the result is keyed by the *original* field as the\n * caller supplied it, and the value is whatever lives at the canonical\n * path inside the partial document Firestore returns.\n *\n * Missing path segments resolve to `null` (not `undefined`) to match the\n * SQLite-shaped backends, where `json_extract` returns SQL NULL for an\n * absent JSON path — the decoder surfaces that as `null`. Aligning here\n * means a consumer iterating over `Object.entries(row)` sees the same\n * shape across SQLite/DO/Firestore: every requested field is present in\n * the row object, and absent values are explicitly `null`. If we returned\n * `undefined`, Firestore rows would silently lose absent keys when\n * serialized through `JSON.stringify`, breaking that contract.\n */\nexport function readProjectionPath(\n obj: Record<string, unknown> | undefined | null,\n path: string,\n): unknown {\n if (obj === undefined || obj === null) return null;\n const raw = !path.includes('.')\n ? obj[path]\n : (() => {\n const parts = path.split('.');\n let cur: unknown = obj;\n for (const part of parts) {\n if (cur === undefined || cur === null) return undefined;\n if (typeof cur !== 'object') return undefined;\n cur = (cur as Record<string, unknown>)[part];\n }\n return cur;\n })();\n return raw === undefined ? null : raw;\n}\n","/**\n * Shared classic-API projection translation for both Firestore editions.\n *\n * Translates a `findEdgesProjected({ select })` call into a\n * `Query.select(...fieldPaths)` query and decodes the partial documents\n * Firestore returns. Both Standard and Enterprise editions delegate to this\n * single helper so the projection contract — bare-name normalization,\n * builtin / `data.*` resolution, dedup semantics, original-key preservation\n * — stays consistent across editions.\n *\n * Why the classic API on both editions: the Enterprise pipeline `select()`\n * stage is a future optimisation. Server-side projection's only deliverable\n * is the byte-savings on the wire, and the classic `Query.select(...)`\n * API already achieves that on both editions. When pipeline `select()`\n * lands on a future SDK release the wiring is additive — swap the\n * implementation behind this helper, callers don't change.\n *\n * Migrations are not applied to the result. The contract on\n * `StorageBackend.findEdgesProjected` documents the rationale: the caller\n * asked for a partial shape, and rehydrating it through the migration\n * pipeline would require synthesising every absent field.\n *\n * Absent fields surface as `null`, not `undefined` — this matches the\n * SQLite-shaped backends (where `json_extract` returns SQL NULL for an\n * absent JSON path) so the projected row shape is identical across\n * SQLite/DO/Firestore. See `readProjectionPath` for the normalisation.\n */\n\nimport type { Query } from '@google-cloud/firestore';\n\nimport { FiregraphError } from '../errors.js';\nimport type { QueryFilter, QueryOptions } from '../types.js';\nimport { normalizeFirestoreProjectionField, readProjectionPath } from './projection.js';\n\n/** Resolved projection field — original (caller-supplied) + canonical Firestore path. */\ninterface ResolvedProjectionField {\n original: string;\n canonical: string;\n}\n\n/**\n * Run a projecting query against a base Firestore `Query`. Returns the\n * decoded rows, each keyed by the *original* field name as the caller\n * supplied it.\n *\n * `select` is rejected when empty (matches the SQLite compiler — both\n * layers fail so a misuse caught by either surfaces a clean\n * `INVALID_QUERY`). Duplicate entries are de-duped while preserving\n * first-occurrence order.\n */\nexport async function runFirestoreFindEdgesProjected(\n base: Query,\n select: ReadonlyArray<string>,\n filters: QueryFilter[],\n options?: QueryOptions,\n): Promise<Array<Record<string, unknown>>> {\n if (select.length === 0) {\n throw new FiregraphError(\n 'findEdgesProjected requires a non-empty select list — ' +\n 'an empty projection has no representation distinct from `findEdges`.',\n 'INVALID_QUERY',\n );\n }\n\n const seen = new Set<string>();\n const fields: ResolvedProjectionField[] = [];\n for (const f of select) {\n if (!seen.has(f)) {\n seen.add(f);\n fields.push({ original: f, canonical: normalizeFirestoreProjectionField(f) });\n }\n }\n\n let q: Query = base;\n for (const f of filters) {\n q = q.where(f.field, f.op, f.value);\n }\n if (options?.orderBy) {\n q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? 'asc');\n }\n if (options?.limit !== undefined) {\n q = q.limit(options.limit);\n }\n q = q.select(...fields.map((p) => p.canonical));\n\n const snap = await q.get();\n return snap.docs.map((doc) => {\n const data = doc.data() as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const { original, canonical } of fields) {\n out[original] = readProjectionPath(data, canonical);\n }\n return out;\n });\n}\n","/**\n * Shared classic-API vector / nearest-neighbour translation for both\n * Firestore editions.\n *\n * Translates a `findNearest({ ... })` call into a `Query.findNearest(opts)`\n * VectorQuery and decodes the snapshot. Standard and Enterprise both\n * delegate here so the field-path normalisation, identifying-filter\n * application, validation surface, and result shape are guaranteed\n * identical across editions.\n *\n * Why the classic API on both editions: the Enterprise pipeline\n * `findNearest` stage is a future optimisation. Vector search's\n * deliverable is \"top-K by similarity,\" and the classic\n * `Query.findNearest(...)` API already produces that on both editions\n * with identical index requirements. When pipeline `findNearest` becomes\n * preferable for some other reason (composing with other pipeline\n * stages), the wiring is additive — swap the implementation behind this\n * helper, callers don't change.\n *\n * Migrations are not applied to the result. The contract on\n * `StorageBackend.findNearest` documents the rationale: the vector\n * index walked the raw stored shape, and rehydrating through the\n * migration pipeline would change the candidate set the index already\n * chose.\n */\n\nimport type { FieldPath } from '@google-cloud/firestore';\nimport { type Query, type VectorValue } from '@google-cloud/firestore';\n\nimport { FiregraphError } from '../errors.js';\nimport type { FindNearestParams, QueryFilter, StoredGraphRecord } from '../types.js';\nimport { applyFiltersToQuery } from './firestore-aggregate.js';\n\n/**\n * Built-in envelope fields that must NOT be passed as `vectorField` or\n * `distanceResultField`. Vectors live inside `data`; the envelope is\n * reserved for firegraph metadata. Mirrors the projection /\n * filter-field contract.\n */\nconst ENVELOPE_FIELDS: ReadonlySet<string> = new Set([\n 'aType',\n 'aUid',\n 'axbType',\n 'bType',\n 'bUid',\n 'createdAt',\n 'updatedAt',\n 'v',\n]);\n\n/**\n * Normalise a caller-supplied vector / distance-result field path. Bare\n * names rewrite to `data.<name>`; `'data'` and `'data.*'` pass through;\n * envelope fields are rejected (they aren't vector-indexable, and the\n * SDK reserves them as `distanceResultField` targets).\n */\nexport function normalizeVectorFieldPath(label: string, field: string): string {\n if (ENVELOPE_FIELDS.has(field)) {\n throw new FiregraphError(\n `findNearest(): ${label} '${field}' is a built-in envelope field — ` +\n `vectors must live under \\`data.*\\`. Use a path like 'data.${field}' ` +\n `if you really meant a nested data field.`,\n 'INVALID_QUERY',\n );\n }\n if (field === 'data' || field.startsWith('data.')) return field;\n return `data.${field}`;\n}\n\n/**\n * Translate firegraph's identifying-filter shape (`aType`, `axbType`,\n * `bType`) plus the optional `where` array into a flat `QueryFilter[]`\n * that `applyFiltersToQuery` can consume. The client wrapper does\n * scan-protection on this same list before dispatching, so the order\n * (identifiers first, then user-supplied where) is the same one the\n * safety check saw.\n */\nexport function buildVectorFilters(params: FindNearestParams): QueryFilter[] {\n const filters: QueryFilter[] = [];\n if (params.aType) filters.push({ field: 'aType', op: '==', value: params.aType });\n if (params.axbType) filters.push({ field: 'axbType', op: '==', value: params.axbType });\n if (params.bType) filters.push({ field: 'bType', op: '==', value: params.bType });\n if (params.where) filters.push(...params.where);\n return filters;\n}\n\n/** Resolve a `queryVector` argument to a plain `number[]`. */\nfunction toNumberArray(qv: number[] | { toArray(): number[] }): number[] {\n if (Array.isArray(qv)) return qv;\n if (typeof (qv as { toArray?: unknown }).toArray === 'function') {\n return (qv as VectorValue).toArray();\n }\n throw new FiregraphError(\n 'findNearest(): queryVector must be a number[] or a Firestore VectorValue.',\n 'INVALID_QUERY',\n );\n}\n\n/**\n * Run a vector query against a base Firestore `Query`. Returns the\n * matching records as `StoredGraphRecord[]`, ordered by similarity (the\n * SDK's natural order — nearest-first for EUCLIDEAN/COSINE,\n * highest-first for DOT_PRODUCT).\n *\n * Validation surface (matches the `VectorExtension` JSDoc):\n *\n * - `vectorField` and `distanceResultField` (if set) must not be\n * envelope fields.\n * - `queryVector` must be a non-empty `number[]` / `VectorValue`.\n * - `limit` must be a positive integer ≤ 1000 (the SDK enforces 1000\n * on the wire; we mirror it client-side for a clearer error).\n */\nexport async function runFirestoreFindNearest(\n base: Query,\n params: FindNearestParams,\n): Promise<StoredGraphRecord[]> {\n const vec = toNumberArray(params.queryVector);\n if (vec.length === 0) {\n throw new FiregraphError(\n 'findNearest(): queryVector is empty — at least one dimension is required.',\n 'INVALID_QUERY',\n );\n }\n if (!Number.isInteger(params.limit) || params.limit <= 0 || params.limit > 1000) {\n throw new FiregraphError(\n `findNearest(): limit must be a positive integer ≤ 1000 (got ${params.limit}).`,\n 'INVALID_QUERY',\n );\n }\n\n const vectorField = normalizeVectorFieldPath('vectorField', params.vectorField);\n const distanceResultField =\n params.distanceResultField !== undefined\n ? normalizeVectorFieldPath('distanceResultField', params.distanceResultField)\n : undefined;\n\n const filtered = applyFiltersToQuery(base, buildVectorFilters(params));\n\n // Firestore's `findNearest({ vectorField, ... })` accepts a `string` or\n // `FieldPath` for both `vectorField` and `distanceResultField`. We pass\n // the dotted path verbatim — Firestore itself interprets `.` as the\n // field-path separator, matching the convention used everywhere else\n // in firegraph.\n const opts: {\n vectorField: string | FieldPath;\n queryVector: number[];\n limit: number;\n distanceMeasure: 'EUCLIDEAN' | 'COSINE' | 'DOT_PRODUCT';\n distanceThreshold?: number;\n distanceResultField?: string | FieldPath;\n } = {\n vectorField,\n queryVector: vec,\n limit: params.limit,\n distanceMeasure: params.distanceMeasure,\n };\n if (params.distanceThreshold !== undefined) opts.distanceThreshold = params.distanceThreshold;\n if (distanceResultField !== undefined) opts.distanceResultField = distanceResultField;\n\n // The classic `findNearest` returns a `VectorQuery` with its own `.get()`\n // that resolves to a `VectorQuerySnapshot`. The doc shape is identical\n // to a normal QueryDocumentSnapshot, so we can decode straight into\n // `StoredGraphRecord` like the regular `query()` adapter does.\n const snap = await filtered.findNearest(opts).get();\n return snap.docs.map((doc) => doc.data() as StoredGraphRecord);\n}\n"],"mappings":"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","names":["chunk"]}
|
|
@@ -81,6 +81,16 @@ var CrossBackendTransactionError = class extends FiregraphError {
|
|
|
81
81
|
this.name = "CrossBackendTransactionError";
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
|
+
var CapabilityNotSupportedError = class extends FiregraphError {
|
|
85
|
+
constructor(capability, backendDescription) {
|
|
86
|
+
super(
|
|
87
|
+
`Capability "${capability}" is not supported by ${backendDescription}.`,
|
|
88
|
+
"CAPABILITY_NOT_SUPPORTED"
|
|
89
|
+
);
|
|
90
|
+
this.capability = capability;
|
|
91
|
+
this.name = "CapabilityNotSupportedError";
|
|
92
|
+
}
|
|
93
|
+
};
|
|
84
94
|
|
|
85
95
|
// src/internal/write-plan.ts
|
|
86
96
|
var DELETE_FIELD = /* @__PURE__ */ Symbol.for("firegraph.deleteField");
|
|
@@ -234,6 +244,7 @@ export {
|
|
|
234
244
|
RegistryScopeError,
|
|
235
245
|
MigrationError,
|
|
236
246
|
CrossBackendTransactionError,
|
|
247
|
+
CapabilityNotSupportedError,
|
|
237
248
|
DELETE_FIELD,
|
|
238
249
|
deleteField,
|
|
239
250
|
isDeleteSentinel,
|
|
@@ -242,4 +253,4 @@ export {
|
|
|
242
253
|
assertSafePath,
|
|
243
254
|
flattenPatch
|
|
244
255
|
};
|
|
245
|
-
//# sourceMappingURL=chunk-
|
|
256
|
+
//# sourceMappingURL=chunk-TK64DNVK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/internal/write-plan.ts"],"sourcesContent":["export class FiregraphError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'FiregraphError';\n }\n}\n\nexport class NodeNotFoundError extends FiregraphError {\n constructor(uid: string) {\n super(`Node not found: ${uid}`, 'NODE_NOT_FOUND');\n this.name = 'NodeNotFoundError';\n }\n}\n\nexport class EdgeNotFoundError extends FiregraphError {\n constructor(aUid: string, axbType: string, bUid: string) {\n super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, 'EDGE_NOT_FOUND');\n this.name = 'EdgeNotFoundError';\n }\n}\n\nexport class ValidationError extends FiregraphError {\n constructor(\n message: string,\n public readonly details?: unknown,\n ) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class RegistryViolationError extends FiregraphError {\n constructor(aType: string, axbType: string, bType: string) {\n super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, 'REGISTRY_VIOLATION');\n this.name = 'RegistryViolationError';\n }\n}\n\nexport class InvalidQueryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'INVALID_QUERY');\n this.name = 'InvalidQueryError';\n }\n}\n\nexport class TraversalError extends FiregraphError {\n constructor(message: string) {\n super(message, 'TRAVERSAL_ERROR');\n this.name = 'TraversalError';\n }\n}\n\nexport class DynamicRegistryError extends FiregraphError {\n constructor(message: string) {\n super(message, 'DYNAMIC_REGISTRY_ERROR');\n this.name = 'DynamicRegistryError';\n }\n}\n\nexport class QuerySafetyError extends FiregraphError {\n constructor(message: string) {\n super(message, 'QUERY_SAFETY');\n this.name = 'QuerySafetyError';\n }\n}\n\nexport class RegistryScopeError extends FiregraphError {\n constructor(\n aType: string,\n axbType: string,\n bType: string,\n scopePath: string,\n allowedIn: string[],\n ) {\n super(\n `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope \"${scopePath || 'root'}\". ` +\n `Allowed in: [${allowedIn.join(', ')}]`,\n 'REGISTRY_SCOPE',\n );\n this.name = 'RegistryScopeError';\n }\n}\n\nexport class MigrationError extends FiregraphError {\n constructor(message: string) {\n super(message, 'MIGRATION_ERROR');\n this.name = 'MigrationError';\n }\n}\n\n/**\n * Thrown when a caller tries to perform an operation that would require\n * atomicity across two physical storage backends — e.g. opening a routed\n * subgraph client from inside a transaction callback. Cross-backend\n * atomicity cannot be honoured by real-world storage engines (Firestore,\n * SQLite drivers over D1/DO/better-sqlite3, etc.), so firegraph surfaces\n * this as a typed error instead of silently confining the write to the\n * base backend.\n *\n * Normally `TransactionBackend` and `BatchBackend` don't expose `subgraph()`\n * at the type level, so this error is unreachable through well-typed code.\n * It exists as a public catchable type for app code that needs to tolerate\n * this case deliberately (e.g. dynamic code paths that bypass the type\n * system) and as future-proofing if the interface ever grows a way to\n * request a sub-scope inside a transaction.\n */\nexport class CrossBackendTransactionError extends FiregraphError {\n constructor(message: string) {\n super(message, 'CROSS_BACKEND_TRANSACTION');\n this.name = 'CrossBackendTransactionError';\n }\n}\n\n/**\n * Thrown when a caller invokes a capability-gated operation on a backend\n * that does not declare the required capability. Capability gating is\n * primarily a compile-time concern (see `BackendCapabilities` and the\n * type-level extension surfaces in `GraphClient<C>`), but this runtime\n * error covers the cases where the type system is bypassed — dynamic\n * registries, `as any` casts, or callers explicitly downcasting through\n * the generic-erased `StorageBackend` shape.\n *\n * The error code is `CAPABILITY_NOT_SUPPORTED`. The message names the\n * missing capability and the backend that was asked, so app code can\n * diagnose without inspecting the cap set itself.\n */\nexport class CapabilityNotSupportedError extends FiregraphError {\n constructor(\n public readonly capability: string,\n backendDescription: string,\n ) {\n super(\n `Capability \"${capability}\" is not supported by ${backendDescription}.`,\n 'CAPABILITY_NOT_SUPPORTED',\n );\n this.name = 'CapabilityNotSupportedError';\n }\n}\n","/**\n * Write-plan helper — flattens partial-update payloads into a list of\n * deep-path operations every backend can execute identically.\n *\n * Background: firegraph used to ship two write semantics that quietly\n * disagreed about depth.\n * - `putNode`/`putEdge` did a full document replace.\n * - `updateNode`/`updateEdge` did a one-level shallow merge: top-level\n * keys were preserved, but nested objects were replaced wholesale.\n *\n * Both behaviours dropped sibling keys silently. The 0.12 contract is that\n * `put*` and `update*` deep-merge by default (sibling keys at any depth\n * survive); `replace*` is the explicit escape hatch.\n *\n * `flattenPatch` walks a partial-update payload and emits one\n * {@link DataPathOp} per terminal value. Plain objects recurse; arrays,\n * primitives, Firestore special types, and tagged firegraph-serialization\n * objects are terminal (replaced as a unit). `undefined` values are\n * skipped; `null` is preserved as a real `null` write; the\n * {@link DELETE_FIELD} sentinel marks a field for removal.\n *\n * The output is deliberately backend-agnostic. Each backend translates ops\n * into its native dialect:\n * - Firestore: dotted field path → `data.a.b.c` for `update()`.\n * - SQLite / DO SQLite: `json_set(data, '$.a.b.c', ?)` /\n * `json_remove(data, '$.a.b.c')`.\n */\n\nimport { isTaggedValue, SERIALIZATION_TAG } from './serialization-tag.js';\n\n// ---------------------------------------------------------------------------\n// Public sentinel\n// ---------------------------------------------------------------------------\n\n/**\n * Sentinel returned by {@link deleteField}. Treated by all backends as\n * \"remove this field from the stored document\".\n *\n * Equivalent to Firestore's `FieldValue.delete()`, but works for SQLite\n * backends too. Use inside `updateNode`/`updateEdge` payloads.\n */\nexport const DELETE_FIELD: unique symbol = Symbol.for('firegraph.deleteField');\nexport type DeleteSentinel = typeof DELETE_FIELD;\n\n/**\n * Returns the firegraph delete sentinel. Place this anywhere in an\n * `updateNode`/`updateEdge` payload to remove the corresponding field.\n *\n * ```ts\n * await client.updateNode('tour', uid, {\n * attrs: { obsoleteFlag: deleteField() },\n * });\n * ```\n */\nexport function deleteField(): DeleteSentinel {\n return DELETE_FIELD;\n}\n\n/** Type guard for the delete sentinel. */\nexport function isDeleteSentinel(value: unknown): value is DeleteSentinel {\n return value === DELETE_FIELD;\n}\n\n// ---------------------------------------------------------------------------\n// Terminal-detection helpers\n// ---------------------------------------------------------------------------\n\nconst FIRESTORE_TERMINAL_CTOR = new Set([\n 'Timestamp',\n 'GeoPoint',\n 'VectorValue',\n 'DocumentReference',\n 'FieldValue',\n 'NumericIncrementTransform',\n 'ArrayUnionTransform',\n 'ArrayRemoveTransform',\n 'ServerTimestampTransform',\n 'DeleteTransform',\n]);\n\n/**\n * Should this value be written as a single terminal op (no recursion)?\n *\n * Plain JS objects (constructor === Object, or no prototype) are recursed.\n * Everything else — arrays, primitives, class instances, Firestore special\n * types, tagged serialization payloads — is terminal.\n */\nexport function isTerminalValue(value: unknown): boolean {\n if (value === null) return true;\n const t = typeof value;\n if (t !== 'object') return true;\n if (Array.isArray(value)) return true;\n // Tagged serialization payloads carry the SERIALIZATION_TAG sentinel and\n // should be persisted whole — never split into per-field ops.\n if (isTaggedValue(value)) return true;\n const proto = Object.getPrototypeOf(value);\n if (proto === null || proto === Object.prototype) return false;\n // Class instances — Firestore types or anything else exotic.\n const ctor = (value as { constructor?: { name?: string } }).constructor;\n if (ctor && typeof ctor.name === 'string' && FIRESTORE_TERMINAL_CTOR.has(ctor.name)) return true;\n // Unknown class instance: treat as terminal. Recursing into a class\n // instance is almost always wrong (Map, Set, Date, Buffer...).\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Core type\n// ---------------------------------------------------------------------------\n\n/**\n * Single terminal write operation produced by {@link flattenPatch}.\n *\n * `path` is a non-empty array of plain object keys. `value` is the value to\n * write; ignored when `delete` is `true`. Arrays / primitives / Firestore\n * special types appear here as whole terminal values.\n */\nexport interface DataPathOp {\n path: readonly string[];\n value: unknown;\n delete: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Path-segment validation\n// ---------------------------------------------------------------------------\n\n/**\n * Object keys that are safe to embed in SQLite `json_set`/`json_remove`\n * paths. The SQLite backend uses an allowlist regex too — keep these in\n * sync (see `JSON_PATH_KEY_RE` in `internal/sqlite-sql.ts` and\n * `cloudflare/sql.ts`).\n *\n * Allows: ASCII letters, digits, `_`, `-`. Must start with a letter or\n * underscore. This rejects keys containing dots, brackets, quotes, or\n * non-ASCII characters that could break path parsing or be used to\n * inject into the path expression.\n */\nconst SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * Mutual-exclusion guard for {@link UpdatePayload}. The two branches of the\n * shape — `dataOps` (deep-merge) and `replaceData` (full replace) — are\n * structurally incompatible: combining them would tell the backend to\n * simultaneously merge AND wipe, and the three backends disagree on which\n * wins. This helper centralises the runtime check so all three backends\n * trip the same error.\n *\n * Imported as a runtime check from `firestore-backend`, `sqlite-sql`, and\n * `cloudflare/sql`. Backend authors implementing the public `StorageBackend`\n * contract should call it too.\n */\nexport function assertUpdatePayloadExclusive(update: {\n dataOps?: unknown;\n replaceData?: unknown;\n}): void {\n if (update.replaceData !== undefined && update.dataOps !== undefined) {\n throw new Error(\n 'firegraph: UpdatePayload cannot specify both `replaceData` and `dataOps`. ' +\n 'Use one or the other — `replaceData` is the migration-write-back form, ' +\n '`dataOps` is the standard partial-update form.',\n );\n }\n}\n\n/**\n * Reject `DELETE_FIELD` sentinels in payloads where field deletion isn't a\n * meaningful operation: full-document replace (`replaceNode`/`replaceEdge`)\n * and the merge-default put surface (`putNode`/`putEdge`).\n *\n * Why both:\n * - In **replace**, the entire `data` field is overwritten. A delete\n * sentinel in that payload either silently disappears (Firestore drops\n * the Symbol during `.set()` serialization) or produces an empty SQLite\n * `json_remove` no-op, depending on backend. Either way the caller's\n * intent — \"remove field X\" — is lost. Use `updateNode` instead.\n * - In **put** (merge mode), behaviour diverges across backends today:\n * SQLite's flattenPatch emits a real delete op, but Firestore's\n * `.set(..., {merge: true})` silently drops the Symbol. Until that's\n * fixed end-to-end, the safest contract is to reject sentinels at the\n * entry point and steer callers to `updateNode`.\n *\n * The walk mirrors `flattenPatch`: plain objects recurse, everything else\n * is terminal. Tagged serialization payloads short-circuit so we don't\n * recurse into the `__firegraph_ser__` envelope.\n */\nexport function assertNoDeleteSentinels(data: unknown, callerLabel: string): void {\n walkForDeleteSentinels(data, [], { kind: 'root' }, ({ path }) => {\n const where = path.length === 0 ? '<root>' : path.map((p) => JSON.stringify(p)).join(' > ');\n throw new Error(\n `firegraph: ${callerLabel} payload contains a deleteField() sentinel at ${where}. ` +\n `deleteField() is only valid inside updateNode/updateEdge — full-data ` +\n `writes (put*, replace*) cannot delete individual fields. Use updateNode ` +\n `with a deleteField() value, or omit the field from the replace payload.`,\n );\n });\n}\n\ntype SentinelParent = { kind: 'root' } | { kind: 'object' } | { kind: 'array'; index: number };\n\nfunction walkForDeleteSentinels(\n node: unknown,\n path: readonly string[],\n parent: SentinelParent,\n visit: (ctx: { path: readonly string[]; parent: SentinelParent }) => void,\n): void {\n if (node === null || node === undefined) return;\n if (isDeleteSentinel(node)) {\n visit({ path, parent });\n return;\n }\n if (typeof node !== 'object') return;\n if (isTaggedValue(node)) return;\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n walkForDeleteSentinels(node[i], [...path, String(i)], { kind: 'array', index: i }, visit);\n }\n return;\n }\n const proto = Object.getPrototypeOf(node);\n if (proto !== null && proto !== Object.prototype) return;\n const obj = node as Record<string, unknown>;\n for (const key of Object.keys(obj)) {\n walkForDeleteSentinels(obj[key], [...path, key], { kind: 'object' }, visit);\n }\n}\n\n/** Throws if any path segment in the patch is unsafe for SQLite paths. */\nexport function assertSafePath(path: readonly string[]): void {\n for (const seg of path) {\n if (!SAFE_KEY_RE.test(seg)) {\n throw new Error(\n `firegraph: unsafe object key ${JSON.stringify(seg)} at path ${path\n .map((p) => JSON.stringify(p))\n .join(' > ')}. Keys used inside update payloads must match ` +\n `/^[A-Za-z_][A-Za-z0-9_-]*$/ so they can be embedded safely in ` +\n `SQLite JSON paths.`,\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// flattenPatch\n// ---------------------------------------------------------------------------\n\n/**\n * Flatten a partial-update payload into a list of terminal {@link DataPathOp}s.\n *\n * Rules:\n * - Plain objects (no prototype or `Object.prototype`) recurse — each\n * key becomes another path segment.\n * - Arrays are terminal: writing `{tags: ['a']}` overwrites the whole\n * `tags` array. Element-wise array merging is intentionally NOT\n * supported — it's almost never what callers actually want, and\n * Firestore `arrayUnion`/`arrayRemove` give precise semantics when\n * they are.\n * - `undefined` values are skipped (no op generated). Use\n * {@link deleteField} if you actually want to remove a field.\n * - `null` is preserved verbatim — emits a terminal op with `value: null`.\n * - {@link DELETE_FIELD} produces an op with `delete: true`.\n * - Firestore special types and tagged serialization payloads are terminal.\n * - Class instances are terminal.\n *\n * Throws if any object key on the recursion path is unsafe (see\n * {@link assertSafePath}).\n */\nexport function flattenPatch(data: Record<string, unknown>): DataPathOp[] {\n const ops: DataPathOp[] = [];\n walk(data, [], ops);\n return ops;\n}\n\nfunction assertNoDeleteSentinelsInArrayValue(\n arr: readonly unknown[],\n arrayPath: readonly string[],\n): void {\n walkForDeleteSentinels(arr, arrayPath, { kind: 'root' }, ({ parent }) => {\n const arrayPathStr =\n arrayPath.length === 0 ? '<root>' : arrayPath.map((p) => JSON.stringify(p)).join(' > ');\n if (parent.kind === 'array') {\n throw new Error(\n `firegraph: deleteField() sentinel at index ${parent.index} inside an array at ` +\n `path ${arrayPathStr}. Arrays are ` +\n `terminal in update payloads (replaced as a unit), so the sentinel ` +\n `would be silently dropped by JSON serialization. To remove the ` +\n `field entirely, pass deleteField() in place of the whole array.`,\n );\n }\n throw new Error(\n `firegraph: deleteField() sentinel inside an array element at ` +\n `path ${arrayPathStr}. ` +\n `Arrays are terminal in update payloads — the sentinel would ` +\n `be silently dropped by JSON serialization.`,\n );\n });\n}\n\nfunction walk(node: unknown, path: string[], out: DataPathOp[]): void {\n // Caller guarantees the root is a plain object; this branch only\n // matters for recursion.\n if (node === undefined) return;\n if (isDeleteSentinel(node)) {\n if (path.length === 0) {\n throw new Error('firegraph: deleteField() cannot be the entire update payload.');\n }\n assertSafePath(path);\n out.push({ path: [...path], value: undefined, delete: true });\n return;\n }\n if (isTerminalValue(node)) {\n if (path.length === 0) {\n // `null` / array / primitive at the root is illegal — patches must\n // describe per-key changes.\n throw new Error(\n 'firegraph: update payload must be a plain object. Got ' +\n (node === null ? 'null' : Array.isArray(node) ? 'array' : typeof node) +\n '.',\n );\n }\n // A DELETE_FIELD sentinel embedded inside an array (which is terminal\n // and replaced as a unit) would silently disappear: JSON.stringify drops\n // Symbols, and Firestore's serializer does likewise. Reject loudly so\n // the divergence between \"user wrote a delete\" and \"field stayed put\"\n // can't happen.\n if (Array.isArray(node)) {\n assertNoDeleteSentinelsInArrayValue(node, path);\n }\n assertSafePath(path);\n out.push({ path: [...path], value: node, delete: false });\n return;\n }\n // Plain object: recurse into its own enumerable keys.\n const obj = node as Record<string, unknown>;\n const keys = Object.keys(obj);\n if (keys.length === 0) {\n // Empty object at non-root: emit terminal op so an empty object can\n // be written explicitly when the caller really wants one. Skip at\n // the root — no-op patches should produce no ops.\n if (path.length > 0) {\n assertSafePath(path);\n out.push({ path: [...path], value: {}, delete: false });\n }\n return;\n }\n for (const key of keys) {\n if (key === SERIALIZATION_TAG) {\n const where = path.length === 0 ? '<root>' : path.map((p) => JSON.stringify(p)).join(' > ');\n throw new Error(\n `firegraph: update payload contains a literal \\`${SERIALIZATION_TAG}\\` key at ` +\n `${where}. That key is reserved for firegraph's serialization envelope and ` +\n `cannot appear on a plain object in user data. Use a different field name, ` +\n `or pass a recognized tagged value through replaceNode/replaceEdge instead.`,\n );\n }\n walk(obj[key], [...path, key], out);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ValidationError,
|
|
10
10
|
assertNoDeleteSentinels,
|
|
11
11
|
flattenPatch
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-TK64DNVK.js";
|
|
13
13
|
|
|
14
14
|
// src/internal/constants.ts
|
|
15
15
|
var NODE_RELATION = "is";
|
|
@@ -1212,6 +1212,15 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1212
1212
|
this.scanProtection = options?.scanProtection ?? "error";
|
|
1213
1213
|
}
|
|
1214
1214
|
scanProtection;
|
|
1215
|
+
/**
|
|
1216
|
+
* Capability set of the underlying backend. Mirrors `backend.capabilities`
|
|
1217
|
+
* verbatim so callers can portability-check (`client.capabilities.has(
|
|
1218
|
+
* 'query.join')`) without reaching for the backend handle. Static for the
|
|
1219
|
+
* lifetime of the client.
|
|
1220
|
+
*/
|
|
1221
|
+
get capabilities() {
|
|
1222
|
+
return this.backend.capabilities;
|
|
1223
|
+
}
|
|
1215
1224
|
// Static mode
|
|
1216
1225
|
staticRegistry;
|
|
1217
1226
|
// Dynamic mode
|
|
@@ -1506,6 +1515,33 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1506
1515
|
return this.applyMigrations(records);
|
|
1507
1516
|
}
|
|
1508
1517
|
// ---------------------------------------------------------------------------
|
|
1518
|
+
// Aggregate query (capability: query.aggregate)
|
|
1519
|
+
// ---------------------------------------------------------------------------
|
|
1520
|
+
async aggregate(params) {
|
|
1521
|
+
if (!this.backend.aggregate) {
|
|
1522
|
+
throw new FiregraphError(
|
|
1523
|
+
"aggregate() is not supported by the current storage backend.",
|
|
1524
|
+
"UNSUPPORTED_OPERATION"
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
const hasAnyFilter = params.aType || params.aUid || params.axbType || params.bType || params.bUid || params.where && params.where.length > 0;
|
|
1528
|
+
if (!hasAnyFilter) {
|
|
1529
|
+
this.checkQuerySafety([], params.allowCollectionScan);
|
|
1530
|
+
const result2 = await this.backend.aggregate(params.aggregates, []);
|
|
1531
|
+
return result2;
|
|
1532
|
+
}
|
|
1533
|
+
const plan = buildEdgeQueryPlan(params);
|
|
1534
|
+
if (plan.strategy === "get") {
|
|
1535
|
+
throw new FiregraphError(
|
|
1536
|
+
"aggregate() requires a query, not a direct document lookup. Omit one of aUid/axbType/bUid to force a query strategy.",
|
|
1537
|
+
"INVALID_QUERY"
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1541
|
+
const result = await this.backend.aggregate(params.aggregates, plan.filters);
|
|
1542
|
+
return result;
|
|
1543
|
+
}
|
|
1544
|
+
// ---------------------------------------------------------------------------
|
|
1509
1545
|
// Bulk operations
|
|
1510
1546
|
// ---------------------------------------------------------------------------
|
|
1511
1547
|
async removeNodeCascade(uid, options) {
|
|
@@ -1515,6 +1551,327 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1515
1551
|
return this.backend.bulkRemoveEdges(params, this, options);
|
|
1516
1552
|
}
|
|
1517
1553
|
// ---------------------------------------------------------------------------
|
|
1554
|
+
// Server-side DML (capability: query.dml)
|
|
1555
|
+
// ---------------------------------------------------------------------------
|
|
1556
|
+
/**
|
|
1557
|
+
* Single-statement bulk DELETE. Translates `params` to a filter list via
|
|
1558
|
+
* `buildEdgeQueryPlan` (the same plan `findEdges` uses) and dispatches to
|
|
1559
|
+
* `backend.bulkDelete`. The fetch-then-delete loop in `bulkRemoveEdges`
|
|
1560
|
+
* is the cap-less fallback; this method is the fast path on backends
|
|
1561
|
+
* declaring `query.dml`.
|
|
1562
|
+
*
|
|
1563
|
+
* Scan-protection rules match `findEdges`: a query with no identifying
|
|
1564
|
+
* fields requires `allowCollectionScan: true` to pass. A bare-empty
|
|
1565
|
+
* filter set (no `aType`, `aUid`, etc., no `where`) is allowed at this
|
|
1566
|
+
* layer — shared SQLite bounds the blast radius via its leading `scope`
|
|
1567
|
+
* predicate — but the DO RPC backend rejects empty filters at the wire
|
|
1568
|
+
* boundary as defense-in-depth. To wipe a routed subgraph DO, use
|
|
1569
|
+
* `removeNodeCascade` on the parent node instead.
|
|
1570
|
+
*/
|
|
1571
|
+
async bulkDelete(params, options) {
|
|
1572
|
+
if (!this.backend.bulkDelete) {
|
|
1573
|
+
throw new FiregraphError(
|
|
1574
|
+
"bulkDelete() is not supported by the current storage backend. Fall back to bulkRemoveEdges() for backends without query.dml (e.g. Firestore Standard).",
|
|
1575
|
+
"UNSUPPORTED_OPERATION"
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
const filters = this.buildDmlFilters(params);
|
|
1579
|
+
return this.backend.bulkDelete(filters, options);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Single-statement bulk UPDATE. Same translation path as `bulkDelete`,
|
|
1583
|
+
* but the patch is deep-merged into each matching row's `data` via the
|
|
1584
|
+
* shared `flattenPatch` pipeline. Identifying columns are immutable
|
|
1585
|
+
* through this path (see `BulkUpdatePatch` JSDoc).
|
|
1586
|
+
*
|
|
1587
|
+
* Empty-patch rejection happens inside the backend (`compileBulkUpdate`)
|
|
1588
|
+
* — a `data: {}` payload would only rewrite `updated_at`, which is
|
|
1589
|
+
* almost certainly a bug.
|
|
1590
|
+
*/
|
|
1591
|
+
async bulkUpdate(params, patch, options) {
|
|
1592
|
+
if (!this.backend.bulkUpdate) {
|
|
1593
|
+
throw new FiregraphError(
|
|
1594
|
+
"bulkUpdate() is not supported by the current storage backend.",
|
|
1595
|
+
"UNSUPPORTED_OPERATION"
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
const filters = this.buildDmlFilters(params);
|
|
1599
|
+
return this.backend.bulkUpdate(filters, patch, options);
|
|
1600
|
+
}
|
|
1601
|
+
// ---------------------------------------------------------------------------
|
|
1602
|
+
// Multi-source fan-out (capability: query.join)
|
|
1603
|
+
// ---------------------------------------------------------------------------
|
|
1604
|
+
/**
|
|
1605
|
+
* Fan out from `params.sources` over a single edge type in one round trip.
|
|
1606
|
+
* On backends without `query.join`, throws `UNSUPPORTED_OPERATION` — the
|
|
1607
|
+
* cap-less fallback is the per-source `findEdges` loop, which lives in
|
|
1608
|
+
* `traverse.ts` (the higher-level traversal walker) rather than here.
|
|
1609
|
+
*
|
|
1610
|
+
* `expand()` is intentionally edge-type-only — the source set is a flat
|
|
1611
|
+
* UID list and the hop matches one `axbType`. Multi-axbType expansions
|
|
1612
|
+
* become multiple `expand()` calls, one per relation.
|
|
1613
|
+
*
|
|
1614
|
+
* `params.sources.length === 0` short-circuits to an empty result. The
|
|
1615
|
+
* backend never sees the call. (`compileExpand` itself rejects empty
|
|
1616
|
+
* because `IN ()` is not valid SQL.)
|
|
1617
|
+
*/
|
|
1618
|
+
async expand(params) {
|
|
1619
|
+
if (!this.backend.expand) {
|
|
1620
|
+
throw new FiregraphError(
|
|
1621
|
+
"expand() is not supported by the current storage backend. Backends without `query.join` can use createTraversal() instead \u2014 the per-hop loop is functionally equivalent (just slower).",
|
|
1622
|
+
"UNSUPPORTED_OPERATION"
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
if (params.sources.length === 0) {
|
|
1626
|
+
return params.hydrate ? { edges: [], targets: [] } : { edges: [] };
|
|
1627
|
+
}
|
|
1628
|
+
return this.backend.expand(params);
|
|
1629
|
+
}
|
|
1630
|
+
// ---------------------------------------------------------------------------
|
|
1631
|
+
// Engine-level multi-hop traversal (capability: traversal.serverSide)
|
|
1632
|
+
// ---------------------------------------------------------------------------
|
|
1633
|
+
/**
|
|
1634
|
+
* Compile a multi-hop traversal spec into one server-side nested
|
|
1635
|
+
* Pipeline and dispatch a single round trip.
|
|
1636
|
+
*
|
|
1637
|
+
* Backends declaring `traversal.serverSide` (Firestore Enterprise
|
|
1638
|
+
* today) install this method; everywhere else, it throws
|
|
1639
|
+
* `UNSUPPORTED_OPERATION`. The capability gate matches the type-level
|
|
1640
|
+
* surface — `GraphClient<C>` only exposes `runEngineTraversal` when
|
|
1641
|
+
* `'traversal.serverSide' extends C`.
|
|
1642
|
+
*
|
|
1643
|
+
* Most callers should not invoke this method directly; the
|
|
1644
|
+
* `createTraversal(...).run()` builder routes through it
|
|
1645
|
+
* automatically when `engineTraversal: 'auto'` (the default) and
|
|
1646
|
+
* the spec is eligible per `firestore-traverse-compiler.ts`. Calling
|
|
1647
|
+
* directly is appropriate for benchmarking or for callers that have
|
|
1648
|
+
* already shaped their hop chain into the strict
|
|
1649
|
+
* `EngineTraversalParams` shape.
|
|
1650
|
+
*
|
|
1651
|
+
* `params.sources.length === 0` short-circuits to empty per-hop
|
|
1652
|
+
* arrays. The backend never sees the call.
|
|
1653
|
+
*/
|
|
1654
|
+
async runEngineTraversal(params) {
|
|
1655
|
+
if (!this.backend.runEngineTraversal) {
|
|
1656
|
+
throw new FiregraphError(
|
|
1657
|
+
"runEngineTraversal() is not supported by the current storage backend. Backends without `traversal.serverSide` can use createTraversal() instead \u2014 the per-hop loop is functionally equivalent for in-graph specs (different round-trip profile).",
|
|
1658
|
+
"UNSUPPORTED_OPERATION"
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
if (params.sources.length === 0) {
|
|
1662
|
+
return {
|
|
1663
|
+
hops: params.hops.map(() => ({ edges: [], sourceCount: 0 })),
|
|
1664
|
+
totalReads: 0
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
return this.backend.runEngineTraversal(params);
|
|
1668
|
+
}
|
|
1669
|
+
// ---------------------------------------------------------------------------
|
|
1670
|
+
// Server-side projection (capability: query.select)
|
|
1671
|
+
// ---------------------------------------------------------------------------
|
|
1672
|
+
/**
|
|
1673
|
+
* Server-side projection — fetch only the requested fields from each
|
|
1674
|
+
* matching edge. The backend translates the call into a projecting query
|
|
1675
|
+
* (`SELECT json_extract(...)` on SQLite/DO, `Query.select(...)` on
|
|
1676
|
+
* Firestore Standard, classic projection on Enterprise) so the wire
|
|
1677
|
+
* payload is reduced to just the requested fields.
|
|
1678
|
+
*
|
|
1679
|
+
* Resolution rules for `select` (mirrored across all backends):
|
|
1680
|
+
*
|
|
1681
|
+
* - Built-in envelope fields (`aType`, `aUid`, `axbType`, `bType`,
|
|
1682
|
+
* `bUid`, `createdAt`, `updatedAt`, `v`) → resolve to the typed
|
|
1683
|
+
* column / Firestore field directly.
|
|
1684
|
+
* - `'data'` literal → returns the whole user payload.
|
|
1685
|
+
* - `'data.<x>'` → explicit nested path, returned at the same shape.
|
|
1686
|
+
* - bare name → rewritten to `data.<name>` (the canonical "give me a
|
|
1687
|
+
* few keys out of the JSON payload" shape).
|
|
1688
|
+
*
|
|
1689
|
+
* Empty `select: []` is rejected with `INVALID_QUERY`. Duplicate entries
|
|
1690
|
+
* are de-duped (first-occurrence order preserved); the result row carries
|
|
1691
|
+
* one slot per unique field.
|
|
1692
|
+
*
|
|
1693
|
+
* Migrations are *not* applied to the result. The caller asked for a
|
|
1694
|
+
* partial shape, and rehydrating it through the migration pipeline would
|
|
1695
|
+
* require synthesising every absent field — see
|
|
1696
|
+
* `StorageBackend.findEdgesProjected` for the rationale.
|
|
1697
|
+
*
|
|
1698
|
+
* Scan protection follows the `findEdges` rules: a query with no
|
|
1699
|
+
* identifying fields requires `allowCollectionScan: true` to pass. The
|
|
1700
|
+
* cap-less fallback would be `findEdges` + JS-side projection, but that
|
|
1701
|
+
* defeats the wire-payload reduction; backends without `query.select`
|
|
1702
|
+
* throw `UNSUPPORTED_OPERATION` rather than silently materialising full
|
|
1703
|
+
* rows.
|
|
1704
|
+
*/
|
|
1705
|
+
async findEdgesProjected(params) {
|
|
1706
|
+
if (!this.backend.findEdgesProjected) {
|
|
1707
|
+
throw new FiregraphError(
|
|
1708
|
+
"findEdgesProjected() is not supported by the current storage backend. There is no client-side fallback because the wire-payload reduction is the entire point of the API \u2014 use findEdges() and project in JS if the backend does not declare `query.select`.",
|
|
1709
|
+
"UNSUPPORTED_OPERATION"
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
if (params.select.length === 0) {
|
|
1713
|
+
throw new FiregraphError(
|
|
1714
|
+
"findEdgesProjected() requires a non-empty `select` list.",
|
|
1715
|
+
"INVALID_QUERY"
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
const plan = buildEdgeQueryPlan(params);
|
|
1719
|
+
let filters;
|
|
1720
|
+
let options;
|
|
1721
|
+
if (plan.strategy === "get") {
|
|
1722
|
+
filters = [
|
|
1723
|
+
{ field: "aUid", op: "==", value: params.aUid },
|
|
1724
|
+
{ field: "axbType", op: "==", value: params.axbType },
|
|
1725
|
+
{ field: "bUid", op: "==", value: params.bUid }
|
|
1726
|
+
];
|
|
1727
|
+
if (params.aType) filters.push({ field: "aType", op: "==", value: params.aType });
|
|
1728
|
+
if (params.bType) filters.push({ field: "bType", op: "==", value: params.bType });
|
|
1729
|
+
options = void 0;
|
|
1730
|
+
} else {
|
|
1731
|
+
filters = plan.filters;
|
|
1732
|
+
options = plan.options;
|
|
1733
|
+
}
|
|
1734
|
+
this.checkQuerySafety(filters, params.allowCollectionScan);
|
|
1735
|
+
const rows = await this.backend.findEdgesProjected(params.select, filters, options);
|
|
1736
|
+
return rows;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Native vector / nearest-neighbour search (capability `search.vector`).
|
|
1740
|
+
*
|
|
1741
|
+
* Resolves to the top-K records by similarity, sorted nearest-first
|
|
1742
|
+
* (`EUCLIDEAN` / `COSINE`) or highest-first (`DOT_PRODUCT`). The wrapper
|
|
1743
|
+
* is intentionally thin: capability check, scan-protection, then forward
|
|
1744
|
+
* `params` verbatim to the backend. All field-path normalisation and
|
|
1745
|
+
* SDK-shape validation lives in the shared
|
|
1746
|
+
* `runFirestoreFindNearest` helper that both Firestore editions call —
|
|
1747
|
+
* keeping it there means the validation surface stays in lockstep with
|
|
1748
|
+
* the SDK call site, regardless of which backend is plugged in.
|
|
1749
|
+
*
|
|
1750
|
+
* Migrations are NOT applied. The vector index walked the raw stored
|
|
1751
|
+
* shape; rehydrating each row through the migration pipeline would
|
|
1752
|
+
* change the candidate set the index already chose. If you need
|
|
1753
|
+
* migrated shape, follow up with `getNode` / `findEdges` on the
|
|
1754
|
+
* returned UIDs — those paths apply migrations normally.
|
|
1755
|
+
*
|
|
1756
|
+
* Scan-protection mirrors `findEdges`: if no identifying filters
|
|
1757
|
+
* (`aType` / `axbType` / `bType`) and no `where` clauses are supplied,
|
|
1758
|
+
* the request must opt in via `allowCollectionScan: true`. The ANN
|
|
1759
|
+
* query still walks the candidate set the WHERE clause produces, so
|
|
1760
|
+
* an unfiltered nearest-neighbour search over a million-row collection
|
|
1761
|
+
* is the same scan trap as an unfiltered `findEdges`.
|
|
1762
|
+
*
|
|
1763
|
+
* Backends without `search.vector` throw `UNSUPPORTED_OPERATION` —
|
|
1764
|
+
* there is no client-side fallback because emulating ANN over the
|
|
1765
|
+
* generic backend surface (`findEdges` + JS-side cosine) doesn't scale
|
|
1766
|
+
* past trivial datasets and would give callers the wrong mental model
|
|
1767
|
+
* about cost.
|
|
1768
|
+
*/
|
|
1769
|
+
async findNearest(params) {
|
|
1770
|
+
if (!this.backend.findNearest) {
|
|
1771
|
+
throw new FiregraphError(
|
|
1772
|
+
"findNearest() is not supported by the current storage backend. Vector search requires a backend that declares `search.vector` (currently Firestore Standard and Enterprise). There is no client-side fallback because emulating ANN on top of the generic backend surface does not scale beyond toy datasets.",
|
|
1773
|
+
"UNSUPPORTED_OPERATION"
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
const filters = [];
|
|
1777
|
+
if (params.aType) filters.push({ field: "aType", op: "==", value: params.aType });
|
|
1778
|
+
if (params.axbType) filters.push({ field: "axbType", op: "==", value: params.axbType });
|
|
1779
|
+
if (params.bType) filters.push({ field: "bType", op: "==", value: params.bType });
|
|
1780
|
+
if (params.where) filters.push(...params.where);
|
|
1781
|
+
this.checkQuerySafety(filters, params.allowCollectionScan);
|
|
1782
|
+
return this.backend.findNearest(params);
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Native full-text search (capability `search.fullText`).
|
|
1786
|
+
*
|
|
1787
|
+
* Returns the top-N records by relevance, ordered by the search
|
|
1788
|
+
* index's score. Only Firestore Enterprise declares this capability
|
|
1789
|
+
* today — the underlying Pipelines `search({ query: documentMatches(...) })`
|
|
1790
|
+
* stage requires Enterprise's FTS index. Standard does not declare
|
|
1791
|
+
* the cap (FTS is an Enterprise-only product feature, not a
|
|
1792
|
+
* typed-API gap), and the SQLite-shaped backends have no native
|
|
1793
|
+
* FTS index. Backends without `search.fullText` throw
|
|
1794
|
+
* `UNSUPPORTED_OPERATION` from this wrapper.
|
|
1795
|
+
*
|
|
1796
|
+
* Scan-protection mirrors `findNearest`: a search with no
|
|
1797
|
+
* identifying filters (`aType` / `axbType` / `bType`) walks every
|
|
1798
|
+
* row the index scored, so the request must opt in via
|
|
1799
|
+
* `allowCollectionScan: true`.
|
|
1800
|
+
*
|
|
1801
|
+
* Migrations are NOT applied. The FTS index walked the raw stored
|
|
1802
|
+
* shape; rehydrating each row through the migration pipeline would
|
|
1803
|
+
* change the candidate set the index already scored. If you need
|
|
1804
|
+
* migrated shape, follow up with `getNode` / `findEdges` on the
|
|
1805
|
+
* returned UIDs.
|
|
1806
|
+
*/
|
|
1807
|
+
async fullTextSearch(params) {
|
|
1808
|
+
if (!this.backend.fullTextSearch) {
|
|
1809
|
+
throw new FiregraphError(
|
|
1810
|
+
"fullTextSearch() is not supported by the current storage backend. Full-text search requires a backend that declares `search.fullText` (currently Firestore Enterprise only \u2014 FTS is an Enterprise product feature). There is no client-side fallback because emulating FTS over the generic backend surface would not scale beyond toy datasets.",
|
|
1811
|
+
"UNSUPPORTED_OPERATION"
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
const filters = [];
|
|
1815
|
+
if (params.aType) filters.push({ field: "aType", op: "==", value: params.aType });
|
|
1816
|
+
if (params.axbType) filters.push({ field: "axbType", op: "==", value: params.axbType });
|
|
1817
|
+
if (params.bType) filters.push({ field: "bType", op: "==", value: params.bType });
|
|
1818
|
+
this.checkQuerySafety(filters, params.allowCollectionScan);
|
|
1819
|
+
return this.backend.fullTextSearch(params);
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Native geospatial distance search (capability `search.geo`).
|
|
1823
|
+
*
|
|
1824
|
+
* Returns rows whose `geoField` lies within `radiusMeters` of
|
|
1825
|
+
* `point`, ordered nearest-first by default. Only Firestore
|
|
1826
|
+
* Enterprise declares this capability — same Enterprise-only
|
|
1827
|
+
* gating as `fullTextSearch`. Backends without `search.geo` throw
|
|
1828
|
+
* `UNSUPPORTED_OPERATION` from this wrapper.
|
|
1829
|
+
*
|
|
1830
|
+
* Scan-protection mirrors `findNearest` and `fullTextSearch`.
|
|
1831
|
+
*
|
|
1832
|
+
* Migrations are NOT applied — same rationale as the other search
|
|
1833
|
+
* extensions.
|
|
1834
|
+
*/
|
|
1835
|
+
async geoSearch(params) {
|
|
1836
|
+
if (!this.backend.geoSearch) {
|
|
1837
|
+
throw new FiregraphError(
|
|
1838
|
+
"geoSearch() is not supported by the current storage backend. Geospatial search requires a backend that declares `search.geo` (currently Firestore Enterprise only \u2014 geo queries are an Enterprise product feature). There is no client-side fallback because emulating geo over the generic backend surface (haversine over `findEdges`) would not scale beyond trivial datasets.",
|
|
1839
|
+
"UNSUPPORTED_OPERATION"
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
const filters = [];
|
|
1843
|
+
if (params.aType) filters.push({ field: "aType", op: "==", value: params.aType });
|
|
1844
|
+
if (params.axbType) filters.push({ field: "axbType", op: "==", value: params.axbType });
|
|
1845
|
+
if (params.bType) filters.push({ field: "bType", op: "==", value: params.bType });
|
|
1846
|
+
this.checkQuerySafety(filters, params.allowCollectionScan);
|
|
1847
|
+
return this.backend.geoSearch(params);
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Translate a `FindEdgesParams` into the `QueryFilter[]` shape the
|
|
1851
|
+
* backend `bulkDelete` / `bulkUpdate` methods expect. Mirrors the
|
|
1852
|
+
* `aggregate()` plan: a bare-empty params object becomes an empty
|
|
1853
|
+
* filter list (after a scan-protection check); a GET-shape (all three
|
|
1854
|
+
* identifiers) is rejected so we never silently turn a single-row
|
|
1855
|
+
* lookup into a server-side DML; otherwise we run `buildEdgeQueryPlan`
|
|
1856
|
+
* and surface its filters.
|
|
1857
|
+
*/
|
|
1858
|
+
buildDmlFilters(params) {
|
|
1859
|
+
const hasAnyFilter = params.aType || params.aUid || params.axbType || params.bType || params.bUid || params.where && params.where.length > 0;
|
|
1860
|
+
if (!hasAnyFilter) {
|
|
1861
|
+
this.checkQuerySafety([], params.allowCollectionScan);
|
|
1862
|
+
return [];
|
|
1863
|
+
}
|
|
1864
|
+
const plan = buildEdgeQueryPlan(params);
|
|
1865
|
+
if (plan.strategy === "get") {
|
|
1866
|
+
throw new FiregraphError(
|
|
1867
|
+
"bulkDelete() / bulkUpdate() require a query, not a direct document lookup. Use removeEdge() / updateEdge() for single-row operations, or omit one of aUid/axbType/bUid to force a query strategy.",
|
|
1868
|
+
"INVALID_QUERY"
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1872
|
+
return plan.filters;
|
|
1873
|
+
}
|
|
1874
|
+
// ---------------------------------------------------------------------------
|
|
1518
1875
|
// Dynamic registry methods
|
|
1519
1876
|
// ---------------------------------------------------------------------------
|
|
1520
1877
|
async defineNodeType(name, jsonSchema, description, options) {
|
|
@@ -1654,21 +2011,10 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1654
2011
|
};
|
|
1655
2012
|
}
|
|
1656
2013
|
};
|
|
1657
|
-
function
|
|
2014
|
+
function createGraphClient(backend, options, metaBackend) {
|
|
1658
2015
|
return new GraphClientImpl(backend, options, metaBackend);
|
|
1659
2016
|
}
|
|
1660
|
-
|
|
1661
|
-
// src/default-indexes.ts
|
|
1662
|
-
var DEFAULT_CORE_INDEXES = Object.freeze([
|
|
1663
|
-
{ fields: ["aUid"] },
|
|
1664
|
-
{ fields: ["bUid"] },
|
|
1665
|
-
{ fields: ["aType"] },
|
|
1666
|
-
{ fields: ["bType"] },
|
|
1667
|
-
{ fields: ["aUid", "axbType"] },
|
|
1668
|
-
{ fields: ["axbType", "bUid"] },
|
|
1669
|
-
{ fields: ["aType", "axbType"] },
|
|
1670
|
-
{ fields: ["axbType", "bType"] }
|
|
1671
|
-
]);
|
|
2017
|
+
var createGraphClientFromBackend = createGraphClient;
|
|
1672
2018
|
|
|
1673
2019
|
// src/id.ts
|
|
1674
2020
|
import { nanoid } from "nanoid";
|
|
@@ -1707,9 +2053,8 @@ export {
|
|
|
1707
2053
|
buildEdgeQueryPlan,
|
|
1708
2054
|
buildNodeQueryPlan,
|
|
1709
2055
|
analyzeQuerySafety,
|
|
1710
|
-
|
|
2056
|
+
createGraphClient,
|
|
1711
2057
|
createGraphClientFromBackend,
|
|
1712
|
-
DEFAULT_CORE_INDEXES,
|
|
1713
2058
|
generateId
|
|
1714
2059
|
};
|
|
1715
|
-
//# sourceMappingURL=chunk-
|
|
2060
|
+
//# sourceMappingURL=chunk-WRTFC5NG.js.map
|