@typicalday/firegraph 0.7.1 → 0.9.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/dist/backend.cjs +222 -0
- package/dist/backend.cjs.map +1 -0
- package/dist/backend.d.cts +121 -0
- package/dist/backend.d.ts +121 -0
- package/dist/backend.js +136 -0
- package/dist/backend.js.map +1 -0
- package/dist/chunk-5753Y42M.js +118 -0
- package/dist/chunk-5753Y42M.js.map +1 -0
- package/dist/chunk-EVUM6ORB.js +1575 -0
- package/dist/chunk-EVUM6ORB.js.map +1 -0
- package/dist/chunk-GLOVWKQH.js +94 -0
- package/dist/chunk-GLOVWKQH.js.map +1 -0
- package/dist/{chunk-KFA7G37W.js → chunk-SU4FNLC3.js} +32 -30
- package/dist/chunk-SU4FNLC3.js.map +1 -0
- package/dist/chunk-SZ6W4VAS.js +701 -0
- package/dist/chunk-SZ6W4VAS.js.map +1 -0
- package/dist/chunk-TYYPRVIE.js +57 -0
- package/dist/chunk-TYYPRVIE.js.map +1 -0
- package/dist/codegen/index.d.cts +25 -1
- package/dist/codegen/index.d.ts +25 -1
- package/dist/d1.cjs +2421 -0
- package/dist/d1.cjs.map +1 -0
- package/dist/d1.d.cts +54 -0
- package/dist/d1.d.ts +54 -0
- package/dist/d1.js +76 -0
- package/dist/d1.js.map +1 -0
- package/dist/do-sqlite.cjs +2424 -0
- package/dist/do-sqlite.cjs.map +1 -0
- package/dist/do-sqlite.d.cts +41 -0
- package/dist/do-sqlite.d.ts +41 -0
- package/dist/do-sqlite.js +79 -0
- package/dist/do-sqlite.js.map +1 -0
- package/dist/editor/client/assets/index-Bq2bfzeY.js +411 -0
- package/dist/editor/client/index.html +1 -1
- package/dist/editor/server/index.mjs +6524 -6355
- package/dist/index.cjs +2881 -2714
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -275
- package/dist/index.d.ts +259 -275
- package/dist/index.js +728 -2304
- package/dist/index.js.map +1 -1
- package/dist/query-client/index.cjs +30 -28
- package/dist/query-client/index.cjs.map +1 -1
- package/dist/query-client/index.d.cts +2 -2
- package/dist/query-client/index.d.ts +2 -2
- package/dist/query-client/index.js +1 -1
- package/dist/react.cjs +0 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +0 -1
- package/dist/react.js.map +1 -1
- package/dist/scope-path-BtajqNK5.d.ts +234 -0
- package/dist/scope-path-D2mNENJ-.d.cts +234 -0
- package/dist/serialization-ZZ7RSDRX.js +13 -0
- package/dist/serialization-ZZ7RSDRX.js.map +1 -0
- package/dist/svelte.cjs +0 -2
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.js +0 -2
- package/dist/svelte.js.map +1 -1
- package/dist/{index-B9aodfYD.d.ts → types-DfWVTsMn.d.cts} +28 -26
- package/dist/{index-B9aodfYD.d.cts → types-DfWVTsMn.d.ts} +28 -26
- package/package.json +35 -1
- package/dist/chunk-KFA7G37W.js.map +0 -1
- package/dist/editor/client/assets/index-tyFcX6qG.js +0 -411
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/routing-backend.ts"],"sourcesContent":["/**\n * Routing `StorageBackend` wrapper.\n *\n * `createRoutingBackend(base, { route })` returns a `StorageBackend` that\n * behaves identically to `base` except for `subgraph(parentUid, name)`:\n * each such call consults the caller-supplied `route` function, and if it\n * returns a non-null `StorageBackend`, that backend is used for the child\n * scope.\n *\n * This is the single seam firegraph ships for splitting a logical graph\n * across multiple physical storage backends — e.g. fanning particular\n * subgraph names out to their own Durable Objects to stay under the 10 GB\n * per-DO limit. The routing policy itself, the RPC protocol, and any\n * live-scope index are left to the caller; firegraph only owns the\n * composition primitive and the invariants that come with it.\n *\n * ## Contract — nested routing\n *\n * Whether `route()` returns a routed backend OR `null` (pass-through), the\n * child returned by `subgraph()` is **always** itself wrapped by the same\n * router. Without that self-wrap, a call chain like\n *\n * ```ts\n * router.subgraph(A, 'memories').subgraph(B, 'context')\n * ```\n *\n * would route the first hop correctly but bypass the router on the second\n * hop (since the routed backend's own `.subgraph()` doesn't know about the\n * caller's policy). Keeping routing active through grandchildren is the\n * load-bearing behaviour; `'continues routing on grandchildren …'` in the\n * unit tests locks it in.\n *\n * ## Contract — `route` is synchronous\n *\n * `.subgraph()` is synchronous in firegraph's public API. Making the\n * routing callback async would require rippling Promises through every\n * client-factory call site. Consequence: `route` can only consult data it\n * already has in hand (DO bindings, naming rules, in-memory caches). If\n * you need \"does this DO exist?\" checks, do them lazily — the first read\n * against the returned backend will surface the failure naturally.\n *\n * ## Contract — cross-backend atomicity is not silently degraded\n *\n * The wrapper's `runTransaction` and `createBatch` delegate to `base` —\n * they run entirely on the base backend. `TransactionBackend` and\n * `BatchBackend` deliberately have no `subgraph()` method, so user code\n * physically cannot open a routed child from inside a transaction\n * callback. Any attempt to bypass that (via `as any` / unchecked casts)\n * should surface as `CrossBackendTransactionError` so app code can catch\n * it cleanly — the error type is part of the public surface.\n *\n * ## Contract — `findEdgesGlobal` is base-scope only\n *\n * When delegated, `findEdgesGlobal` runs against the base backend only.\n * It does **not** fan out to routed children — firegraph has no\n * enumeration index for which routed backends exist. Callers who need\n * cross-shard collection-group queries must maintain their own scope\n * directory and query it directly. This keeps the common case (local\n * analytics inside one DO) fast.\n */\n\nimport { FiregraphError } from '../errors.js';\nimport type {\n BulkOptions,\n BulkResult,\n CascadeResult,\n FindEdgesParams,\n GraphReader,\n QueryFilter,\n QueryOptions,\n StoredGraphRecord,\n} from '../types.js';\nimport type {\n BatchBackend,\n StorageBackend,\n TransactionBackend,\n UpdatePayload,\n WritableRecord,\n} from './backend.js';\n\n/**\n * Context passed to a routing callback when `subgraph(parentUid, name)` is\n * called on a routed backend. All four strings describe the *child* scope\n * the caller is requesting, so the router can key its decision off whichever\n * representation is most convenient:\n *\n * - `parentUid` / `subgraphName` — the arguments just passed to `subgraph()`.\n * - `scopePath` — logical, names-only chain (`'memories'`, `'memories/context'`).\n * This is what `allowedIn` patterns match against.\n * - `storageScope` — the materialized-path form (`'A/memories'`,\n * `'A/memories/B/context'`), suitable for use as a DO name or shard key\n * because it's globally unique within a root graph.\n */\nexport interface RoutingContext {\n parentUid: string;\n subgraphName: string;\n scopePath: string;\n storageScope: string;\n}\n\nexport interface RoutingBackendOptions {\n /**\n * Decide whether a `subgraph(parentUid, name)` call should route to a\n * different backend. Return the target backend to route; return `null`\n * (or `undefined`) to fall through to the wrapped base backend.\n *\n * The returned backend is itself wrapped by the same router so that\n * nested `.subgraph()` calls on the returned child continue to be\n * consulted.\n */\n route: (ctx: RoutingContext) => StorageBackend | null | undefined;\n}\n\nfunction assertValidSubgraphArgs(parentNodeUid: string, name: string): void {\n if (!parentNodeUid || parentNodeUid.includes('/')) {\n throw new FiregraphError(\n `Invalid parentNodeUid for subgraph: \"${parentNodeUid}\". ` +\n 'Must be a non-empty string without \"/\".',\n 'INVALID_SUBGRAPH',\n );\n }\n if (!name || name.includes('/')) {\n throw new FiregraphError(\n `Subgraph name must not contain \"/\" and must be non-empty: got \"${name}\". ` +\n 'Use chained .subgraph() calls for nested subgraphs.',\n 'INVALID_SUBGRAPH',\n );\n }\n}\n\nclass RoutingStorageBackend implements StorageBackend {\n readonly collectionPath: string;\n /**\n * Logical (names-only) scope path for *this* wrapper. Tracked\n * independently of `base.scopePath` because a routed backend returned by\n * `options.route()` typically represents its own physical root and has\n * no knowledge of the caller's logical chain. The wrapper is the\n * authoritative source of the logical scope for routing decisions and\n * for satisfying the `StorageBackend.scopePath` contract surfaced to\n * client code.\n */\n readonly scopePath: string;\n /**\n * Materialized-path form of `scopePath` — interleaved `<uid>/<name>`\n * pairs. Not a property on the underlying `StorageBackend` interface\n * (Firestore doesn't produce one), so we track it ourselves from\n * `.subgraph()` arguments. Root routers start with `''`.\n */\n private readonly storageScope: string;\n /**\n * Conditionally installed in the constructor — only present when the\n * wrapped base backend supports it. Declared as an optional instance\n * property (rather than a prototype method) so `typeof router.findEdgesGlobal\n * === 'function'` reflects the base's capability, matching the optional\n * shape in the `StorageBackend` interface.\n */\n findEdgesGlobal?: StorageBackend['findEdgesGlobal'];\n\n constructor(\n private readonly base: StorageBackend,\n private readonly options: RoutingBackendOptions,\n storageScope: string,\n logicalScopePath: string,\n ) {\n this.collectionPath = base.collectionPath;\n this.scopePath = logicalScopePath;\n this.storageScope = storageScope;\n if (base.findEdgesGlobal) {\n // We deliberately do *not* fan out across routed children: we have no\n // enumeration index for which backends exist. Callers needing\n // cross-shard collection-group queries must maintain their own index.\n this.findEdgesGlobal = (params, collectionName) =>\n base.findEdgesGlobal!(params, collectionName);\n }\n }\n\n // --- Pass-through reads ---\n\n getDoc(docId: string): Promise<StoredGraphRecord | null> {\n return this.base.getDoc(docId);\n }\n\n query(filters: QueryFilter[], options?: QueryOptions): Promise<StoredGraphRecord[]> {\n return this.base.query(filters, options);\n }\n\n // --- Pass-through writes ---\n\n setDoc(docId: string, record: WritableRecord): Promise<void> {\n return this.base.setDoc(docId, record);\n }\n\n updateDoc(docId: string, update: UpdatePayload): Promise<void> {\n return this.base.updateDoc(docId, update);\n }\n\n deleteDoc(docId: string): Promise<void> {\n return this.base.deleteDoc(docId);\n }\n\n // --- Transactions / batches run against the base backend only ---\n\n runTransaction<T>(fn: (tx: TransactionBackend) => Promise<T>): Promise<T> {\n // Transactions cannot span base + routed backends (different DBs /\n // DOs / Firestore projects). `TransactionBackend` has no `subgraph()`\n // method, so the user physically cannot open a routed child from\n // inside the callback — the compiler rejects it. At runtime, all\n // reads/writes are confined to the base backend.\n return this.base.runTransaction(fn);\n }\n\n createBatch(): BatchBackend {\n // Same constraint as transactions: `BatchBackend` has no `subgraph()`\n // so all buffered ops target the base backend. The router itself\n // doesn't need to guard anything here.\n return this.base.createBatch();\n }\n\n // --- Subgraphs: the only method that actually routes ---\n\n subgraph(parentNodeUid: string, name: string): StorageBackend {\n assertValidSubgraphArgs(parentNodeUid, name);\n\n const childScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;\n const childStorageScope = this.storageScope\n ? `${this.storageScope}/${parentNodeUid}/${name}`\n : `${parentNodeUid}/${name}`;\n\n const routed = this.options.route({\n parentUid: parentNodeUid,\n subgraphName: name,\n scopePath: childScopePath,\n storageScope: childStorageScope,\n });\n\n if (routed) {\n // The user returned a different backend. We still wrap it so that\n // further `.subgraph()` calls on the returned child continue to\n // consult the router. The routed backend's own `scopePath` / storage\n // layout is its business — for routing purposes we carry *our*\n // logical view forward (`childScopePath`) so grandchildren see a\n // correct context regardless of what `routed.scopePath` happens to\n // be (typically `''` for a freshly-minted per-DO backend).\n return new RoutingStorageBackend(routed, this.options, childStorageScope, childScopePath);\n }\n\n // No route — delegate to the base backend and keep routing in effect\n // for grandchildren.\n const childBase = this.base.subgraph(parentNodeUid, name);\n return new RoutingStorageBackend(childBase, this.options, childStorageScope, childScopePath);\n }\n\n // --- Bulk operations: delegate, but cascade is base-scope only ---\n\n removeNodeCascade(\n uid: string,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<CascadeResult> {\n // `removeNodeCascade` on the base backend cannot see rows that live\n // in routed child backends — each routed backend is a different\n // physical store. Callers with routed subgraphs under `uid` are\n // responsible for cascading those themselves (see routing.md).\n return this.base.removeNodeCascade(uid, reader, options);\n }\n\n bulkRemoveEdges(\n params: FindEdgesParams,\n reader: GraphReader,\n options?: BulkOptions,\n ): Promise<BulkResult> {\n return this.base.bulkRemoveEdges(params, reader, options);\n }\n\n // --- Collection-group queries are base-scope only ---\n //\n // `findEdgesGlobal` is installed in the constructor *only* when the base\n // backend supports it, so `typeof router.findEdgesGlobal === 'function'`\n // reflects the base's capability — matching the optional shape declared\n // on `StorageBackend`.\n}\n\n/**\n * Wrap a `StorageBackend` so that `subgraph(parentUid, name)` calls can be\n * routed to a different backend based on a user-supplied callback.\n *\n * See the module docstring for the atomicity rules. In short: transactions\n * and batches opened on a routing backend run entirely on the *base*\n * backend — they cannot span routed children, by design.\n *\n * @example\n * ```ts\n * const base = createDOSqliteBackend(ctx.storage, 'fg');\n * const routed = createRoutingBackend(base, {\n * route: ({ subgraphName, storageScope }) => {\n * if (subgraphName !== 'memories') return null;\n * const stub = env.MEMORIES.get(env.MEMORIES.idFromName(storageScope));\n * return createMyRpcBackend(stub); // caller-owned\n * },\n * });\n * const client = createGraphClientFromBackend(routed, { registry });\n * ```\n */\nexport function createRoutingBackend(\n base: StorageBackend,\n options: RoutingBackendOptions,\n): StorageBackend {\n if (typeof options?.route !== 'function') {\n throw new FiregraphError(\n 'createRoutingBackend: `options.route` must be a function.',\n 'INVALID_ARGUMENT',\n );\n }\n return new RoutingStorageBackend(base, options, '', base.scopePath);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/serialization.ts
|
|
2
|
+
import { FieldValue, GeoPoint, Timestamp } from "@google-cloud/firestore";
|
|
3
|
+
var SERIALIZATION_TAG = "__firegraph_ser__";
|
|
4
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set(["Timestamp", "GeoPoint", "VectorValue", "DocumentReference"]);
|
|
5
|
+
var _docRefWarned = false;
|
|
6
|
+
function isTaggedValue(value) {
|
|
7
|
+
if (value === null || typeof value !== "object") return false;
|
|
8
|
+
const tag = value[SERIALIZATION_TAG];
|
|
9
|
+
return typeof tag === "string" && KNOWN_TYPES.has(tag);
|
|
10
|
+
}
|
|
11
|
+
function isTimestamp(value) {
|
|
12
|
+
return value instanceof Timestamp;
|
|
13
|
+
}
|
|
14
|
+
function isGeoPoint(value) {
|
|
15
|
+
return value instanceof GeoPoint;
|
|
16
|
+
}
|
|
17
|
+
function isDocumentReference(value) {
|
|
18
|
+
if (value === null || typeof value !== "object") return false;
|
|
19
|
+
const v = value;
|
|
20
|
+
return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
|
|
21
|
+
}
|
|
22
|
+
function isVectorValue(value) {
|
|
23
|
+
if (value === null || typeof value !== "object") return false;
|
|
24
|
+
const v = value;
|
|
25
|
+
return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
|
|
26
|
+
}
|
|
27
|
+
function serializeFirestoreTypes(data) {
|
|
28
|
+
return serializeValue(data);
|
|
29
|
+
}
|
|
30
|
+
function serializeValue(value) {
|
|
31
|
+
if (value === null || value === void 0) return value;
|
|
32
|
+
if (typeof value !== "object") return value;
|
|
33
|
+
if (isTimestamp(value)) {
|
|
34
|
+
return {
|
|
35
|
+
[SERIALIZATION_TAG]: "Timestamp",
|
|
36
|
+
seconds: value.seconds,
|
|
37
|
+
nanoseconds: value.nanoseconds
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (isGeoPoint(value)) {
|
|
41
|
+
return {
|
|
42
|
+
[SERIALIZATION_TAG]: "GeoPoint",
|
|
43
|
+
latitude: value.latitude,
|
|
44
|
+
longitude: value.longitude
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (isDocumentReference(value)) {
|
|
48
|
+
return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
|
|
49
|
+
}
|
|
50
|
+
if (isVectorValue(value)) {
|
|
51
|
+
const v = value;
|
|
52
|
+
const values = typeof v.toArray === "function" ? v.toArray() : v._values;
|
|
53
|
+
return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value.map(serializeValue);
|
|
57
|
+
}
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const key of Object.keys(value)) {
|
|
60
|
+
result[key] = serializeValue(value[key]);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function deserializeFirestoreTypes(data, db) {
|
|
65
|
+
return deserializeValue(data, db);
|
|
66
|
+
}
|
|
67
|
+
function deserializeValue(value, db) {
|
|
68
|
+
if (value === null || value === void 0) return value;
|
|
69
|
+
if (typeof value !== "object") return value;
|
|
70
|
+
if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
return value.map((v) => deserializeValue(v, db));
|
|
75
|
+
}
|
|
76
|
+
const obj = value;
|
|
77
|
+
if (isTaggedValue(obj)) {
|
|
78
|
+
const tag = obj[SERIALIZATION_TAG];
|
|
79
|
+
switch (tag) {
|
|
80
|
+
case "Timestamp":
|
|
81
|
+
if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
|
|
82
|
+
return new Timestamp(obj.seconds, obj.nanoseconds);
|
|
83
|
+
case "GeoPoint":
|
|
84
|
+
if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
|
|
85
|
+
return new GeoPoint(obj.latitude, obj.longitude);
|
|
86
|
+
case "VectorValue":
|
|
87
|
+
if (!Array.isArray(obj.values)) return obj;
|
|
88
|
+
return FieldValue.vector(obj.values);
|
|
89
|
+
case "DocumentReference":
|
|
90
|
+
if (typeof obj.path !== "string") return obj;
|
|
91
|
+
if (db) {
|
|
92
|
+
return db.doc(obj.path);
|
|
93
|
+
}
|
|
94
|
+
if (!_docRefWarned) {
|
|
95
|
+
_docRefWarned = true;
|
|
96
|
+
console.warn(
|
|
97
|
+
"[firegraph] DocumentReference encountered during migration deserialization but no Firestore instance available. The reference will remain as a tagged object with its path. Enable write-back for full reconstruction."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return obj;
|
|
101
|
+
default:
|
|
102
|
+
return obj;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const result = {};
|
|
106
|
+
for (const key of Object.keys(obj)) {
|
|
107
|
+
result[key] = deserializeValue(obj[key], db);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
SERIALIZATION_TAG,
|
|
114
|
+
isTaggedValue,
|
|
115
|
+
serializeFirestoreTypes,
|
|
116
|
+
deserializeFirestoreTypes
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=chunk-5753Y42M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/serialization.ts"],"sourcesContent":["/**\n * Firestore-aware serialization for the sandbox migration pipeline.\n *\n * Firestore documents can contain special types (Timestamp, GeoPoint,\n * VectorValue, DocumentReference) that don't survive plain JSON\n * round-tripping. This module provides tagged serialization: Firestore\n * types are wrapped in tagged plain objects before JSON marshaling and\n * reconstructed after.\n *\n * Only used by the `defaultExecutor` sandbox path. Static migrations\n * (in-memory functions) receive raw Firestore objects directly.\n */\n\nimport type { DocumentReference, Firestore } from '@google-cloud/firestore';\nimport { FieldValue, GeoPoint, Timestamp } from '@google-cloud/firestore';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Sentinel key used to tag serialized Firestore types. */\nexport const SERIALIZATION_TAG = '__firegraph_ser__' as const;\n\n/** Known discriminator values for tagged types. */\nconst KNOWN_TYPES = new Set(['Timestamp', 'GeoPoint', 'VectorValue', 'DocumentReference']);\n\n// One-time warning for DocumentReference deserialization without db\nlet _docRefWarned = false;\n\n// ---------------------------------------------------------------------------\n// Type guard\n// ---------------------------------------------------------------------------\n\n/** Check if a value is a tagged serialized Firestore type. */\nexport function isTaggedValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const tag = (value as Record<string, unknown>)[SERIALIZATION_TAG];\n return typeof tag === 'string' && KNOWN_TYPES.has(tag);\n}\n\n// ---------------------------------------------------------------------------\n// Detection helpers\n// ---------------------------------------------------------------------------\n\nfunction isTimestamp(value: unknown): value is Timestamp {\n return value instanceof Timestamp;\n}\n\nfunction isGeoPoint(value: unknown): value is GeoPoint {\n return value instanceof GeoPoint;\n}\n\nfunction isDocumentReference(value: unknown): value is DocumentReference {\n // Duck-type check: DocumentReference has path (string) and firestore properties\n if (value === null || typeof value !== 'object') return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.path === 'string' &&\n v.firestore !== undefined &&\n typeof v.id === 'string' &&\n v.constructor?.name === 'DocumentReference'\n );\n}\n\nfunction isVectorValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const v = value as Record<string, unknown>;\n return (\n v.constructor?.name === 'VectorValue' && Array.isArray((v as Record<string, unknown>)._values)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Serialize\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively walk a data object and replace Firestore types with tagged\n * plain objects suitable for JSON serialization.\n *\n * Returns a new object tree — the input is never mutated.\n */\nexport function serializeFirestoreTypes(data: Record<string, unknown>): Record<string, unknown> {\n return serializeValue(data) as Record<string, unknown>;\n}\n\nfunction serializeValue(value: unknown): unknown {\n // Primitives\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n\n // Firestore types (check before generic object/array)\n if (isTimestamp(value)) {\n return {\n [SERIALIZATION_TAG]: 'Timestamp',\n seconds: value.seconds,\n nanoseconds: value.nanoseconds,\n };\n }\n if (isGeoPoint(value)) {\n return {\n [SERIALIZATION_TAG]: 'GeoPoint',\n latitude: value.latitude,\n longitude: value.longitude,\n };\n }\n if (isDocumentReference(value)) {\n return { [SERIALIZATION_TAG]: 'DocumentReference', path: (value as DocumentReference).path };\n }\n if (isVectorValue(value)) {\n // Prefer toArray() (public API) over _values (private internal property)\n const v = value as Record<string, unknown>;\n const values =\n typeof v.toArray === 'function' ? (v.toArray as () => number[])() : (v._values as number[]);\n return { [SERIALIZATION_TAG]: 'VectorValue', values: [...values] };\n }\n\n // Arrays\n if (Array.isArray(value)) {\n return value.map(serializeValue);\n }\n\n // Plain objects — recurse\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>)) {\n result[key] = serializeValue((value as Record<string, unknown>)[key]);\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Deserialize\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively walk a data object and reconstruct Firestore types from\n * tagged plain objects.\n *\n * @param data - The data to deserialize (typically from JSON.parse)\n * @param db - Optional Firestore instance for DocumentReference reconstruction.\n * If not provided, tagged DocumentReferences are left as-is with a one-time warning.\n *\n * Returns a new object tree — the input is never mutated.\n */\nexport function deserializeFirestoreTypes(\n data: Record<string, unknown>,\n db?: Firestore,\n): Record<string, unknown> {\n return deserializeValue(data, db) as Record<string, unknown>;\n}\n\nfunction deserializeValue(value: unknown, db?: Firestore): unknown {\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n\n // Short-circuit for values that are already real Firestore types.\n // This makes deserializeFirestoreTypes idempotent — safe to call on data\n // that has already been deserialized (e.g., write-back after defaultExecutor\n // already reconstructed types, or static migrations that return raw types).\n if (\n isTimestamp(value) ||\n isGeoPoint(value) ||\n isDocumentReference(value) ||\n isVectorValue(value)\n ) {\n return value;\n }\n\n // Arrays\n if (Array.isArray(value)) {\n return value.map((v) => deserializeValue(v, db));\n }\n\n const obj = value as Record<string, unknown>;\n\n // Check for tagged Firestore type\n if (isTaggedValue(obj)) {\n const tag = obj[SERIALIZATION_TAG] as string;\n\n switch (tag) {\n case 'Timestamp':\n // Validate expected fields before reconstruction\n if (typeof obj.seconds !== 'number' || typeof obj.nanoseconds !== 'number') return obj;\n return new Timestamp(obj.seconds, obj.nanoseconds);\n\n case 'GeoPoint':\n if (typeof obj.latitude !== 'number' || typeof obj.longitude !== 'number') return obj;\n return new GeoPoint(obj.latitude, obj.longitude);\n\n case 'VectorValue':\n if (!Array.isArray(obj.values)) return obj;\n return FieldValue.vector(obj.values as number[]);\n\n case 'DocumentReference':\n if (typeof obj.path !== 'string') return obj;\n if (db) {\n return db.doc(obj.path);\n }\n // No db available — leave as tagged object with one-time warning\n if (!_docRefWarned) {\n _docRefWarned = true;\n console.warn(\n '[firegraph] DocumentReference encountered during migration deserialization ' +\n 'but no Firestore instance available. The reference will remain as a tagged ' +\n 'object with its path. Enable write-back for full reconstruction.',\n );\n }\n return obj;\n\n default:\n // Unknown tag — leave as-is (forward compatibility)\n return obj;\n }\n }\n\n // Plain object — recurse\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n result[key] = deserializeValue(obj[key], db);\n }\n return result;\n}\n"],"mappings":"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","names":[]}
|