@typicalday/firegraph 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +355 -78
- package/dist/backend-DuvHGgK1.d.cts +1897 -0
- package/dist/backend-DuvHGgK1.d.ts +1897 -0
- package/dist/backend.cjs +365 -5
- package/dist/backend.cjs.map +1 -1
- package/dist/backend.d.cts +25 -5
- package/dist/backend.d.ts +25 -5
- package/dist/backend.js +209 -7
- package/dist/backend.js.map +1 -1
- package/dist/chunk-2DHMNTV6.js +16 -0
- package/dist/chunk-2DHMNTV6.js.map +1 -0
- package/dist/chunk-4MMQ5W74.js +288 -0
- package/dist/chunk-4MMQ5W74.js.map +1 -0
- package/dist/{chunk-5753Y42M.js → chunk-C2QMD7RY.js} +6 -10
- package/dist/chunk-C2QMD7RY.js.map +1 -0
- package/dist/chunk-D4J7Z4FE.js +67 -0
- package/dist/chunk-D4J7Z4FE.js.map +1 -0
- package/dist/chunk-EQJUUVFG.js +14 -0
- package/dist/chunk-EQJUUVFG.js.map +1 -0
- package/dist/chunk-N5HFDWQX.js +23 -0
- package/dist/chunk-N5HFDWQX.js.map +1 -0
- package/dist/chunk-PAD7WFFU.js +573 -0
- package/dist/chunk-PAD7WFFU.js.map +1 -0
- package/dist/chunk-TK64DNVK.js +256 -0
- package/dist/chunk-TK64DNVK.js.map +1 -0
- package/dist/{chunk-NJSOD64C.js → chunk-WRTFC5NG.js} +438 -30
- package/dist/chunk-WRTFC5NG.js.map +1 -0
- package/dist/client-BKi3vk0Q.d.ts +34 -0
- package/dist/client-BrsaXtDV.d.cts +34 -0
- package/dist/cloudflare/index.cjs +1386 -74
- package/dist/cloudflare/index.cjs.map +1 -1
- package/dist/cloudflare/index.d.cts +217 -13
- package/dist/cloudflare/index.d.ts +217 -13
- package/dist/cloudflare/index.js +639 -180
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/errors-BRc3I_eH.d.cts +73 -0
- package/dist/errors-BRc3I_eH.d.ts +73 -0
- package/dist/firestore-enterprise/index.cjs +3877 -0
- package/dist/firestore-enterprise/index.cjs.map +1 -0
- package/dist/firestore-enterprise/index.d.cts +141 -0
- package/dist/firestore-enterprise/index.d.ts +141 -0
- package/dist/firestore-enterprise/index.js +985 -0
- package/dist/firestore-enterprise/index.js.map +1 -0
- package/dist/firestore-standard/index.cjs +3117 -0
- package/dist/firestore-standard/index.cjs.map +1 -0
- package/dist/firestore-standard/index.d.cts +49 -0
- package/dist/firestore-standard/index.d.ts +49 -0
- package/dist/firestore-standard/index.js +283 -0
- package/dist/firestore-standard/index.js.map +1 -0
- package/dist/index.cjs +809 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -100
- package/dist/index.d.ts +24 -100
- package/dist/index.js +184 -531
- package/dist/index.js.map +1 -1
- package/dist/registry-Bc7h6WTM.d.cts +64 -0
- package/dist/registry-C2KUPVZj.d.ts +64 -0
- package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.cts} +1 -56
- package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.ts} +1 -56
- package/dist/{serialization-ZZ7RSDRX.js → serialization-OE2PFZMY.js} +6 -4
- package/dist/sqlite/index.cjs +3631 -0
- package/dist/sqlite/index.cjs.map +1 -0
- package/dist/sqlite/index.d.cts +111 -0
- package/dist/sqlite/index.d.ts +111 -0
- package/dist/sqlite/index.js +1164 -0
- package/dist/sqlite/index.js.map +1 -0
- package/package.json +33 -3
- package/dist/backend-U-MLShlg.d.ts +0 -97
- package/dist/backend-np4gEVhB.d.cts +0 -97
- package/dist/chunk-5753Y42M.js.map +0 -1
- package/dist/chunk-NJSOD64C.js.map +0 -1
- package/dist/chunk-R7CRGYY4.js +0 -94
- package/dist/chunk-R7CRGYY4.js.map +0 -1
- package/dist/types-BGWxcpI_.d.cts +0 -736
- package/dist/types-BGWxcpI_.d.ts +0 -736
- /package/dist/{serialization-ZZ7RSDRX.js.map → serialization-OE2PFZMY.js.map} +0 -0
package/dist/cloudflare/index.js
CHANGED
|
@@ -1,137 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
GraphTimestampImpl,
|
|
3
|
+
assertJsonSafePayload,
|
|
4
|
+
buildIndexDDL,
|
|
5
|
+
compileDataOpsExpr,
|
|
6
|
+
dedupeIndexSpecs,
|
|
7
|
+
isFirestoreSpecialType,
|
|
8
|
+
validateJsonPathKey
|
|
9
|
+
} from "../chunk-4MMQ5W74.js";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_CORE_INDEXES
|
|
12
|
+
} from "../chunk-2DHMNTV6.js";
|
|
13
|
+
import {
|
|
14
|
+
createCapabilities,
|
|
15
|
+
intersectCapabilities
|
|
16
|
+
} from "../chunk-N5HFDWQX.js";
|
|
17
|
+
import {
|
|
18
|
+
META_EDGE_TYPE,
|
|
19
|
+
META_NODE_TYPE,
|
|
3
20
|
NODE_RELATION,
|
|
4
21
|
buildEdgeQueryPlan,
|
|
5
22
|
computeEdgeDocId,
|
|
6
23
|
computeNodeDocId,
|
|
7
|
-
createGraphClientFromBackend
|
|
8
|
-
|
|
24
|
+
createGraphClientFromBackend,
|
|
25
|
+
createMergedRegistry,
|
|
26
|
+
createRegistry,
|
|
27
|
+
generateId
|
|
28
|
+
} from "../chunk-WRTFC5NG.js";
|
|
9
29
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
this.nanoseconds = nanoseconds;
|
|
18
|
-
}
|
|
19
|
-
toDate() {
|
|
20
|
-
return new Date(this.toMillis());
|
|
21
|
-
}
|
|
22
|
-
toMillis() {
|
|
23
|
-
return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
|
|
24
|
-
}
|
|
25
|
-
toJSON() {
|
|
26
|
-
return { seconds: this.seconds, nanoseconds: this.nanoseconds };
|
|
27
|
-
}
|
|
28
|
-
static fromMillis(ms) {
|
|
29
|
-
const seconds = Math.floor(ms / 1e3);
|
|
30
|
-
const nanoseconds = (ms - seconds * 1e3) * 1e6;
|
|
31
|
-
return new _GraphTimestampImpl(seconds, nanoseconds);
|
|
32
|
-
}
|
|
33
|
-
static now() {
|
|
34
|
-
return _GraphTimestampImpl.fromMillis(Date.now());
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// src/internal/sqlite-index-ddl.ts
|
|
39
|
-
var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
40
|
-
var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
41
|
-
function quoteIdent(name) {
|
|
42
|
-
if (!IDENT_RE.test(name)) {
|
|
43
|
-
throw new FiregraphError(
|
|
44
|
-
`Invalid SQL identifier in index DDL: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
|
|
45
|
-
"INVALID_INDEX"
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return `"${name}"`;
|
|
49
|
-
}
|
|
50
|
-
function fnv1a32(str) {
|
|
51
|
-
let h = 2166136261;
|
|
52
|
-
for (let i = 0; i < str.length; i++) {
|
|
53
|
-
h ^= str.charCodeAt(i);
|
|
54
|
-
h = Math.imul(h, 16777619);
|
|
55
|
-
}
|
|
56
|
-
return (h >>> 0).toString(16).padStart(8, "0");
|
|
57
|
-
}
|
|
58
|
-
function normalizeFields(fields) {
|
|
59
|
-
return fields.map((f) => {
|
|
60
|
-
if (typeof f === "string") return { path: f, desc: false };
|
|
61
|
-
if (!f.path || typeof f.path !== "string") {
|
|
62
|
-
throw new FiregraphError(
|
|
63
|
-
`IndexSpec field must be a string or { path: string, desc?: boolean }; got ${JSON.stringify(f)}`,
|
|
64
|
-
"INVALID_INDEX"
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return { path: f.path, desc: !!f.desc };
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
function specFingerprint(spec, leadingColumns) {
|
|
71
|
-
const normalized = {
|
|
72
|
-
lead: leadingColumns,
|
|
73
|
-
fields: normalizeFields(spec.fields),
|
|
74
|
-
where: spec.where ?? ""
|
|
75
|
-
};
|
|
76
|
-
return fnv1a32(JSON.stringify(normalized));
|
|
77
|
-
}
|
|
78
|
-
function compileFieldExpr(path, fieldToColumn) {
|
|
79
|
-
const col = fieldToColumn[path];
|
|
80
|
-
if (col) return quoteIdent(col);
|
|
81
|
-
if (path === "data") {
|
|
82
|
-
return `json_extract("data", '$')`;
|
|
83
|
-
}
|
|
84
|
-
if (path.startsWith("data.")) {
|
|
85
|
-
const suffix = path.slice(5);
|
|
86
|
-
const parts = suffix.split(".");
|
|
87
|
-
for (const part of parts) {
|
|
88
|
-
if (!JSON_PATH_KEY_RE.test(part)) {
|
|
89
|
-
throw new FiregraphError(
|
|
90
|
-
`IndexSpec data path "${path}" has invalid component "${part}". Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,
|
|
91
|
-
"INVALID_INDEX"
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return `json_extract("data", '$.${suffix}')`;
|
|
96
|
-
}
|
|
97
|
-
throw new FiregraphError(
|
|
98
|
-
`IndexSpec field "${path}" is not a known firegraph field. Use a top-level field (aType, aUid, axbType, bType, bUid, createdAt, updatedAt, v) or a dotted data path like 'data.status'.`,
|
|
99
|
-
"INVALID_INDEX"
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
function buildIndexDDL(spec, options) {
|
|
103
|
-
const { table, fieldToColumn, leadingColumns = [] } = options;
|
|
104
|
-
if (!spec.fields || spec.fields.length === 0) {
|
|
105
|
-
throw new FiregraphError("IndexSpec.fields must be a non-empty array", "INVALID_INDEX");
|
|
106
|
-
}
|
|
107
|
-
const normalized = normalizeFields(spec.fields);
|
|
108
|
-
const hash = specFingerprint(spec, leadingColumns);
|
|
109
|
-
const indexName = `${table}_idx_${hash}`;
|
|
110
|
-
const cols = [];
|
|
111
|
-
for (const col of leadingColumns) {
|
|
112
|
-
cols.push(quoteIdent(col));
|
|
113
|
-
}
|
|
114
|
-
for (const f of normalized) {
|
|
115
|
-
const expr = compileFieldExpr(f.path, fieldToColumn);
|
|
116
|
-
cols.push(f.desc ? `${expr} DESC` : expr);
|
|
117
|
-
}
|
|
118
|
-
let ddl = `CREATE INDEX IF NOT EXISTS ${quoteIdent(indexName)} ON ${quoteIdent(table)}(${cols.join(", ")})`;
|
|
119
|
-
if (spec.where) {
|
|
120
|
-
ddl += ` WHERE ${spec.where}`;
|
|
121
|
-
}
|
|
122
|
-
return ddl;
|
|
123
|
-
}
|
|
124
|
-
function dedupeIndexSpecs(specs, leadingColumns = []) {
|
|
125
|
-
const seen = /* @__PURE__ */ new Set();
|
|
126
|
-
const out = [];
|
|
127
|
-
for (const spec of specs) {
|
|
128
|
-
const fp = specFingerprint(spec, leadingColumns);
|
|
129
|
-
if (seen.has(fp)) continue;
|
|
130
|
-
seen.add(fp);
|
|
131
|
-
out.push(spec);
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
30
|
+
CapabilityNotSupportedError,
|
|
31
|
+
FiregraphError,
|
|
32
|
+
assertUpdatePayloadExclusive,
|
|
33
|
+
deleteField,
|
|
34
|
+
flattenPatch
|
|
35
|
+
} from "../chunk-TK64DNVK.js";
|
|
36
|
+
import "../chunk-EQJUUVFG.js";
|
|
135
37
|
|
|
136
38
|
// src/cloudflare/schema.ts
|
|
137
39
|
var DO_FIELD_TO_COLUMN = {
|
|
@@ -144,9 +46,9 @@ var DO_FIELD_TO_COLUMN = {
|
|
|
144
46
|
createdAt: "created_at",
|
|
145
47
|
updatedAt: "updated_at"
|
|
146
48
|
};
|
|
147
|
-
var
|
|
49
|
+
var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
148
50
|
function validateDOTableName(name) {
|
|
149
|
-
if (!
|
|
51
|
+
if (!IDENT_RE.test(name)) {
|
|
150
52
|
throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
151
53
|
}
|
|
152
54
|
}
|
|
@@ -154,6 +56,9 @@ function quoteDOIdent(name) {
|
|
|
154
56
|
validateDOTableName(name);
|
|
155
57
|
return `"${name}"`;
|
|
156
58
|
}
|
|
59
|
+
function quoteDOColumnAlias(label) {
|
|
60
|
+
return `"${label.replace(/"/g, '""')}"`;
|
|
61
|
+
}
|
|
157
62
|
function buildDOSchemaStatements(table, options = {}) {
|
|
158
63
|
const t = quoteDOIdent(table);
|
|
159
64
|
const statements = [
|
|
@@ -180,6 +85,8 @@ function buildDOSchemaStatements(table, options = {}) {
|
|
|
180
85
|
}
|
|
181
86
|
|
|
182
87
|
// src/cloudflare/sql.ts
|
|
88
|
+
var DO_BACKEND_LABEL = "DO SQLite";
|
|
89
|
+
var DO_BACKEND_ERR_LABEL = "DO SQLite backend";
|
|
183
90
|
function compileFieldRef(field) {
|
|
184
91
|
const column = DO_FIELD_TO_COLUMN[field];
|
|
185
92
|
if (column) {
|
|
@@ -188,7 +95,7 @@ function compileFieldRef(field) {
|
|
|
188
95
|
if (field.startsWith("data.")) {
|
|
189
96
|
const suffix = field.slice(5);
|
|
190
97
|
for (const part of suffix.split(".")) {
|
|
191
|
-
validateJsonPathKey(part);
|
|
98
|
+
validateJsonPathKey(part, DO_BACKEND_ERR_LABEL);
|
|
192
99
|
}
|
|
193
100
|
return { expr: `json_extract("data", '$.${suffix}')` };
|
|
194
101
|
}
|
|
@@ -200,18 +107,6 @@ function compileFieldRef(field) {
|
|
|
200
107
|
"INVALID_QUERY"
|
|
201
108
|
);
|
|
202
109
|
}
|
|
203
|
-
var FIRESTORE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
204
|
-
"Timestamp",
|
|
205
|
-
"GeoPoint",
|
|
206
|
-
"VectorValue",
|
|
207
|
-
"DocumentReference",
|
|
208
|
-
"FieldValue"
|
|
209
|
-
]);
|
|
210
|
-
function isFirestoreSpecialType(value) {
|
|
211
|
-
const ctorName = value.constructor?.name;
|
|
212
|
-
if (ctorName && FIRESTORE_TYPE_NAMES.has(ctorName)) return ctorName;
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
110
|
function bindValue(value) {
|
|
216
111
|
if (value === null || value === void 0) return null;
|
|
217
112
|
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
@@ -230,21 +125,6 @@ function bindValue(value) {
|
|
|
230
125
|
}
|
|
231
126
|
return String(value);
|
|
232
127
|
}
|
|
233
|
-
var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
234
|
-
function validateJsonPathKey(key) {
|
|
235
|
-
if (key.length === 0) {
|
|
236
|
-
throw new FiregraphError(
|
|
237
|
-
"DO SQLite backend: empty JSON path component is not allowed",
|
|
238
|
-
"INVALID_QUERY"
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
if (!JSON_PATH_KEY_RE2.test(key)) {
|
|
242
|
-
throw new FiregraphError(
|
|
243
|
-
`DO SQLite backend: data field path component "${key}" is not a safe JSON-path identifier. Allowed pattern: /^[A-Za-z_][A-Za-z0-9_-]*$/. Use replaceData (full-data overwrite) for keys with reserved characters (whitespace, dots, brackets, quotes, etc.).`,
|
|
244
|
-
"INVALID_QUERY"
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
128
|
function compileFilter(filter, params) {
|
|
249
129
|
const { expr } = compileFieldRef(filter.field);
|
|
250
130
|
switch (filter.op) {
|
|
@@ -325,17 +205,244 @@ function compileDOSelect(table, filters, options) {
|
|
|
325
205
|
sql += compileLimit(options, params);
|
|
326
206
|
return { sql, params };
|
|
327
207
|
}
|
|
208
|
+
function compileDOExpand(table, params) {
|
|
209
|
+
if (params.sources.length === 0) {
|
|
210
|
+
throw new FiregraphError(
|
|
211
|
+
"compileDOExpand requires a non-empty sources list \u2014 empty IN () is invalid SQL.",
|
|
212
|
+
"INVALID_QUERY"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const direction = params.direction ?? "forward";
|
|
216
|
+
const aUidCol = compileFieldRef("aUid").expr;
|
|
217
|
+
const bUidCol = compileFieldRef("bUid").expr;
|
|
218
|
+
const aTypeCol = compileFieldRef("aType").expr;
|
|
219
|
+
const bTypeCol = compileFieldRef("bType").expr;
|
|
220
|
+
const axbTypeCol = compileFieldRef("axbType").expr;
|
|
221
|
+
const sourceColumn = direction === "forward" ? aUidCol : bUidCol;
|
|
222
|
+
const sqlParams = [params.axbType];
|
|
223
|
+
const conditions = [`${axbTypeCol} = ?`];
|
|
224
|
+
const placeholders = params.sources.map(() => "?").join(", ");
|
|
225
|
+
conditions.push(`${sourceColumn} IN (${placeholders})`);
|
|
226
|
+
for (const uid of params.sources) sqlParams.push(uid);
|
|
227
|
+
if (params.aType !== void 0) {
|
|
228
|
+
conditions.push(`${aTypeCol} = ?`);
|
|
229
|
+
sqlParams.push(params.aType);
|
|
230
|
+
}
|
|
231
|
+
if (params.bType !== void 0) {
|
|
232
|
+
conditions.push(`${bTypeCol} = ?`);
|
|
233
|
+
sqlParams.push(params.bType);
|
|
234
|
+
}
|
|
235
|
+
if (params.axbType === NODE_RELATION) {
|
|
236
|
+
conditions.push(`${aUidCol} != ${bUidCol}`);
|
|
237
|
+
}
|
|
238
|
+
let sql = `SELECT * FROM ${quoteDOIdent(table)} WHERE ${conditions.join(" AND ")}`;
|
|
239
|
+
if (params.orderBy) {
|
|
240
|
+
sql += compileOrderBy({ orderBy: params.orderBy }, sqlParams);
|
|
241
|
+
}
|
|
242
|
+
if (params.limitPerSource !== void 0) {
|
|
243
|
+
const totalLimit = params.sources.length * params.limitPerSource;
|
|
244
|
+
sql += ` LIMIT ?`;
|
|
245
|
+
sqlParams.push(totalLimit);
|
|
246
|
+
}
|
|
247
|
+
return { sql, params: sqlParams };
|
|
248
|
+
}
|
|
249
|
+
function compileDOExpandHydrate(table, targetUids) {
|
|
250
|
+
if (targetUids.length === 0) {
|
|
251
|
+
throw new FiregraphError(
|
|
252
|
+
"compileDOExpandHydrate requires a non-empty target list \u2014 empty IN () is invalid SQL.",
|
|
253
|
+
"INVALID_QUERY"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const placeholders = targetUids.map(() => "?").join(", ");
|
|
257
|
+
const sqlParams = [NODE_RELATION];
|
|
258
|
+
for (const uid of targetUids) sqlParams.push(uid);
|
|
259
|
+
const aUidCol = compileFieldRef("aUid").expr;
|
|
260
|
+
const bUidCol = compileFieldRef("bUid").expr;
|
|
261
|
+
const axbTypeCol = compileFieldRef("axbType").expr;
|
|
262
|
+
return {
|
|
263
|
+
sql: `SELECT * FROM ${quoteDOIdent(table)} WHERE ${axbTypeCol} = ? AND ${aUidCol} = ${bUidCol} AND ${bUidCol} IN (${placeholders})`,
|
|
264
|
+
params: sqlParams
|
|
265
|
+
};
|
|
266
|
+
}
|
|
328
267
|
function compileDOSelectByDocId(table, docId) {
|
|
329
268
|
return {
|
|
330
269
|
sql: `SELECT * FROM ${quoteDOIdent(table)} WHERE "doc_id" = ? LIMIT 1`,
|
|
331
270
|
params: [docId]
|
|
332
271
|
};
|
|
333
272
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
273
|
+
function normalizeDOProjectionField(field) {
|
|
274
|
+
if (field in DO_FIELD_TO_COLUMN) return field;
|
|
275
|
+
if (field === "data" || field.startsWith("data.")) return field;
|
|
276
|
+
return `data.${field}`;
|
|
277
|
+
}
|
|
278
|
+
function compileDOFindEdgesProjected(table, select, filters, options) {
|
|
279
|
+
if (select.length === 0) {
|
|
280
|
+
throw new FiregraphError(
|
|
281
|
+
"compileDOFindEdgesProjected requires a non-empty select list \u2014 an empty projection has no SQL representation distinct from `findEdges`.",
|
|
282
|
+
"INVALID_QUERY"
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
286
|
+
const uniqueFields = [];
|
|
287
|
+
for (const f of select) {
|
|
288
|
+
if (!seen.has(f)) {
|
|
289
|
+
seen.add(f);
|
|
290
|
+
uniqueFields.push(f);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const projections = [];
|
|
294
|
+
const columns = [];
|
|
295
|
+
for (let idx = 0; idx < uniqueFields.length; idx++) {
|
|
296
|
+
const field = uniqueFields[idx];
|
|
297
|
+
const canonical = normalizeDOProjectionField(field);
|
|
298
|
+
const { expr } = compileFieldRef(canonical);
|
|
299
|
+
const alias = quoteDOColumnAlias(field);
|
|
300
|
+
projections.push(`${expr} AS ${alias}`);
|
|
301
|
+
let kind;
|
|
302
|
+
let typeAliasName;
|
|
303
|
+
if (canonical === "data") {
|
|
304
|
+
kind = "data";
|
|
305
|
+
} else if (canonical.startsWith("data.")) {
|
|
306
|
+
kind = "json";
|
|
307
|
+
typeAliasName = `__fg_t_${idx}`;
|
|
308
|
+
const typeAlias = quoteDOColumnAlias(typeAliasName);
|
|
309
|
+
projections.push(`json_type("data", '$.${canonical.slice(5)}') AS ${typeAlias}`);
|
|
310
|
+
} else {
|
|
311
|
+
if (canonical === "v") kind = "builtin-int";
|
|
312
|
+
else if (canonical === "createdAt" || canonical === "updatedAt") kind = "builtin-timestamp";
|
|
313
|
+
else kind = "builtin-text";
|
|
314
|
+
}
|
|
315
|
+
columns.push({ field, kind, typeAlias: typeAliasName });
|
|
316
|
+
}
|
|
317
|
+
const params = [];
|
|
318
|
+
const conditions = [];
|
|
319
|
+
for (const f of filters) {
|
|
320
|
+
conditions.push(compileFilter(f, params));
|
|
321
|
+
}
|
|
322
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
323
|
+
let sql = `SELECT ${projections.join(", ")} FROM ${quoteDOIdent(table)}${where}`;
|
|
324
|
+
sql += compileOrderBy(options, params);
|
|
325
|
+
sql += compileLimit(options, params);
|
|
326
|
+
return { stmt: { sql, params }, columns };
|
|
327
|
+
}
|
|
328
|
+
function decodeDOProjectedRow(row, columns) {
|
|
329
|
+
const out = {};
|
|
330
|
+
for (const c of columns) {
|
|
331
|
+
const raw = row[c.field];
|
|
332
|
+
switch (c.kind) {
|
|
333
|
+
case "builtin-text":
|
|
334
|
+
out[c.field] = raw === null || raw === void 0 ? null : String(raw);
|
|
335
|
+
break;
|
|
336
|
+
case "builtin-int":
|
|
337
|
+
if (raw === null || raw === void 0) {
|
|
338
|
+
out[c.field] = null;
|
|
339
|
+
} else if (typeof raw === "bigint") {
|
|
340
|
+
out[c.field] = Number(raw);
|
|
341
|
+
} else if (typeof raw === "number") {
|
|
342
|
+
out[c.field] = raw;
|
|
343
|
+
} else {
|
|
344
|
+
out[c.field] = Number(raw);
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
case "builtin-timestamp": {
|
|
348
|
+
const ms = toMillis(raw);
|
|
349
|
+
out[c.field] = GraphTimestampImpl.fromMillis(ms);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "data":
|
|
353
|
+
if (raw === null || raw === void 0 || raw === "") {
|
|
354
|
+
out[c.field] = {};
|
|
355
|
+
} else {
|
|
356
|
+
out[c.field] = JSON.parse(raw);
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
case "json": {
|
|
360
|
+
const t = row[c.typeAlias];
|
|
361
|
+
if (raw === null || raw === void 0) {
|
|
362
|
+
out[c.field] = null;
|
|
363
|
+
} else if (t === "object" || t === "array") {
|
|
364
|
+
out[c.field] = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
365
|
+
} else if (t === "integer" && typeof raw === "bigint") {
|
|
366
|
+
out[c.field] = Number(raw);
|
|
367
|
+
} else {
|
|
368
|
+
out[c.field] = raw;
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
function compileDOAggregate(table, spec, filters) {
|
|
377
|
+
const aliases = Object.keys(spec);
|
|
378
|
+
if (aliases.length === 0) {
|
|
379
|
+
throw new FiregraphError(
|
|
380
|
+
"aggregate() requires at least one aggregation in the `aggregates` map.",
|
|
381
|
+
"INVALID_QUERY"
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const projections = [];
|
|
385
|
+
for (const alias of aliases) {
|
|
386
|
+
const { op, field } = spec[alias];
|
|
387
|
+
validateJsonPathKey(alias, DO_BACKEND_ERR_LABEL);
|
|
388
|
+
if (op === "count") {
|
|
389
|
+
if (field !== void 0) {
|
|
390
|
+
throw new FiregraphError(
|
|
391
|
+
`Aggregate '${alias}' op 'count' must not specify a field \u2014 count operates on rows, not a column expression.`,
|
|
392
|
+
"INVALID_QUERY"
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
projections.push(`COUNT(*) AS ${quoteDOIdent(alias)}`);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (!field) {
|
|
399
|
+
throw new FiregraphError(
|
|
400
|
+
`Aggregate '${alias}' op '${op}' requires a field.`,
|
|
401
|
+
"INVALID_QUERY"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const { expr } = compileFieldRef(field);
|
|
405
|
+
const numeric = `CAST(${expr} AS REAL)`;
|
|
406
|
+
if (op === "sum") projections.push(`SUM(${numeric}) AS ${quoteDOIdent(alias)}`);
|
|
407
|
+
else if (op === "avg") projections.push(`AVG(${numeric}) AS ${quoteDOIdent(alias)}`);
|
|
408
|
+
else if (op === "min") projections.push(`MIN(${numeric}) AS ${quoteDOIdent(alias)}`);
|
|
409
|
+
else if (op === "max") projections.push(`MAX(${numeric}) AS ${quoteDOIdent(alias)}`);
|
|
410
|
+
else
|
|
411
|
+
throw new FiregraphError(
|
|
412
|
+
`DO SQLite backend does not support aggregate op: ${String(op)}`,
|
|
413
|
+
"INVALID_QUERY"
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const params = [];
|
|
417
|
+
const conditions = [];
|
|
418
|
+
for (const f of filters) {
|
|
419
|
+
conditions.push(compileFilter(f, params));
|
|
420
|
+
}
|
|
421
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
422
|
+
const sql = `SELECT ${projections.join(", ")} FROM ${quoteDOIdent(table)}${where}`;
|
|
423
|
+
return { stmt: { sql, params }, aliases };
|
|
424
|
+
}
|
|
425
|
+
function compileDOSet(table, docId, record, nowMillis, mode) {
|
|
426
|
+
assertJsonSafePayload(record.data, DO_BACKEND_LABEL);
|
|
427
|
+
if (mode === "replace") {
|
|
428
|
+
const sql2 = `INSERT OR REPLACE INTO ${quoteDOIdent(table)} (
|
|
429
|
+
doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
|
|
430
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
431
|
+
const params = [
|
|
432
|
+
docId,
|
|
433
|
+
record.aType,
|
|
434
|
+
record.aUid,
|
|
435
|
+
record.axbType,
|
|
436
|
+
record.bType,
|
|
437
|
+
record.bUid,
|
|
438
|
+
JSON.stringify(record.data ?? {}),
|
|
439
|
+
record.v ?? null,
|
|
440
|
+
nowMillis,
|
|
441
|
+
nowMillis
|
|
442
|
+
];
|
|
443
|
+
return { sql: sql2, params };
|
|
444
|
+
}
|
|
445
|
+
const insertParams = [
|
|
339
446
|
docId,
|
|
340
447
|
record.aType,
|
|
341
448
|
record.aUid,
|
|
@@ -347,22 +454,44 @@ function compileDOSet(table, docId, record, nowMillis) {
|
|
|
347
454
|
nowMillis,
|
|
348
455
|
nowMillis
|
|
349
456
|
];
|
|
350
|
-
|
|
457
|
+
const ops = flattenPatch(record.data ?? {});
|
|
458
|
+
const updateParams = [];
|
|
459
|
+
const dataExpr = compileDataOpsExpr(ops, `COALESCE("data", '{}')`, updateParams, DO_BACKEND_ERR_LABEL) ?? `COALESCE("data", '{}')`;
|
|
460
|
+
const sql = `INSERT INTO ${quoteDOIdent(table)} (
|
|
461
|
+
doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
|
|
462
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
463
|
+
ON CONFLICT(doc_id) DO UPDATE SET
|
|
464
|
+
"a_type" = excluded."a_type",
|
|
465
|
+
"a_uid" = excluded."a_uid",
|
|
466
|
+
"axb_type" = excluded."axb_type",
|
|
467
|
+
"b_type" = excluded."b_type",
|
|
468
|
+
"b_uid" = excluded."b_uid",
|
|
469
|
+
"data" = ${dataExpr},
|
|
470
|
+
"v" = COALESCE(excluded."v", "v"),
|
|
471
|
+
"created_at" = excluded."created_at",
|
|
472
|
+
"updated_at" = excluded."updated_at"`;
|
|
473
|
+
return { sql, params: [...insertParams, ...updateParams] };
|
|
351
474
|
}
|
|
352
475
|
function compileDOUpdate(table, docId, update, nowMillis) {
|
|
476
|
+
assertUpdatePayloadExclusive(update);
|
|
353
477
|
const setClauses = [];
|
|
354
478
|
const params = [];
|
|
355
479
|
if (update.replaceData) {
|
|
480
|
+
assertJsonSafePayload(update.replaceData, DO_BACKEND_LABEL);
|
|
356
481
|
setClauses.push(`"data" = ?`);
|
|
357
482
|
params.push(JSON.stringify(update.replaceData));
|
|
358
|
-
} else if (update.
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
params
|
|
483
|
+
} else if (update.dataOps && update.dataOps.length > 0) {
|
|
484
|
+
for (const op of update.dataOps) {
|
|
485
|
+
if (!op.delete) assertJsonSafePayload(op.value, DO_BACKEND_LABEL);
|
|
486
|
+
}
|
|
487
|
+
const expr = compileDataOpsExpr(
|
|
488
|
+
update.dataOps,
|
|
489
|
+
`COALESCE("data", '{}')`,
|
|
490
|
+
params,
|
|
491
|
+
DO_BACKEND_ERR_LABEL
|
|
492
|
+
);
|
|
493
|
+
if (expr !== null) {
|
|
494
|
+
setClauses.push(`"data" = ${expr}`);
|
|
366
495
|
}
|
|
367
496
|
}
|
|
368
497
|
if (update.v !== void 0) {
|
|
@@ -383,6 +512,55 @@ function compileDODelete(table, docId) {
|
|
|
383
512
|
params: [docId]
|
|
384
513
|
};
|
|
385
514
|
}
|
|
515
|
+
function compileDOBulkDelete(table, filters) {
|
|
516
|
+
const params = [];
|
|
517
|
+
const conditions = [];
|
|
518
|
+
for (const f of filters) {
|
|
519
|
+
conditions.push(compileFilter(f, params));
|
|
520
|
+
}
|
|
521
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
522
|
+
return {
|
|
523
|
+
sql: `DELETE FROM ${quoteDOIdent(table)}${where}`,
|
|
524
|
+
params
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function compileDOBulkUpdate(table, filters, patchData, nowMillis) {
|
|
528
|
+
const dataOps = flattenPatch(patchData);
|
|
529
|
+
if (dataOps.length === 0) {
|
|
530
|
+
throw new FiregraphError(
|
|
531
|
+
"bulkUpdate() patch.data must contain at least one leaf \u2014 an empty patch would only rewrite `updated_at`, which is almost certainly a bug. Use `setDoc` with merge mode if you want to stamp without editing data.",
|
|
532
|
+
"INVALID_QUERY"
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
for (const op of dataOps) {
|
|
536
|
+
if (!op.delete) assertJsonSafePayload(op.value, DO_BACKEND_LABEL);
|
|
537
|
+
}
|
|
538
|
+
const setParams = [];
|
|
539
|
+
const expr = compileDataOpsExpr(
|
|
540
|
+
dataOps,
|
|
541
|
+
`COALESCE("data", '{}')`,
|
|
542
|
+
setParams,
|
|
543
|
+
DO_BACKEND_ERR_LABEL
|
|
544
|
+
);
|
|
545
|
+
if (expr === null) {
|
|
546
|
+
throw new FiregraphError(
|
|
547
|
+
"bulkUpdate() patch produced no SQL operations \u2014 internal invariant violated.",
|
|
548
|
+
"INVALID_ARGUMENT"
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const setClauses = [`"data" = ${expr}`, `"updated_at" = ?`];
|
|
552
|
+
setParams.push(nowMillis);
|
|
553
|
+
const whereParams = [];
|
|
554
|
+
const conditions = [];
|
|
555
|
+
for (const f of filters) {
|
|
556
|
+
conditions.push(compileFilter(f, whereParams));
|
|
557
|
+
}
|
|
558
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
559
|
+
return {
|
|
560
|
+
sql: `UPDATE ${quoteDOIdent(table)} SET ${setClauses.join(", ")}${where}`,
|
|
561
|
+
params: [...setParams, ...whereParams]
|
|
562
|
+
};
|
|
563
|
+
}
|
|
386
564
|
function compileDODeleteAll(table) {
|
|
387
565
|
return {
|
|
388
566
|
sql: `DELETE FROM ${quoteDOIdent(table)}`,
|
|
@@ -458,8 +636,8 @@ var DORPCBatchBackend = class {
|
|
|
458
636
|
this.getStub = getStub;
|
|
459
637
|
}
|
|
460
638
|
ops = [];
|
|
461
|
-
setDoc(docId, record) {
|
|
462
|
-
this.ops.push({ kind: "set", docId, record });
|
|
639
|
+
setDoc(docId, record, mode) {
|
|
640
|
+
this.ops.push({ kind: "set", docId, record, mode });
|
|
463
641
|
}
|
|
464
642
|
updateDoc(docId, update) {
|
|
465
643
|
this.ops.push({ kind: "update", docId, update });
|
|
@@ -474,7 +652,18 @@ var DORPCBatchBackend = class {
|
|
|
474
652
|
await this.getStub()._fgBatch(ops);
|
|
475
653
|
}
|
|
476
654
|
};
|
|
655
|
+
var DO_CAPS = /* @__PURE__ */ new Set([
|
|
656
|
+
"core.read",
|
|
657
|
+
"core.write",
|
|
658
|
+
"core.batch",
|
|
659
|
+
"core.subgraph",
|
|
660
|
+
"query.aggregate",
|
|
661
|
+
"query.dml",
|
|
662
|
+
"query.join",
|
|
663
|
+
"query.select"
|
|
664
|
+
]);
|
|
477
665
|
var DORPCBackend = class _DORPCBackend {
|
|
666
|
+
capabilities = createCapabilities(DO_CAPS);
|
|
478
667
|
collectionPath = "firegraph";
|
|
479
668
|
scopePath;
|
|
480
669
|
/** @internal */
|
|
@@ -508,9 +697,37 @@ var DORPCBackend = class _DORPCBackend {
|
|
|
508
697
|
const wires = await this.stub._fgQuery(filters, options);
|
|
509
698
|
return wires.map(hydrateDORecord);
|
|
510
699
|
}
|
|
700
|
+
// --- Aggregate ---
|
|
701
|
+
/**
|
|
702
|
+
* Run an aggregate query inside the backing DO. The DO returns a row of
|
|
703
|
+
* `{ alias: number | null }` (null = SQLite NULL for SUM/MIN/MAX over an
|
|
704
|
+
* empty set, or the count being literally 0); this method resolves NULL
|
|
705
|
+
* to 0 for SUM/MIN/MAX and to NaN for AVG, matching the SQLite backend
|
|
706
|
+
* and the Firestore Standard helper.
|
|
707
|
+
*/
|
|
708
|
+
async aggregate(spec, filters) {
|
|
709
|
+
const stub = this.stub;
|
|
710
|
+
if (!stub._fgAggregate) {
|
|
711
|
+
throw new FiregraphError(
|
|
712
|
+
"aggregate() not supported by this Durable Object stub. The wrapped stub does not implement `_fgAggregate`. If you control the stub wrapper, forward `_fgAggregate` to the underlying DO.",
|
|
713
|
+
"UNSUPPORTED_OPERATION"
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const wire = await stub._fgAggregate(spec, filters);
|
|
717
|
+
const out = {};
|
|
718
|
+
for (const [alias, { op }] of Object.entries(spec)) {
|
|
719
|
+
const v = wire[alias];
|
|
720
|
+
if (v === null || v === void 0) {
|
|
721
|
+
out[alias] = op === "avg" ? Number.NaN : 0;
|
|
722
|
+
} else {
|
|
723
|
+
out[alias] = v;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return out;
|
|
727
|
+
}
|
|
511
728
|
// --- Writes ---
|
|
512
|
-
async setDoc(docId, record) {
|
|
513
|
-
return this.stub._fgSetDoc(docId, record);
|
|
729
|
+
async setDoc(docId, record, mode) {
|
|
730
|
+
return this.stub._fgSetDoc(docId, record, mode);
|
|
514
731
|
}
|
|
515
732
|
async updateDoc(docId, update) {
|
|
516
733
|
return this.stub._fgUpdateDoc(docId, update);
|
|
@@ -564,6 +781,105 @@ var DORPCBackend = class _DORPCBackend {
|
|
|
564
781
|
void _reader;
|
|
565
782
|
return this.stub._fgBulkRemoveEdges(params, options);
|
|
566
783
|
}
|
|
784
|
+
// --- Server-side DML (capability: query.dml) ---
|
|
785
|
+
/**
|
|
786
|
+
* Single-statement bulk DELETE inside the backing DO. The DO compiles
|
|
787
|
+
* the filter list to one `DELETE … WHERE …` statement and returns a
|
|
788
|
+
* `BulkResult` whose `deleted` is the affected-row count.
|
|
789
|
+
*
|
|
790
|
+
* Defensive `_fgBulkDelete` presence check mirrors `aggregate()`: the
|
|
791
|
+
* RPC method is optional on `FiregraphStub` so external worker code with
|
|
792
|
+
* a hand-rolled stub wrapper still type-checks. Surface a clear
|
|
793
|
+
* `UNSUPPORTED_OPERATION` rather than `TypeError: stub._fgBulkDelete is
|
|
794
|
+
* not a function` when the wrapper hasn't forwarded the method.
|
|
795
|
+
*/
|
|
796
|
+
async bulkDelete(filters, options) {
|
|
797
|
+
const stub = this.stub;
|
|
798
|
+
if (!stub._fgBulkDelete) {
|
|
799
|
+
throw new FiregraphError(
|
|
800
|
+
"bulkDelete() not supported by this Durable Object stub. The wrapped stub does not implement `_fgBulkDelete`. If you control the stub wrapper, forward `_fgBulkDelete` to the underlying DO.",
|
|
801
|
+
"UNSUPPORTED_OPERATION"
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
return stub._fgBulkDelete(filters, options);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Single-statement bulk UPDATE inside the backing DO. Same contract as
|
|
808
|
+
* `bulkDelete` for the missing-method case; the DO compiles the patch to
|
|
809
|
+
* one `UPDATE … SET data = json_patch(...) WHERE …` statement.
|
|
810
|
+
*/
|
|
811
|
+
async bulkUpdate(filters, patch, options) {
|
|
812
|
+
const stub = this.stub;
|
|
813
|
+
if (!stub._fgBulkUpdate) {
|
|
814
|
+
throw new FiregraphError(
|
|
815
|
+
"bulkUpdate() not supported by this Durable Object stub. The wrapped stub does not implement `_fgBulkUpdate`. If you control the stub wrapper, forward `_fgBulkUpdate` to the underlying DO.",
|
|
816
|
+
"UNSUPPORTED_OPERATION"
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
return stub._fgBulkUpdate(filters, patch, options);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Multi-source fan-out — `query.join` capability. Routes the call through
|
|
823
|
+
* the DO's `_fgExpand` RPC, which compiles to one `SELECT … WHERE
|
|
824
|
+
* "aUid" IN (?, …)` statement (plus, when `params.hydrate === true`, a
|
|
825
|
+
* second IN-clause statement against the node rows).
|
|
826
|
+
*
|
|
827
|
+
* Defensive `_fgExpand` presence check matches the bulk-DML pattern: the
|
|
828
|
+
* RPC method is optional on `FiregraphStub` so external worker code with
|
|
829
|
+
* a hand-rolled stub wrapper still type-checks. We surface a clear
|
|
830
|
+
* `UNSUPPORTED_OPERATION` rather than `TypeError: stub._fgExpand is not a
|
|
831
|
+
* function` if the wrapper hasn't forwarded the method.
|
|
832
|
+
*/
|
|
833
|
+
async expand(params) {
|
|
834
|
+
const stub = this.stub;
|
|
835
|
+
if (!stub._fgExpand) {
|
|
836
|
+
throw new FiregraphError(
|
|
837
|
+
"expand() not supported by this Durable Object stub. The wrapped stub does not implement `_fgExpand`. If you control the stub wrapper, forward `_fgExpand` to the underlying DO.",
|
|
838
|
+
"UNSUPPORTED_OPERATION"
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
if (params.sources.length === 0) {
|
|
842
|
+
return params.hydrate ? { edges: [], targets: [] } : { edges: [] };
|
|
843
|
+
}
|
|
844
|
+
const wire = await stub._fgExpand(params);
|
|
845
|
+
const edges = wire.edges.map(hydrateDORecord);
|
|
846
|
+
if (!params.hydrate) {
|
|
847
|
+
return { edges };
|
|
848
|
+
}
|
|
849
|
+
const targets = (wire.targets ?? []).map((row) => row ? hydrateDORecord(row) : null);
|
|
850
|
+
return { edges, targets };
|
|
851
|
+
}
|
|
852
|
+
// --- Server-side projection (capability: query.select) ---
|
|
853
|
+
/**
|
|
854
|
+
* Server-side projection — `query.select` capability. Forwards the call to
|
|
855
|
+
* the DO's `_fgFindEdgesProjected` RPC, which compiles to a single
|
|
856
|
+
* `SELECT json_extract(...) AS …, json_type(...) AS …__t FROM <table>
|
|
857
|
+
* WHERE …` statement. The DO returns raw rows + per-column metadata; this
|
|
858
|
+
* method decodes each row locally via `decodeDOProjectedRow`.
|
|
859
|
+
*
|
|
860
|
+
* Decoding lives on this side (not inside the DO) because
|
|
861
|
+
* `GraphTimestampImpl` is a class — its prototype does not survive
|
|
862
|
+
* workerd's structured-clone boundary — so timestamp rehydration must
|
|
863
|
+
* happen wherever the rows are consumed by the GraphClient.
|
|
864
|
+
*
|
|
865
|
+
* Defensive `_fgFindEdgesProjected` presence check matches the `expand` /
|
|
866
|
+
* bulk-DML / aggregate pattern: the RPC method is optional on
|
|
867
|
+
* `FiregraphStub` so external worker code with a hand-rolled stub wrapper
|
|
868
|
+
* still type-checks. Surface a clear `UNSUPPORTED_OPERATION` rather than
|
|
869
|
+
* `TypeError: stub._fgFindEdgesProjected is not a function` if the
|
|
870
|
+
* wrapper hasn't forwarded the method.
|
|
871
|
+
*/
|
|
872
|
+
async findEdgesProjected(select, filters, options) {
|
|
873
|
+
const stub = this.stub;
|
|
874
|
+
if (!stub._fgFindEdgesProjected) {
|
|
875
|
+
throw new FiregraphError(
|
|
876
|
+
"findEdgesProjected() not supported by this Durable Object stub. The wrapped stub does not implement `_fgFindEdgesProjected`. If you control the stub wrapper, forward `_fgFindEdgesProjected` to the underlying DO.",
|
|
877
|
+
"UNSUPPORTED_OPERATION"
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const { rows, columns } = await stub._fgFindEdgesProjected(select, filters, options);
|
|
881
|
+
return rows.map((row) => decodeDOProjectedRow(row, columns));
|
|
882
|
+
}
|
|
567
883
|
// --- Cross-scope queries ---
|
|
568
884
|
//
|
|
569
885
|
// `findEdgesGlobal` is deliberately NOT defined on this class. The
|
|
@@ -757,11 +1073,32 @@ var FiregraphDO = class extends DurableObject {
|
|
|
757
1073
|
const rows = this.execAll(stmt);
|
|
758
1074
|
return rows.map(rowToDORecord);
|
|
759
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Aggregate query (capability `query.aggregate`). Compiles a single
|
|
1078
|
+
* `SELECT` projecting one column per alias; SQLite handles count, sum,
|
|
1079
|
+
* avg, min, max natively. Empty-set fix-ups (NULL → 0 for sum/min/max,
|
|
1080
|
+
* NaN for avg) happen on the client side in `DORPCBackend.aggregate` so
|
|
1081
|
+
* the wire payload stays a plain row of (alias → number | null).
|
|
1082
|
+
*/
|
|
1083
|
+
async _fgAggregate(spec, filters) {
|
|
1084
|
+
const { stmt, aliases } = compileDOAggregate(this.table, spec, filters);
|
|
1085
|
+
const rows = this.execAll(stmt);
|
|
1086
|
+
const row = rows[0] ?? {};
|
|
1087
|
+
const out = {};
|
|
1088
|
+
for (const alias of aliases) {
|
|
1089
|
+
const v = row[alias];
|
|
1090
|
+
if (v === null || v === void 0) out[alias] = null;
|
|
1091
|
+
else if (typeof v === "bigint") out[alias] = Number(v);
|
|
1092
|
+
else if (typeof v === "number") out[alias] = v;
|
|
1093
|
+
else out[alias] = Number(v);
|
|
1094
|
+
}
|
|
1095
|
+
return out;
|
|
1096
|
+
}
|
|
760
1097
|
// ---------------------------------------------------------------------------
|
|
761
1098
|
// RPC: writes
|
|
762
1099
|
// ---------------------------------------------------------------------------
|
|
763
|
-
async _fgSetDoc(docId, record) {
|
|
764
|
-
const stmt = compileDOSet(this.table, docId, record, Date.now());
|
|
1100
|
+
async _fgSetDoc(docId, record, mode) {
|
|
1101
|
+
const stmt = compileDOSet(this.table, docId, record, Date.now(), mode);
|
|
765
1102
|
this.execRun(stmt);
|
|
766
1103
|
}
|
|
767
1104
|
async _fgUpdateDoc(docId, update) {
|
|
@@ -791,7 +1128,7 @@ var FiregraphDO = class extends DurableObject {
|
|
|
791
1128
|
const statements = ops.map((op) => {
|
|
792
1129
|
switch (op.kind) {
|
|
793
1130
|
case "set":
|
|
794
|
-
return compileDOSet(this.table, op.docId, op.record, now);
|
|
1131
|
+
return compileDOSet(this.table, op.docId, op.record, now, op.mode);
|
|
795
1132
|
case "update":
|
|
796
1133
|
return compileDOUpdate(this.table, op.docId, op.update, now);
|
|
797
1134
|
case "delete":
|
|
@@ -907,6 +1244,119 @@ var FiregraphDO = class extends DurableObject {
|
|
|
907
1244
|
}
|
|
908
1245
|
}
|
|
909
1246
|
// ---------------------------------------------------------------------------
|
|
1247
|
+
// RPC: server-side DML (capability `query.dml`)
|
|
1248
|
+
//
|
|
1249
|
+
// Single-statement DELETE/UPDATE WHERE that the SQLite engine handles in
|
|
1250
|
+
// one shot — the cap-less alternative is `_fgBulkRemoveEdges` which fetches
|
|
1251
|
+
// doc IDs first, then deletes them one-by-one inside a transaction. The
|
|
1252
|
+
// DML path skips the round-trip and lets SQLite optimize the WHERE.
|
|
1253
|
+
//
|
|
1254
|
+
// RETURNING "doc_id" gives us an authoritative affected-row count; SQLite
|
|
1255
|
+
// ≥3.35 supports it for both DELETE and UPDATE and DO SQLite is always
|
|
1256
|
+
// recent enough.
|
|
1257
|
+
//
|
|
1258
|
+
// Retry policy: unlike `SqliteBackendImpl.bulkDelete` / `bulkUpdate`, which
|
|
1259
|
+
// wrap a chunked retry/backoff loop around each batch (D1's 1000-statement
|
|
1260
|
+
// cap forces chunking, so a single transient failure shouldn't kill the
|
|
1261
|
+
// whole job), the DO path runs a single un-chunked statement against
|
|
1262
|
+
// `state.storage.sql` synchronously. There's nothing to retry inside the
|
|
1263
|
+
// DO — the engine commits or it doesn't. If a caller wants retry semantics
|
|
1264
|
+
// on the wire, they wrap the `bulkDelete` / `bulkUpdate` call themselves.
|
|
1265
|
+
// ---------------------------------------------------------------------------
|
|
1266
|
+
async _fgBulkDelete(filters, _options) {
|
|
1267
|
+
void _options;
|
|
1268
|
+
if (filters.length === 0) {
|
|
1269
|
+
throw new FiregraphError(
|
|
1270
|
+
"bulkDelete() requires at least one filter when targeting a Durable Object backend. An empty filter list would wipe every row in the DO. To wipe a routed subgraph DO, use `removeNodeCascade` on the parent node or `_fgDestroy` directly on the stub.",
|
|
1271
|
+
"INVALID_ARGUMENT"
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
const stmt = compileDOBulkDelete(this.table, filters);
|
|
1275
|
+
return this.execDmlWithReturning(stmt);
|
|
1276
|
+
}
|
|
1277
|
+
async _fgBulkUpdate(filters, patch, _options) {
|
|
1278
|
+
void _options;
|
|
1279
|
+
const stmt = compileDOBulkUpdate(this.table, filters, patch.data, Date.now());
|
|
1280
|
+
return this.execDmlWithReturning(stmt);
|
|
1281
|
+
}
|
|
1282
|
+
// ---------------------------------------------------------------------------
|
|
1283
|
+
// RPC: multi-source fan-out (`query.join`)
|
|
1284
|
+
//
|
|
1285
|
+
// One `SELECT … WHERE "aUid" IN (?, ?, …)` (or `"bUid"` for reverse)
|
|
1286
|
+
// collapses N per-source `findEdges` round trips into one. When the
|
|
1287
|
+
// caller asks for hydration, a second IN-clause statement fetches the
|
|
1288
|
+
// target node rows; the DO does the alignment in JS so the wire payload
|
|
1289
|
+
// is two `DORecordWire[]` arrays instead of a JOIN-shaped row that
|
|
1290
|
+
// would force a custom client-side decoder.
|
|
1291
|
+
// ---------------------------------------------------------------------------
|
|
1292
|
+
async _fgExpand(params) {
|
|
1293
|
+
if (params.sources.length === 0) {
|
|
1294
|
+
return params.hydrate ? { edges: [], targets: [] } : { edges: [] };
|
|
1295
|
+
}
|
|
1296
|
+
const stmt = compileDOExpand(this.table, params);
|
|
1297
|
+
const rows = this.state.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
|
|
1298
|
+
const edges = rows.map((row) => rowToDORecord(row));
|
|
1299
|
+
if (!params.hydrate) {
|
|
1300
|
+
return { edges };
|
|
1301
|
+
}
|
|
1302
|
+
const direction = params.direction ?? "forward";
|
|
1303
|
+
const targetUids = edges.map((e) => direction === "forward" ? e.bUid : e.aUid);
|
|
1304
|
+
const uniqueTargets = [...new Set(targetUids)];
|
|
1305
|
+
if (uniqueTargets.length === 0) {
|
|
1306
|
+
return { edges, targets: [] };
|
|
1307
|
+
}
|
|
1308
|
+
const hydrateStmt = compileDOExpandHydrate(this.table, uniqueTargets);
|
|
1309
|
+
const hydrateRows = this.state.storage.sql.exec(hydrateStmt.sql, ...hydrateStmt.params).toArray();
|
|
1310
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
1311
|
+
for (const row of hydrateRows) {
|
|
1312
|
+
const node = rowToDORecord(row);
|
|
1313
|
+
byUid.set(node.bUid, node);
|
|
1314
|
+
}
|
|
1315
|
+
const targets = targetUids.map((uid) => byUid.get(uid) ?? null);
|
|
1316
|
+
return { edges, targets };
|
|
1317
|
+
}
|
|
1318
|
+
// ---------------------------------------------------------------------------
|
|
1319
|
+
// RPC: server-side projection (`query.select`)
|
|
1320
|
+
//
|
|
1321
|
+
// One `SELECT json_extract(data, '$.f1'), …` returns the projected fields.
|
|
1322
|
+
// The DO leaves decoding to the client because timestamp values need to
|
|
1323
|
+
// rewrap as `GraphTimestampImpl` (a class instance, lost by structured
|
|
1324
|
+
// clone) — instead of inventing per-field timestamp sentinels, we send the
|
|
1325
|
+
// raw rows and the column spec, and let `DORPCBackend.findEdgesProjected`
|
|
1326
|
+
// call `decodeDOProjectedRow` once. The spec is small (≤ ~100 bytes for
|
|
1327
|
+
// a typical projection); structured clone copes happily.
|
|
1328
|
+
// ---------------------------------------------------------------------------
|
|
1329
|
+
async _fgFindEdgesProjected(select, filters, options) {
|
|
1330
|
+
const { stmt, columns } = compileDOFindEdgesProjected(this.table, select, filters, options);
|
|
1331
|
+
const rows = this.state.storage.sql.exec(stmt.sql, ...stmt.params).toArray();
|
|
1332
|
+
return { rows, columns };
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Run a DML statement with `RETURNING "doc_id"` so the affected-row count
|
|
1336
|
+
* comes back authoritatively. Errors are caught and surfaced via the
|
|
1337
|
+
* `BulkResult.errors` array (single batch, batchIndex 0) so the wire
|
|
1338
|
+
* payload stays a regular `BulkResult` and the client doesn't have to
|
|
1339
|
+
* differentiate "RPC threw" from "single-statement failure."
|
|
1340
|
+
*/
|
|
1341
|
+
execDmlWithReturning(stmt) {
|
|
1342
|
+
const sqlWithReturning = `${stmt.sql} RETURNING "doc_id"`;
|
|
1343
|
+
try {
|
|
1344
|
+
const rows = this.state.storage.sql.exec(sqlWithReturning, ...stmt.params).toArray();
|
|
1345
|
+
return { deleted: rows.length, batches: 1, errors: [] };
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1348
|
+
return {
|
|
1349
|
+
deleted: 0,
|
|
1350
|
+
batches: 0,
|
|
1351
|
+
// Like `_fgBulkRemoveEdges`'s catch arm: a single failed statement
|
|
1352
|
+
// is one batch, and the operationCount is "unknown" for a server-
|
|
1353
|
+
// side DML — we report 0 as the lower bound. Callers that care
|
|
1354
|
+
// about partial state should re-query and reconcile.
|
|
1355
|
+
errors: [{ batchIndex: 0, error, operationCount: 0 }]
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
// ---------------------------------------------------------------------------
|
|
910
1360
|
// RPC: admin
|
|
911
1361
|
// ---------------------------------------------------------------------------
|
|
912
1362
|
/**
|
|
@@ -938,10 +1388,19 @@ var FiregraphDO = class extends DurableObject {
|
|
|
938
1388
|
}
|
|
939
1389
|
};
|
|
940
1390
|
export {
|
|
1391
|
+
CapabilityNotSupportedError,
|
|
941
1392
|
DORPCBackend,
|
|
942
1393
|
FiregraphDO,
|
|
1394
|
+
META_EDGE_TYPE,
|
|
1395
|
+
META_NODE_TYPE,
|
|
943
1396
|
buildDOSchemaStatements,
|
|
1397
|
+
createCapabilities,
|
|
944
1398
|
createDOClient,
|
|
945
|
-
|
|
1399
|
+
createMergedRegistry,
|
|
1400
|
+
createRegistry,
|
|
1401
|
+
createSiblingClient,
|
|
1402
|
+
deleteField,
|
|
1403
|
+
generateId,
|
|
1404
|
+
intersectCapabilities
|
|
946
1405
|
};
|
|
947
1406
|
//# sourceMappingURL=index.js.map
|