@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
package/dist/index.js
CHANGED
|
@@ -1,838 +1,357 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SERIALIZATION_TAG,
|
|
3
|
+
deserializeFirestoreTypes,
|
|
4
|
+
isTaggedValue,
|
|
5
|
+
serializeFirestoreTypes
|
|
6
|
+
} from "./chunk-5753Y42M.js";
|
|
7
|
+
import {
|
|
8
|
+
appendStorageScope,
|
|
9
|
+
isAncestorScopeUid,
|
|
10
|
+
parseStorageScope,
|
|
11
|
+
resolveAncestorScope
|
|
12
|
+
} from "./chunk-TYYPRVIE.js";
|
|
13
|
+
import {
|
|
14
|
+
BOOTSTRAP_ENTRIES,
|
|
15
|
+
DEFAULT_QUERY_LIMIT,
|
|
16
|
+
EDGE_TYPE_SCHEMA,
|
|
17
|
+
GraphClientImpl,
|
|
18
|
+
META_EDGE_TYPE,
|
|
19
|
+
META_NODE_TYPE,
|
|
20
|
+
NODE_RELATION,
|
|
21
|
+
NODE_TYPE_SCHEMA,
|
|
22
|
+
analyzeQuerySafety,
|
|
23
|
+
applyMigrationChain,
|
|
24
|
+
buildEdgeQueryPlan,
|
|
25
|
+
buildNodeQueryPlan,
|
|
26
|
+
compileMigrationFn,
|
|
27
|
+
compileMigrations,
|
|
28
|
+
compileSchema,
|
|
29
|
+
computeEdgeDocId,
|
|
30
|
+
computeNodeDocId,
|
|
31
|
+
createBootstrapRegistry,
|
|
32
|
+
createGraphClientFromBackend,
|
|
33
|
+
createMergedRegistry,
|
|
34
|
+
createRegistry,
|
|
35
|
+
createRegistryFromGraph,
|
|
36
|
+
defaultExecutor,
|
|
37
|
+
destroySandboxWorker,
|
|
38
|
+
generateDeterministicUid,
|
|
39
|
+
jsonSchemaToFieldMeta,
|
|
40
|
+
matchScope,
|
|
41
|
+
matchScopeAny,
|
|
42
|
+
migrateRecord,
|
|
43
|
+
migrateRecords,
|
|
44
|
+
precompileSource,
|
|
45
|
+
validateMigrationChain
|
|
46
|
+
} from "./chunk-EVUM6ORB.js";
|
|
47
|
+
import {
|
|
48
|
+
CrossBackendTransactionError,
|
|
49
|
+
DynamicRegistryError,
|
|
50
|
+
EdgeNotFoundError,
|
|
51
|
+
FiregraphError,
|
|
52
|
+
InvalidQueryError,
|
|
53
|
+
MigrationError,
|
|
54
|
+
NodeNotFoundError,
|
|
55
|
+
QuerySafetyError,
|
|
56
|
+
RegistryScopeError,
|
|
57
|
+
RegistryViolationError,
|
|
58
|
+
TraversalError,
|
|
59
|
+
ValidationError
|
|
60
|
+
} from "./chunk-GLOVWKQH.js";
|
|
1
61
|
import {
|
|
2
62
|
generateTypes
|
|
3
63
|
} from "./chunk-YLGXLEUE.js";
|
|
4
64
|
import {
|
|
5
65
|
QueryClient,
|
|
6
66
|
QueryClientError
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
|
|
9
|
-
// src/client.ts
|
|
10
|
-
import { FieldValue as FieldValue5 } from "@google-cloud/firestore";
|
|
11
|
-
|
|
12
|
-
// src/docid.ts
|
|
13
|
-
import { createHash } from "crypto";
|
|
67
|
+
} from "./chunk-SU4FNLC3.js";
|
|
14
68
|
|
|
15
|
-
// src/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
var BUILTIN_FIELDS = /* @__PURE__ */ new Set([
|
|
19
|
-
"aType",
|
|
20
|
-
"aUid",
|
|
21
|
-
"axbType",
|
|
22
|
-
"bType",
|
|
23
|
-
"bUid",
|
|
24
|
-
"createdAt",
|
|
25
|
-
"updatedAt"
|
|
26
|
-
]);
|
|
27
|
-
var SHARD_SEPARATOR = ":";
|
|
28
|
-
|
|
29
|
-
// src/docid.ts
|
|
30
|
-
function computeNodeDocId(uid) {
|
|
31
|
-
return uid;
|
|
69
|
+
// src/config.ts
|
|
70
|
+
function defineConfig(config) {
|
|
71
|
+
return config;
|
|
32
72
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
function resolveView(resolverConfig, availableViewNames, context) {
|
|
74
|
+
if (!resolverConfig) return "json";
|
|
75
|
+
const available = new Set(availableViewNames);
|
|
76
|
+
if (context) {
|
|
77
|
+
const contextDefault = resolverConfig[context];
|
|
78
|
+
if (contextDefault && available.has(contextDefault)) {
|
|
79
|
+
return contextDefault;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (resolverConfig.default && available.has(resolverConfig.default)) {
|
|
83
|
+
return resolverConfig.default;
|
|
84
|
+
}
|
|
85
|
+
return "json";
|
|
38
86
|
}
|
|
39
87
|
|
|
40
|
-
// src/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
bUid: uid,
|
|
50
|
-
data,
|
|
51
|
-
createdAt: now,
|
|
52
|
-
updatedAt: now
|
|
53
|
-
};
|
|
88
|
+
// src/cross-graph.ts
|
|
89
|
+
function resolveAncestorCollection(collectionPath, uid) {
|
|
90
|
+
const segments = collectionPath.split("/");
|
|
91
|
+
for (let i = 1; i < segments.length; i += 2) {
|
|
92
|
+
if (segments[i] === uid) {
|
|
93
|
+
return segments.slice(0, i).join("/");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
54
97
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
aType,
|
|
59
|
-
aUid,
|
|
60
|
-
axbType,
|
|
61
|
-
bType,
|
|
62
|
-
bUid,
|
|
63
|
-
data,
|
|
64
|
-
createdAt: now,
|
|
65
|
-
updatedAt: now
|
|
66
|
-
};
|
|
98
|
+
function isAncestorUid(collectionPath, uid) {
|
|
99
|
+
return resolveAncestorCollection(collectionPath, uid) !== null;
|
|
67
100
|
}
|
|
68
101
|
|
|
69
|
-
// src/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.name = "FiregraphError";
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
var NodeNotFoundError = class extends FiregraphError {
|
|
78
|
-
constructor(uid) {
|
|
79
|
-
super(`Node not found: ${uid}`, "NODE_NOT_FOUND");
|
|
80
|
-
this.name = "NodeNotFoundError";
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
var EdgeNotFoundError = class extends FiregraphError {
|
|
84
|
-
constructor(aUid, axbType, bUid) {
|
|
85
|
-
super(`Edge not found: ${aUid} -[${axbType}]-> ${bUid}`, "EDGE_NOT_FOUND");
|
|
86
|
-
this.name = "EdgeNotFoundError";
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
var ValidationError = class extends FiregraphError {
|
|
90
|
-
constructor(message, details) {
|
|
91
|
-
super(message, "VALIDATION_ERROR");
|
|
92
|
-
this.details = details;
|
|
93
|
-
this.name = "ValidationError";
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
var RegistryViolationError = class extends FiregraphError {
|
|
97
|
-
constructor(aType, axbType, bType) {
|
|
98
|
-
super(
|
|
99
|
-
`Unregistered triple: (${aType}) -[${axbType}]-> (${bType})`,
|
|
100
|
-
"REGISTRY_VIOLATION"
|
|
101
|
-
);
|
|
102
|
-
this.name = "RegistryViolationError";
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
var InvalidQueryError = class extends FiregraphError {
|
|
106
|
-
constructor(message) {
|
|
107
|
-
super(message, "INVALID_QUERY");
|
|
108
|
-
this.name = "InvalidQueryError";
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
var TraversalError = class extends FiregraphError {
|
|
102
|
+
// src/discover.ts
|
|
103
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
104
|
+
import { createRequire } from "module";
|
|
105
|
+
import { join, resolve } from "path";
|
|
106
|
+
var DiscoveryError = class extends FiregraphError {
|
|
112
107
|
constructor(message) {
|
|
113
|
-
super(message, "
|
|
114
|
-
this.name = "
|
|
108
|
+
super(message, "DISCOVERY_ERROR");
|
|
109
|
+
this.name = "DiscoveryError";
|
|
115
110
|
}
|
|
116
111
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
function readJson(filePath) {
|
|
113
|
+
try {
|
|
114
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
115
|
+
return JSON.parse(raw);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
|
|
118
|
+
throw new DiscoveryError(msg);
|
|
121
119
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
}
|
|
121
|
+
function readJsonIfExists(filePath) {
|
|
122
|
+
if (!existsSync(filePath)) return void 0;
|
|
123
|
+
return readJson(filePath);
|
|
124
|
+
}
|
|
125
|
+
var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
126
|
+
function loadSchema(dir, entityLabel) {
|
|
127
|
+
for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
|
|
128
|
+
const candidate = join(dir, `schema${ext}`);
|
|
129
|
+
if (existsSync(candidate)) {
|
|
130
|
+
return loadSchemaModule(candidate, entityLabel);
|
|
131
|
+
}
|
|
127
132
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
super(
|
|
132
|
-
`Type (${aType}) -[${axbType}]-> (${bType}) is not allowed at scope "${scopePath || "root"}". Allowed in: [${allowedIn.join(", ")}]`,
|
|
133
|
-
"REGISTRY_SCOPE"
|
|
134
|
-
);
|
|
135
|
-
this.name = "RegistryScopeError";
|
|
133
|
+
const jsonPath = join(dir, "schema.json");
|
|
134
|
+
if (existsSync(jsonPath)) {
|
|
135
|
+
return readJson(jsonPath);
|
|
136
136
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
throw new DiscoveryError(
|
|
138
|
+
`Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
var _jiti;
|
|
142
|
+
function getJiti() {
|
|
143
|
+
if (!_jiti) {
|
|
144
|
+
const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
|
|
145
|
+
const esmRequire = createRequire(base);
|
|
146
|
+
const { createJiti } = esmRequire("jiti");
|
|
147
|
+
_jiti = createJiti(base, { interopDefault: true });
|
|
142
148
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (axbType) filters.push({ field: "axbType", op: "==", value: axbType });
|
|
155
|
-
if (bType) filters.push({ field: "bType", op: "==", value: bType });
|
|
156
|
-
if (bUid) filters.push({ field: "bUid", op: "==", value: bUid });
|
|
157
|
-
if (params.where) {
|
|
158
|
-
for (const clause of params.where) {
|
|
159
|
-
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
160
|
-
filters.push({ field, op: clause.op, value: clause.value });
|
|
149
|
+
return _jiti;
|
|
150
|
+
}
|
|
151
|
+
function loadSchemaModule(filePath, entityLabel) {
|
|
152
|
+
try {
|
|
153
|
+
const jiti = getJiti();
|
|
154
|
+
const mod = jiti(filePath);
|
|
155
|
+
const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
156
|
+
if (!schema || typeof schema !== "object") {
|
|
157
|
+
throw new DiscoveryError(
|
|
158
|
+
`Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
|
|
159
|
+
);
|
|
161
160
|
}
|
|
161
|
+
return schema;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err instanceof DiscoveryError) throw err;
|
|
164
|
+
throw new DiscoveryError(
|
|
165
|
+
`Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
|
|
166
|
+
);
|
|
162
167
|
}
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
}
|
|
169
|
+
var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
170
|
+
function findViewsFile(dir) {
|
|
171
|
+
for (const ext of VIEW_EXTENSIONS) {
|
|
172
|
+
const candidate = join(dir, `views${ext}`);
|
|
173
|
+
if (existsSync(candidate)) return candidate;
|
|
165
174
|
}
|
|
166
|
-
|
|
167
|
-
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
175
|
+
return void 0;
|
|
168
176
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
];
|
|
175
|
-
if (params.where) {
|
|
176
|
-
for (const clause of params.where) {
|
|
177
|
-
const field = BUILTIN_FIELDS.has(clause.field) ? clause.field : clause.field.startsWith("data.") ? clause.field : `data.${clause.field}`;
|
|
178
|
-
filters.push({ field, op: clause.op, value: clause.value });
|
|
179
|
-
}
|
|
177
|
+
var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
178
|
+
function findMigrationsFile(dir) {
|
|
179
|
+
for (const ext of MIGRATION_EXTENSIONS) {
|
|
180
|
+
const candidate = join(dir, `migrations${ext}`);
|
|
181
|
+
if (existsSync(candidate)) return candidate;
|
|
180
182
|
}
|
|
181
|
-
|
|
182
|
-
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
183
|
+
return void 0;
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return snap.data();
|
|
194
|
-
},
|
|
195
|
-
async setDoc(docId, data) {
|
|
196
|
-
await collectionRef.doc(docId).set(data);
|
|
197
|
-
},
|
|
198
|
-
async updateDoc(docId, data) {
|
|
199
|
-
await collectionRef.doc(docId).update(data);
|
|
200
|
-
},
|
|
201
|
-
async deleteDoc(docId) {
|
|
202
|
-
await collectionRef.doc(docId).delete();
|
|
203
|
-
},
|
|
204
|
-
async query(filters, options) {
|
|
205
|
-
let q = collectionRef;
|
|
206
|
-
for (const f of filters) {
|
|
207
|
-
q = q.where(f.field, f.op, f.value);
|
|
208
|
-
}
|
|
209
|
-
if (options?.orderBy) {
|
|
210
|
-
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
211
|
-
}
|
|
212
|
-
if (options?.limit !== void 0) {
|
|
213
|
-
q = q.limit(options.limit);
|
|
214
|
-
}
|
|
215
|
-
const snap = await q.get();
|
|
216
|
-
return snap.docs.map((doc) => doc.data());
|
|
185
|
+
function loadMigrations(filePath, entityLabel) {
|
|
186
|
+
try {
|
|
187
|
+
const jiti = getJiti();
|
|
188
|
+
const mod = jiti(filePath);
|
|
189
|
+
const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
190
|
+
if (!Array.isArray(migrations)) {
|
|
191
|
+
throw new DiscoveryError(
|
|
192
|
+
`Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
|
|
193
|
+
);
|
|
217
194
|
}
|
|
218
|
-
|
|
195
|
+
return migrations;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err instanceof DiscoveryError) throw err;
|
|
198
|
+
throw new DiscoveryError(
|
|
199
|
+
`Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
219
202
|
}
|
|
220
|
-
function
|
|
221
|
-
const
|
|
203
|
+
function loadNodeEntity(dir, name) {
|
|
204
|
+
const schema = loadSchema(dir, `node type "${name}"`);
|
|
205
|
+
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
206
|
+
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
207
|
+
const viewsPath = findViewsFile(dir);
|
|
208
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
209
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
|
|
222
210
|
return {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
tx.delete(collectionRef.doc(docId));
|
|
236
|
-
},
|
|
237
|
-
async query(filters, options) {
|
|
238
|
-
let q = collectionRef;
|
|
239
|
-
for (const f of filters) {
|
|
240
|
-
q = q.where(f.field, f.op, f.value);
|
|
241
|
-
}
|
|
242
|
-
if (options?.orderBy) {
|
|
243
|
-
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
244
|
-
}
|
|
245
|
-
if (options?.limit !== void 0) {
|
|
246
|
-
q = q.limit(options.limit);
|
|
247
|
-
}
|
|
248
|
-
const snap = await tx.get(q);
|
|
249
|
-
return snap.docs.map((doc) => doc.data());
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
function createBatchAdapter(db, collectionPath) {
|
|
254
|
-
const collectionRef = db.collection(collectionPath);
|
|
255
|
-
const batch = db.batch();
|
|
256
|
-
return {
|
|
257
|
-
setDoc(docId, data) {
|
|
258
|
-
batch.set(collectionRef.doc(docId), data);
|
|
259
|
-
},
|
|
260
|
-
updateDoc(docId, data) {
|
|
261
|
-
batch.update(collectionRef.doc(docId), data);
|
|
262
|
-
},
|
|
263
|
-
deleteDoc(docId) {
|
|
264
|
-
batch.delete(collectionRef.doc(docId));
|
|
265
|
-
},
|
|
266
|
-
async commit() {
|
|
267
|
-
await batch.commit();
|
|
268
|
-
}
|
|
211
|
+
kind: "node",
|
|
212
|
+
name,
|
|
213
|
+
schema,
|
|
214
|
+
description: meta?.description,
|
|
215
|
+
titleField: meta?.titleField,
|
|
216
|
+
subtitleField: meta?.subtitleField,
|
|
217
|
+
viewDefaults: meta?.viewDefaults,
|
|
218
|
+
viewsPath,
|
|
219
|
+
sampleData,
|
|
220
|
+
allowedIn: meta?.allowedIn,
|
|
221
|
+
migrations,
|
|
222
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
269
223
|
};
|
|
270
224
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
225
|
+
function loadEdgeEntity(dir, name) {
|
|
226
|
+
const schema = loadSchema(dir, `edge type "${name}"`);
|
|
227
|
+
const edgePath = join(dir, "edge.json");
|
|
228
|
+
if (!existsSync(edgePath)) {
|
|
229
|
+
throw new DiscoveryError(
|
|
230
|
+
`Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
|
|
231
|
+
);
|
|
278
232
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const { field: fieldName, op, value } = filter;
|
|
283
|
-
switch (op) {
|
|
284
|
-
case "==":
|
|
285
|
-
return P.equal(fieldName, value);
|
|
286
|
-
case "!=":
|
|
287
|
-
return P.notEqual(fieldName, value);
|
|
288
|
-
case "<":
|
|
289
|
-
return P.lessThan(fieldName, value);
|
|
290
|
-
case "<=":
|
|
291
|
-
return P.lessThanOrEqual(fieldName, value);
|
|
292
|
-
case ">":
|
|
293
|
-
return P.greaterThan(fieldName, value);
|
|
294
|
-
case ">=":
|
|
295
|
-
return P.greaterThanOrEqual(fieldName, value);
|
|
296
|
-
case "in":
|
|
297
|
-
return P.equalAny(fieldName, value);
|
|
298
|
-
case "not-in":
|
|
299
|
-
return P.notEqualAny(fieldName, value);
|
|
300
|
-
case "array-contains":
|
|
301
|
-
return P.arrayContains(fieldName, value);
|
|
302
|
-
case "array-contains-any":
|
|
303
|
-
return P.arrayContainsAny(fieldName, value);
|
|
304
|
-
default:
|
|
305
|
-
throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
|
|
233
|
+
const topology = readJson(edgePath);
|
|
234
|
+
if (!topology.from) {
|
|
235
|
+
throw new DiscoveryError(`edge.json for "${name}" is missing required "from" field`);
|
|
306
236
|
}
|
|
307
|
-
|
|
308
|
-
|
|
237
|
+
if (!topology.to) {
|
|
238
|
+
throw new DiscoveryError(`edge.json for "${name}" is missing required "to" field`);
|
|
239
|
+
}
|
|
240
|
+
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
241
|
+
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
242
|
+
const viewsPath = findViewsFile(dir);
|
|
243
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
244
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
|
|
309
245
|
return {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (options?.limit !== void 0) {
|
|
325
|
-
pipeline = pipeline.limit(options.limit);
|
|
326
|
-
}
|
|
327
|
-
const snap = await pipeline.execute();
|
|
328
|
-
return snap.results.map((r) => r.data());
|
|
329
|
-
}
|
|
246
|
+
kind: "edge",
|
|
247
|
+
name,
|
|
248
|
+
schema,
|
|
249
|
+
topology,
|
|
250
|
+
description: meta?.description,
|
|
251
|
+
titleField: meta?.titleField,
|
|
252
|
+
subtitleField: meta?.subtitleField,
|
|
253
|
+
viewDefaults: meta?.viewDefaults,
|
|
254
|
+
viewsPath,
|
|
255
|
+
sampleData,
|
|
256
|
+
allowedIn: meta?.allowedIn,
|
|
257
|
+
targetGraph: topology.targetGraph ?? meta?.targetGraph,
|
|
258
|
+
migrations,
|
|
259
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
330
260
|
};
|
|
331
261
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
/* @__PURE__ */ new Set(["aType", "axbType"]),
|
|
341
|
-
/* @__PURE__ */ new Set(["axbType", "bType"])
|
|
342
|
-
];
|
|
343
|
-
function analyzeQuerySafety(filters) {
|
|
344
|
-
const builtinFieldsPresent = /* @__PURE__ */ new Set();
|
|
345
|
-
let hasDataFilters = false;
|
|
346
|
-
for (const f of filters) {
|
|
347
|
-
if (BUILTIN_FIELDS.has(f.field)) {
|
|
348
|
-
builtinFieldsPresent.add(f.field);
|
|
349
|
-
} else {
|
|
350
|
-
hasDataFilters = true;
|
|
351
|
-
}
|
|
262
|
+
function getSubdirectories(dir) {
|
|
263
|
+
if (!existsSync(dir)) return [];
|
|
264
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
265
|
+
}
|
|
266
|
+
function discoverEntities(entitiesDir) {
|
|
267
|
+
const absDir = resolve(entitiesDir);
|
|
268
|
+
if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
|
|
269
|
+
throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
|
|
352
270
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (matched) {
|
|
362
|
-
return { safe: true };
|
|
363
|
-
}
|
|
271
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
272
|
+
const edges = /* @__PURE__ */ new Map();
|
|
273
|
+
const warnings = [];
|
|
274
|
+
const nodesDir = join(absDir, "nodes");
|
|
275
|
+
for (const name of getSubdirectories(nodesDir)) {
|
|
276
|
+
nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
|
|
364
277
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
safe: false,
|
|
369
|
-
reason: "Query filters only use data.* fields with no builtin field constraints. This requires a full collection scan. Add aType, aUid, axbType, bType, or bUid filters, or set allowCollectionScan: true."
|
|
370
|
-
};
|
|
278
|
+
const edgesDir = join(absDir, "edges");
|
|
279
|
+
for (const name of getSubdirectories(edgesDir)) {
|
|
280
|
+
edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
|
|
371
281
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
282
|
+
const nodeNames = new Set(nodes.keys());
|
|
283
|
+
for (const [axbType, entity] of edges) {
|
|
284
|
+
const topology = entity.topology;
|
|
285
|
+
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
286
|
+
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
287
|
+
for (const ref of [...fromTypes, ...toTypes]) {
|
|
288
|
+
if (!nodeNames.has(ref)) {
|
|
289
|
+
warnings.push({
|
|
290
|
+
code: "DANGLING_TOPOLOGY_REF",
|
|
291
|
+
message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
377
295
|
}
|
|
378
296
|
return {
|
|
379
|
-
|
|
380
|
-
|
|
297
|
+
result: { nodes, edges },
|
|
298
|
+
warnings
|
|
381
299
|
};
|
|
382
300
|
}
|
|
383
301
|
|
|
384
|
-
// src/
|
|
385
|
-
import {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
var
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return
|
|
393
|
-
}
|
|
394
|
-
function isTimestamp(value) {
|
|
395
|
-
return value instanceof Timestamp;
|
|
396
|
-
}
|
|
397
|
-
function isGeoPoint(value) {
|
|
398
|
-
return value instanceof GeoPoint;
|
|
399
|
-
}
|
|
400
|
-
function isDocumentReference(value) {
|
|
401
|
-
if (value === null || typeof value !== "object") return false;
|
|
402
|
-
const v = value;
|
|
403
|
-
return typeof v.path === "string" && v.firestore !== void 0 && typeof v.id === "string" && v.constructor?.name === "DocumentReference";
|
|
404
|
-
}
|
|
405
|
-
function isVectorValue(value) {
|
|
406
|
-
if (value === null || typeof value !== "object") return false;
|
|
407
|
-
const v = value;
|
|
408
|
-
return v.constructor?.name === "VectorValue" && Array.isArray(v._values);
|
|
409
|
-
}
|
|
410
|
-
function serializeFirestoreTypes(data) {
|
|
411
|
-
return serializeValue(data);
|
|
302
|
+
// src/internal/firestore-backend.ts
|
|
303
|
+
import { FieldValue } from "@google-cloud/firestore";
|
|
304
|
+
|
|
305
|
+
// src/bulk.ts
|
|
306
|
+
var MAX_BATCH_SIZE = 500;
|
|
307
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
308
|
+
var BASE_DELAY_MS = 200;
|
|
309
|
+
function sleep(ms) {
|
|
310
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
412
311
|
}
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return { [SERIALIZATION_TAG]: "Timestamp", seconds: value.seconds, nanoseconds: value.nanoseconds };
|
|
418
|
-
}
|
|
419
|
-
if (isGeoPoint(value)) {
|
|
420
|
-
return { [SERIALIZATION_TAG]: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
|
|
421
|
-
}
|
|
422
|
-
if (isDocumentReference(value)) {
|
|
423
|
-
return { [SERIALIZATION_TAG]: "DocumentReference", path: value.path };
|
|
424
|
-
}
|
|
425
|
-
if (isVectorValue(value)) {
|
|
426
|
-
const v = value;
|
|
427
|
-
const values = typeof v.toArray === "function" ? v.toArray() : v._values;
|
|
428
|
-
return { [SERIALIZATION_TAG]: "VectorValue", values: [...values] };
|
|
429
|
-
}
|
|
430
|
-
if (Array.isArray(value)) {
|
|
431
|
-
return value.map(serializeValue);
|
|
432
|
-
}
|
|
433
|
-
const result = {};
|
|
434
|
-
for (const key of Object.keys(value)) {
|
|
435
|
-
result[key] = serializeValue(value[key]);
|
|
312
|
+
function chunk(arr, size) {
|
|
313
|
+
const chunks = [];
|
|
314
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
315
|
+
chunks.push(arr.slice(i, i + size));
|
|
436
316
|
}
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
function deserializeFirestoreTypes(data, db) {
|
|
440
|
-
return deserializeValue(data, db);
|
|
317
|
+
return chunks;
|
|
441
318
|
}
|
|
442
|
-
function
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
if (isTimestamp(value) || isGeoPoint(value) || isDocumentReference(value) || isVectorValue(value)) {
|
|
446
|
-
return value;
|
|
447
|
-
}
|
|
448
|
-
if (Array.isArray(value)) {
|
|
449
|
-
return value.map((v) => deserializeValue(v, db));
|
|
450
|
-
}
|
|
451
|
-
const obj = value;
|
|
452
|
-
if (isTaggedValue(obj)) {
|
|
453
|
-
const tag = obj[SERIALIZATION_TAG];
|
|
454
|
-
switch (tag) {
|
|
455
|
-
case "Timestamp":
|
|
456
|
-
if (typeof obj.seconds !== "number" || typeof obj.nanoseconds !== "number") return obj;
|
|
457
|
-
return new Timestamp(obj.seconds, obj.nanoseconds);
|
|
458
|
-
case "GeoPoint":
|
|
459
|
-
if (typeof obj.latitude !== "number" || typeof obj.longitude !== "number") return obj;
|
|
460
|
-
return new GeoPoint(obj.latitude, obj.longitude);
|
|
461
|
-
case "VectorValue":
|
|
462
|
-
if (!Array.isArray(obj.values)) return obj;
|
|
463
|
-
return FieldValue2.vector(obj.values);
|
|
464
|
-
case "DocumentReference":
|
|
465
|
-
if (typeof obj.path !== "string") return obj;
|
|
466
|
-
if (db) {
|
|
467
|
-
return db.doc(obj.path);
|
|
468
|
-
}
|
|
469
|
-
if (!_docRefWarned) {
|
|
470
|
-
_docRefWarned = true;
|
|
471
|
-
console.warn(
|
|
472
|
-
"[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."
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
return obj;
|
|
476
|
-
default:
|
|
477
|
-
return obj;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const result = {};
|
|
481
|
-
for (const key of Object.keys(obj)) {
|
|
482
|
-
result[key] = deserializeValue(obj[key], db);
|
|
319
|
+
async function bulkDeleteDocIds(db, collectionPath, docIds, options) {
|
|
320
|
+
if (docIds.length === 0) {
|
|
321
|
+
return { deleted: 0, batches: 0, errors: [] };
|
|
483
322
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
let
|
|
491
|
-
let
|
|
492
|
-
|
|
493
|
-
|
|
323
|
+
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
324
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
325
|
+
const onProgress = options?.onProgress;
|
|
326
|
+
const chunks = chunk(docIds, batchSize);
|
|
327
|
+
const errors = [];
|
|
328
|
+
let deleted = 0;
|
|
329
|
+
let completedBatches = 0;
|
|
330
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
331
|
+
const ids = chunks[i];
|
|
332
|
+
let committed = false;
|
|
333
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
494
334
|
try {
|
|
495
|
-
|
|
335
|
+
const batch = db.batch();
|
|
336
|
+
const collectionRef = db.collection(collectionPath);
|
|
337
|
+
for (const id of ids) {
|
|
338
|
+
batch.delete(collectionRef.doc(id));
|
|
339
|
+
}
|
|
340
|
+
await batch.commit();
|
|
341
|
+
committed = true;
|
|
342
|
+
deleted += ids.length;
|
|
343
|
+
break;
|
|
496
344
|
} catch (err) {
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
version = step.toVersion;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if (version !== targetVersion) {
|
|
511
|
-
throw new MigrationError(
|
|
512
|
-
`Incomplete migration chain: reached v${version} but target is v${targetVersion}`
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
return result;
|
|
516
|
-
}
|
|
517
|
-
function validateMigrationChain(migrations, label) {
|
|
518
|
-
if (migrations.length === 0) return;
|
|
519
|
-
const seen = /* @__PURE__ */ new Set();
|
|
520
|
-
for (const step of migrations) {
|
|
521
|
-
if (step.toVersion <= step.fromVersion) {
|
|
522
|
-
throw new MigrationError(
|
|
523
|
-
`${label}: migration step has toVersion (${step.toVersion}) <= fromVersion (${step.fromVersion})`
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
if (seen.has(step.fromVersion)) {
|
|
527
|
-
throw new MigrationError(
|
|
528
|
-
`${label}: duplicate migration step for fromVersion ${step.fromVersion}`
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
seen.add(step.fromVersion);
|
|
532
|
-
}
|
|
533
|
-
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
534
|
-
const targetVersion = Math.max(...migrations.map((m) => m.toVersion));
|
|
535
|
-
let version = 0;
|
|
536
|
-
for (const step of sorted) {
|
|
537
|
-
if (step.fromVersion === version) {
|
|
538
|
-
version = step.toVersion;
|
|
539
|
-
} else if (step.fromVersion > version) {
|
|
540
|
-
throw new MigrationError(
|
|
541
|
-
`${label}: migration chain has a gap \u2014 no step covers v${version} \u2192 v${step.fromVersion}`
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
if (version !== targetVersion) {
|
|
546
|
-
throw new MigrationError(
|
|
547
|
-
`${label}: migration chain does not reach v${targetVersion} (stuck at v${version})`
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
async function migrateRecord(record, registry, globalWriteBack = "off") {
|
|
552
|
-
const entry = registry.lookup(record.aType, record.axbType, record.bType);
|
|
553
|
-
if (!entry?.migrations?.length || !entry.schemaVersion) {
|
|
554
|
-
return { record, migrated: false, writeBack: "off" };
|
|
555
|
-
}
|
|
556
|
-
const currentVersion = record.v ?? 0;
|
|
557
|
-
if (currentVersion >= entry.schemaVersion) {
|
|
558
|
-
return { record, migrated: false, writeBack: "off" };
|
|
559
|
-
}
|
|
560
|
-
const migratedData = await applyMigrationChain(
|
|
561
|
-
record.data,
|
|
562
|
-
currentVersion,
|
|
563
|
-
entry.schemaVersion,
|
|
564
|
-
entry.migrations
|
|
565
|
-
);
|
|
566
|
-
const writeBack = entry.migrationWriteBack ?? globalWriteBack ?? "off";
|
|
567
|
-
return {
|
|
568
|
-
record: { ...record, data: migratedData, v: entry.schemaVersion },
|
|
569
|
-
migrated: true,
|
|
570
|
-
writeBack
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
async function migrateRecords(records, registry, globalWriteBack = "off") {
|
|
574
|
-
return Promise.all(
|
|
575
|
-
records.map((r) => migrateRecord(r, registry, globalWriteBack))
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// src/transaction.ts
|
|
580
|
-
var GraphTransactionImpl = class {
|
|
581
|
-
constructor(adapter, registry, scanProtection = "error", scopePath = "", globalWriteBack = "off", db) {
|
|
582
|
-
this.adapter = adapter;
|
|
583
|
-
this.registry = registry;
|
|
584
|
-
this.scanProtection = scanProtection;
|
|
585
|
-
this.scopePath = scopePath;
|
|
586
|
-
this.globalWriteBack = globalWriteBack;
|
|
587
|
-
this.db = db;
|
|
588
|
-
}
|
|
589
|
-
async getNode(uid) {
|
|
590
|
-
const docId = computeNodeDocId(uid);
|
|
591
|
-
const record = await this.adapter.getDoc(docId);
|
|
592
|
-
if (!record || !this.registry) return record;
|
|
593
|
-
const result = await migrateRecord(record, this.registry, this.globalWriteBack);
|
|
594
|
-
if (result.migrated && result.writeBack !== "off") {
|
|
595
|
-
const update = {
|
|
596
|
-
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
597
|
-
updatedAt: FieldValue3.serverTimestamp()
|
|
598
|
-
};
|
|
599
|
-
if (result.record.v !== void 0) {
|
|
600
|
-
update.v = result.record.v;
|
|
601
|
-
}
|
|
602
|
-
this.adapter.updateDoc(docId, update);
|
|
603
|
-
}
|
|
604
|
-
return result.record;
|
|
605
|
-
}
|
|
606
|
-
async getEdge(aUid, axbType, bUid) {
|
|
607
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
608
|
-
const record = await this.adapter.getDoc(docId);
|
|
609
|
-
if (!record || !this.registry) return record;
|
|
610
|
-
const result = await migrateRecord(record, this.registry, this.globalWriteBack);
|
|
611
|
-
if (result.migrated && result.writeBack !== "off") {
|
|
612
|
-
const update = {
|
|
613
|
-
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
614
|
-
updatedAt: FieldValue3.serverTimestamp()
|
|
615
|
-
};
|
|
616
|
-
if (result.record.v !== void 0) {
|
|
617
|
-
update.v = result.record.v;
|
|
618
|
-
}
|
|
619
|
-
this.adapter.updateDoc(docId, update);
|
|
620
|
-
}
|
|
621
|
-
return result.record;
|
|
622
|
-
}
|
|
623
|
-
async edgeExists(aUid, axbType, bUid) {
|
|
624
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
625
|
-
const record = await this.adapter.getDoc(docId);
|
|
626
|
-
return record !== null;
|
|
627
|
-
}
|
|
628
|
-
checkQuerySafety(filters, allowCollectionScan) {
|
|
629
|
-
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
630
|
-
const result = analyzeQuerySafety(filters);
|
|
631
|
-
if (result.safe) return;
|
|
632
|
-
if (this.scanProtection === "error") {
|
|
633
|
-
throw new QuerySafetyError(result.reason);
|
|
634
|
-
}
|
|
635
|
-
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
636
|
-
}
|
|
637
|
-
async findEdges(params) {
|
|
638
|
-
const plan = buildEdgeQueryPlan(params);
|
|
639
|
-
let records;
|
|
640
|
-
if (plan.strategy === "get") {
|
|
641
|
-
const record = await this.adapter.getDoc(plan.docId);
|
|
642
|
-
records = record ? [record] : [];
|
|
643
|
-
} else {
|
|
644
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
645
|
-
records = await this.adapter.query(plan.filters, plan.options);
|
|
646
|
-
}
|
|
647
|
-
return this.applyMigrations(records);
|
|
648
|
-
}
|
|
649
|
-
async findNodes(params) {
|
|
650
|
-
const plan = buildNodeQueryPlan(params);
|
|
651
|
-
let records;
|
|
652
|
-
if (plan.strategy === "get") {
|
|
653
|
-
const record = await this.adapter.getDoc(plan.docId);
|
|
654
|
-
records = record ? [record] : [];
|
|
655
|
-
} else {
|
|
656
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
657
|
-
records = await this.adapter.query(plan.filters, plan.options);
|
|
658
|
-
}
|
|
659
|
-
return this.applyMigrations(records);
|
|
660
|
-
}
|
|
661
|
-
async applyMigrations(records) {
|
|
662
|
-
if (!this.registry || records.length === 0) return records;
|
|
663
|
-
const results = await migrateRecords(records, this.registry, this.globalWriteBack);
|
|
664
|
-
for (const result of results) {
|
|
665
|
-
if (result.migrated && result.writeBack !== "off") {
|
|
666
|
-
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
667
|
-
const update = {
|
|
668
|
-
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
669
|
-
updatedAt: FieldValue3.serverTimestamp()
|
|
670
|
-
};
|
|
671
|
-
if (result.record.v !== void 0) {
|
|
672
|
-
update.v = result.record.v;
|
|
673
|
-
}
|
|
674
|
-
this.adapter.updateDoc(docId, update);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return results.map((r) => r.record);
|
|
678
|
-
}
|
|
679
|
-
async putNode(aType, uid, data) {
|
|
680
|
-
if (this.registry) {
|
|
681
|
-
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
682
|
-
}
|
|
683
|
-
const docId = computeNodeDocId(uid);
|
|
684
|
-
const record = buildNodeRecord(aType, uid, data);
|
|
685
|
-
if (this.registry) {
|
|
686
|
-
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
687
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
688
|
-
record.v = entry.schemaVersion;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
this.adapter.setDoc(docId, record);
|
|
692
|
-
}
|
|
693
|
-
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
694
|
-
if (this.registry) {
|
|
695
|
-
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
696
|
-
}
|
|
697
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
698
|
-
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
699
|
-
if (this.registry) {
|
|
700
|
-
const entry = this.registry.lookup(aType, axbType, bType);
|
|
701
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
702
|
-
record.v = entry.schemaVersion;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
this.adapter.setDoc(docId, record);
|
|
706
|
-
}
|
|
707
|
-
async updateNode(uid, data) {
|
|
708
|
-
const docId = computeNodeDocId(uid);
|
|
709
|
-
const update = {
|
|
710
|
-
updatedAt: FieldValue3.serverTimestamp()
|
|
711
|
-
};
|
|
712
|
-
for (const [k, v] of Object.entries(data)) {
|
|
713
|
-
update[`data.${k}`] = v;
|
|
714
|
-
}
|
|
715
|
-
this.adapter.updateDoc(docId, update);
|
|
716
|
-
}
|
|
717
|
-
async removeNode(uid) {
|
|
718
|
-
const docId = computeNodeDocId(uid);
|
|
719
|
-
this.adapter.deleteDoc(docId);
|
|
720
|
-
}
|
|
721
|
-
async removeEdge(aUid, axbType, bUid) {
|
|
722
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
723
|
-
this.adapter.deleteDoc(docId);
|
|
724
|
-
}
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
// src/batch.ts
|
|
728
|
-
import { FieldValue as FieldValue4 } from "@google-cloud/firestore";
|
|
729
|
-
var GraphBatchImpl = class {
|
|
730
|
-
constructor(adapter, registry, scopePath = "") {
|
|
731
|
-
this.adapter = adapter;
|
|
732
|
-
this.registry = registry;
|
|
733
|
-
this.scopePath = scopePath;
|
|
734
|
-
}
|
|
735
|
-
async putNode(aType, uid, data) {
|
|
736
|
-
if (this.registry) {
|
|
737
|
-
this.registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
738
|
-
}
|
|
739
|
-
const docId = computeNodeDocId(uid);
|
|
740
|
-
const record = buildNodeRecord(aType, uid, data);
|
|
741
|
-
if (this.registry) {
|
|
742
|
-
const entry = this.registry.lookup(aType, NODE_RELATION, aType);
|
|
743
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
744
|
-
record.v = entry.schemaVersion;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
this.adapter.setDoc(docId, record);
|
|
748
|
-
}
|
|
749
|
-
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
750
|
-
if (this.registry) {
|
|
751
|
-
this.registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
752
|
-
}
|
|
753
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
754
|
-
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
755
|
-
if (this.registry) {
|
|
756
|
-
const entry = this.registry.lookup(aType, axbType, bType);
|
|
757
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
758
|
-
record.v = entry.schemaVersion;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
this.adapter.setDoc(docId, record);
|
|
762
|
-
}
|
|
763
|
-
async updateNode(uid, data) {
|
|
764
|
-
const docId = computeNodeDocId(uid);
|
|
765
|
-
const update = {
|
|
766
|
-
updatedAt: FieldValue4.serverTimestamp()
|
|
767
|
-
};
|
|
768
|
-
for (const [k, v] of Object.entries(data)) {
|
|
769
|
-
update[`data.${k}`] = v;
|
|
770
|
-
}
|
|
771
|
-
this.adapter.updateDoc(docId, update);
|
|
772
|
-
}
|
|
773
|
-
async removeNode(uid) {
|
|
774
|
-
const docId = computeNodeDocId(uid);
|
|
775
|
-
this.adapter.deleteDoc(docId);
|
|
776
|
-
}
|
|
777
|
-
async removeEdge(aUid, axbType, bUid) {
|
|
778
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
779
|
-
this.adapter.deleteDoc(docId);
|
|
780
|
-
}
|
|
781
|
-
async commit() {
|
|
782
|
-
await this.adapter.commit();
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
// src/bulk.ts
|
|
787
|
-
var MAX_BATCH_SIZE = 500;
|
|
788
|
-
var DEFAULT_MAX_RETRIES = 3;
|
|
789
|
-
var BASE_DELAY_MS = 200;
|
|
790
|
-
function sleep(ms) {
|
|
791
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
792
|
-
}
|
|
793
|
-
function chunk(arr, size) {
|
|
794
|
-
const chunks = [];
|
|
795
|
-
for (let i = 0; i < arr.length; i += size) {
|
|
796
|
-
chunks.push(arr.slice(i, i + size));
|
|
797
|
-
}
|
|
798
|
-
return chunks;
|
|
799
|
-
}
|
|
800
|
-
async function bulkDeleteDocIds(db, collectionPath, docIds, options) {
|
|
801
|
-
if (docIds.length === 0) {
|
|
802
|
-
return { deleted: 0, batches: 0, errors: [] };
|
|
803
|
-
}
|
|
804
|
-
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
805
|
-
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
806
|
-
const onProgress = options?.onProgress;
|
|
807
|
-
const chunks = chunk(docIds, batchSize);
|
|
808
|
-
const errors = [];
|
|
809
|
-
let deleted = 0;
|
|
810
|
-
let completedBatches = 0;
|
|
811
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
812
|
-
const ids = chunks[i];
|
|
813
|
-
let committed = false;
|
|
814
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
815
|
-
try {
|
|
816
|
-
const batch = db.batch();
|
|
817
|
-
const collectionRef = db.collection(collectionPath);
|
|
818
|
-
for (const id of ids) {
|
|
819
|
-
batch.delete(collectionRef.doc(id));
|
|
820
|
-
}
|
|
821
|
-
await batch.commit();
|
|
822
|
-
committed = true;
|
|
823
|
-
deleted += ids.length;
|
|
824
|
-
break;
|
|
825
|
-
} catch (err) {
|
|
826
|
-
if (attempt < maxRetries) {
|
|
827
|
-
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
828
|
-
await sleep(delay);
|
|
829
|
-
} else {
|
|
830
|
-
errors.push({
|
|
831
|
-
batchIndex: i,
|
|
832
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
833
|
-
operationCount: ids.length
|
|
834
|
-
});
|
|
835
|
-
}
|
|
345
|
+
if (attempt < maxRetries) {
|
|
346
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
347
|
+
await sleep(delay);
|
|
348
|
+
} else {
|
|
349
|
+
errors.push({
|
|
350
|
+
batchIndex: i,
|
|
351
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
352
|
+
operationCount: ids.length
|
|
353
|
+
});
|
|
354
|
+
}
|
|
836
355
|
}
|
|
837
356
|
}
|
|
838
357
|
if (committed) {
|
|
@@ -925,1044 +444,288 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
|
|
|
925
444
|
};
|
|
926
445
|
}
|
|
927
446
|
|
|
928
|
-
// src/
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
);
|
|
447
|
+
// src/internal/firestore-adapter.ts
|
|
448
|
+
function createFirestoreAdapter(db, collectionPath) {
|
|
449
|
+
const collectionRef = db.collection(collectionPath);
|
|
450
|
+
return {
|
|
451
|
+
collectionPath,
|
|
452
|
+
async getDoc(docId) {
|
|
453
|
+
const snap = await collectionRef.doc(docId).get();
|
|
454
|
+
if (!snap.exists) return null;
|
|
455
|
+
return snap.data();
|
|
456
|
+
},
|
|
457
|
+
async setDoc(docId, data) {
|
|
458
|
+
await collectionRef.doc(docId).set(data);
|
|
459
|
+
},
|
|
460
|
+
async updateDoc(docId, data) {
|
|
461
|
+
await collectionRef.doc(docId).update(data);
|
|
462
|
+
},
|
|
463
|
+
async deleteDoc(docId) {
|
|
464
|
+
await collectionRef.doc(docId).delete();
|
|
465
|
+
},
|
|
466
|
+
async query(filters, options) {
|
|
467
|
+
let q = collectionRef;
|
|
468
|
+
for (const f of filters) {
|
|
469
|
+
q = q.where(f.field, f.op, f.value);
|
|
470
|
+
}
|
|
471
|
+
if (options?.orderBy) {
|
|
472
|
+
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
473
|
+
}
|
|
474
|
+
if (options?.limit !== void 0) {
|
|
475
|
+
q = q.limit(options.limit);
|
|
476
|
+
}
|
|
477
|
+
const snap = await q.get();
|
|
478
|
+
return snap.docs.map((doc) => doc.data());
|
|
946
479
|
}
|
|
947
480
|
};
|
|
948
481
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
const requiredSet = new Set(
|
|
952
|
-
Array.isArray(schema.required) ? schema.required : []
|
|
953
|
-
);
|
|
954
|
-
return Object.entries(schema.properties).map(
|
|
955
|
-
([name, prop]) => propertyToFieldMeta(name, prop, requiredSet.has(name))
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
function propertyToFieldMeta(name, prop, required) {
|
|
959
|
-
if (!prop) return { name, type: "unknown", required };
|
|
960
|
-
if (Array.isArray(prop.enum)) {
|
|
961
|
-
return {
|
|
962
|
-
name,
|
|
963
|
-
type: "enum",
|
|
964
|
-
required,
|
|
965
|
-
enumValues: prop.enum,
|
|
966
|
-
description: prop.description
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
if (Array.isArray(prop.oneOf) || Array.isArray(prop.anyOf)) {
|
|
970
|
-
const variants = prop.oneOf ?? prop.anyOf;
|
|
971
|
-
const nonNull = variants.filter((v) => v.type !== "null");
|
|
972
|
-
if (nonNull.length === 1) {
|
|
973
|
-
return propertyToFieldMeta(name, nonNull[0], false);
|
|
974
|
-
}
|
|
975
|
-
return { name, type: "unknown", required, description: prop.description };
|
|
976
|
-
}
|
|
977
|
-
const type = prop.type;
|
|
978
|
-
if (type === "string") {
|
|
979
|
-
return {
|
|
980
|
-
name,
|
|
981
|
-
type: "string",
|
|
982
|
-
required,
|
|
983
|
-
minLength: prop.minLength,
|
|
984
|
-
maxLength: prop.maxLength,
|
|
985
|
-
pattern: prop.pattern,
|
|
986
|
-
description: prop.description
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
if (type === "number" || type === "integer") {
|
|
990
|
-
return {
|
|
991
|
-
name,
|
|
992
|
-
type: "number",
|
|
993
|
-
required,
|
|
994
|
-
min: prop.minimum,
|
|
995
|
-
max: prop.maximum,
|
|
996
|
-
isInt: type === "integer" ? true : void 0,
|
|
997
|
-
description: prop.description
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
if (type === "boolean") {
|
|
1001
|
-
return { name, type: "boolean", required, description: prop.description };
|
|
1002
|
-
}
|
|
1003
|
-
if (type === "array") {
|
|
1004
|
-
const itemMeta = prop.items ? propertyToFieldMeta("item", prop.items, true) : void 0;
|
|
1005
|
-
return {
|
|
1006
|
-
name,
|
|
1007
|
-
type: "array",
|
|
1008
|
-
required,
|
|
1009
|
-
itemMeta,
|
|
1010
|
-
description: prop.description
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
if (type === "object") {
|
|
1014
|
-
return {
|
|
1015
|
-
name,
|
|
1016
|
-
type: "object",
|
|
1017
|
-
required,
|
|
1018
|
-
fields: jsonSchemaToFieldMeta(prop),
|
|
1019
|
-
description: prop.description
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
return { name, type: "unknown", required, description: prop.description };
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// src/scope.ts
|
|
1026
|
-
function matchScope(scopePath, pattern) {
|
|
1027
|
-
if (pattern === "root") return scopePath === "";
|
|
1028
|
-
if (pattern === "**") return true;
|
|
1029
|
-
const pathSegments = scopePath === "" ? [] : scopePath.split("/");
|
|
1030
|
-
const patternSegments = pattern.split("/");
|
|
1031
|
-
return matchSegments(pathSegments, 0, patternSegments, 0);
|
|
1032
|
-
}
|
|
1033
|
-
function matchScopeAny(scopePath, patterns) {
|
|
1034
|
-
if (!patterns || patterns.length === 0) return true;
|
|
1035
|
-
return patterns.some((p) => matchScope(scopePath, p));
|
|
1036
|
-
}
|
|
1037
|
-
function matchSegments(path, pi, pattern, qi) {
|
|
1038
|
-
if (pi === path.length && qi === pattern.length) return true;
|
|
1039
|
-
if (qi === pattern.length) return false;
|
|
1040
|
-
const seg = pattern[qi];
|
|
1041
|
-
if (seg === "**") {
|
|
1042
|
-
if (qi === pattern.length - 1) return true;
|
|
1043
|
-
for (let skip = 0; skip <= path.length - pi; skip++) {
|
|
1044
|
-
if (matchSegments(path, pi + skip, pattern, qi + 1)) return true;
|
|
1045
|
-
}
|
|
1046
|
-
return false;
|
|
1047
|
-
}
|
|
1048
|
-
if (pi === path.length) return false;
|
|
1049
|
-
if (seg === "*") {
|
|
1050
|
-
return matchSegments(path, pi + 1, pattern, qi + 1);
|
|
1051
|
-
}
|
|
1052
|
-
if (path[pi] === seg) {
|
|
1053
|
-
return matchSegments(path, pi + 1, pattern, qi + 1);
|
|
1054
|
-
}
|
|
1055
|
-
return false;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// src/registry.ts
|
|
1059
|
-
function tripleKey(aType, axbType, bType) {
|
|
1060
|
-
return `${aType}:${axbType}:${bType}`;
|
|
1061
|
-
}
|
|
1062
|
-
function tripleKeyFor(e) {
|
|
1063
|
-
return tripleKey(e.aType, e.axbType, e.bType);
|
|
1064
|
-
}
|
|
1065
|
-
function createRegistry(input) {
|
|
1066
|
-
const map = /* @__PURE__ */ new Map();
|
|
1067
|
-
let entries;
|
|
1068
|
-
if (Array.isArray(input)) {
|
|
1069
|
-
entries = input;
|
|
1070
|
-
} else {
|
|
1071
|
-
entries = discoveryToEntries(input);
|
|
1072
|
-
}
|
|
1073
|
-
const entryList = Object.freeze([...entries]);
|
|
1074
|
-
for (const entry of entries) {
|
|
1075
|
-
if (entry.targetGraph && entry.targetGraph.includes("/")) {
|
|
1076
|
-
throw new ValidationError(
|
|
1077
|
-
`Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType}) has invalid targetGraph "${entry.targetGraph}" \u2014 must be a single segment (no "/")`
|
|
1078
|
-
);
|
|
1079
|
-
}
|
|
1080
|
-
if (entry.migrations?.length) {
|
|
1081
|
-
const label = `Entry (${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`;
|
|
1082
|
-
validateMigrationChain(entry.migrations, label);
|
|
1083
|
-
entry.schemaVersion = Math.max(...entry.migrations.map((m) => m.toVersion));
|
|
1084
|
-
} else {
|
|
1085
|
-
entry.schemaVersion = void 0;
|
|
1086
|
-
}
|
|
1087
|
-
const key = tripleKey(entry.aType, entry.axbType, entry.bType);
|
|
1088
|
-
const validator = entry.jsonSchema ? compileSchema(entry.jsonSchema, `(${entry.aType}) -[${entry.axbType}]-> (${entry.bType})`) : void 0;
|
|
1089
|
-
map.set(key, { entry, validate: validator });
|
|
1090
|
-
}
|
|
1091
|
-
const axbIndex = /* @__PURE__ */ new Map();
|
|
1092
|
-
const axbBuild = /* @__PURE__ */ new Map();
|
|
1093
|
-
for (const entry of entries) {
|
|
1094
|
-
const existing = axbBuild.get(entry.axbType);
|
|
1095
|
-
if (existing) {
|
|
1096
|
-
existing.push(entry);
|
|
1097
|
-
} else {
|
|
1098
|
-
axbBuild.set(entry.axbType, [entry]);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
for (const [key, arr] of axbBuild) {
|
|
1102
|
-
axbIndex.set(key, Object.freeze(arr));
|
|
1103
|
-
}
|
|
482
|
+
function createTransactionAdapter(db, collectionPath, tx) {
|
|
483
|
+
const collectionRef = db.collection(collectionPath);
|
|
1104
484
|
return {
|
|
1105
|
-
|
|
1106
|
-
|
|
485
|
+
async getDoc(docId) {
|
|
486
|
+
const snap = await tx.get(collectionRef.doc(docId));
|
|
487
|
+
if (!snap.exists) return null;
|
|
488
|
+
return snap.data();
|
|
489
|
+
},
|
|
490
|
+
setDoc(docId, data) {
|
|
491
|
+
tx.set(collectionRef.doc(docId), data);
|
|
492
|
+
},
|
|
493
|
+
updateDoc(docId, data) {
|
|
494
|
+
tx.update(collectionRef.doc(docId), data);
|
|
1107
495
|
},
|
|
1108
|
-
|
|
1109
|
-
|
|
496
|
+
deleteDoc(docId) {
|
|
497
|
+
tx.delete(collectionRef.doc(docId));
|
|
1110
498
|
},
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
499
|
+
async query(filters, options) {
|
|
500
|
+
let q = collectionRef;
|
|
501
|
+
for (const f of filters) {
|
|
502
|
+
q = q.where(f.field, f.op, f.value);
|
|
1115
503
|
}
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
|
|
1119
|
-
}
|
|
504
|
+
if (options?.orderBy) {
|
|
505
|
+
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
1120
506
|
}
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1123
|
-
rec.validate(data);
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
if (err instanceof ValidationError) throw err;
|
|
1126
|
-
throw new ValidationError(
|
|
1127
|
-
`Data validation failed for (${aType}) -[${axbType}]-> (${bType})`,
|
|
1128
|
-
err
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
507
|
+
if (options?.limit !== void 0) {
|
|
508
|
+
q = q.limit(options.limit);
|
|
1131
509
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return entryList;
|
|
510
|
+
const snap = await tx.get(q);
|
|
511
|
+
return snap.docs.map((doc) => doc.data());
|
|
1135
512
|
}
|
|
1136
513
|
};
|
|
1137
514
|
}
|
|
1138
|
-
function
|
|
1139
|
-
const
|
|
515
|
+
function createBatchAdapter(db, collectionPath) {
|
|
516
|
+
const collectionRef = db.collection(collectionPath);
|
|
517
|
+
const batch = db.batch();
|
|
1140
518
|
return {
|
|
1141
|
-
|
|
1142
|
-
|
|
519
|
+
setDoc(docId, data) {
|
|
520
|
+
batch.set(collectionRef.doc(docId), data);
|
|
1143
521
|
},
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const extResults = extension.lookupByAxbType(axbType);
|
|
1147
|
-
if (extResults.length === 0) return baseResults;
|
|
1148
|
-
if (baseResults.length === 0) return extResults;
|
|
1149
|
-
const seen = new Set(baseResults.map(tripleKeyFor));
|
|
1150
|
-
const merged = [...baseResults];
|
|
1151
|
-
for (const entry of extResults) {
|
|
1152
|
-
if (!seen.has(tripleKeyFor(entry))) {
|
|
1153
|
-
merged.push(entry);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
return Object.freeze(merged);
|
|
522
|
+
updateDoc(docId, data) {
|
|
523
|
+
batch.update(collectionRef.doc(docId), data);
|
|
1157
524
|
},
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
return base.validate(aType, axbType, bType, data, scopePath);
|
|
1161
|
-
}
|
|
1162
|
-
return extension.validate(aType, axbType, bType, data, scopePath);
|
|
525
|
+
deleteDoc(docId) {
|
|
526
|
+
batch.delete(collectionRef.doc(docId));
|
|
1163
527
|
},
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
if (extEntries.length === 0) return base.entries();
|
|
1167
|
-
const merged = [...base.entries()];
|
|
1168
|
-
for (const entry of extEntries) {
|
|
1169
|
-
if (!baseKeys.has(tripleKeyFor(entry))) {
|
|
1170
|
-
merged.push(entry);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
return Object.freeze(merged);
|
|
528
|
+
async commit() {
|
|
529
|
+
await batch.commit();
|
|
1174
530
|
}
|
|
1175
531
|
};
|
|
1176
532
|
}
|
|
1177
|
-
function discoveryToEntries(discovery) {
|
|
1178
|
-
const entries = [];
|
|
1179
|
-
for (const [name, entity] of discovery.nodes) {
|
|
1180
|
-
entries.push({
|
|
1181
|
-
aType: name,
|
|
1182
|
-
axbType: NODE_RELATION,
|
|
1183
|
-
bType: name,
|
|
1184
|
-
jsonSchema: entity.schema,
|
|
1185
|
-
description: entity.description,
|
|
1186
|
-
titleField: entity.titleField,
|
|
1187
|
-
subtitleField: entity.subtitleField,
|
|
1188
|
-
allowedIn: entity.allowedIn,
|
|
1189
|
-
migrations: entity.migrations,
|
|
1190
|
-
migrationWriteBack: entity.migrationWriteBack
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
for (const [axbType, entity] of discovery.edges) {
|
|
1194
|
-
const topology = entity.topology;
|
|
1195
|
-
if (!topology) continue;
|
|
1196
|
-
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
1197
|
-
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
1198
|
-
const resolvedTargetGraph = entity.targetGraph ?? topology.targetGraph;
|
|
1199
|
-
if (resolvedTargetGraph && resolvedTargetGraph.includes("/")) {
|
|
1200
|
-
throw new ValidationError(
|
|
1201
|
-
`Edge "${axbType}" has invalid targetGraph "${resolvedTargetGraph}" \u2014 must be a single segment (no "/")`
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
for (const aType of fromTypes) {
|
|
1205
|
-
for (const bType of toTypes) {
|
|
1206
|
-
entries.push({
|
|
1207
|
-
aType,
|
|
1208
|
-
axbType,
|
|
1209
|
-
bType,
|
|
1210
|
-
jsonSchema: entity.schema,
|
|
1211
|
-
description: entity.description,
|
|
1212
|
-
inverseLabel: topology.inverseLabel,
|
|
1213
|
-
titleField: entity.titleField,
|
|
1214
|
-
subtitleField: entity.subtitleField,
|
|
1215
|
-
allowedIn: entity.allowedIn,
|
|
1216
|
-
targetGraph: resolvedTargetGraph,
|
|
1217
|
-
migrations: entity.migrations,
|
|
1218
|
-
migrationWriteBack: entity.migrationWriteBack
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
return entries;
|
|
1224
|
-
}
|
|
1225
533
|
|
|
1226
|
-
// src/
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
`var _wt = require('node:worker_threads');`,
|
|
1235
|
-
`var _mod = require('node:module');`,
|
|
1236
|
-
`var _crypto = require('node:crypto');`,
|
|
1237
|
-
`var parentPort = _wt.parentPort;`,
|
|
1238
|
-
`var workerData = _wt.workerData;`,
|
|
1239
|
-
``,
|
|
1240
|
-
`// Load SES using the parent module's resolution context`,
|
|
1241
|
-
`var esmRequire = _mod.createRequire(workerData.parentUrl);`,
|
|
1242
|
-
`esmRequire('ses');`,
|
|
1243
|
-
``,
|
|
1244
|
-
`lockdown({`,
|
|
1245
|
-
` errorTaming: 'unsafe',`,
|
|
1246
|
-
` consoleTaming: 'unsafe',`,
|
|
1247
|
-
` evalTaming: 'safe-eval',`,
|
|
1248
|
-
` overrideTaming: 'moderate',`,
|
|
1249
|
-
` stackFiltering: 'verbose'`,
|
|
1250
|
-
`});`,
|
|
1251
|
-
``,
|
|
1252
|
-
`// Defense-in-depth: verify lockdown() actually hardened JSON.`,
|
|
1253
|
-
`if (!Object.isFrozen(JSON)) {`,
|
|
1254
|
-
` throw new Error('SES lockdown failed: JSON is not frozen');`,
|
|
1255
|
-
`}`,
|
|
1256
|
-
``,
|
|
1257
|
-
`var cache = new Map();`,
|
|
1258
|
-
``,
|
|
1259
|
-
`function hashSource(s) {`,
|
|
1260
|
-
` return _crypto.createHash('sha256').update(s).digest('hex');`,
|
|
1261
|
-
`}`,
|
|
1262
|
-
``,
|
|
1263
|
-
`function buildWrapper(source) {`,
|
|
1264
|
-
` return '(function() {' +`,
|
|
1265
|
-
` ' var fn = (' + source + ');\\n' +`,
|
|
1266
|
-
` ' if (typeof fn !== "function") return null;\\n' +`,
|
|
1267
|
-
` ' return function(jsonIn) {\\n' +`,
|
|
1268
|
-
` ' var data = JSON.parse(jsonIn);\\n' +`,
|
|
1269
|
-
` ' var result = fn(data);\\n' +`,
|
|
1270
|
-
` ' if (result !== null && typeof result === "object" && typeof result.then === "function") {\\n' +`,
|
|
1271
|
-
` ' return result.then(function(r) { return JSON.stringify(r); });\\n' +`,
|
|
1272
|
-
` ' }\\n' +`,
|
|
1273
|
-
` ' return JSON.stringify(result);\\n' +`,
|
|
1274
|
-
` ' };\\n' +`,
|
|
1275
|
-
` '})()';`,
|
|
1276
|
-
`}`,
|
|
1277
|
-
``,
|
|
1278
|
-
`function compileSource(source) {`,
|
|
1279
|
-
` var key = hashSource(source);`,
|
|
1280
|
-
` var cached = cache.get(key);`,
|
|
1281
|
-
` if (cached) return cached;`,
|
|
1282
|
-
``,
|
|
1283
|
-
` var compartmentFn;`,
|
|
1284
|
-
` try {`,
|
|
1285
|
-
` var c = new Compartment({ JSON: JSON });`,
|
|
1286
|
-
` compartmentFn = c.evaluate(buildWrapper(source));`,
|
|
1287
|
-
` } catch (err) {`,
|
|
1288
|
-
` throw new Error('Failed to compile migration source: ' + (err.message || String(err)));`,
|
|
1289
|
-
` }`,
|
|
1290
|
-
``,
|
|
1291
|
-
` if (typeof compartmentFn !== 'function') {`,
|
|
1292
|
-
` throw new Error('Migration source did not produce a function: ' + source.slice(0, 80));`,
|
|
1293
|
-
` }`,
|
|
1294
|
-
``,
|
|
1295
|
-
` cache.set(key, compartmentFn);`,
|
|
1296
|
-
` return compartmentFn;`,
|
|
1297
|
-
`}`,
|
|
1298
|
-
``,
|
|
1299
|
-
`parentPort.on('message', function(msg) {`,
|
|
1300
|
-
` var id = msg.id;`,
|
|
1301
|
-
` try {`,
|
|
1302
|
-
` if (msg.type === 'compile') {`,
|
|
1303
|
-
` compileSource(msg.source);`,
|
|
1304
|
-
` parentPort.postMessage({ id: id, type: 'compiled' });`,
|
|
1305
|
-
` return;`,
|
|
1306
|
-
` }`,
|
|
1307
|
-
` if (msg.type === 'execute') {`,
|
|
1308
|
-
` var fn = compileSource(msg.source);`,
|
|
1309
|
-
` var raw;`,
|
|
1310
|
-
` try {`,
|
|
1311
|
-
` raw = fn(msg.jsonData);`,
|
|
1312
|
-
` } catch (err) {`,
|
|
1313
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration function threw: ' + (err.message || String(err)) });`,
|
|
1314
|
-
` return;`,
|
|
1315
|
-
` }`,
|
|
1316
|
-
` if (raw !== null && typeof raw === 'object' && typeof raw.then === 'function') {`,
|
|
1317
|
-
` raw.then(`,
|
|
1318
|
-
` function(jsonResult) {`,
|
|
1319
|
-
` if (jsonResult === undefined || jsonResult === null) {`,
|
|
1320
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
1321
|
-
` } else {`,
|
|
1322
|
-
` parentPort.postMessage({ id: id, type: 'result', jsonResult: jsonResult });`,
|
|
1323
|
-
` }`,
|
|
1324
|
-
` },`,
|
|
1325
|
-
` function(err) {`,
|
|
1326
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Async migration function threw: ' + (err.message || String(err)) });`,
|
|
1327
|
-
` }`,
|
|
1328
|
-
` );`,
|
|
1329
|
-
` return;`,
|
|
1330
|
-
` }`,
|
|
1331
|
-
` if (raw === undefined || raw === null) {`,
|
|
1332
|
-
` parentPort.postMessage({ id: id, type: 'error', message: 'Migration returned a non-JSON-serializable value' });`,
|
|
1333
|
-
` } else {`,
|
|
1334
|
-
` parentPort.postMessage({ id: id, type: 'result', jsonResult: raw });`,
|
|
1335
|
-
` }`,
|
|
1336
|
-
` }`,
|
|
1337
|
-
` } catch (err) {`,
|
|
1338
|
-
` parentPort.postMessage({ id: id, type: 'error', message: err.message || String(err) });`,
|
|
1339
|
-
` }`,
|
|
1340
|
-
`});`
|
|
1341
|
-
].join("\n");
|
|
1342
|
-
function ensureWorker() {
|
|
1343
|
-
if (_worker) return _worker;
|
|
1344
|
-
_worker = new Worker(WORKER_SOURCE, {
|
|
1345
|
-
eval: true,
|
|
1346
|
-
workerData: { parentUrl: import.meta.url }
|
|
1347
|
-
});
|
|
1348
|
-
_worker.unref();
|
|
1349
|
-
_worker.on("message", (msg) => {
|
|
1350
|
-
if (msg.id === void 0) return;
|
|
1351
|
-
const pending = _pending.get(msg.id);
|
|
1352
|
-
if (!pending) return;
|
|
1353
|
-
_pending.delete(msg.id);
|
|
1354
|
-
if (msg.type === "error") {
|
|
1355
|
-
pending.reject(new MigrationError(msg.message ?? "Unknown sandbox error"));
|
|
1356
|
-
} else {
|
|
1357
|
-
pending.resolve(msg);
|
|
1358
|
-
}
|
|
1359
|
-
});
|
|
1360
|
-
_worker.on("error", (err) => {
|
|
1361
|
-
for (const [, p] of _pending) {
|
|
1362
|
-
p.reject(new MigrationError(`Sandbox worker error: ${err.message}`));
|
|
1363
|
-
}
|
|
1364
|
-
_pending.clear();
|
|
1365
|
-
_worker = null;
|
|
1366
|
-
});
|
|
1367
|
-
_worker.on("exit", (code) => {
|
|
1368
|
-
if (_pending.size > 0) {
|
|
1369
|
-
for (const [, p] of _pending) {
|
|
1370
|
-
p.reject(new MigrationError(`Sandbox worker exited with code ${code}`));
|
|
1371
|
-
}
|
|
1372
|
-
_pending.clear();
|
|
1373
|
-
}
|
|
1374
|
-
_worker = null;
|
|
1375
|
-
});
|
|
1376
|
-
return _worker;
|
|
1377
|
-
}
|
|
1378
|
-
function sendToWorker(msg) {
|
|
1379
|
-
const worker = ensureWorker();
|
|
1380
|
-
if (_requestId >= Number.MAX_SAFE_INTEGER) _requestId = 0;
|
|
1381
|
-
const id = ++_requestId;
|
|
1382
|
-
return new Promise((resolve2, reject) => {
|
|
1383
|
-
_pending.set(id, { resolve: resolve2, reject });
|
|
1384
|
-
worker.postMessage({ ...msg, id });
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
var compiledCache = /* @__PURE__ */ new WeakMap();
|
|
1388
|
-
function getExecutorCache(executor) {
|
|
1389
|
-
let cache = compiledCache.get(executor);
|
|
1390
|
-
if (!cache) {
|
|
1391
|
-
cache = /* @__PURE__ */ new Map();
|
|
1392
|
-
compiledCache.set(executor, cache);
|
|
1393
|
-
}
|
|
1394
|
-
return cache;
|
|
534
|
+
// src/internal/pipeline-adapter.ts
|
|
535
|
+
var _Pipelines = null;
|
|
536
|
+
async function getPipelines() {
|
|
537
|
+
if (!_Pipelines) {
|
|
538
|
+
const mod = await import("@google-cloud/firestore");
|
|
539
|
+
_Pipelines = mod.Pipelines;
|
|
540
|
+
}
|
|
541
|
+
return _Pipelines;
|
|
1395
542
|
}
|
|
1396
|
-
function
|
|
1397
|
-
|
|
543
|
+
function buildFilterExpression(P, filter) {
|
|
544
|
+
const { field: fieldName, op, value } = filter;
|
|
545
|
+
switch (op) {
|
|
546
|
+
case "==":
|
|
547
|
+
return P.equal(fieldName, value);
|
|
548
|
+
case "!=":
|
|
549
|
+
return P.notEqual(fieldName, value);
|
|
550
|
+
case "<":
|
|
551
|
+
return P.lessThan(fieldName, value);
|
|
552
|
+
case "<=":
|
|
553
|
+
return P.lessThanOrEqual(fieldName, value);
|
|
554
|
+
case ">":
|
|
555
|
+
return P.greaterThan(fieldName, value);
|
|
556
|
+
case ">=":
|
|
557
|
+
return P.greaterThanOrEqual(fieldName, value);
|
|
558
|
+
case "in":
|
|
559
|
+
return P.equalAny(fieldName, value);
|
|
560
|
+
case "not-in":
|
|
561
|
+
return P.notEqualAny(fieldName, value);
|
|
562
|
+
case "array-contains":
|
|
563
|
+
return P.arrayContains(fieldName, value);
|
|
564
|
+
case "array-contains-any":
|
|
565
|
+
return P.arrayContainsAny(fieldName, value);
|
|
566
|
+
default:
|
|
567
|
+
throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
|
|
568
|
+
}
|
|
1398
569
|
}
|
|
1399
|
-
function
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
(
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
return deserializeFirestoreTypes(JSON.parse(response.jsonResult));
|
|
1410
|
-
} catch {
|
|
1411
|
-
throw new MigrationError("Migration returned a non-JSON-serializable value");
|
|
1412
|
-
}
|
|
570
|
+
function createPipelineQueryAdapter(db, collectionPath) {
|
|
571
|
+
return {
|
|
572
|
+
async query(filters, options) {
|
|
573
|
+
const P = await getPipelines();
|
|
574
|
+
let pipeline = db.pipeline().collection(collectionPath);
|
|
575
|
+
if (filters.length === 1) {
|
|
576
|
+
pipeline = pipeline.where(buildFilterExpression(P, filters[0]));
|
|
577
|
+
} else if (filters.length > 1) {
|
|
578
|
+
const [first, second, ...rest] = filters.map((f) => buildFilterExpression(P, f));
|
|
579
|
+
pipeline = pipeline.where(P.and(first, second, ...rest));
|
|
1413
580
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
581
|
+
if (options?.orderBy) {
|
|
582
|
+
const f = P.field(options.orderBy.field);
|
|
583
|
+
const ordering = options.orderBy.direction === "desc" ? f.descending() : f.ascending();
|
|
584
|
+
pipeline = pipeline.sort(ordering);
|
|
585
|
+
}
|
|
586
|
+
if (options?.limit !== void 0) {
|
|
587
|
+
pipeline = pipeline.limit(options.limit);
|
|
588
|
+
}
|
|
589
|
+
const snap = await pipeline.execute();
|
|
590
|
+
return snap.results.map((r) => r.data());
|
|
591
|
+
}
|
|
592
|
+
};
|
|
1416
593
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
594
|
+
|
|
595
|
+
// src/internal/firestore-backend.ts
|
|
596
|
+
function buildFirestoreUpdate(update, db) {
|
|
597
|
+
const out = {
|
|
598
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
599
|
+
};
|
|
600
|
+
if (update.replaceData) {
|
|
601
|
+
out.data = deserializeFirestoreTypes(update.replaceData, db);
|
|
602
|
+
}
|
|
603
|
+
if (update.dataFields) {
|
|
604
|
+
for (const [k, v] of Object.entries(update.dataFields)) {
|
|
605
|
+
out[`data.${k}`] = v;
|
|
1426
606
|
}
|
|
1427
|
-
return;
|
|
1428
607
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
function compileMigrationFn(source, executor = defaultExecutor) {
|
|
1432
|
-
const cache = getExecutorCache(executor);
|
|
1433
|
-
const key = hashSource(source);
|
|
1434
|
-
const cached = cache.get(key);
|
|
1435
|
-
if (cached) return cached;
|
|
1436
|
-
try {
|
|
1437
|
-
const fn = executor(source);
|
|
1438
|
-
cache.set(key, fn);
|
|
1439
|
-
return fn;
|
|
1440
|
-
} catch (err) {
|
|
1441
|
-
if (err instanceof MigrationError) throw err;
|
|
1442
|
-
throw new MigrationError(
|
|
1443
|
-
`Failed to compile migration source: ${err.message}`
|
|
1444
|
-
);
|
|
608
|
+
if (update.v !== void 0) {
|
|
609
|
+
out.v = update.v;
|
|
1445
610
|
}
|
|
611
|
+
return out;
|
|
1446
612
|
}
|
|
1447
|
-
function
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
_pending.clear();
|
|
1462
|
-
await w.terminate();
|
|
613
|
+
function stampWritableRecord(record) {
|
|
614
|
+
const now = FieldValue.serverTimestamp();
|
|
615
|
+
const out = {
|
|
616
|
+
aType: record.aType,
|
|
617
|
+
aUid: record.aUid,
|
|
618
|
+
axbType: record.axbType,
|
|
619
|
+
bType: record.bType,
|
|
620
|
+
bUid: record.bUid,
|
|
621
|
+
data: record.data,
|
|
622
|
+
createdAt: now,
|
|
623
|
+
updatedAt: now
|
|
624
|
+
};
|
|
625
|
+
if (record.v !== void 0) out.v = record.v;
|
|
626
|
+
return out;
|
|
1463
627
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
description: { type: "string" },
|
|
1485
|
-
titleField: { type: "string" },
|
|
1486
|
-
subtitleField: { type: "string" },
|
|
1487
|
-
viewTemplate: { type: "string" },
|
|
1488
|
-
viewCss: { type: "string" },
|
|
1489
|
-
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
1490
|
-
schemaVersion: { type: "integer", minimum: 0 },
|
|
1491
|
-
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
1492
|
-
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
1493
|
-
},
|
|
1494
|
-
additionalProperties: false
|
|
1495
|
-
};
|
|
1496
|
-
var EDGE_TYPE_SCHEMA = {
|
|
1497
|
-
type: "object",
|
|
1498
|
-
required: ["name", "from", "to"],
|
|
1499
|
-
properties: {
|
|
1500
|
-
name: { type: "string", minLength: 1 },
|
|
1501
|
-
from: {
|
|
1502
|
-
oneOf: [
|
|
1503
|
-
{ type: "string", minLength: 1 },
|
|
1504
|
-
{ type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
|
|
1505
|
-
]
|
|
1506
|
-
},
|
|
1507
|
-
to: {
|
|
1508
|
-
oneOf: [
|
|
1509
|
-
{ type: "string", minLength: 1 },
|
|
1510
|
-
{ type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
|
|
1511
|
-
]
|
|
1512
|
-
},
|
|
1513
|
-
jsonSchema: { type: "object" },
|
|
1514
|
-
inverseLabel: { type: "string" },
|
|
1515
|
-
description: { type: "string" },
|
|
1516
|
-
titleField: { type: "string" },
|
|
1517
|
-
subtitleField: { type: "string" },
|
|
1518
|
-
viewTemplate: { type: "string" },
|
|
1519
|
-
viewCss: { type: "string" },
|
|
1520
|
-
allowedIn: { type: "array", items: { type: "string", minLength: 1 } },
|
|
1521
|
-
targetGraph: { type: "string", minLength: 1, pattern: "^[^/]+$" },
|
|
1522
|
-
schemaVersion: { type: "integer", minimum: 0 },
|
|
1523
|
-
migrations: { type: "array", items: STORED_MIGRATION_STEP_SCHEMA },
|
|
1524
|
-
migrationWriteBack: { type: "string", enum: ["off", "eager", "background"] }
|
|
1525
|
-
},
|
|
1526
|
-
additionalProperties: false
|
|
628
|
+
var FirestoreTransactionBackend = class {
|
|
629
|
+
constructor(adapter, db) {
|
|
630
|
+
this.adapter = adapter;
|
|
631
|
+
this.db = db;
|
|
632
|
+
}
|
|
633
|
+
getDoc(docId) {
|
|
634
|
+
return this.adapter.getDoc(docId);
|
|
635
|
+
}
|
|
636
|
+
query(filters, options) {
|
|
637
|
+
return this.adapter.query(filters, options);
|
|
638
|
+
}
|
|
639
|
+
async setDoc(docId, record) {
|
|
640
|
+
this.adapter.setDoc(docId, stampWritableRecord(record));
|
|
641
|
+
}
|
|
642
|
+
async updateDoc(docId, update) {
|
|
643
|
+
this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
644
|
+
}
|
|
645
|
+
async deleteDoc(docId) {
|
|
646
|
+
this.adapter.deleteDoc(docId);
|
|
647
|
+
}
|
|
1527
648
|
};
|
|
1528
|
-
var
|
|
1529
|
-
{
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
bType: META_NODE_TYPE,
|
|
1533
|
-
jsonSchema: NODE_TYPE_SCHEMA,
|
|
1534
|
-
description: "Meta-type: defines a node type"
|
|
1535
|
-
},
|
|
1536
|
-
{
|
|
1537
|
-
aType: META_EDGE_TYPE,
|
|
1538
|
-
axbType: NODE_RELATION,
|
|
1539
|
-
bType: META_EDGE_TYPE,
|
|
1540
|
-
jsonSchema: EDGE_TYPE_SCHEMA,
|
|
1541
|
-
description: "Meta-type: defines an edge type"
|
|
649
|
+
var FirestoreBatchBackend = class {
|
|
650
|
+
constructor(adapter, db) {
|
|
651
|
+
this.adapter = adapter;
|
|
652
|
+
this.db = db;
|
|
1542
653
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
return createRegistry([...BOOTSTRAP_ENTRIES]);
|
|
1546
|
-
}
|
|
1547
|
-
function generateDeterministicUid(metaType, name) {
|
|
1548
|
-
const hash = createHash3("sha256").update(`${metaType}:${name}`).digest("base64url");
|
|
1549
|
-
return hash.slice(0, 21);
|
|
1550
|
-
}
|
|
1551
|
-
async function createRegistryFromGraph(reader, executor) {
|
|
1552
|
-
const [nodeTypes, edgeTypes] = await Promise.all([
|
|
1553
|
-
reader.findNodes({ aType: META_NODE_TYPE }),
|
|
1554
|
-
reader.findNodes({ aType: META_EDGE_TYPE })
|
|
1555
|
-
]);
|
|
1556
|
-
const entries = [...BOOTSTRAP_ENTRIES];
|
|
1557
|
-
const prevalidations = [];
|
|
1558
|
-
for (const record of nodeTypes) {
|
|
1559
|
-
const data = record.data;
|
|
1560
|
-
if (data.migrations) {
|
|
1561
|
-
for (const m of data.migrations) {
|
|
1562
|
-
prevalidations.push(precompileSource(m.up, executor));
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
654
|
+
setDoc(docId, record) {
|
|
655
|
+
this.adapter.setDoc(docId, stampWritableRecord(record));
|
|
1565
656
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
if (data.migrations) {
|
|
1569
|
-
for (const m of data.migrations) {
|
|
1570
|
-
prevalidations.push(precompileSource(m.up, executor));
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
657
|
+
updateDoc(docId, update) {
|
|
658
|
+
this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
1573
659
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
const data = record.data;
|
|
1577
|
-
entries.push({
|
|
1578
|
-
aType: data.name,
|
|
1579
|
-
axbType: NODE_RELATION,
|
|
1580
|
-
bType: data.name,
|
|
1581
|
-
jsonSchema: data.jsonSchema,
|
|
1582
|
-
description: data.description,
|
|
1583
|
-
titleField: data.titleField,
|
|
1584
|
-
subtitleField: data.subtitleField,
|
|
1585
|
-
allowedIn: data.allowedIn,
|
|
1586
|
-
migrations: data.migrations ? compileMigrations(data.migrations, executor) : void 0,
|
|
1587
|
-
migrationWriteBack: data.migrationWriteBack
|
|
1588
|
-
});
|
|
660
|
+
deleteDoc(docId) {
|
|
661
|
+
this.adapter.deleteDoc(docId);
|
|
1589
662
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
const fromTypes = Array.isArray(data.from) ? data.from : [data.from];
|
|
1593
|
-
const toTypes = Array.isArray(data.to) ? data.to : [data.to];
|
|
1594
|
-
const compiledMigrations = data.migrations ? compileMigrations(data.migrations, executor) : void 0;
|
|
1595
|
-
for (const aType of fromTypes) {
|
|
1596
|
-
for (const bType of toTypes) {
|
|
1597
|
-
entries.push({
|
|
1598
|
-
aType,
|
|
1599
|
-
axbType: data.name,
|
|
1600
|
-
bType,
|
|
1601
|
-
jsonSchema: data.jsonSchema,
|
|
1602
|
-
description: data.description,
|
|
1603
|
-
inverseLabel: data.inverseLabel,
|
|
1604
|
-
titleField: data.titleField,
|
|
1605
|
-
subtitleField: data.subtitleField,
|
|
1606
|
-
allowedIn: data.allowedIn,
|
|
1607
|
-
targetGraph: data.targetGraph,
|
|
1608
|
-
migrations: compiledMigrations,
|
|
1609
|
-
migrationWriteBack: data.migrationWriteBack
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
663
|
+
commit() {
|
|
664
|
+
return this.adapter.commit();
|
|
1613
665
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
// src/client.ts
|
|
1618
|
-
var _standardModeWarned = false;
|
|
1619
|
-
var RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([META_NODE_TYPE, META_EDGE_TYPE]);
|
|
1620
|
-
var GraphClientImpl = class _GraphClientImpl {
|
|
1621
|
-
constructor(db, collectionPath, options, scopePath = "") {
|
|
666
|
+
};
|
|
667
|
+
var FirestoreBackendImpl = class _FirestoreBackendImpl {
|
|
668
|
+
constructor(db, collectionPath, queryMode, scopePath) {
|
|
1622
669
|
this.db = db;
|
|
670
|
+
this.queryMode = queryMode;
|
|
671
|
+
this.collectionPath = collectionPath;
|
|
1623
672
|
this.scopePath = scopePath;
|
|
1624
673
|
this.adapter = createFirestoreAdapter(db, collectionPath);
|
|
1625
|
-
|
|
1626
|
-
this.migrationSandbox = options?.migrationSandbox;
|
|
1627
|
-
if (options?.registryMode) {
|
|
1628
|
-
this.dynamicConfig = options.registryMode;
|
|
1629
|
-
this.bootstrapRegistry = createBootstrapRegistry();
|
|
1630
|
-
if (options.registry) {
|
|
1631
|
-
this.staticRegistry = options.registry;
|
|
1632
|
-
}
|
|
1633
|
-
const metaCollectionPath = options.registryMode.collection;
|
|
1634
|
-
if (metaCollectionPath && metaCollectionPath !== collectionPath) {
|
|
1635
|
-
this.metaAdapter = createFirestoreAdapter(db, metaCollectionPath);
|
|
1636
|
-
}
|
|
1637
|
-
} else {
|
|
1638
|
-
this.staticRegistry = options?.registry;
|
|
1639
|
-
}
|
|
1640
|
-
const requestedMode = options?.queryMode ?? "pipeline";
|
|
1641
|
-
const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
|
|
1642
|
-
if (isEmulator) {
|
|
1643
|
-
this.queryMode = "standard";
|
|
1644
|
-
} else {
|
|
1645
|
-
this.queryMode = requestedMode;
|
|
1646
|
-
}
|
|
1647
|
-
if (this.queryMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
|
|
1648
|
-
_standardModeWarned = true;
|
|
1649
|
-
console.warn(
|
|
1650
|
-
"[firegraph] Standard query mode enabled. This is NOT recommended for production:\n - Enterprise Firestore: data.* filters cause full collection scans (high billing)\n - Standard Firestore: data.* filters without composite indexes will fail\n See: https://github.com/typicalday/firegraph#query-modes"
|
|
1651
|
-
);
|
|
1652
|
-
}
|
|
1653
|
-
this.scanProtection = options?.scanProtection ?? "error";
|
|
1654
|
-
if (this.queryMode === "pipeline") {
|
|
674
|
+
if (queryMode === "pipeline") {
|
|
1655
675
|
this.pipelineAdapter = createPipelineQueryAdapter(db, collectionPath);
|
|
1656
|
-
if (this.metaAdapter) {
|
|
1657
|
-
this.metaPipelineAdapter = createPipelineQueryAdapter(
|
|
1658
|
-
db,
|
|
1659
|
-
options.registryMode.collection
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
676
|
}
|
|
1663
677
|
}
|
|
678
|
+
collectionPath;
|
|
679
|
+
scopePath;
|
|
1664
680
|
adapter;
|
|
1665
681
|
pipelineAdapter;
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
staticRegistry;
|
|
1670
|
-
// Dynamic mode
|
|
1671
|
-
dynamicConfig;
|
|
1672
|
-
bootstrapRegistry;
|
|
1673
|
-
dynamicRegistry;
|
|
1674
|
-
metaAdapter;
|
|
1675
|
-
metaPipelineAdapter;
|
|
1676
|
-
// Subgraph scope tracking
|
|
1677
|
-
scopePath;
|
|
1678
|
-
// Migration settings
|
|
1679
|
-
globalWriteBack;
|
|
1680
|
-
migrationSandbox;
|
|
1681
|
-
// ---------------------------------------------------------------------------
|
|
1682
|
-
// Registry routing
|
|
1683
|
-
// ---------------------------------------------------------------------------
|
|
1684
|
-
/**
|
|
1685
|
-
* Get the appropriate registry for validating a write to the given type.
|
|
1686
|
-
*
|
|
1687
|
-
* - Static-only mode: returns staticRegistry (or undefined if none set)
|
|
1688
|
-
* - Dynamic mode (pure or merged):
|
|
1689
|
-
* - Meta-types (nodeType, edgeType): validated against bootstrapRegistry
|
|
1690
|
-
* - Domain types: validated against dynamicRegistry (falls back to
|
|
1691
|
-
* bootstrapRegistry which rejects unknown types)
|
|
1692
|
-
* - Merged mode: dynamicRegistry is a merged wrapper (static + dynamic
|
|
1693
|
-
* extension), so static entries take priority automatically.
|
|
1694
|
-
*/
|
|
1695
|
-
getRegistryForType(aType) {
|
|
1696
|
-
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1697
|
-
if (aType === META_NODE_TYPE || aType === META_EDGE_TYPE) {
|
|
1698
|
-
return this.bootstrapRegistry;
|
|
1699
|
-
}
|
|
1700
|
-
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
1701
|
-
}
|
|
1702
|
-
/**
|
|
1703
|
-
* Get the Firestore adapter for writing the given type.
|
|
1704
|
-
* Meta-types route to metaAdapter if a separate collection is configured.
|
|
1705
|
-
*/
|
|
1706
|
-
getAdapterForType(aType) {
|
|
1707
|
-
if (this.metaAdapter && (aType === META_NODE_TYPE || aType === META_EDGE_TYPE)) {
|
|
1708
|
-
return this.metaAdapter;
|
|
1709
|
-
}
|
|
1710
|
-
return this.adapter;
|
|
1711
|
-
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Get the combined registry for transaction/batch context.
|
|
1714
|
-
* In static-only mode, returns staticRegistry.
|
|
1715
|
-
* In dynamic mode, returns dynamicRegistry (which includes bootstrap entries)
|
|
1716
|
-
* or falls back to staticRegistry (merged mode) or bootstrapRegistry.
|
|
1717
|
-
*/
|
|
1718
|
-
getCombinedRegistry() {
|
|
1719
|
-
if (!this.dynamicConfig) return this.staticRegistry;
|
|
1720
|
-
return this.dynamicRegistry ?? this.staticRegistry ?? this.bootstrapRegistry;
|
|
682
|
+
// --- Reads ---
|
|
683
|
+
getDoc(docId) {
|
|
684
|
+
return this.adapter.getDoc(docId);
|
|
1721
685
|
}
|
|
1722
|
-
|
|
1723
|
-
// Query dispatch
|
|
1724
|
-
// ---------------------------------------------------------------------------
|
|
1725
|
-
/**
|
|
1726
|
-
* Dispatch a query to the appropriate adapter based on queryMode.
|
|
1727
|
-
* Pipeline queries use the PipelineQueryAdapter; standard queries
|
|
1728
|
-
* use the FirestoreAdapter.
|
|
1729
|
-
*/
|
|
1730
|
-
executeQuery(filters, options) {
|
|
686
|
+
query(filters, options) {
|
|
1731
687
|
if (this.pipelineAdapter) {
|
|
1732
688
|
return this.pipelineAdapter.query(filters, options);
|
|
1733
689
|
}
|
|
1734
690
|
return this.adapter.query(filters, options);
|
|
1735
691
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
*/
|
|
1740
|
-
checkQuerySafety(filters, allowCollectionScan) {
|
|
1741
|
-
if (allowCollectionScan || this.scanProtection === "off") return;
|
|
1742
|
-
const result = analyzeQuerySafety(filters);
|
|
1743
|
-
if (result.safe) return;
|
|
1744
|
-
if (this.scanProtection === "error") {
|
|
1745
|
-
throw new QuerySafetyError(result.reason);
|
|
1746
|
-
}
|
|
1747
|
-
console.warn(`[firegraph] Query safety warning: ${result.reason}`);
|
|
1748
|
-
}
|
|
1749
|
-
// ---------------------------------------------------------------------------
|
|
1750
|
-
// Migration helpers
|
|
1751
|
-
// ---------------------------------------------------------------------------
|
|
1752
|
-
/**
|
|
1753
|
-
* Apply migration to a single record. Returns the (possibly migrated)
|
|
1754
|
-
* record and triggers write-back if applicable.
|
|
1755
|
-
*/
|
|
1756
|
-
async applyMigration(record, docId) {
|
|
1757
|
-
const registry = this.getCombinedRegistry();
|
|
1758
|
-
if (!registry) return record;
|
|
1759
|
-
const result = await migrateRecord(record, registry, this.globalWriteBack);
|
|
1760
|
-
if (result.migrated) {
|
|
1761
|
-
this.handleWriteBack(result, docId);
|
|
1762
|
-
}
|
|
1763
|
-
return result.record;
|
|
1764
|
-
}
|
|
1765
|
-
/**
|
|
1766
|
-
* Apply migrations to an array of records. Returns all records
|
|
1767
|
-
* (migrated where applicable) and triggers write-backs.
|
|
1768
|
-
*/
|
|
1769
|
-
async applyMigrations(records) {
|
|
1770
|
-
const registry = this.getCombinedRegistry();
|
|
1771
|
-
if (!registry || records.length === 0) return records;
|
|
1772
|
-
const results = await migrateRecords(records, registry, this.globalWriteBack);
|
|
1773
|
-
for (const result of results) {
|
|
1774
|
-
if (result.migrated) {
|
|
1775
|
-
const docId = result.record.axbType === NODE_RELATION ? computeNodeDocId(result.record.aUid) : computeEdgeDocId(result.record.aUid, result.record.axbType, result.record.bUid);
|
|
1776
|
-
this.handleWriteBack(result, docId);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
return results.map((r) => r.record);
|
|
1780
|
-
}
|
|
1781
|
-
/**
|
|
1782
|
-
* Handle write-back for a migrated record based on the resolved mode.
|
|
1783
|
-
*
|
|
1784
|
-
* Both `'eager'` and `'background'` are fire-and-forget (not awaited by
|
|
1785
|
-
* the caller). The difference is logging level on failure:
|
|
1786
|
-
* - `eager`: logs an error via `console.error`
|
|
1787
|
-
* - `background`: logs a warning via `console.warn`
|
|
1788
|
-
*
|
|
1789
|
-
* For truly synchronous write-back guarantees, use transactions — the
|
|
1790
|
-
* `GraphTransactionImpl` performs write-back inline within the transaction.
|
|
1791
|
-
*/
|
|
1792
|
-
handleWriteBack(result, docId) {
|
|
1793
|
-
if (result.writeBack === "off") return;
|
|
1794
|
-
const doWriteBack = async () => {
|
|
1795
|
-
try {
|
|
1796
|
-
const update = {
|
|
1797
|
-
data: deserializeFirestoreTypes(result.record.data, this.db),
|
|
1798
|
-
updatedAt: FieldValue5.serverTimestamp()
|
|
1799
|
-
};
|
|
1800
|
-
if (result.record.v !== void 0) {
|
|
1801
|
-
update.v = result.record.v;
|
|
1802
|
-
}
|
|
1803
|
-
await this.adapter.updateDoc(docId, update);
|
|
1804
|
-
} catch (err) {
|
|
1805
|
-
const msg = `[firegraph] Migration write-back failed for ${docId}: ${err.message}`;
|
|
1806
|
-
if (result.writeBack === "eager") {
|
|
1807
|
-
console.error(msg);
|
|
1808
|
-
} else {
|
|
1809
|
-
console.warn(msg);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
};
|
|
1813
|
-
void doWriteBack();
|
|
1814
|
-
}
|
|
1815
|
-
// ---------------------------------------------------------------------------
|
|
1816
|
-
// GraphReader
|
|
1817
|
-
// ---------------------------------------------------------------------------
|
|
1818
|
-
async getNode(uid) {
|
|
1819
|
-
const docId = computeNodeDocId(uid);
|
|
1820
|
-
const record = await this.adapter.getDoc(docId);
|
|
1821
|
-
if (!record) return null;
|
|
1822
|
-
return this.applyMigration(record, docId);
|
|
1823
|
-
}
|
|
1824
|
-
async getEdge(aUid, axbType, bUid) {
|
|
1825
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1826
|
-
const record = await this.adapter.getDoc(docId);
|
|
1827
|
-
if (!record) return null;
|
|
1828
|
-
return this.applyMigration(record, docId);
|
|
1829
|
-
}
|
|
1830
|
-
async edgeExists(aUid, axbType, bUid) {
|
|
1831
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1832
|
-
const record = await this.adapter.getDoc(docId);
|
|
1833
|
-
return record !== null;
|
|
1834
|
-
}
|
|
1835
|
-
async findEdges(params) {
|
|
1836
|
-
const plan = buildEdgeQueryPlan(params);
|
|
1837
|
-
let records;
|
|
1838
|
-
if (plan.strategy === "get") {
|
|
1839
|
-
const record = await this.adapter.getDoc(plan.docId);
|
|
1840
|
-
records = record ? [record] : [];
|
|
1841
|
-
} else {
|
|
1842
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1843
|
-
records = await this.executeQuery(plan.filters, plan.options);
|
|
1844
|
-
}
|
|
1845
|
-
return this.applyMigrations(records);
|
|
1846
|
-
}
|
|
1847
|
-
async findNodes(params) {
|
|
1848
|
-
const plan = buildNodeQueryPlan(params);
|
|
1849
|
-
let records;
|
|
1850
|
-
if (plan.strategy === "get") {
|
|
1851
|
-
const record = await this.adapter.getDoc(plan.docId);
|
|
1852
|
-
records = record ? [record] : [];
|
|
1853
|
-
} else {
|
|
1854
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1855
|
-
records = await this.executeQuery(plan.filters, plan.options);
|
|
1856
|
-
}
|
|
1857
|
-
return this.applyMigrations(records);
|
|
1858
|
-
}
|
|
1859
|
-
// ---------------------------------------------------------------------------
|
|
1860
|
-
// GraphWriter
|
|
1861
|
-
// ---------------------------------------------------------------------------
|
|
1862
|
-
async putNode(aType, uid, data) {
|
|
1863
|
-
const registry = this.getRegistryForType(aType);
|
|
1864
|
-
if (registry) {
|
|
1865
|
-
registry.validate(aType, NODE_RELATION, aType, data, this.scopePath);
|
|
1866
|
-
}
|
|
1867
|
-
const adapter = this.getAdapterForType(aType);
|
|
1868
|
-
const docId = computeNodeDocId(uid);
|
|
1869
|
-
const record = buildNodeRecord(aType, uid, data);
|
|
1870
|
-
if (registry) {
|
|
1871
|
-
const entry = registry.lookup(aType, NODE_RELATION, aType);
|
|
1872
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1873
|
-
record.v = entry.schemaVersion;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
await adapter.setDoc(docId, record);
|
|
1877
|
-
}
|
|
1878
|
-
async putEdge(aType, aUid, axbType, bType, bUid, data) {
|
|
1879
|
-
const registry = this.getRegistryForType(aType);
|
|
1880
|
-
if (registry) {
|
|
1881
|
-
registry.validate(aType, axbType, bType, data, this.scopePath);
|
|
1882
|
-
}
|
|
1883
|
-
const adapter = this.getAdapterForType(aType);
|
|
1884
|
-
const docId = computeEdgeDocId(aUid, axbType, bUid);
|
|
1885
|
-
const record = buildEdgeRecord(aType, aUid, axbType, bType, bUid, data);
|
|
1886
|
-
if (registry) {
|
|
1887
|
-
const entry = registry.lookup(aType, axbType, bType);
|
|
1888
|
-
if (entry?.schemaVersion && entry.schemaVersion > 0) {
|
|
1889
|
-
record.v = entry.schemaVersion;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
await adapter.setDoc(docId, record);
|
|
1893
|
-
}
|
|
1894
|
-
async updateNode(uid, data) {
|
|
1895
|
-
const docId = computeNodeDocId(uid);
|
|
1896
|
-
const update = {
|
|
1897
|
-
updatedAt: FieldValue5.serverTimestamp()
|
|
1898
|
-
};
|
|
1899
|
-
for (const [k, v] of Object.entries(data)) {
|
|
1900
|
-
update[`data.${k}`] = v;
|
|
1901
|
-
}
|
|
1902
|
-
await this.adapter.updateDoc(docId, update);
|
|
692
|
+
// --- Writes ---
|
|
693
|
+
setDoc(docId, record) {
|
|
694
|
+
return this.adapter.setDoc(docId, stampWritableRecord(record));
|
|
1903
695
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
await this.adapter.deleteDoc(docId);
|
|
696
|
+
updateDoc(docId, update) {
|
|
697
|
+
return this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
1907
698
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
await this.adapter.deleteDoc(docId);
|
|
699
|
+
deleteDoc(docId) {
|
|
700
|
+
return this.adapter.deleteDoc(docId);
|
|
1911
701
|
}
|
|
1912
|
-
//
|
|
1913
|
-
|
|
1914
|
-
// ---------------------------------------------------------------------------
|
|
1915
|
-
async runTransaction(fn) {
|
|
702
|
+
// --- Transactions / Batches ---
|
|
703
|
+
runTransaction(fn) {
|
|
1916
704
|
return this.db.runTransaction(async (firestoreTx) => {
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
this.adapter.collectionPath,
|
|
1920
|
-
firestoreTx
|
|
1921
|
-
);
|
|
1922
|
-
const graphTx = new GraphTransactionImpl(adapter, this.getCombinedRegistry(), this.scanProtection, this.scopePath, this.globalWriteBack, this.db);
|
|
1923
|
-
return fn(graphTx);
|
|
705
|
+
const txAdapter = createTransactionAdapter(this.db, this.collectionPath, firestoreTx);
|
|
706
|
+
return fn(new FirestoreTransactionBackend(txAdapter, this.db));
|
|
1924
707
|
});
|
|
1925
708
|
}
|
|
1926
|
-
|
|
1927
|
-
const
|
|
1928
|
-
return new
|
|
709
|
+
createBatch() {
|
|
710
|
+
const batchAdapter = createBatchAdapter(this.db, this.collectionPath);
|
|
711
|
+
return new FirestoreBatchBackend(batchAdapter, this.db);
|
|
1929
712
|
}
|
|
1930
|
-
//
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
}
|
|
1940
|
-
if (name.includes("/")) {
|
|
1941
|
-
throw new FiregraphError(
|
|
1942
|
-
`Subgraph name must not contain "/": got "${name}". Use chained .subgraph() calls for nested subgraphs.`,
|
|
1943
|
-
"INVALID_SUBGRAPH"
|
|
1944
|
-
);
|
|
1945
|
-
}
|
|
1946
|
-
const subCollectionPath = `${this.adapter.collectionPath}/${parentNodeUid}/${name}`;
|
|
1947
|
-
const newScopePath = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
1948
|
-
return new _GraphClientImpl(
|
|
1949
|
-
this.db,
|
|
1950
|
-
subCollectionPath,
|
|
1951
|
-
{
|
|
1952
|
-
registry: this.getCombinedRegistry(),
|
|
1953
|
-
queryMode: this.queryMode === "pipeline" ? "pipeline" : "standard",
|
|
1954
|
-
scanProtection: this.scanProtection,
|
|
1955
|
-
migrationWriteBack: this.globalWriteBack,
|
|
1956
|
-
migrationSandbox: this.migrationSandbox
|
|
1957
|
-
},
|
|
1958
|
-
newScopePath
|
|
1959
|
-
);
|
|
713
|
+
// --- Subgraphs ---
|
|
714
|
+
subgraph(parentNodeUid, name) {
|
|
715
|
+
const subPath = `${this.collectionPath}/${parentNodeUid}/${name}`;
|
|
716
|
+
const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
717
|
+
return new _FirestoreBackendImpl(this.db, subPath, this.queryMode, newScope);
|
|
718
|
+
}
|
|
719
|
+
// --- Cascade & bulk ---
|
|
720
|
+
removeNodeCascade(uid, reader, options) {
|
|
721
|
+
return removeNodeCascade(this.db, this.collectionPath, reader, uid, options);
|
|
1960
722
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
723
|
+
bulkRemoveEdges(params, reader, options) {
|
|
724
|
+
return bulkRemoveEdges(this.db, this.collectionPath, reader, params, options);
|
|
725
|
+
}
|
|
726
|
+
// --- Cross-collection ---
|
|
1964
727
|
async findEdgesGlobal(params, collectionName) {
|
|
1965
|
-
const name = collectionName ?? this.
|
|
728
|
+
const name = collectionName ?? this.collectionPath.split("/").pop();
|
|
1966
729
|
const plan = buildEdgeQueryPlan(params);
|
|
1967
730
|
if (plan.strategy === "get") {
|
|
1968
731
|
throw new FiregraphError(
|
|
@@ -1970,7 +733,6 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1970
733
|
"INVALID_QUERY"
|
|
1971
734
|
);
|
|
1972
735
|
}
|
|
1973
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1974
736
|
const collectionGroupRef = this.db.collectionGroup(name);
|
|
1975
737
|
let q = collectionGroupRef;
|
|
1976
738
|
for (const f of plan.filters) {
|
|
@@ -1983,170 +745,35 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1983
745
|
q = q.limit(plan.options.limit);
|
|
1984
746
|
}
|
|
1985
747
|
const snap = await q.get();
|
|
1986
|
-
|
|
1987
|
-
return this.applyMigrations(records);
|
|
1988
|
-
}
|
|
1989
|
-
// ---------------------------------------------------------------------------
|
|
1990
|
-
// Bulk operations
|
|
1991
|
-
// ---------------------------------------------------------------------------
|
|
1992
|
-
async removeNodeCascade(uid, options) {
|
|
1993
|
-
return removeNodeCascade(this.db, this.adapter.collectionPath, this, uid, options);
|
|
1994
|
-
}
|
|
1995
|
-
async bulkRemoveEdges(params, options) {
|
|
1996
|
-
return bulkRemoveEdges(this.db, this.adapter.collectionPath, this, params, options);
|
|
1997
|
-
}
|
|
1998
|
-
// ---------------------------------------------------------------------------
|
|
1999
|
-
// Dynamic registry methods
|
|
2000
|
-
// ---------------------------------------------------------------------------
|
|
2001
|
-
async defineNodeType(name, jsonSchema, description, options) {
|
|
2002
|
-
if (!this.dynamicConfig) {
|
|
2003
|
-
throw new DynamicRegistryError(
|
|
2004
|
-
'defineNodeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
2005
|
-
);
|
|
2006
|
-
}
|
|
2007
|
-
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
2008
|
-
throw new DynamicRegistryError(
|
|
2009
|
-
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
if (this.staticRegistry?.lookup(name, NODE_RELATION, name)) {
|
|
2013
|
-
throw new DynamicRegistryError(
|
|
2014
|
-
`Cannot define node type "${name}": already defined in the static registry.`
|
|
2015
|
-
);
|
|
2016
|
-
}
|
|
2017
|
-
const uid = generateDeterministicUid(META_NODE_TYPE, name);
|
|
2018
|
-
const data = { name, jsonSchema };
|
|
2019
|
-
if (description !== void 0) data.description = description;
|
|
2020
|
-
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
2021
|
-
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
2022
|
-
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
2023
|
-
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
2024
|
-
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
2025
|
-
if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
|
|
2026
|
-
if (options?.migrations !== void 0) {
|
|
2027
|
-
data.migrations = await this.serializeMigrations(options.migrations);
|
|
2028
|
-
}
|
|
2029
|
-
await this.putNode(META_NODE_TYPE, uid, data);
|
|
2030
|
-
}
|
|
2031
|
-
async defineEdgeType(name, topology, jsonSchema, description, options) {
|
|
2032
|
-
if (!this.dynamicConfig) {
|
|
2033
|
-
throw new DynamicRegistryError(
|
|
2034
|
-
'defineEdgeType() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
2035
|
-
);
|
|
2036
|
-
}
|
|
2037
|
-
if (RESERVED_TYPE_NAMES.has(name)) {
|
|
2038
|
-
throw new DynamicRegistryError(
|
|
2039
|
-
`Cannot define type "${name}": this name is reserved for the meta-registry.`
|
|
2040
|
-
);
|
|
2041
|
-
}
|
|
2042
|
-
if (this.staticRegistry) {
|
|
2043
|
-
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
2044
|
-
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
2045
|
-
for (const aType of fromTypes) {
|
|
2046
|
-
for (const bType of toTypes) {
|
|
2047
|
-
if (this.staticRegistry.lookup(aType, name, bType)) {
|
|
2048
|
-
throw new DynamicRegistryError(
|
|
2049
|
-
`Cannot define edge type "${name}" for (${aType}) -> (${bType}): already defined in the static registry.`
|
|
2050
|
-
);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
const uid = generateDeterministicUid(META_EDGE_TYPE, name);
|
|
2056
|
-
const data = {
|
|
2057
|
-
name,
|
|
2058
|
-
from: topology.from,
|
|
2059
|
-
to: topology.to
|
|
2060
|
-
};
|
|
2061
|
-
if (jsonSchema !== void 0) data.jsonSchema = jsonSchema;
|
|
2062
|
-
if (topology.inverseLabel !== void 0) data.inverseLabel = topology.inverseLabel;
|
|
2063
|
-
if (topology.targetGraph !== void 0) data.targetGraph = topology.targetGraph;
|
|
2064
|
-
if (description !== void 0) data.description = description;
|
|
2065
|
-
if (options?.titleField !== void 0) data.titleField = options.titleField;
|
|
2066
|
-
if (options?.subtitleField !== void 0) data.subtitleField = options.subtitleField;
|
|
2067
|
-
if (options?.viewTemplate !== void 0) data.viewTemplate = options.viewTemplate;
|
|
2068
|
-
if (options?.viewCss !== void 0) data.viewCss = options.viewCss;
|
|
2069
|
-
if (options?.allowedIn !== void 0) data.allowedIn = options.allowedIn;
|
|
2070
|
-
if (options?.migrationWriteBack !== void 0) data.migrationWriteBack = options.migrationWriteBack;
|
|
2071
|
-
if (options?.migrations !== void 0) {
|
|
2072
|
-
data.migrations = await this.serializeMigrations(options.migrations);
|
|
2073
|
-
}
|
|
2074
|
-
await this.putNode(META_EDGE_TYPE, uid, data);
|
|
2075
|
-
}
|
|
2076
|
-
async reloadRegistry() {
|
|
2077
|
-
if (!this.dynamicConfig) {
|
|
2078
|
-
throw new DynamicRegistryError(
|
|
2079
|
-
'reloadRegistry() is only available in dynamic registry mode. Pass registryMode: { mode: "dynamic" } to createGraphClient().'
|
|
2080
|
-
);
|
|
2081
|
-
}
|
|
2082
|
-
const reader = this.createMetaReader();
|
|
2083
|
-
const dynamicOnly = await createRegistryFromGraph(reader, this.migrationSandbox);
|
|
2084
|
-
if (this.staticRegistry) {
|
|
2085
|
-
this.dynamicRegistry = createMergedRegistry(this.staticRegistry, dynamicOnly);
|
|
2086
|
-
} else {
|
|
2087
|
-
this.dynamicRegistry = dynamicOnly;
|
|
2088
|
-
}
|
|
748
|
+
return snap.docs.map((doc) => doc.data());
|
|
2089
749
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
750
|
+
};
|
|
751
|
+
function createFirestoreBackend(db, collectionPath, options = {}) {
|
|
752
|
+
const queryMode = options.queryMode ?? "pipeline";
|
|
753
|
+
const scopePath = options.scopePath ?? "";
|
|
754
|
+
return new FirestoreBackendImpl(db, collectionPath, queryMode, scopePath);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/firestore.ts
|
|
758
|
+
var _standardModeWarned = false;
|
|
759
|
+
function createGraphClient(db, collectionPath, options) {
|
|
760
|
+
const requestedMode = options?.queryMode ?? "pipeline";
|
|
761
|
+
const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
|
|
762
|
+
const effectiveMode = isEmulator ? "standard" : requestedMode;
|
|
763
|
+
if (effectiveMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
|
|
764
|
+
_standardModeWarned = true;
|
|
765
|
+
console.warn(
|
|
766
|
+
"[firegraph] Standard query mode enabled. This is NOT recommended for production:\n - Enterprise Firestore: data.* filters cause full collection scans (high billing)\n - Standard Firestore: data.* filters without composite indexes will fail\n See: https://github.com/typicalday/firegraph#query-modes"
|
|
2102
767
|
);
|
|
2103
|
-
return result;
|
|
2104
768
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
if (!this.metaAdapter) return this;
|
|
2112
|
-
const adapter = this.metaAdapter;
|
|
2113
|
-
const pipelineAdapter = this.metaPipelineAdapter;
|
|
2114
|
-
const executeMetaQuery = (filters, options) => {
|
|
2115
|
-
if (pipelineAdapter) return pipelineAdapter.query(filters, options);
|
|
2116
|
-
return adapter.query(filters, options);
|
|
2117
|
-
};
|
|
2118
|
-
return {
|
|
2119
|
-
async getNode(uid) {
|
|
2120
|
-
return adapter.getDoc(computeNodeDocId(uid));
|
|
2121
|
-
},
|
|
2122
|
-
async getEdge(aUid, axbType, bUid) {
|
|
2123
|
-
return adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
2124
|
-
},
|
|
2125
|
-
async edgeExists(aUid, axbType, bUid) {
|
|
2126
|
-
const record = await adapter.getDoc(computeEdgeDocId(aUid, axbType, bUid));
|
|
2127
|
-
return record !== null;
|
|
2128
|
-
},
|
|
2129
|
-
async findEdges(params) {
|
|
2130
|
-
const plan = buildEdgeQueryPlan(params);
|
|
2131
|
-
if (plan.strategy === "get") {
|
|
2132
|
-
const record = await adapter.getDoc(plan.docId);
|
|
2133
|
-
return record ? [record] : [];
|
|
2134
|
-
}
|
|
2135
|
-
return executeMetaQuery(plan.filters, plan.options);
|
|
2136
|
-
},
|
|
2137
|
-
async findNodes(params) {
|
|
2138
|
-
const plan = buildNodeQueryPlan(params);
|
|
2139
|
-
if (plan.strategy === "get") {
|
|
2140
|
-
const record = await adapter.getDoc(plan.docId);
|
|
2141
|
-
return record ? [record] : [];
|
|
2142
|
-
}
|
|
2143
|
-
return executeMetaQuery(plan.filters, plan.options);
|
|
2144
|
-
}
|
|
2145
|
-
};
|
|
769
|
+
const backend = createFirestoreBackend(db, collectionPath, { queryMode: effectiveMode });
|
|
770
|
+
let metaBackend;
|
|
771
|
+
if (options?.registryMode?.collection && options.registryMode.collection !== collectionPath) {
|
|
772
|
+
metaBackend = createFirestoreBackend(db, options.registryMode.collection, {
|
|
773
|
+
queryMode: effectiveMode
|
|
774
|
+
});
|
|
2146
775
|
}
|
|
2147
|
-
|
|
2148
|
-
function createGraphClient(db, collectionPath, options) {
|
|
2149
|
-
return new GraphClientImpl(db, collectionPath, options);
|
|
776
|
+
return new GraphClientImpl(backend, options, metaBackend);
|
|
2150
777
|
}
|
|
2151
778
|
|
|
2152
779
|
// src/id.ts
|
|
@@ -2155,22 +782,175 @@ function generateId() {
|
|
|
2155
782
|
return nanoid();
|
|
2156
783
|
}
|
|
2157
784
|
|
|
2158
|
-
// src/
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
785
|
+
// src/indexes.ts
|
|
786
|
+
function baseIndexes(collection) {
|
|
787
|
+
return [
|
|
788
|
+
{
|
|
789
|
+
collectionGroup: collection,
|
|
790
|
+
queryScope: "COLLECTION",
|
|
791
|
+
fields: [
|
|
792
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
793
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
794
|
+
]
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
collectionGroup: collection,
|
|
798
|
+
queryScope: "COLLECTION",
|
|
799
|
+
fields: [
|
|
800
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
801
|
+
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
802
|
+
]
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
collectionGroup: collection,
|
|
806
|
+
queryScope: "COLLECTION",
|
|
807
|
+
fields: [
|
|
808
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
809
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
810
|
+
]
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
collectionGroup: collection,
|
|
814
|
+
queryScope: "COLLECTION",
|
|
815
|
+
fields: [
|
|
816
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
817
|
+
{ fieldPath: "bType", order: "ASCENDING" }
|
|
818
|
+
]
|
|
819
|
+
}
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
function extractSchemaFields(schema) {
|
|
823
|
+
const s = schema;
|
|
824
|
+
if (s.type !== "object" || !s.properties) return [];
|
|
825
|
+
return Object.keys(s.properties);
|
|
826
|
+
}
|
|
827
|
+
function collectionGroupIndexes(collectionName) {
|
|
828
|
+
return [
|
|
829
|
+
{
|
|
830
|
+
collectionGroup: collectionName,
|
|
831
|
+
queryScope: "COLLECTION_GROUP",
|
|
832
|
+
fields: [
|
|
833
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
834
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
835
|
+
]
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
collectionGroup: collectionName,
|
|
839
|
+
queryScope: "COLLECTION_GROUP",
|
|
840
|
+
fields: [
|
|
841
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
842
|
+
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
843
|
+
]
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
collectionGroup: collectionName,
|
|
847
|
+
queryScope: "COLLECTION_GROUP",
|
|
848
|
+
fields: [
|
|
849
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
850
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
851
|
+
]
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
collectionGroup: collectionName,
|
|
855
|
+
queryScope: "COLLECTION_GROUP",
|
|
856
|
+
fields: [
|
|
857
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
858
|
+
{ fieldPath: "bType", order: "ASCENDING" }
|
|
859
|
+
]
|
|
860
|
+
}
|
|
861
|
+
];
|
|
862
|
+
}
|
|
863
|
+
function generateIndexConfig(collection, entities, registryEntries) {
|
|
864
|
+
const indexes = baseIndexes(collection);
|
|
865
|
+
if (entities) {
|
|
866
|
+
for (const [, entity] of entities.nodes) {
|
|
867
|
+
const fields = extractSchemaFields(entity.schema);
|
|
868
|
+
for (const field of fields) {
|
|
869
|
+
indexes.push({
|
|
870
|
+
collectionGroup: collection,
|
|
871
|
+
queryScope: "COLLECTION",
|
|
872
|
+
fields: [
|
|
873
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
874
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
875
|
+
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
876
|
+
]
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
for (const [, entity] of entities.edges) {
|
|
881
|
+
const fields = extractSchemaFields(entity.schema);
|
|
882
|
+
for (const field of fields) {
|
|
883
|
+
indexes.push({
|
|
884
|
+
collectionGroup: collection,
|
|
885
|
+
queryScope: "COLLECTION",
|
|
886
|
+
fields: [
|
|
887
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
888
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
889
|
+
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
890
|
+
]
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (registryEntries) {
|
|
896
|
+
const targetGraphNames = /* @__PURE__ */ new Set();
|
|
897
|
+
for (const entry of registryEntries) {
|
|
898
|
+
if (entry.targetGraph) {
|
|
899
|
+
targetGraphNames.add(entry.targetGraph);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
for (const name of targetGraphNames) {
|
|
903
|
+
indexes.push(...collectionGroupIndexes(name));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return { indexes, fieldOverrides: [] };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/record.ts
|
|
910
|
+
import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
|
|
911
|
+
function buildNodeRecord(aType, uid, data) {
|
|
912
|
+
const now = FieldValue2.serverTimestamp();
|
|
913
|
+
return {
|
|
914
|
+
aType,
|
|
915
|
+
aUid: uid,
|
|
916
|
+
axbType: NODE_RELATION,
|
|
917
|
+
bType: aType,
|
|
918
|
+
bUid: uid,
|
|
919
|
+
data,
|
|
920
|
+
createdAt: now,
|
|
921
|
+
updatedAt: now
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function buildEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
|
|
925
|
+
const now = FieldValue2.serverTimestamp();
|
|
926
|
+
return {
|
|
927
|
+
aType,
|
|
928
|
+
aUid,
|
|
929
|
+
axbType,
|
|
930
|
+
bType,
|
|
931
|
+
bUid,
|
|
932
|
+
data,
|
|
933
|
+
createdAt: now,
|
|
934
|
+
updatedAt: now
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/traverse.ts
|
|
939
|
+
var DEFAULT_LIMIT = 10;
|
|
940
|
+
var DEFAULT_MAX_READS = 100;
|
|
941
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
942
|
+
var _crossGraphWarned = false;
|
|
943
|
+
function isGraphClient(reader) {
|
|
944
|
+
return "subgraph" in reader && typeof reader.subgraph === "function";
|
|
945
|
+
}
|
|
946
|
+
var Semaphore = class {
|
|
947
|
+
constructor(slots) {
|
|
948
|
+
this.slots = slots;
|
|
949
|
+
}
|
|
950
|
+
queue = [];
|
|
951
|
+
active = 0;
|
|
952
|
+
async acquire() {
|
|
953
|
+
if (this.active < this.slots) {
|
|
2174
954
|
this.active++;
|
|
2175
955
|
return;
|
|
2176
956
|
}
|
|
@@ -2359,7 +1139,6 @@ function getCustomElements() {
|
|
|
2359
1139
|
function resilientView(ViewClass, tagName) {
|
|
2360
1140
|
const g = globalThis;
|
|
2361
1141
|
if (!g.HTMLElement) return ViewClass;
|
|
2362
|
-
const Base = g.HTMLElement;
|
|
2363
1142
|
const Wrapped = class extends ViewClass {
|
|
2364
1143
|
connectedCallback() {
|
|
2365
1144
|
try {
|
|
@@ -2444,369 +1223,9 @@ function defineViews(input) {
|
|
|
2444
1223
|
}
|
|
2445
1224
|
return { nodes, edges };
|
|
2446
1225
|
}
|
|
2447
|
-
|
|
2448
|
-
// src/config.ts
|
|
2449
|
-
function defineConfig(config) {
|
|
2450
|
-
return config;
|
|
2451
|
-
}
|
|
2452
|
-
function resolveView(resolverConfig, availableViewNames, context) {
|
|
2453
|
-
if (!resolverConfig) return "json";
|
|
2454
|
-
const available = new Set(availableViewNames);
|
|
2455
|
-
if (context) {
|
|
2456
|
-
const contextDefault = resolverConfig[context];
|
|
2457
|
-
if (contextDefault && available.has(contextDefault)) {
|
|
2458
|
-
return contextDefault;
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
if (resolverConfig.default && available.has(resolverConfig.default)) {
|
|
2462
|
-
return resolverConfig.default;
|
|
2463
|
-
}
|
|
2464
|
-
return "json";
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
// src/discover.ts
|
|
2468
|
-
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
2469
|
-
import { createRequire } from "module";
|
|
2470
|
-
import { join, resolve } from "path";
|
|
2471
|
-
var DiscoveryError = class extends FiregraphError {
|
|
2472
|
-
constructor(message) {
|
|
2473
|
-
super(message, "DISCOVERY_ERROR");
|
|
2474
|
-
this.name = "DiscoveryError";
|
|
2475
|
-
}
|
|
2476
|
-
};
|
|
2477
|
-
function readJson(filePath) {
|
|
2478
|
-
try {
|
|
2479
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
2480
|
-
return JSON.parse(raw);
|
|
2481
|
-
} catch (err) {
|
|
2482
|
-
const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
|
|
2483
|
-
throw new DiscoveryError(msg);
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
function readJsonIfExists(filePath) {
|
|
2487
|
-
if (!existsSync(filePath)) return void 0;
|
|
2488
|
-
return readJson(filePath);
|
|
2489
|
-
}
|
|
2490
|
-
var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
2491
|
-
function loadSchema(dir, entityLabel) {
|
|
2492
|
-
for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
|
|
2493
|
-
const candidate = join(dir, `schema${ext}`);
|
|
2494
|
-
if (existsSync(candidate)) {
|
|
2495
|
-
return loadSchemaModule(candidate, entityLabel);
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2498
|
-
const jsonPath = join(dir, "schema.json");
|
|
2499
|
-
if (existsSync(jsonPath)) {
|
|
2500
|
-
return readJson(jsonPath);
|
|
2501
|
-
}
|
|
2502
|
-
throw new DiscoveryError(
|
|
2503
|
-
`Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
|
|
2504
|
-
);
|
|
2505
|
-
}
|
|
2506
|
-
var _jiti;
|
|
2507
|
-
function getJiti() {
|
|
2508
|
-
if (!_jiti) {
|
|
2509
|
-
const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
|
|
2510
|
-
const esmRequire = createRequire(base);
|
|
2511
|
-
const { createJiti } = esmRequire("jiti");
|
|
2512
|
-
_jiti = createJiti(base, { interopDefault: true });
|
|
2513
|
-
}
|
|
2514
|
-
return _jiti;
|
|
2515
|
-
}
|
|
2516
|
-
function loadSchemaModule(filePath, entityLabel) {
|
|
2517
|
-
try {
|
|
2518
|
-
const jiti = getJiti();
|
|
2519
|
-
const mod = jiti(filePath);
|
|
2520
|
-
const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
2521
|
-
if (!schema || typeof schema !== "object") {
|
|
2522
|
-
throw new DiscoveryError(
|
|
2523
|
-
`Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
|
|
2524
|
-
);
|
|
2525
|
-
}
|
|
2526
|
-
return schema;
|
|
2527
|
-
} catch (err) {
|
|
2528
|
-
if (err instanceof DiscoveryError) throw err;
|
|
2529
|
-
throw new DiscoveryError(
|
|
2530
|
-
`Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
|
|
2531
|
-
);
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
2535
|
-
function findViewsFile(dir) {
|
|
2536
|
-
for (const ext of VIEW_EXTENSIONS) {
|
|
2537
|
-
const candidate = join(dir, `views${ext}`);
|
|
2538
|
-
if (existsSync(candidate)) return candidate;
|
|
2539
|
-
}
|
|
2540
|
-
return void 0;
|
|
2541
|
-
}
|
|
2542
|
-
var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
2543
|
-
function findMigrationsFile(dir) {
|
|
2544
|
-
for (const ext of MIGRATION_EXTENSIONS) {
|
|
2545
|
-
const candidate = join(dir, `migrations${ext}`);
|
|
2546
|
-
if (existsSync(candidate)) return candidate;
|
|
2547
|
-
}
|
|
2548
|
-
return void 0;
|
|
2549
|
-
}
|
|
2550
|
-
function loadMigrations(filePath, entityLabel) {
|
|
2551
|
-
try {
|
|
2552
|
-
const jiti = getJiti();
|
|
2553
|
-
const mod = jiti(filePath);
|
|
2554
|
-
const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
2555
|
-
if (!Array.isArray(migrations)) {
|
|
2556
|
-
throw new DiscoveryError(
|
|
2557
|
-
`Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
|
|
2558
|
-
);
|
|
2559
|
-
}
|
|
2560
|
-
return migrations;
|
|
2561
|
-
} catch (err) {
|
|
2562
|
-
if (err instanceof DiscoveryError) throw err;
|
|
2563
|
-
throw new DiscoveryError(
|
|
2564
|
-
`Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
|
|
2565
|
-
);
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
function loadNodeEntity(dir, name) {
|
|
2569
|
-
const schema = loadSchema(dir, `node type "${name}"`);
|
|
2570
|
-
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
2571
|
-
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
2572
|
-
const viewsPath = findViewsFile(dir);
|
|
2573
|
-
const migrationsPath = findMigrationsFile(dir);
|
|
2574
|
-
const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
|
|
2575
|
-
return {
|
|
2576
|
-
kind: "node",
|
|
2577
|
-
name,
|
|
2578
|
-
schema,
|
|
2579
|
-
description: meta?.description,
|
|
2580
|
-
titleField: meta?.titleField,
|
|
2581
|
-
subtitleField: meta?.subtitleField,
|
|
2582
|
-
viewDefaults: meta?.viewDefaults,
|
|
2583
|
-
viewsPath,
|
|
2584
|
-
sampleData,
|
|
2585
|
-
allowedIn: meta?.allowedIn,
|
|
2586
|
-
migrations,
|
|
2587
|
-
migrationWriteBack: meta?.migrationWriteBack
|
|
2588
|
-
};
|
|
2589
|
-
}
|
|
2590
|
-
function loadEdgeEntity(dir, name) {
|
|
2591
|
-
const schema = loadSchema(dir, `edge type "${name}"`);
|
|
2592
|
-
const edgePath = join(dir, "edge.json");
|
|
2593
|
-
if (!existsSync(edgePath)) {
|
|
2594
|
-
throw new DiscoveryError(
|
|
2595
|
-
`Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
|
|
2596
|
-
);
|
|
2597
|
-
}
|
|
2598
|
-
const topology = readJson(edgePath);
|
|
2599
|
-
if (!topology.from) {
|
|
2600
|
-
throw new DiscoveryError(
|
|
2601
|
-
`edge.json for "${name}" is missing required "from" field`
|
|
2602
|
-
);
|
|
2603
|
-
}
|
|
2604
|
-
if (!topology.to) {
|
|
2605
|
-
throw new DiscoveryError(
|
|
2606
|
-
`edge.json for "${name}" is missing required "to" field`
|
|
2607
|
-
);
|
|
2608
|
-
}
|
|
2609
|
-
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
2610
|
-
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
2611
|
-
const viewsPath = findViewsFile(dir);
|
|
2612
|
-
const migrationsPath = findMigrationsFile(dir);
|
|
2613
|
-
const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
|
|
2614
|
-
return {
|
|
2615
|
-
kind: "edge",
|
|
2616
|
-
name,
|
|
2617
|
-
schema,
|
|
2618
|
-
topology,
|
|
2619
|
-
description: meta?.description,
|
|
2620
|
-
titleField: meta?.titleField,
|
|
2621
|
-
subtitleField: meta?.subtitleField,
|
|
2622
|
-
viewDefaults: meta?.viewDefaults,
|
|
2623
|
-
viewsPath,
|
|
2624
|
-
sampleData,
|
|
2625
|
-
allowedIn: meta?.allowedIn,
|
|
2626
|
-
targetGraph: topology.targetGraph ?? meta?.targetGraph,
|
|
2627
|
-
migrations,
|
|
2628
|
-
migrationWriteBack: meta?.migrationWriteBack
|
|
2629
|
-
};
|
|
2630
|
-
}
|
|
2631
|
-
function getSubdirectories(dir) {
|
|
2632
|
-
if (!existsSync(dir)) return [];
|
|
2633
|
-
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2634
|
-
}
|
|
2635
|
-
function discoverEntities(entitiesDir) {
|
|
2636
|
-
const absDir = resolve(entitiesDir);
|
|
2637
|
-
if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
|
|
2638
|
-
throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
|
|
2639
|
-
}
|
|
2640
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
2641
|
-
const edges = /* @__PURE__ */ new Map();
|
|
2642
|
-
const warnings = [];
|
|
2643
|
-
const nodesDir = join(absDir, "nodes");
|
|
2644
|
-
for (const name of getSubdirectories(nodesDir)) {
|
|
2645
|
-
nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
|
|
2646
|
-
}
|
|
2647
|
-
const edgesDir = join(absDir, "edges");
|
|
2648
|
-
for (const name of getSubdirectories(edgesDir)) {
|
|
2649
|
-
edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
|
|
2650
|
-
}
|
|
2651
|
-
const nodeNames = new Set(nodes.keys());
|
|
2652
|
-
for (const [axbType, entity] of edges) {
|
|
2653
|
-
const topology = entity.topology;
|
|
2654
|
-
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
2655
|
-
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
2656
|
-
for (const ref of [...fromTypes, ...toTypes]) {
|
|
2657
|
-
if (!nodeNames.has(ref)) {
|
|
2658
|
-
warnings.push({
|
|
2659
|
-
code: "DANGLING_TOPOLOGY_REF",
|
|
2660
|
-
message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
|
|
2661
|
-
});
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
return {
|
|
2666
|
-
result: { nodes, edges },
|
|
2667
|
-
warnings
|
|
2668
|
-
};
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
// src/cross-graph.ts
|
|
2672
|
-
function resolveAncestorCollection(collectionPath, uid) {
|
|
2673
|
-
const segments = collectionPath.split("/");
|
|
2674
|
-
for (let i = 1; i < segments.length; i += 2) {
|
|
2675
|
-
if (segments[i] === uid) {
|
|
2676
|
-
return segments.slice(0, i).join("/");
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
return null;
|
|
2680
|
-
}
|
|
2681
|
-
function isAncestorUid(collectionPath, uid) {
|
|
2682
|
-
return resolveAncestorCollection(collectionPath, uid) !== null;
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
// src/indexes.ts
|
|
2686
|
-
function baseIndexes(collection) {
|
|
2687
|
-
return [
|
|
2688
|
-
{
|
|
2689
|
-
collectionGroup: collection,
|
|
2690
|
-
queryScope: "COLLECTION",
|
|
2691
|
-
fields: [
|
|
2692
|
-
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
2693
|
-
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2694
|
-
]
|
|
2695
|
-
},
|
|
2696
|
-
{
|
|
2697
|
-
collectionGroup: collection,
|
|
2698
|
-
queryScope: "COLLECTION",
|
|
2699
|
-
fields: [
|
|
2700
|
-
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2701
|
-
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
2702
|
-
]
|
|
2703
|
-
},
|
|
2704
|
-
{
|
|
2705
|
-
collectionGroup: collection,
|
|
2706
|
-
queryScope: "COLLECTION",
|
|
2707
|
-
fields: [
|
|
2708
|
-
{ fieldPath: "aType", order: "ASCENDING" },
|
|
2709
|
-
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2710
|
-
]
|
|
2711
|
-
},
|
|
2712
|
-
{
|
|
2713
|
-
collectionGroup: collection,
|
|
2714
|
-
queryScope: "COLLECTION",
|
|
2715
|
-
fields: [
|
|
2716
|
-
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2717
|
-
{ fieldPath: "bType", order: "ASCENDING" }
|
|
2718
|
-
]
|
|
2719
|
-
}
|
|
2720
|
-
];
|
|
2721
|
-
}
|
|
2722
|
-
function extractSchemaFields(schema) {
|
|
2723
|
-
const s = schema;
|
|
2724
|
-
if (s.type !== "object" || !s.properties) return [];
|
|
2725
|
-
return Object.keys(s.properties);
|
|
2726
|
-
}
|
|
2727
|
-
function collectionGroupIndexes(collectionName) {
|
|
2728
|
-
return [
|
|
2729
|
-
{
|
|
2730
|
-
collectionGroup: collectionName,
|
|
2731
|
-
queryScope: "COLLECTION_GROUP",
|
|
2732
|
-
fields: [
|
|
2733
|
-
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
2734
|
-
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2735
|
-
]
|
|
2736
|
-
},
|
|
2737
|
-
{
|
|
2738
|
-
collectionGroup: collectionName,
|
|
2739
|
-
queryScope: "COLLECTION_GROUP",
|
|
2740
|
-
fields: [
|
|
2741
|
-
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2742
|
-
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
2743
|
-
]
|
|
2744
|
-
},
|
|
2745
|
-
{
|
|
2746
|
-
collectionGroup: collectionName,
|
|
2747
|
-
queryScope: "COLLECTION_GROUP",
|
|
2748
|
-
fields: [
|
|
2749
|
-
{ fieldPath: "aType", order: "ASCENDING" },
|
|
2750
|
-
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
2751
|
-
]
|
|
2752
|
-
},
|
|
2753
|
-
{
|
|
2754
|
-
collectionGroup: collectionName,
|
|
2755
|
-
queryScope: "COLLECTION_GROUP",
|
|
2756
|
-
fields: [
|
|
2757
|
-
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2758
|
-
{ fieldPath: "bType", order: "ASCENDING" }
|
|
2759
|
-
]
|
|
2760
|
-
}
|
|
2761
|
-
];
|
|
2762
|
-
}
|
|
2763
|
-
function generateIndexConfig(collection, entities, registryEntries) {
|
|
2764
|
-
const indexes = baseIndexes(collection);
|
|
2765
|
-
if (entities) {
|
|
2766
|
-
for (const [, entity] of entities.nodes) {
|
|
2767
|
-
const fields = extractSchemaFields(entity.schema);
|
|
2768
|
-
for (const field of fields) {
|
|
2769
|
-
indexes.push({
|
|
2770
|
-
collectionGroup: collection,
|
|
2771
|
-
queryScope: "COLLECTION",
|
|
2772
|
-
fields: [
|
|
2773
|
-
{ fieldPath: "aType", order: "ASCENDING" },
|
|
2774
|
-
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2775
|
-
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
2776
|
-
]
|
|
2777
|
-
});
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
for (const [, entity] of entities.edges) {
|
|
2781
|
-
const fields = extractSchemaFields(entity.schema);
|
|
2782
|
-
for (const field of fields) {
|
|
2783
|
-
indexes.push({
|
|
2784
|
-
collectionGroup: collection,
|
|
2785
|
-
queryScope: "COLLECTION",
|
|
2786
|
-
fields: [
|
|
2787
|
-
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
2788
|
-
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
2789
|
-
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
2790
|
-
]
|
|
2791
|
-
});
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
if (registryEntries) {
|
|
2796
|
-
const targetGraphNames = /* @__PURE__ */ new Set();
|
|
2797
|
-
for (const entry of registryEntries) {
|
|
2798
|
-
if (entry.targetGraph) {
|
|
2799
|
-
targetGraphNames.add(entry.targetGraph);
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
for (const name of targetGraphNames) {
|
|
2803
|
-
indexes.push(...collectionGroupIndexes(name));
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
return { indexes, fieldOverrides: [] };
|
|
2807
|
-
}
|
|
2808
1226
|
export {
|
|
2809
1227
|
BOOTSTRAP_ENTRIES,
|
|
1228
|
+
CrossBackendTransactionError,
|
|
2810
1229
|
DEFAULT_QUERY_LIMIT,
|
|
2811
1230
|
DiscoveryError,
|
|
2812
1231
|
DynamicRegistryError,
|
|
@@ -2828,6 +1247,7 @@ export {
|
|
|
2828
1247
|
TraversalError,
|
|
2829
1248
|
ValidationError,
|
|
2830
1249
|
analyzeQuerySafety,
|
|
1250
|
+
appendStorageScope,
|
|
2831
1251
|
applyMigrationChain,
|
|
2832
1252
|
buildEdgeQueryPlan,
|
|
2833
1253
|
buildEdgeRecord,
|
|
@@ -2840,6 +1260,7 @@ export {
|
|
|
2840
1260
|
computeNodeDocId,
|
|
2841
1261
|
createBootstrapRegistry,
|
|
2842
1262
|
createGraphClient,
|
|
1263
|
+
createGraphClientFromBackend,
|
|
2843
1264
|
createMergedRegistry,
|
|
2844
1265
|
createRegistry,
|
|
2845
1266
|
createRegistryFromGraph,
|
|
@@ -2854,6 +1275,7 @@ export {
|
|
|
2854
1275
|
generateId,
|
|
2855
1276
|
generateIndexConfig,
|
|
2856
1277
|
generateTypes,
|
|
1278
|
+
isAncestorScopeUid,
|
|
2857
1279
|
isAncestorUid,
|
|
2858
1280
|
isTaggedValue,
|
|
2859
1281
|
jsonSchemaToFieldMeta,
|
|
@@ -2861,8 +1283,10 @@ export {
|
|
|
2861
1283
|
matchScopeAny,
|
|
2862
1284
|
migrateRecord,
|
|
2863
1285
|
migrateRecords,
|
|
1286
|
+
parseStorageScope,
|
|
2864
1287
|
precompileSource,
|
|
2865
1288
|
resolveAncestorCollection,
|
|
1289
|
+
resolveAncestorScope,
|
|
2866
1290
|
resolveView,
|
|
2867
1291
|
serializeFirestoreTypes,
|
|
2868
1292
|
validateMigrationChain
|