@typicalday/firegraph 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +355 -78
  2. package/dist/backend-DuvHGgK1.d.cts +1897 -0
  3. package/dist/backend-DuvHGgK1.d.ts +1897 -0
  4. package/dist/backend.cjs +365 -5
  5. package/dist/backend.cjs.map +1 -1
  6. package/dist/backend.d.cts +25 -5
  7. package/dist/backend.d.ts +25 -5
  8. package/dist/backend.js +209 -7
  9. package/dist/backend.js.map +1 -1
  10. package/dist/chunk-2DHMNTV6.js +16 -0
  11. package/dist/chunk-2DHMNTV6.js.map +1 -0
  12. package/dist/chunk-4MMQ5W74.js +288 -0
  13. package/dist/chunk-4MMQ5W74.js.map +1 -0
  14. package/dist/{chunk-5753Y42M.js → chunk-C2QMD7RY.js} +6 -10
  15. package/dist/chunk-C2QMD7RY.js.map +1 -0
  16. package/dist/chunk-D4J7Z4FE.js +67 -0
  17. package/dist/chunk-D4J7Z4FE.js.map +1 -0
  18. package/dist/chunk-EQJUUVFG.js +14 -0
  19. package/dist/chunk-EQJUUVFG.js.map +1 -0
  20. package/dist/chunk-N5HFDWQX.js +23 -0
  21. package/dist/chunk-N5HFDWQX.js.map +1 -0
  22. package/dist/chunk-PAD7WFFU.js +573 -0
  23. package/dist/chunk-PAD7WFFU.js.map +1 -0
  24. package/dist/chunk-TK64DNVK.js +256 -0
  25. package/dist/chunk-TK64DNVK.js.map +1 -0
  26. package/dist/{chunk-NJSOD64C.js → chunk-WRTFC5NG.js} +438 -30
  27. package/dist/chunk-WRTFC5NG.js.map +1 -0
  28. package/dist/client-BKi3vk0Q.d.ts +34 -0
  29. package/dist/client-BrsaXtDV.d.cts +34 -0
  30. package/dist/cloudflare/index.cjs +1386 -74
  31. package/dist/cloudflare/index.cjs.map +1 -1
  32. package/dist/cloudflare/index.d.cts +217 -13
  33. package/dist/cloudflare/index.d.ts +217 -13
  34. package/dist/cloudflare/index.js +639 -180
  35. package/dist/cloudflare/index.js.map +1 -1
  36. package/dist/codegen/index.d.cts +1 -1
  37. package/dist/codegen/index.d.ts +1 -1
  38. package/dist/errors-BRc3I_eH.d.cts +73 -0
  39. package/dist/errors-BRc3I_eH.d.ts +73 -0
  40. package/dist/firestore-enterprise/index.cjs +3877 -0
  41. package/dist/firestore-enterprise/index.cjs.map +1 -0
  42. package/dist/firestore-enterprise/index.d.cts +141 -0
  43. package/dist/firestore-enterprise/index.d.ts +141 -0
  44. package/dist/firestore-enterprise/index.js +985 -0
  45. package/dist/firestore-enterprise/index.js.map +1 -0
  46. package/dist/firestore-standard/index.cjs +3117 -0
  47. package/dist/firestore-standard/index.cjs.map +1 -0
  48. package/dist/firestore-standard/index.d.cts +49 -0
  49. package/dist/firestore-standard/index.d.ts +49 -0
  50. package/dist/firestore-standard/index.js +283 -0
  51. package/dist/firestore-standard/index.js.map +1 -0
  52. package/dist/index.cjs +809 -534
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.d.cts +24 -100
  55. package/dist/index.d.ts +24 -100
  56. package/dist/index.js +184 -531
  57. package/dist/index.js.map +1 -1
  58. package/dist/registry-Bc7h6WTM.d.cts +64 -0
  59. package/dist/registry-C2KUPVZj.d.ts +64 -0
  60. package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.cts} +1 -56
  61. package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.ts} +1 -56
  62. package/dist/{serialization-ZZ7RSDRX.js → serialization-OE2PFZMY.js} +6 -4
  63. package/dist/sqlite/index.cjs +3631 -0
  64. package/dist/sqlite/index.cjs.map +1 -0
  65. package/dist/sqlite/index.d.cts +111 -0
  66. package/dist/sqlite/index.d.ts +111 -0
  67. package/dist/sqlite/index.js +1164 -0
  68. package/dist/sqlite/index.js.map +1 -0
  69. package/package.json +33 -3
  70. package/dist/backend-U-MLShlg.d.ts +0 -97
  71. package/dist/backend-np4gEVhB.d.cts +0 -97
  72. package/dist/chunk-5753Y42M.js.map +0 -1
  73. package/dist/chunk-NJSOD64C.js.map +0 -1
  74. package/dist/chunk-R7CRGYY4.js +0 -94
  75. package/dist/chunk-R7CRGYY4.js.map +0 -1
  76. package/dist/types-BGWxcpI_.d.cts +0 -736
  77. package/dist/types-BGWxcpI_.d.ts +0 -736
  78. /package/dist/{serialization-ZZ7RSDRX.js.map → serialization-OE2PFZMY.js.map} +0 -0
@@ -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"]}
@@ -0,0 +1,256 @@
1
+ import {
2
+ SERIALIZATION_TAG,
3
+ isTaggedValue
4
+ } from "./chunk-EQJUUVFG.js";
5
+
6
+ // src/errors.ts
7
+ var FiregraphError = class extends Error {
8
+ constructor(message, code) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = "FiregraphError";
12
+ }
13
+ };
14
+ var NodeNotFoundError = class extends FiregraphError {
15
+ constructor(uid) {
16
+ super(`Node not found: ${uid}`, "NODE_NOT_FOUND");
17
+ this.name = "NodeNotFoundError";
18
+ }
19
+ };
20
+ var EdgeNotFoundError = class extends FiregraphError {
21
+ constructor(aUid, axbType, bUid) {
22
+ super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, "EDGE_NOT_FOUND");
23
+ this.name = "EdgeNotFoundError";
24
+ }
25
+ };
26
+ var ValidationError = class extends FiregraphError {
27
+ constructor(message, details) {
28
+ super(message, "VALIDATION_ERROR");
29
+ this.details = details;
30
+ this.name = "ValidationError";
31
+ }
32
+ };
33
+ var RegistryViolationError = class extends FiregraphError {
34
+ constructor(aType, axbType, bType) {
35
+ super(`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`, "REGISTRY_VIOLATION");
36
+ this.name = "RegistryViolationError";
37
+ }
38
+ };
39
+ var InvalidQueryError = class extends FiregraphError {
40
+ constructor(message) {
41
+ super(message, "INVALID_QUERY");
42
+ this.name = "InvalidQueryError";
43
+ }
44
+ };
45
+ var TraversalError = class extends FiregraphError {
46
+ constructor(message) {
47
+ super(message, "TRAVERSAL_ERROR");
48
+ this.name = "TraversalError";
49
+ }
50
+ };
51
+ var DynamicRegistryError = class extends FiregraphError {
52
+ constructor(message) {
53
+ super(message, "DYNAMIC_REGISTRY_ERROR");
54
+ this.name = "DynamicRegistryError";
55
+ }
56
+ };
57
+ var QuerySafetyError = class extends FiregraphError {
58
+ constructor(message) {
59
+ super(message, "QUERY_SAFETY");
60
+ this.name = "QuerySafetyError";
61
+ }
62
+ };
63
+ var RegistryScopeError = class extends FiregraphError {
64
+ constructor(aType, axbType, bType, scopePath, allowedIn) {
65
+ super(
66
+ `Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope "${scopePath || "root"}". Allowed in: [${allowedIn.join(", ")}]`,
67
+ "REGISTRY_SCOPE"
68
+ );
69
+ this.name = "RegistryScopeError";
70
+ }
71
+ };
72
+ var MigrationError = class extends FiregraphError {
73
+ constructor(message) {
74
+ super(message, "MIGRATION_ERROR");
75
+ this.name = "MigrationError";
76
+ }
77
+ };
78
+ var CrossBackendTransactionError = class extends FiregraphError {
79
+ constructor(message) {
80
+ super(message, "CROSS_BACKEND_TRANSACTION");
81
+ this.name = "CrossBackendTransactionError";
82
+ }
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
+ };
94
+
95
+ // src/internal/write-plan.ts
96
+ var DELETE_FIELD = /* @__PURE__ */ Symbol.for("firegraph.deleteField");
97
+ function deleteField() {
98
+ return DELETE_FIELD;
99
+ }
100
+ function isDeleteSentinel(value) {
101
+ return value === DELETE_FIELD;
102
+ }
103
+ var FIRESTORE_TERMINAL_CTOR = /* @__PURE__ */ new Set([
104
+ "Timestamp",
105
+ "GeoPoint",
106
+ "VectorValue",
107
+ "DocumentReference",
108
+ "FieldValue",
109
+ "NumericIncrementTransform",
110
+ "ArrayUnionTransform",
111
+ "ArrayRemoveTransform",
112
+ "ServerTimestampTransform",
113
+ "DeleteTransform"
114
+ ]);
115
+ function isTerminalValue(value) {
116
+ if (value === null) return true;
117
+ const t = typeof value;
118
+ if (t !== "object") return true;
119
+ if (Array.isArray(value)) return true;
120
+ if (isTaggedValue(value)) return true;
121
+ const proto = Object.getPrototypeOf(value);
122
+ if (proto === null || proto === Object.prototype) return false;
123
+ const ctor = value.constructor;
124
+ if (ctor && typeof ctor.name === "string" && FIRESTORE_TERMINAL_CTOR.has(ctor.name)) return true;
125
+ return true;
126
+ }
127
+ var SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
128
+ function assertUpdatePayloadExclusive(update) {
129
+ if (update.replaceData !== void 0 && update.dataOps !== void 0) {
130
+ throw new Error(
131
+ "firegraph: UpdatePayload cannot specify both `replaceData` and `dataOps`. Use one or the other \u2014 `replaceData` is the migration-write-back form, `dataOps` is the standard partial-update form."
132
+ );
133
+ }
134
+ }
135
+ function assertNoDeleteSentinels(data, callerLabel) {
136
+ walkForDeleteSentinels(data, [], { kind: "root" }, ({ path }) => {
137
+ const where = path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
138
+ throw new Error(
139
+ `firegraph: ${callerLabel} payload contains a deleteField() sentinel at ${where}. deleteField() is only valid inside updateNode/updateEdge \u2014 full-data writes (put*, replace*) cannot delete individual fields. Use updateNode with a deleteField() value, or omit the field from the replace payload.`
140
+ );
141
+ });
142
+ }
143
+ function walkForDeleteSentinels(node, path, parent, visit) {
144
+ if (node === null || node === void 0) return;
145
+ if (isDeleteSentinel(node)) {
146
+ visit({ path, parent });
147
+ return;
148
+ }
149
+ if (typeof node !== "object") return;
150
+ if (isTaggedValue(node)) return;
151
+ if (Array.isArray(node)) {
152
+ for (let i = 0; i < node.length; i++) {
153
+ walkForDeleteSentinels(node[i], [...path, String(i)], { kind: "array", index: i }, visit);
154
+ }
155
+ return;
156
+ }
157
+ const proto = Object.getPrototypeOf(node);
158
+ if (proto !== null && proto !== Object.prototype) return;
159
+ const obj = node;
160
+ for (const key of Object.keys(obj)) {
161
+ walkForDeleteSentinels(obj[key], [...path, key], { kind: "object" }, visit);
162
+ }
163
+ }
164
+ function assertSafePath(path) {
165
+ for (const seg of path) {
166
+ if (!SAFE_KEY_RE.test(seg)) {
167
+ throw new Error(
168
+ `firegraph: unsafe object key ${JSON.stringify(seg)} at path ${path.map((p) => JSON.stringify(p)).join(" > ")}. Keys used inside update payloads must match /^[A-Za-z_][A-Za-z0-9_-]*$/ so they can be embedded safely in SQLite JSON paths.`
169
+ );
170
+ }
171
+ }
172
+ }
173
+ function flattenPatch(data) {
174
+ const ops = [];
175
+ walk(data, [], ops);
176
+ return ops;
177
+ }
178
+ function assertNoDeleteSentinelsInArrayValue(arr, arrayPath) {
179
+ walkForDeleteSentinels(arr, arrayPath, { kind: "root" }, ({ parent }) => {
180
+ const arrayPathStr = arrayPath.length === 0 ? "<root>" : arrayPath.map((p) => JSON.stringify(p)).join(" > ");
181
+ if (parent.kind === "array") {
182
+ throw new Error(
183
+ `firegraph: deleteField() sentinel at index ${parent.index} inside an array at path ${arrayPathStr}. Arrays are terminal in update payloads (replaced as a unit), so the sentinel would be silently dropped by JSON serialization. To remove the field entirely, pass deleteField() in place of the whole array.`
184
+ );
185
+ }
186
+ throw new Error(
187
+ `firegraph: deleteField() sentinel inside an array element at path ${arrayPathStr}. Arrays are terminal in update payloads \u2014 the sentinel would be silently dropped by JSON serialization.`
188
+ );
189
+ });
190
+ }
191
+ function walk(node, path, out) {
192
+ if (node === void 0) return;
193
+ if (isDeleteSentinel(node)) {
194
+ if (path.length === 0) {
195
+ throw new Error("firegraph: deleteField() cannot be the entire update payload.");
196
+ }
197
+ assertSafePath(path);
198
+ out.push({ path: [...path], value: void 0, delete: true });
199
+ return;
200
+ }
201
+ if (isTerminalValue(node)) {
202
+ if (path.length === 0) {
203
+ throw new Error(
204
+ "firegraph: update payload must be a plain object. Got " + (node === null ? "null" : Array.isArray(node) ? "array" : typeof node) + "."
205
+ );
206
+ }
207
+ if (Array.isArray(node)) {
208
+ assertNoDeleteSentinelsInArrayValue(node, path);
209
+ }
210
+ assertSafePath(path);
211
+ out.push({ path: [...path], value: node, delete: false });
212
+ return;
213
+ }
214
+ const obj = node;
215
+ const keys = Object.keys(obj);
216
+ if (keys.length === 0) {
217
+ if (path.length > 0) {
218
+ assertSafePath(path);
219
+ out.push({ path: [...path], value: {}, delete: false });
220
+ }
221
+ return;
222
+ }
223
+ for (const key of keys) {
224
+ if (key === SERIALIZATION_TAG) {
225
+ const where = path.length === 0 ? "<root>" : path.map((p) => JSON.stringify(p)).join(" > ");
226
+ throw new Error(
227
+ `firegraph: update payload contains a literal \`${SERIALIZATION_TAG}\` key at ${where}. That key is reserved for firegraph's serialization envelope and cannot appear on a plain object in user data. Use a different field name, or pass a recognized tagged value through replaceNode/replaceEdge instead.`
228
+ );
229
+ }
230
+ walk(obj[key], [...path, key], out);
231
+ }
232
+ }
233
+
234
+ export {
235
+ FiregraphError,
236
+ NodeNotFoundError,
237
+ EdgeNotFoundError,
238
+ ValidationError,
239
+ RegistryViolationError,
240
+ InvalidQueryError,
241
+ TraversalError,
242
+ DynamicRegistryError,
243
+ QuerySafetyError,
244
+ RegistryScopeError,
245
+ MigrationError,
246
+ CrossBackendTransactionError,
247
+ CapabilityNotSupportedError,
248
+ DELETE_FIELD,
249
+ deleteField,
250
+ isDeleteSentinel,
251
+ assertUpdatePayloadExclusive,
252
+ assertNoDeleteSentinels,
253
+ assertSafePath,
254
+ flattenPatch
255
+ };
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":";;;;;;AAAO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YACE,SACgB,MAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,oBAAN,cAAgC,eAAe;AAAA,EACpD,YAAY,KAAa;AACvB,UAAM,mBAAmB,GAAG,IAAI,gBAAgB;AAChD,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,oBAAN,cAAgC,eAAe;AAAA,EACpD,YAAY,MAAc,SAAiB,MAAc;AACvD,UAAM,mBAAmB,IAAI,MAAM,OAAO,OAAO,IAAI,IAAI,gBAAgB;AACzE,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,kBAAN,cAA8B,eAAe;AAAA,EAClD,YACE,SACgB,SAChB;AACA,UAAM,SAAS,kBAAkB;AAFjB;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,yBAAN,cAAqC,eAAe;AAAA,EACzD,YAAY,OAAe,SAAiB,OAAe;AACzD,UAAM,yBAAyB,KAAK,OAAO,OAAO,QAAQ,KAAK,KAAK,oBAAoB;AACxF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,oBAAN,cAAgC,eAAe;AAAA,EACpD,YAAY,SAAiB;AAC3B,UAAM,SAAS,eAAe;AAC9B,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,iBAAN,cAA6B,eAAe;AAAA,EACjD,YAAY,SAAiB;AAC3B,UAAM,SAAS,iBAAiB;AAChC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,uBAAN,cAAmC,eAAe;AAAA,EACvD,YAAY,SAAiB;AAC3B,UAAM,SAAS,wBAAwB;AACvC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,mBAAN,cAA+B,eAAe;AAAA,EACnD,YAAY,SAAiB;AAC3B,UAAM,SAAS,cAAc;AAC7B,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAiC,eAAe;AAAA,EACrD,YACE,OACA,SACA,OACA,WACA,WACA;AACA;AAAA,MACE,SAAS,KAAK,OAAO,OAAO,QAAQ,KAAK,8BAA8B,aAAa,MAAM,mBACxE,UAAU,KAAK,IAAI,CAAC;AAAA,MACtC;AAAA,IACF;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,iBAAN,cAA6B,eAAe;AAAA,EACjD,YAAY,SAAiB;AAC3B,UAAM,SAAS,iBAAiB;AAChC,SAAK,OAAO;AAAA,EACd;AACF;AAkBO,IAAM,+BAAN,cAA2C,eAAe;AAAA,EAC/D,YAAY,SAAiB;AAC3B,UAAM,SAAS,2BAA2B;AAC1C,SAAK,OAAO;AAAA,EACd;AACF;AAeO,IAAM,8BAAN,cAA0C,eAAe;AAAA,EAC9D,YACkB,YAChB,oBACA;AACA;AAAA,MACE,eAAe,UAAU,yBAAyB,kBAAkB;AAAA,MACpE;AAAA,IACF;AANgB;AAOhB,SAAK,OAAO;AAAA,EACd;AACF;;;ACnGO,IAAM,eAA8B,uBAAO,IAAI,uBAAuB;AAatE,SAAS,cAA8B;AAC5C,SAAO;AACT;AAGO,SAAS,iBAAiB,OAAyC;AACxE,SAAO,UAAU;AACnB;AAMA,IAAM,0BAA0B,oBAAI,IAAI;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASM,SAAS,gBAAgB,OAAyB;AACvD,MAAI,UAAU,KAAM,QAAO;AAC3B,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,SAAU,QAAO;AAC3B,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AAGjC,MAAI,cAAc,KAAK,EAAG,QAAO;AACjC,QAAM,QAAQ,OAAO,eAAe,KAAK;AACzC,MAAI,UAAU,QAAQ,UAAU,OAAO,UAAW,QAAO;AAEzD,QAAM,OAAQ,MAA8C;AAC5D,MAAI,QAAQ,OAAO,KAAK,SAAS,YAAY,wBAAwB,IAAI,KAAK,IAAI,EAAG,QAAO;AAG5F,SAAO;AACT;AAkCA,IAAM,cAAc;AAcb,SAAS,6BAA6B,QAGpC;AACP,MAAI,OAAO,gBAAgB,UAAa,OAAO,YAAY,QAAW;AACpE,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACF;AAuBO,SAAS,wBAAwB,MAAe,aAA2B;AAChF,yBAAuB,MAAM,CAAC,GAAG,EAAE,MAAM,OAAO,GAAG,CAAC,EAAE,KAAK,MAAM;AAC/D,UAAM,QAAQ,KAAK,WAAW,IAAI,WAAW,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,KAAK;AAC1F,UAAM,IAAI;AAAA,MACR,cAAc,WAAW,iDAAiD,KAAK;AAAA,IAIjF;AAAA,EACF,CAAC;AACH;AAIA,SAAS,uBACP,MACA,MACA,QACA,OACM;AACN,MAAI,SAAS,QAAQ,SAAS,OAAW;AACzC,MAAI,iBAAiB,IAAI,GAAG;AAC1B,UAAM,EAAE,MAAM,OAAO,CAAC;AACtB;AAAA,EACF;AACA,MAAI,OAAO,SAAS,SAAU;AAC9B,MAAI,cAAc,IAAI,EAAG;AACzB,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,6BAAuB,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,SAAS,OAAO,EAAE,GAAG,KAAK;AAAA,IAC1F;AACA;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,eAAe,IAAI;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAO,UAAW;AAClD,QAAM,MAAM;AACZ,aAAW,OAAO,OAAO,KAAK,GAAG,GAAG;AAClC,2BAAuB,IAAI,GAAG,GAAG,CAAC,GAAG,MAAM,GAAG,GAAG,EAAE,MAAM,SAAS,GAAG,KAAK;AAAA,EAC5E;AACF;AAGO,SAAS,eAAe,MAA+B;AAC5D,aAAW,OAAO,MAAM;AACtB,QAAI,CAAC,YAAY,KAAK,GAAG,GAAG;AAC1B,YAAM,IAAI;AAAA,QACR,gCAAgC,KAAK,UAAU,GAAG,CAAC,YAAY,KAC5D,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAC5B,KAAK,KAAK,CAAC;AAAA,MAGhB;AAAA,IACF;AAAA,EACF;AACF;AA2BO,SAAS,aAAa,MAA6C;AACxE,QAAM,MAAoB,CAAC;AAC3B,OAAK,MAAM,CAAC,GAAG,GAAG;AAClB,SAAO;AACT;AAEA,SAAS,oCACP,KACA,WACM;AACN,yBAAuB,KAAK,WAAW,EAAE,MAAM,OAAO,GAAG,CAAC,EAAE,OAAO,MAAM;AACvE,UAAM,eACJ,UAAU,WAAW,IAAI,WAAW,UAAU,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,KAAK;AACxF,QAAI,OAAO,SAAS,SAAS;AAC3B,YAAM,IAAI;AAAA,QACR,8CAA8C,OAAO,KAAK,4BAChD,YAAY;AAAA,MAIxB;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,qEACU,YAAY;AAAA,IAGxB;AAAA,EACF,CAAC;AACH;AAEA,SAAS,KAAK,MAAe,MAAgB,KAAyB;AAGpE,MAAI,SAAS,OAAW;AACxB,MAAI,iBAAiB,IAAI,GAAG;AAC1B,QAAI,KAAK,WAAW,GAAG;AACrB,YAAM,IAAI,MAAM,+DAA+D;AAAA,IACjF;AACA,mBAAe,IAAI;AACnB,QAAI,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,OAAO,QAAW,QAAQ,KAAK,CAAC;AAC5D;AAAA,EACF;AACA,MAAI,gBAAgB,IAAI,GAAG;AACzB,QAAI,KAAK,WAAW,GAAG;AAGrB,YAAM,IAAI;AAAA,QACR,4DACG,SAAS,OAAO,SAAS,MAAM,QAAQ,IAAI,IAAI,UAAU,OAAO,QACjE;AAAA,MACJ;AAAA,IACF;AAMA,QAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,0CAAoC,MAAM,IAAI;AAAA,IAChD;AACA,mBAAe,IAAI;AACnB,QAAI,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,OAAO,MAAM,QAAQ,MAAM,CAAC;AACxD;AAAA,EACF;AAEA,QAAM,MAAM;AACZ,QAAM,OAAO,OAAO,KAAK,GAAG;AAC5B,MAAI,KAAK,WAAW,GAAG;AAIrB,QAAI,KAAK,SAAS,GAAG;AACnB,qBAAe,IAAI;AACnB,UAAI,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,GAAG,QAAQ,MAAM,CAAC;AAAA,IACxD;AACA;AAAA,EACF;AACA,aAAW,OAAO,MAAM;AACtB,QAAI,QAAQ,mBAAmB;AAC7B,YAAM,QAAQ,KAAK,WAAW,IAAI,WAAW,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,KAAK;AAC1F,YAAM,IAAI;AAAA,QACR,kDAAkD,iBAAiB,aAC9D,KAAK;AAAA,MAGZ;AAAA,IACF;AACA,SAAK,IAAI,GAAG,GAAG,CAAC,GAAG,MAAM,GAAG,GAAG,GAAG;AAAA,EACpC;AACF;","names":[]}