@typicalday/firegraph 0.7.0 → 0.8.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/chunk-6OQW5OKO.js +110 -0
- package/dist/chunk-6OQW5OKO.js.map +1 -0
- package/dist/chunk-WOAJRVHD.js +699 -0
- package/dist/chunk-WOAJRVHD.js.map +1 -0
- package/dist/chunk-YUXOALMR.js +1653 -0
- package/dist/chunk-YUXOALMR.js.map +1 -0
- package/dist/codegen/index.d.cts +25 -1
- package/dist/codegen/index.d.ts +25 -1
- package/dist/d1.cjs +2416 -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 +75 -0
- package/dist/d1.js.map +1 -0
- package/dist/do-sqlite.cjs +2419 -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 +78 -0
- package/dist/do-sqlite.js.map +1 -0
- package/dist/editor/client/assets/{index-BIwgcRWJ.js → index-tyFcX6qG.js} +1 -1
- package/dist/editor/client/index.html +1 -1
- package/dist/editor/server/index.mjs +2054 -1984
- package/dist/index.cjs +2845 -2744
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +345 -233
- package/dist/index.d.ts +345 -233
- package/dist/index.js +710 -2295
- package/dist/index.js.map +1 -1
- package/dist/serialization-C6JNNOCS.js +13 -0
- package/dist/serialization-C6JNNOCS.js.map +1 -0
- package/dist/{index-B9aodfYD.d.cts → types-BVtx9zLv.d.cts} +26 -25
- package/dist/{index-B9aodfYD.d.ts → types-BVtx9zLv.d.ts} +26 -25
- package/package.json +39 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOOTSTRAP_ENTRIES,
|
|
3
|
+
DEFAULT_QUERY_LIMIT,
|
|
4
|
+
DynamicRegistryError,
|
|
5
|
+
EDGE_TYPE_SCHEMA,
|
|
6
|
+
EdgeNotFoundError,
|
|
7
|
+
FiregraphError,
|
|
8
|
+
GraphClientImpl,
|
|
9
|
+
InvalidQueryError,
|
|
10
|
+
META_EDGE_TYPE,
|
|
11
|
+
META_NODE_TYPE,
|
|
12
|
+
MigrationError,
|
|
13
|
+
NODE_RELATION,
|
|
14
|
+
NODE_TYPE_SCHEMA,
|
|
15
|
+
NodeNotFoundError,
|
|
16
|
+
QuerySafetyError,
|
|
17
|
+
RegistryScopeError,
|
|
18
|
+
RegistryViolationError,
|
|
19
|
+
TraversalError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
analyzeQuerySafety,
|
|
22
|
+
applyMigrationChain,
|
|
23
|
+
buildEdgeQueryPlan,
|
|
24
|
+
buildNodeQueryPlan,
|
|
25
|
+
compileMigrationFn,
|
|
26
|
+
compileMigrations,
|
|
27
|
+
compileSchema,
|
|
28
|
+
computeEdgeDocId,
|
|
29
|
+
computeNodeDocId,
|
|
30
|
+
createBootstrapRegistry,
|
|
31
|
+
createGraphClientFromBackend,
|
|
32
|
+
createMergedRegistry,
|
|
33
|
+
createRegistry,
|
|
34
|
+
createRegistryFromGraph,
|
|
35
|
+
defaultExecutor,
|
|
36
|
+
destroySandboxWorker,
|
|
37
|
+
generateDeterministicUid,
|
|
38
|
+
jsonSchemaToFieldMeta,
|
|
39
|
+
matchScope,
|
|
40
|
+
matchScopeAny,
|
|
41
|
+
migrateRecord,
|
|
42
|
+
migrateRecords,
|
|
43
|
+
precompileSource,
|
|
44
|
+
validateMigrationChain
|
|
45
|
+
} from "./chunk-YUXOALMR.js";
|
|
1
46
|
import {
|
|
2
47
|
generateTypes
|
|
3
48
|
} from "./chunk-YLGXLEUE.js";
|
|
@@ -5,834 +50,303 @@ import {
|
|
|
5
50
|
QueryClient,
|
|
6
51
|
QueryClientError
|
|
7
52
|
} from "./chunk-KFA7G37W.js";
|
|
53
|
+
import {
|
|
54
|
+
SERIALIZATION_TAG,
|
|
55
|
+
deserializeFirestoreTypes,
|
|
56
|
+
isTaggedValue,
|
|
57
|
+
serializeFirestoreTypes
|
|
58
|
+
} from "./chunk-6OQW5OKO.js";
|
|
8
59
|
|
|
9
|
-
// src/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// src/docid.ts
|
|
13
|
-
import { createHash } from "crypto";
|
|
14
|
-
|
|
15
|
-
// src/internal/constants.ts
|
|
16
|
-
var NODE_RELATION = "is";
|
|
17
|
-
var DEFAULT_QUERY_LIMIT = 500;
|
|
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;
|
|
60
|
+
// src/config.ts
|
|
61
|
+
function defineConfig(config) {
|
|
62
|
+
return config;
|
|
32
63
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
64
|
+
function resolveView(resolverConfig, availableViewNames, context) {
|
|
65
|
+
if (!resolverConfig) return "json";
|
|
66
|
+
const available = new Set(availableViewNames);
|
|
67
|
+
if (context) {
|
|
68
|
+
const contextDefault = resolverConfig[context];
|
|
69
|
+
if (contextDefault && available.has(contextDefault)) {
|
|
70
|
+
return contextDefault;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (resolverConfig.default && available.has(resolverConfig.default)) {
|
|
74
|
+
return resolverConfig.default;
|
|
75
|
+
}
|
|
76
|
+
return "json";
|
|
38
77
|
}
|
|
39
78
|
|
|
40
|
-
// src/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
bUid: uid,
|
|
50
|
-
data,
|
|
51
|
-
createdAt: now,
|
|
52
|
-
updatedAt: now
|
|
53
|
-
};
|
|
79
|
+
// src/cross-graph.ts
|
|
80
|
+
function resolveAncestorCollection(collectionPath, uid) {
|
|
81
|
+
const segments = collectionPath.split("/");
|
|
82
|
+
for (let i = 1; i < segments.length; i += 2) {
|
|
83
|
+
if (segments[i] === uid) {
|
|
84
|
+
return segments.slice(0, i).join("/");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
54
88
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
aType,
|
|
59
|
-
aUid,
|
|
60
|
-
axbType,
|
|
61
|
-
bType,
|
|
62
|
-
bUid,
|
|
63
|
-
data,
|
|
64
|
-
createdAt: now,
|
|
65
|
-
updatedAt: now
|
|
66
|
-
};
|
|
89
|
+
function isAncestorUid(collectionPath, uid) {
|
|
90
|
+
return resolveAncestorCollection(collectionPath, uid) !== null;
|
|
67
91
|
}
|
|
68
92
|
|
|
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 {
|
|
93
|
+
// src/discover.ts
|
|
94
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
95
|
+
import { createRequire } from "module";
|
|
96
|
+
import { join, resolve } from "path";
|
|
97
|
+
var DiscoveryError = class extends FiregraphError {
|
|
112
98
|
constructor(message) {
|
|
113
|
-
super(message, "
|
|
114
|
-
this.name = "
|
|
99
|
+
super(message, "DISCOVERY_ERROR");
|
|
100
|
+
this.name = "DiscoveryError";
|
|
115
101
|
}
|
|
116
102
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
function readJson(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
106
|
+
return JSON.parse(raw);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const msg = err instanceof SyntaxError ? `Invalid JSON in ${filePath}: ${err.message}` : `Cannot read ${filePath}: ${err.message}`;
|
|
109
|
+
throw new DiscoveryError(msg);
|
|
121
110
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
}
|
|
112
|
+
function readJsonIfExists(filePath) {
|
|
113
|
+
if (!existsSync(filePath)) return void 0;
|
|
114
|
+
return readJson(filePath);
|
|
115
|
+
}
|
|
116
|
+
var SCHEMA_SCRIPT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
117
|
+
function loadSchema(dir, entityLabel) {
|
|
118
|
+
for (const ext of SCHEMA_SCRIPT_EXTENSIONS) {
|
|
119
|
+
const candidate = join(dir, `schema${ext}`);
|
|
120
|
+
if (existsSync(candidate)) {
|
|
121
|
+
return loadSchemaModule(candidate, entityLabel);
|
|
122
|
+
}
|
|
127
123
|
}
|
|
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";
|
|
124
|
+
const jsonPath = join(dir, "schema.json");
|
|
125
|
+
if (existsSync(jsonPath)) {
|
|
126
|
+
return readJson(jsonPath);
|
|
136
127
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
throw new DiscoveryError(
|
|
129
|
+
`Missing schema for ${entityLabel} in ${dir}. Provide a schema.ts (or .js/.mts/.mjs) or schema.json file.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
var _jiti;
|
|
133
|
+
function getJiti() {
|
|
134
|
+
if (!_jiti) {
|
|
135
|
+
const base = typeof __filename !== "undefined" ? __filename : import.meta.url;
|
|
136
|
+
const esmRequire = createRequire(base);
|
|
137
|
+
const { createJiti } = esmRequire("jiti");
|
|
138
|
+
_jiti = createJiti(base, { interopDefault: true });
|
|
142
139
|
}
|
|
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 });
|
|
140
|
+
return _jiti;
|
|
141
|
+
}
|
|
142
|
+
function loadSchemaModule(filePath, entityLabel) {
|
|
143
|
+
try {
|
|
144
|
+
const jiti = getJiti();
|
|
145
|
+
const mod = jiti(filePath);
|
|
146
|
+
const schema = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
147
|
+
if (!schema || typeof schema !== "object") {
|
|
148
|
+
throw new DiscoveryError(
|
|
149
|
+
`Schema file ${filePath} for ${entityLabel} must default-export a JSON Schema object.`
|
|
150
|
+
);
|
|
161
151
|
}
|
|
152
|
+
return schema;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err instanceof DiscoveryError) throw err;
|
|
155
|
+
throw new DiscoveryError(
|
|
156
|
+
`Failed to load schema module ${filePath} for ${entityLabel}: ${err.message}`
|
|
157
|
+
);
|
|
162
158
|
}
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
}
|
|
160
|
+
var VIEW_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
161
|
+
function findViewsFile(dir) {
|
|
162
|
+
for (const ext of VIEW_EXTENSIONS) {
|
|
163
|
+
const candidate = join(dir, `views${ext}`);
|
|
164
|
+
if (existsSync(candidate)) return candidate;
|
|
165
165
|
}
|
|
166
|
-
|
|
167
|
-
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
166
|
+
return void 0;
|
|
168
167
|
}
|
|
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
|
-
}
|
|
168
|
+
var MIGRATION_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
|
|
169
|
+
function findMigrationsFile(dir) {
|
|
170
|
+
for (const ext of MIGRATION_EXTENSIONS) {
|
|
171
|
+
const candidate = join(dir, `migrations${ext}`);
|
|
172
|
+
if (existsSync(candidate)) return candidate;
|
|
180
173
|
}
|
|
181
|
-
|
|
182
|
-
return { strategy: "query", filters, options: { limit: effectiveLimit, orderBy } };
|
|
174
|
+
return void 0;
|
|
183
175
|
}
|
|
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());
|
|
176
|
+
function loadMigrations(filePath, entityLabel) {
|
|
177
|
+
try {
|
|
178
|
+
const jiti = getJiti();
|
|
179
|
+
const mod = jiti(filePath);
|
|
180
|
+
const migrations = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
|
|
181
|
+
if (!Array.isArray(migrations)) {
|
|
182
|
+
throw new DiscoveryError(
|
|
183
|
+
`Migrations file ${filePath} for ${entityLabel} must default-export an array of MigrationStep.`
|
|
184
|
+
);
|
|
217
185
|
}
|
|
218
|
-
|
|
186
|
+
return migrations;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof DiscoveryError) throw err;
|
|
189
|
+
throw new DiscoveryError(
|
|
190
|
+
`Failed to load migrations ${filePath} for ${entityLabel}: ${err.message}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
219
193
|
}
|
|
220
|
-
function
|
|
221
|
-
const
|
|
194
|
+
function loadNodeEntity(dir, name) {
|
|
195
|
+
const schema = loadSchema(dir, `node type "${name}"`);
|
|
196
|
+
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
197
|
+
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
198
|
+
const viewsPath = findViewsFile(dir);
|
|
199
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
200
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `node type "${name}"`) : void 0;
|
|
222
201
|
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
|
-
}
|
|
202
|
+
kind: "node",
|
|
203
|
+
name,
|
|
204
|
+
schema,
|
|
205
|
+
description: meta?.description,
|
|
206
|
+
titleField: meta?.titleField,
|
|
207
|
+
subtitleField: meta?.subtitleField,
|
|
208
|
+
viewDefaults: meta?.viewDefaults,
|
|
209
|
+
viewsPath,
|
|
210
|
+
sampleData,
|
|
211
|
+
allowedIn: meta?.allowedIn,
|
|
212
|
+
migrations,
|
|
213
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
269
214
|
};
|
|
270
215
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
216
|
+
function loadEdgeEntity(dir, name) {
|
|
217
|
+
const schema = loadSchema(dir, `edge type "${name}"`);
|
|
218
|
+
const edgePath = join(dir, "edge.json");
|
|
219
|
+
if (!existsSync(edgePath)) {
|
|
220
|
+
throw new DiscoveryError(
|
|
221
|
+
`Missing edge.json for edge type "${name}" in ${dir}. Edge entities must declare topology (from/to node types).`
|
|
222
|
+
);
|
|
278
223
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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}`);
|
|
224
|
+
const topology = readJson(edgePath);
|
|
225
|
+
if (!topology.from) {
|
|
226
|
+
throw new DiscoveryError(
|
|
227
|
+
`edge.json for "${name}" is missing required "from" field`
|
|
228
|
+
);
|
|
306
229
|
}
|
|
307
|
-
|
|
308
|
-
|
|
230
|
+
if (!topology.to) {
|
|
231
|
+
throw new DiscoveryError(
|
|
232
|
+
`edge.json for "${name}" is missing required "to" field`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const meta = readJsonIfExists(join(dir, "meta.json"));
|
|
236
|
+
const sampleData = readJsonIfExists(join(dir, "sample.json"));
|
|
237
|
+
const viewsPath = findViewsFile(dir);
|
|
238
|
+
const migrationsPath = findMigrationsFile(dir);
|
|
239
|
+
const migrations = migrationsPath ? loadMigrations(migrationsPath, `edge type "${name}"`) : void 0;
|
|
309
240
|
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
|
-
}
|
|
241
|
+
kind: "edge",
|
|
242
|
+
name,
|
|
243
|
+
schema,
|
|
244
|
+
topology,
|
|
245
|
+
description: meta?.description,
|
|
246
|
+
titleField: meta?.titleField,
|
|
247
|
+
subtitleField: meta?.subtitleField,
|
|
248
|
+
viewDefaults: meta?.viewDefaults,
|
|
249
|
+
viewsPath,
|
|
250
|
+
sampleData,
|
|
251
|
+
allowedIn: meta?.allowedIn,
|
|
252
|
+
targetGraph: topology.targetGraph ?? meta?.targetGraph,
|
|
253
|
+
migrations,
|
|
254
|
+
migrationWriteBack: meta?.migrationWriteBack
|
|
330
255
|
};
|
|
331
256
|
}
|
|
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
|
-
}
|
|
257
|
+
function getSubdirectories(dir) {
|
|
258
|
+
if (!existsSync(dir)) return [];
|
|
259
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
260
|
+
}
|
|
261
|
+
function discoverEntities(entitiesDir) {
|
|
262
|
+
const absDir = resolve(entitiesDir);
|
|
263
|
+
if (!existsSync(absDir) || !statSync(absDir).isDirectory()) {
|
|
264
|
+
throw new DiscoveryError(`Entities directory not found: ${entitiesDir}`);
|
|
352
265
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (matched) {
|
|
362
|
-
return { safe: true };
|
|
363
|
-
}
|
|
266
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
267
|
+
const edges = /* @__PURE__ */ new Map();
|
|
268
|
+
const warnings = [];
|
|
269
|
+
const nodesDir = join(absDir, "nodes");
|
|
270
|
+
for (const name of getSubdirectories(nodesDir)) {
|
|
271
|
+
nodes.set(name, loadNodeEntity(join(nodesDir, name), name));
|
|
364
272
|
}
|
|
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
|
-
};
|
|
273
|
+
const edgesDir = join(absDir, "edges");
|
|
274
|
+
for (const name of getSubdirectories(edgesDir)) {
|
|
275
|
+
edges.set(name, loadEdgeEntity(join(edgesDir, name), name));
|
|
371
276
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
277
|
+
const nodeNames = new Set(nodes.keys());
|
|
278
|
+
for (const [axbType, entity] of edges) {
|
|
279
|
+
const topology = entity.topology;
|
|
280
|
+
const fromTypes = Array.isArray(topology.from) ? topology.from : [topology.from];
|
|
281
|
+
const toTypes = Array.isArray(topology.to) ? topology.to : [topology.to];
|
|
282
|
+
for (const ref of [...fromTypes, ...toTypes]) {
|
|
283
|
+
if (!nodeNames.has(ref)) {
|
|
284
|
+
warnings.push({
|
|
285
|
+
code: "DANGLING_TOPOLOGY_REF",
|
|
286
|
+
message: `Edge "${axbType}" references node type "${ref}" which was not found in the nodes directory`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
377
290
|
}
|
|
378
291
|
return {
|
|
379
|
-
|
|
380
|
-
|
|
292
|
+
result: { nodes, edges },
|
|
293
|
+
warnings
|
|
381
294
|
};
|
|
382
295
|
}
|
|
383
296
|
|
|
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);
|
|
297
|
+
// src/internal/firestore-backend.ts
|
|
298
|
+
import { FieldValue } from "@google-cloud/firestore";
|
|
299
|
+
|
|
300
|
+
// src/bulk.ts
|
|
301
|
+
var MAX_BATCH_SIZE = 500;
|
|
302
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
303
|
+
var BASE_DELAY_MS = 200;
|
|
304
|
+
function sleep(ms) {
|
|
305
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
412
306
|
}
|
|
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]);
|
|
307
|
+
function chunk(arr, size) {
|
|
308
|
+
const chunks = [];
|
|
309
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
310
|
+
chunks.push(arr.slice(i, i + size));
|
|
436
311
|
}
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
function deserializeFirestoreTypes(data, db) {
|
|
440
|
-
return deserializeValue(data, db);
|
|
312
|
+
return chunks;
|
|
441
313
|
}
|
|
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);
|
|
314
|
+
async function bulkDeleteDocIds(db, collectionPath, docIds, options) {
|
|
315
|
+
if (docIds.length === 0) {
|
|
316
|
+
return { deleted: 0, batches: 0, errors: [] };
|
|
483
317
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
let
|
|
491
|
-
let
|
|
492
|
-
|
|
493
|
-
|
|
318
|
+
const batchSize = Math.min(options?.batchSize ?? MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
319
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
320
|
+
const onProgress = options?.onProgress;
|
|
321
|
+
const chunks = chunk(docIds, batchSize);
|
|
322
|
+
const errors = [];
|
|
323
|
+
let deleted = 0;
|
|
324
|
+
let completedBatches = 0;
|
|
325
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
326
|
+
const ids = chunks[i];
|
|
327
|
+
let committed = false;
|
|
328
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
494
329
|
try {
|
|
495
|
-
|
|
330
|
+
const batch = db.batch();
|
|
331
|
+
const collectionRef = db.collection(collectionPath);
|
|
332
|
+
for (const id of ids) {
|
|
333
|
+
batch.delete(collectionRef.doc(id));
|
|
334
|
+
}
|
|
335
|
+
await batch.commit();
|
|
336
|
+
committed = true;
|
|
337
|
+
deleted += ids.length;
|
|
338
|
+
break;
|
|
496
339
|
} 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
|
-
}
|
|
340
|
+
if (attempt < maxRetries) {
|
|
341
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
342
|
+
await sleep(delay);
|
|
343
|
+
} else {
|
|
344
|
+
errors.push({
|
|
345
|
+
batchIndex: i,
|
|
346
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
347
|
+
operationCount: ids.length
|
|
348
|
+
});
|
|
349
|
+
}
|
|
836
350
|
}
|
|
837
351
|
}
|
|
838
352
|
if (committed) {
|
|
@@ -925,1044 +439,288 @@ async function removeNodeCascade(db, collectionPath, reader, uid, options) {
|
|
|
925
439
|
};
|
|
926
440
|
}
|
|
927
441
|
|
|
928
|
-
// src/
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
// src/json-schema.ts
|
|
932
|
-
import Ajv from "ajv";
|
|
933
|
-
import addFormats from "ajv-formats";
|
|
934
|
-
var ajv = new Ajv({ allErrors: true, strict: false });
|
|
935
|
-
addFormats(ajv);
|
|
936
|
-
function compileSchema(schema, label) {
|
|
937
|
-
const validate = ajv.compile(schema);
|
|
938
|
-
return (data) => {
|
|
939
|
-
if (!validate(data)) {
|
|
940
|
-
const errors = validate.errors ?? [];
|
|
941
|
-
const messages = errors.map((err) => `${err.instancePath || "/"}${err.message ? ": " + err.message : ""}`).join("; ");
|
|
942
|
-
throw new ValidationError(
|
|
943
|
-
`Data validation failed${label ? " for " + label : ""}: ${messages}`,
|
|
944
|
-
errors
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
function jsonSchemaToFieldMeta(schema) {
|
|
950
|
-
if (!schema || schema.type !== "object" || !schema.properties) return [];
|
|
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
|
-
}
|
|
442
|
+
// src/internal/firestore-adapter.ts
|
|
443
|
+
function createFirestoreAdapter(db, collectionPath) {
|
|
444
|
+
const collectionRef = db.collection(collectionPath);
|
|
1104
445
|
return {
|
|
1105
|
-
|
|
1106
|
-
|
|
446
|
+
collectionPath,
|
|
447
|
+
async getDoc(docId) {
|
|
448
|
+
const snap = await collectionRef.doc(docId).get();
|
|
449
|
+
if (!snap.exists) return null;
|
|
450
|
+
return snap.data();
|
|
451
|
+
},
|
|
452
|
+
async setDoc(docId, data) {
|
|
453
|
+
await collectionRef.doc(docId).set(data);
|
|
1107
454
|
},
|
|
1108
|
-
|
|
1109
|
-
|
|
455
|
+
async updateDoc(docId, data) {
|
|
456
|
+
await collectionRef.doc(docId).update(data);
|
|
457
|
+
},
|
|
458
|
+
async deleteDoc(docId) {
|
|
459
|
+
await collectionRef.doc(docId).delete();
|
|
1110
460
|
},
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
461
|
+
async query(filters, options) {
|
|
462
|
+
let q = collectionRef;
|
|
463
|
+
for (const f of filters) {
|
|
464
|
+
q = q.where(f.field, f.op, f.value);
|
|
1115
465
|
}
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
throw new RegistryScopeError(aType, axbType, bType, scopePath, rec.entry.allowedIn);
|
|
1119
|
-
}
|
|
466
|
+
if (options?.orderBy) {
|
|
467
|
+
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
1120
468
|
}
|
|
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
|
-
}
|
|
469
|
+
if (options?.limit !== void 0) {
|
|
470
|
+
q = q.limit(options.limit);
|
|
1131
471
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return entryList;
|
|
472
|
+
const snap = await q.get();
|
|
473
|
+
return snap.docs.map((doc) => doc.data());
|
|
1135
474
|
}
|
|
1136
475
|
};
|
|
1137
476
|
}
|
|
1138
|
-
function
|
|
1139
|
-
const
|
|
477
|
+
function createTransactionAdapter(db, collectionPath, tx) {
|
|
478
|
+
const collectionRef = db.collection(collectionPath);
|
|
1140
479
|
return {
|
|
1141
|
-
|
|
1142
|
-
|
|
480
|
+
async getDoc(docId) {
|
|
481
|
+
const snap = await tx.get(collectionRef.doc(docId));
|
|
482
|
+
if (!snap.exists) return null;
|
|
483
|
+
return snap.data();
|
|
1143
484
|
},
|
|
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);
|
|
485
|
+
setDoc(docId, data) {
|
|
486
|
+
tx.set(collectionRef.doc(docId), data);
|
|
1157
487
|
},
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
return base.validate(aType, axbType, bType, data, scopePath);
|
|
1161
|
-
}
|
|
1162
|
-
return extension.validate(aType, axbType, bType, data, scopePath);
|
|
488
|
+
updateDoc(docId, data) {
|
|
489
|
+
tx.update(collectionRef.doc(docId), data);
|
|
1163
490
|
},
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
}
|
|
491
|
+
deleteDoc(docId) {
|
|
492
|
+
tx.delete(collectionRef.doc(docId));
|
|
493
|
+
},
|
|
494
|
+
async query(filters, options) {
|
|
495
|
+
let q = collectionRef;
|
|
496
|
+
for (const f of filters) {
|
|
497
|
+
q = q.where(f.field, f.op, f.value);
|
|
1172
498
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
});
|
|
499
|
+
if (options?.orderBy) {
|
|
500
|
+
q = q.orderBy(options.orderBy.field, options.orderBy.direction ?? "asc");
|
|
501
|
+
}
|
|
502
|
+
if (options?.limit !== void 0) {
|
|
503
|
+
q = q.limit(options.limit);
|
|
1220
504
|
}
|
|
505
|
+
const snap = await tx.get(q);
|
|
506
|
+
return snap.docs.map((doc) => doc.data());
|
|
1221
507
|
}
|
|
1222
|
-
}
|
|
1223
|
-
return entries;
|
|
508
|
+
};
|
|
1224
509
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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();
|
|
510
|
+
function createBatchAdapter(db, collectionPath) {
|
|
511
|
+
const collectionRef = db.collection(collectionPath);
|
|
512
|
+
const batch = db.batch();
|
|
513
|
+
return {
|
|
514
|
+
setDoc(docId, data) {
|
|
515
|
+
batch.set(collectionRef.doc(docId), data);
|
|
516
|
+
},
|
|
517
|
+
updateDoc(docId, data) {
|
|
518
|
+
batch.update(collectionRef.doc(docId), data);
|
|
519
|
+
},
|
|
520
|
+
deleteDoc(docId) {
|
|
521
|
+
batch.delete(collectionRef.doc(docId));
|
|
522
|
+
},
|
|
523
|
+
async commit() {
|
|
524
|
+
await batch.commit();
|
|
1373
525
|
}
|
|
1374
|
-
|
|
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
|
-
});
|
|
526
|
+
};
|
|
1386
527
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
528
|
+
|
|
529
|
+
// src/internal/pipeline-adapter.ts
|
|
530
|
+
var _Pipelines = null;
|
|
531
|
+
async function getPipelines() {
|
|
532
|
+
if (!_Pipelines) {
|
|
533
|
+
const mod = await import("@google-cloud/firestore");
|
|
534
|
+
_Pipelines = mod.Pipelines;
|
|
535
|
+
}
|
|
536
|
+
return _Pipelines;
|
|
1395
537
|
}
|
|
1396
|
-
function
|
|
1397
|
-
|
|
538
|
+
function buildFilterExpression(P, filter) {
|
|
539
|
+
const { field: fieldName, op, value } = filter;
|
|
540
|
+
switch (op) {
|
|
541
|
+
case "==":
|
|
542
|
+
return P.equal(fieldName, value);
|
|
543
|
+
case "!=":
|
|
544
|
+
return P.notEqual(fieldName, value);
|
|
545
|
+
case "<":
|
|
546
|
+
return P.lessThan(fieldName, value);
|
|
547
|
+
case "<=":
|
|
548
|
+
return P.lessThanOrEqual(fieldName, value);
|
|
549
|
+
case ">":
|
|
550
|
+
return P.greaterThan(fieldName, value);
|
|
551
|
+
case ">=":
|
|
552
|
+
return P.greaterThanOrEqual(fieldName, value);
|
|
553
|
+
case "in":
|
|
554
|
+
return P.equalAny(fieldName, value);
|
|
555
|
+
case "not-in":
|
|
556
|
+
return P.notEqualAny(fieldName, value);
|
|
557
|
+
case "array-contains":
|
|
558
|
+
return P.arrayContains(fieldName, value);
|
|
559
|
+
case "array-contains-any":
|
|
560
|
+
return P.arrayContainsAny(fieldName, value);
|
|
561
|
+
default:
|
|
562
|
+
throw new Error(`Unsupported filter op for pipeline mode: ${op}`);
|
|
563
|
+
}
|
|
1398
564
|
}
|
|
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
|
-
}
|
|
565
|
+
function createPipelineQueryAdapter(db, collectionPath) {
|
|
566
|
+
return {
|
|
567
|
+
async query(filters, options) {
|
|
568
|
+
const P = await getPipelines();
|
|
569
|
+
let pipeline = db.pipeline().collection(collectionPath);
|
|
570
|
+
if (filters.length === 1) {
|
|
571
|
+
pipeline = pipeline.where(buildFilterExpression(P, filters[0]));
|
|
572
|
+
} else if (filters.length > 1) {
|
|
573
|
+
const [first, second, ...rest] = filters.map((f) => buildFilterExpression(P, f));
|
|
574
|
+
pipeline = pipeline.where(P.and(first, second, ...rest));
|
|
1413
575
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
576
|
+
if (options?.orderBy) {
|
|
577
|
+
const f = P.field(options.orderBy.field);
|
|
578
|
+
const ordering = options.orderBy.direction === "desc" ? f.descending() : f.ascending();
|
|
579
|
+
pipeline = pipeline.sort(ordering);
|
|
580
|
+
}
|
|
581
|
+
if (options?.limit !== void 0) {
|
|
582
|
+
pipeline = pipeline.limit(options.limit);
|
|
583
|
+
}
|
|
584
|
+
const snap = await pipeline.execute();
|
|
585
|
+
return snap.results.map((r) => r.data());
|
|
586
|
+
}
|
|
587
|
+
};
|
|
1416
588
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
589
|
+
|
|
590
|
+
// src/internal/firestore-backend.ts
|
|
591
|
+
function buildFirestoreUpdate(update, db) {
|
|
592
|
+
const out = {
|
|
593
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
594
|
+
};
|
|
595
|
+
if (update.replaceData) {
|
|
596
|
+
out.data = deserializeFirestoreTypes(update.replaceData, db);
|
|
597
|
+
}
|
|
598
|
+
if (update.dataFields) {
|
|
599
|
+
for (const [k, v] of Object.entries(update.dataFields)) {
|
|
600
|
+
out[`data.${k}`] = v;
|
|
1426
601
|
}
|
|
1427
|
-
return;
|
|
1428
602
|
}
|
|
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
|
-
);
|
|
603
|
+
if (update.v !== void 0) {
|
|
604
|
+
out.v = update.v;
|
|
1445
605
|
}
|
|
606
|
+
return out;
|
|
1446
607
|
}
|
|
1447
|
-
function
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
_pending.clear();
|
|
1462
|
-
await w.terminate();
|
|
608
|
+
function stampWritableRecord(record) {
|
|
609
|
+
const now = FieldValue.serverTimestamp();
|
|
610
|
+
const out = {
|
|
611
|
+
aType: record.aType,
|
|
612
|
+
aUid: record.aUid,
|
|
613
|
+
axbType: record.axbType,
|
|
614
|
+
bType: record.bType,
|
|
615
|
+
bUid: record.bUid,
|
|
616
|
+
data: record.data,
|
|
617
|
+
createdAt: now,
|
|
618
|
+
updatedAt: now
|
|
619
|
+
};
|
|
620
|
+
if (record.v !== void 0) out.v = record.v;
|
|
621
|
+
return out;
|
|
1463
622
|
}
|
|
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
|
|
623
|
+
var FirestoreTransactionBackend = class {
|
|
624
|
+
constructor(adapter, db) {
|
|
625
|
+
this.adapter = adapter;
|
|
626
|
+
this.db = db;
|
|
627
|
+
}
|
|
628
|
+
getDoc(docId) {
|
|
629
|
+
return this.adapter.getDoc(docId);
|
|
630
|
+
}
|
|
631
|
+
query(filters, options) {
|
|
632
|
+
return this.adapter.query(filters, options);
|
|
633
|
+
}
|
|
634
|
+
async setDoc(docId, record) {
|
|
635
|
+
this.adapter.setDoc(docId, stampWritableRecord(record));
|
|
636
|
+
}
|
|
637
|
+
async updateDoc(docId, update) {
|
|
638
|
+
this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
639
|
+
}
|
|
640
|
+
async deleteDoc(docId) {
|
|
641
|
+
this.adapter.deleteDoc(docId);
|
|
642
|
+
}
|
|
1527
643
|
};
|
|
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"
|
|
644
|
+
var FirestoreBatchBackend = class {
|
|
645
|
+
constructor(adapter, db) {
|
|
646
|
+
this.adapter = adapter;
|
|
647
|
+
this.db = db;
|
|
1542
648
|
}
|
|
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
|
-
}
|
|
649
|
+
setDoc(docId, record) {
|
|
650
|
+
this.adapter.setDoc(docId, stampWritableRecord(record));
|
|
1565
651
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
if (data.migrations) {
|
|
1569
|
-
for (const m of data.migrations) {
|
|
1570
|
-
prevalidations.push(precompileSource(m.up, executor));
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
652
|
+
updateDoc(docId, update) {
|
|
653
|
+
this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
1573
654
|
}
|
|
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
|
-
});
|
|
655
|
+
deleteDoc(docId) {
|
|
656
|
+
this.adapter.deleteDoc(docId);
|
|
1589
657
|
}
|
|
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
|
-
}
|
|
658
|
+
commit() {
|
|
659
|
+
return this.adapter.commit();
|
|
1613
660
|
}
|
|
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 = "") {
|
|
661
|
+
};
|
|
662
|
+
var FirestoreBackendImpl = class _FirestoreBackendImpl {
|
|
663
|
+
constructor(db, collectionPath, queryMode, scopePath) {
|
|
1622
664
|
this.db = db;
|
|
665
|
+
this.queryMode = queryMode;
|
|
666
|
+
this.collectionPath = collectionPath;
|
|
1623
667
|
this.scopePath = scopePath;
|
|
1624
668
|
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") {
|
|
669
|
+
if (queryMode === "pipeline") {
|
|
1655
670
|
this.pipelineAdapter = createPipelineQueryAdapter(db, collectionPath);
|
|
1656
|
-
if (this.metaAdapter) {
|
|
1657
|
-
this.metaPipelineAdapter = createPipelineQueryAdapter(
|
|
1658
|
-
db,
|
|
1659
|
-
options.registryMode.collection
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
671
|
}
|
|
1663
672
|
}
|
|
673
|
+
collectionPath;
|
|
674
|
+
scopePath;
|
|
1664
675
|
adapter;
|
|
1665
676
|
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;
|
|
677
|
+
// --- Reads ---
|
|
678
|
+
getDoc(docId) {
|
|
679
|
+
return this.adapter.getDoc(docId);
|
|
1721
680
|
}
|
|
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) {
|
|
681
|
+
query(filters, options) {
|
|
1731
682
|
if (this.pipelineAdapter) {
|
|
1732
683
|
return this.pipelineAdapter.query(filters, options);
|
|
1733
684
|
}
|
|
1734
685
|
return this.adapter.query(filters, options);
|
|
1735
686
|
}
|
|
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);
|
|
687
|
+
// --- Writes ---
|
|
688
|
+
setDoc(docId, record) {
|
|
689
|
+
return this.adapter.setDoc(docId, stampWritableRecord(record));
|
|
1903
690
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
await this.adapter.deleteDoc(docId);
|
|
691
|
+
updateDoc(docId, update) {
|
|
692
|
+
return this.adapter.updateDoc(docId, buildFirestoreUpdate(update, this.db));
|
|
1907
693
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
await this.adapter.deleteDoc(docId);
|
|
694
|
+
deleteDoc(docId) {
|
|
695
|
+
return this.adapter.deleteDoc(docId);
|
|
1911
696
|
}
|
|
1912
|
-
//
|
|
1913
|
-
|
|
1914
|
-
// ---------------------------------------------------------------------------
|
|
1915
|
-
async runTransaction(fn) {
|
|
697
|
+
// --- Transactions / Batches ---
|
|
698
|
+
runTransaction(fn) {
|
|
1916
699
|
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);
|
|
700
|
+
const txAdapter = createTransactionAdapter(this.db, this.collectionPath, firestoreTx);
|
|
701
|
+
return fn(new FirestoreTransactionBackend(txAdapter, this.db));
|
|
1924
702
|
});
|
|
1925
703
|
}
|
|
1926
|
-
|
|
1927
|
-
const
|
|
1928
|
-
return new
|
|
704
|
+
createBatch() {
|
|
705
|
+
const batchAdapter = createBatchAdapter(this.db, this.collectionPath);
|
|
706
|
+
return new FirestoreBatchBackend(batchAdapter, this.db);
|
|
1929
707
|
}
|
|
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
|
-
);
|
|
708
|
+
// --- Subgraphs ---
|
|
709
|
+
subgraph(parentNodeUid, name) {
|
|
710
|
+
const subPath = `${this.collectionPath}/${parentNodeUid}/${name}`;
|
|
711
|
+
const newScope = this.scopePath ? `${this.scopePath}/${name}` : name;
|
|
712
|
+
return new _FirestoreBackendImpl(this.db, subPath, this.queryMode, newScope);
|
|
713
|
+
}
|
|
714
|
+
// --- Cascade & bulk ---
|
|
715
|
+
removeNodeCascade(uid, reader, options) {
|
|
716
|
+
return removeNodeCascade(this.db, this.collectionPath, reader, uid, options);
|
|
1960
717
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
718
|
+
bulkRemoveEdges(params, reader, options) {
|
|
719
|
+
return bulkRemoveEdges(this.db, this.collectionPath, reader, params, options);
|
|
720
|
+
}
|
|
721
|
+
// --- Cross-collection ---
|
|
1964
722
|
async findEdgesGlobal(params, collectionName) {
|
|
1965
|
-
const name = collectionName ?? this.
|
|
723
|
+
const name = collectionName ?? this.collectionPath.split("/").pop();
|
|
1966
724
|
const plan = buildEdgeQueryPlan(params);
|
|
1967
725
|
if (plan.strategy === "get") {
|
|
1968
726
|
throw new FiregraphError(
|
|
@@ -1970,7 +728,6 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1970
728
|
"INVALID_QUERY"
|
|
1971
729
|
);
|
|
1972
730
|
}
|
|
1973
|
-
this.checkQuerySafety(plan.filters, params.allowCollectionScan);
|
|
1974
731
|
const collectionGroupRef = this.db.collectionGroup(name);
|
|
1975
732
|
let q = collectionGroupRef;
|
|
1976
733
|
for (const f of plan.filters) {
|
|
@@ -1983,170 +740,35 @@ var GraphClientImpl = class _GraphClientImpl {
|
|
|
1983
740
|
q = q.limit(plan.options.limit);
|
|
1984
741
|
}
|
|
1985
742
|
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
|
-
}
|
|
743
|
+
return snap.docs.map((doc) => doc.data());
|
|
2089
744
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
745
|
+
};
|
|
746
|
+
function createFirestoreBackend(db, collectionPath, options = {}) {
|
|
747
|
+
const queryMode = options.queryMode ?? "pipeline";
|
|
748
|
+
const scopePath = options.scopePath ?? "";
|
|
749
|
+
return new FirestoreBackendImpl(db, collectionPath, queryMode, scopePath);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/firestore.ts
|
|
753
|
+
var _standardModeWarned = false;
|
|
754
|
+
function createGraphClient(db, collectionPath, options) {
|
|
755
|
+
const requestedMode = options?.queryMode ?? "pipeline";
|
|
756
|
+
const isEmulator = !!process.env.FIRESTORE_EMULATOR_HOST;
|
|
757
|
+
const effectiveMode = isEmulator ? "standard" : requestedMode;
|
|
758
|
+
if (effectiveMode === "standard" && !isEmulator && requestedMode === "standard" && !_standardModeWarned) {
|
|
759
|
+
_standardModeWarned = true;
|
|
760
|
+
console.warn(
|
|
761
|
+
"[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
762
|
);
|
|
2103
|
-
return result;
|
|
2104
763
|
}
|
|
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
|
-
};
|
|
764
|
+
const backend = createFirestoreBackend(db, collectionPath, { queryMode: effectiveMode });
|
|
765
|
+
let metaBackend;
|
|
766
|
+
if (options?.registryMode?.collection && options.registryMode.collection !== collectionPath) {
|
|
767
|
+
metaBackend = createFirestoreBackend(db, options.registryMode.collection, {
|
|
768
|
+
queryMode: effectiveMode
|
|
769
|
+
});
|
|
2146
770
|
}
|
|
2147
|
-
|
|
2148
|
-
function createGraphClient(db, collectionPath, options) {
|
|
2149
|
-
return new GraphClientImpl(db, collectionPath, options);
|
|
771
|
+
return new GraphClientImpl(backend, options, metaBackend);
|
|
2150
772
|
}
|
|
2151
773
|
|
|
2152
774
|
// src/id.ts
|
|
@@ -2155,14 +777,167 @@ function generateId() {
|
|
|
2155
777
|
return nanoid();
|
|
2156
778
|
}
|
|
2157
779
|
|
|
2158
|
-
// src/
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
}
|
|
780
|
+
// src/indexes.ts
|
|
781
|
+
function baseIndexes(collection) {
|
|
782
|
+
return [
|
|
783
|
+
{
|
|
784
|
+
collectionGroup: collection,
|
|
785
|
+
queryScope: "COLLECTION",
|
|
786
|
+
fields: [
|
|
787
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
788
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
789
|
+
]
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
collectionGroup: collection,
|
|
793
|
+
queryScope: "COLLECTION",
|
|
794
|
+
fields: [
|
|
795
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
796
|
+
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
797
|
+
]
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
collectionGroup: collection,
|
|
801
|
+
queryScope: "COLLECTION",
|
|
802
|
+
fields: [
|
|
803
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
804
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
805
|
+
]
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
collectionGroup: collection,
|
|
809
|
+
queryScope: "COLLECTION",
|
|
810
|
+
fields: [
|
|
811
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
812
|
+
{ fieldPath: "bType", order: "ASCENDING" }
|
|
813
|
+
]
|
|
814
|
+
}
|
|
815
|
+
];
|
|
816
|
+
}
|
|
817
|
+
function extractSchemaFields(schema) {
|
|
818
|
+
const s = schema;
|
|
819
|
+
if (s.type !== "object" || !s.properties) return [];
|
|
820
|
+
return Object.keys(s.properties);
|
|
821
|
+
}
|
|
822
|
+
function collectionGroupIndexes(collectionName) {
|
|
823
|
+
return [
|
|
824
|
+
{
|
|
825
|
+
collectionGroup: collectionName,
|
|
826
|
+
queryScope: "COLLECTION_GROUP",
|
|
827
|
+
fields: [
|
|
828
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
829
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
830
|
+
]
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
collectionGroup: collectionName,
|
|
834
|
+
queryScope: "COLLECTION_GROUP",
|
|
835
|
+
fields: [
|
|
836
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
837
|
+
{ fieldPath: "bUid", order: "ASCENDING" }
|
|
838
|
+
]
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
collectionGroup: collectionName,
|
|
842
|
+
queryScope: "COLLECTION_GROUP",
|
|
843
|
+
fields: [
|
|
844
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
845
|
+
{ fieldPath: "axbType", order: "ASCENDING" }
|
|
846
|
+
]
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
collectionGroup: collectionName,
|
|
850
|
+
queryScope: "COLLECTION_GROUP",
|
|
851
|
+
fields: [
|
|
852
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
853
|
+
{ fieldPath: "bType", order: "ASCENDING" }
|
|
854
|
+
]
|
|
855
|
+
}
|
|
856
|
+
];
|
|
857
|
+
}
|
|
858
|
+
function generateIndexConfig(collection, entities, registryEntries) {
|
|
859
|
+
const indexes = baseIndexes(collection);
|
|
860
|
+
if (entities) {
|
|
861
|
+
for (const [, entity] of entities.nodes) {
|
|
862
|
+
const fields = extractSchemaFields(entity.schema);
|
|
863
|
+
for (const field of fields) {
|
|
864
|
+
indexes.push({
|
|
865
|
+
collectionGroup: collection,
|
|
866
|
+
queryScope: "COLLECTION",
|
|
867
|
+
fields: [
|
|
868
|
+
{ fieldPath: "aType", order: "ASCENDING" },
|
|
869
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
870
|
+
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
871
|
+
]
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
for (const [, entity] of entities.edges) {
|
|
876
|
+
const fields = extractSchemaFields(entity.schema);
|
|
877
|
+
for (const field of fields) {
|
|
878
|
+
indexes.push({
|
|
879
|
+
collectionGroup: collection,
|
|
880
|
+
queryScope: "COLLECTION",
|
|
881
|
+
fields: [
|
|
882
|
+
{ fieldPath: "aUid", order: "ASCENDING" },
|
|
883
|
+
{ fieldPath: "axbType", order: "ASCENDING" },
|
|
884
|
+
{ fieldPath: `data.${field}`, order: "ASCENDING" }
|
|
885
|
+
]
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (registryEntries) {
|
|
891
|
+
const targetGraphNames = /* @__PURE__ */ new Set();
|
|
892
|
+
for (const entry of registryEntries) {
|
|
893
|
+
if (entry.targetGraph) {
|
|
894
|
+
targetGraphNames.add(entry.targetGraph);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
for (const name of targetGraphNames) {
|
|
898
|
+
indexes.push(...collectionGroupIndexes(name));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return { indexes, fieldOverrides: [] };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/record.ts
|
|
905
|
+
import { FieldValue as FieldValue2 } from "@google-cloud/firestore";
|
|
906
|
+
function buildNodeRecord(aType, uid, data) {
|
|
907
|
+
const now = FieldValue2.serverTimestamp();
|
|
908
|
+
return {
|
|
909
|
+
aType,
|
|
910
|
+
aUid: uid,
|
|
911
|
+
axbType: NODE_RELATION,
|
|
912
|
+
bType: aType,
|
|
913
|
+
bUid: uid,
|
|
914
|
+
data,
|
|
915
|
+
createdAt: now,
|
|
916
|
+
updatedAt: now
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function buildEdgeRecord(aType, aUid, axbType, bType, bUid, data) {
|
|
920
|
+
const now = FieldValue2.serverTimestamp();
|
|
921
|
+
return {
|
|
922
|
+
aType,
|
|
923
|
+
aUid,
|
|
924
|
+
axbType,
|
|
925
|
+
bType,
|
|
926
|
+
bUid,
|
|
927
|
+
data,
|
|
928
|
+
createdAt: now,
|
|
929
|
+
updatedAt: now
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/traverse.ts
|
|
934
|
+
var DEFAULT_LIMIT = 10;
|
|
935
|
+
var DEFAULT_MAX_READS = 100;
|
|
936
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
937
|
+
var _crossGraphWarned = false;
|
|
938
|
+
function isGraphClient(reader) {
|
|
939
|
+
return "subgraph" in reader && typeof reader.subgraph === "function";
|
|
940
|
+
}
|
|
2166
941
|
var Semaphore = class {
|
|
2167
942
|
constructor(slots) {
|
|
2168
943
|
this.slots = slots;
|
|
@@ -2444,367 +1219,6 @@ function defineViews(input) {
|
|
|
2444
1219
|
}
|
|
2445
1220
|
return { nodes, edges };
|
|
2446
1221
|
}
|
|
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
1222
|
export {
|
|
2809
1223
|
BOOTSTRAP_ENTRIES,
|
|
2810
1224
|
DEFAULT_QUERY_LIMIT,
|
|
@@ -2840,6 +1254,7 @@ export {
|
|
|
2840
1254
|
computeNodeDocId,
|
|
2841
1255
|
createBootstrapRegistry,
|
|
2842
1256
|
createGraphClient,
|
|
1257
|
+
createGraphClientFromBackend,
|
|
2843
1258
|
createMergedRegistry,
|
|
2844
1259
|
createRegistry,
|
|
2845
1260
|
createRegistryFromGraph,
|