@typicalday/firegraph 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +355 -78
  2. package/dist/backend-DuvHGgK1.d.cts +1897 -0
  3. package/dist/backend-DuvHGgK1.d.ts +1897 -0
  4. package/dist/backend.cjs +365 -5
  5. package/dist/backend.cjs.map +1 -1
  6. package/dist/backend.d.cts +25 -5
  7. package/dist/backend.d.ts +25 -5
  8. package/dist/backend.js +209 -7
  9. package/dist/backend.js.map +1 -1
  10. package/dist/chunk-2DHMNTV6.js +16 -0
  11. package/dist/chunk-2DHMNTV6.js.map +1 -0
  12. package/dist/chunk-4MMQ5W74.js +288 -0
  13. package/dist/chunk-4MMQ5W74.js.map +1 -0
  14. package/dist/{chunk-5753Y42M.js → chunk-C2QMD7RY.js} +6 -10
  15. package/dist/chunk-C2QMD7RY.js.map +1 -0
  16. package/dist/chunk-D4J7Z4FE.js +67 -0
  17. package/dist/chunk-D4J7Z4FE.js.map +1 -0
  18. package/dist/chunk-EQJUUVFG.js +14 -0
  19. package/dist/chunk-EQJUUVFG.js.map +1 -0
  20. package/dist/chunk-N5HFDWQX.js +23 -0
  21. package/dist/chunk-N5HFDWQX.js.map +1 -0
  22. package/dist/chunk-PAD7WFFU.js +573 -0
  23. package/dist/chunk-PAD7WFFU.js.map +1 -0
  24. package/dist/chunk-TK64DNVK.js +256 -0
  25. package/dist/chunk-TK64DNVK.js.map +1 -0
  26. package/dist/{chunk-NJSOD64C.js → chunk-WRTFC5NG.js} +438 -30
  27. package/dist/chunk-WRTFC5NG.js.map +1 -0
  28. package/dist/client-BKi3vk0Q.d.ts +34 -0
  29. package/dist/client-BrsaXtDV.d.cts +34 -0
  30. package/dist/cloudflare/index.cjs +1386 -74
  31. package/dist/cloudflare/index.cjs.map +1 -1
  32. package/dist/cloudflare/index.d.cts +217 -13
  33. package/dist/cloudflare/index.d.ts +217 -13
  34. package/dist/cloudflare/index.js +639 -180
  35. package/dist/cloudflare/index.js.map +1 -1
  36. package/dist/codegen/index.d.cts +1 -1
  37. package/dist/codegen/index.d.ts +1 -1
  38. package/dist/errors-BRc3I_eH.d.cts +73 -0
  39. package/dist/errors-BRc3I_eH.d.ts +73 -0
  40. package/dist/firestore-enterprise/index.cjs +3877 -0
  41. package/dist/firestore-enterprise/index.cjs.map +1 -0
  42. package/dist/firestore-enterprise/index.d.cts +141 -0
  43. package/dist/firestore-enterprise/index.d.ts +141 -0
  44. package/dist/firestore-enterprise/index.js +985 -0
  45. package/dist/firestore-enterprise/index.js.map +1 -0
  46. package/dist/firestore-standard/index.cjs +3117 -0
  47. package/dist/firestore-standard/index.cjs.map +1 -0
  48. package/dist/firestore-standard/index.d.cts +49 -0
  49. package/dist/firestore-standard/index.d.ts +49 -0
  50. package/dist/firestore-standard/index.js +283 -0
  51. package/dist/firestore-standard/index.js.map +1 -0
  52. package/dist/index.cjs +809 -534
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.d.cts +24 -100
  55. package/dist/index.d.ts +24 -100
  56. package/dist/index.js +184 -531
  57. package/dist/index.js.map +1 -1
  58. package/dist/registry-Bc7h6WTM.d.cts +64 -0
  59. package/dist/registry-C2KUPVZj.d.ts +64 -0
  60. package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.cts} +1 -56
  61. package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.ts} +1 -56
  62. package/dist/{serialization-ZZ7RSDRX.js → serialization-OE2PFZMY.js} +6 -4
  63. package/dist/sqlite/index.cjs +3631 -0
  64. package/dist/sqlite/index.cjs.map +1 -0
  65. package/dist/sqlite/index.d.cts +111 -0
  66. package/dist/sqlite/index.d.ts +111 -0
  67. package/dist/sqlite/index.js +1164 -0
  68. package/dist/sqlite/index.js.map +1 -0
  69. package/package.json +33 -3
  70. package/dist/backend-U-MLShlg.d.ts +0 -97
  71. package/dist/backend-np4gEVhB.d.cts +0 -97
  72. package/dist/chunk-5753Y42M.js.map +0 -1
  73. package/dist/chunk-NJSOD64C.js.map +0 -1
  74. package/dist/chunk-R7CRGYY4.js +0 -94
  75. package/dist/chunk-R7CRGYY4.js.map +0 -1
  76. package/dist/types-BGWxcpI_.d.cts +0 -736
  77. package/dist/types-BGWxcpI_.d.ts +0 -736
  78. /package/dist/{serialization-ZZ7RSDRX.js.map → serialization-OE2PFZMY.js.map} +0 -0
@@ -1,137 +1,39 @@
1
1
  import {
2
- DEFAULT_CORE_INDEXES,
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
- } from "../chunk-NJSOD64C.js";
24
+ createGraphClientFromBackend,
25
+ createMergedRegistry,
26
+ createRegistry,
27
+ generateId
28
+ } from "../chunk-WRTFC5NG.js";
9
29
  import {
10
- FiregraphError
11
- } from "../chunk-R7CRGYY4.js";
12
-
13
- // src/timestamp.ts
14
- var GraphTimestampImpl = class _GraphTimestampImpl {
15
- constructor(seconds, nanoseconds) {
16
- this.seconds = seconds;
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 IDENT_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
49
+ var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
148
50
  function validateDOTableName(name) {
149
- if (!IDENT_RE2.test(name)) {
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 compileDOSet(table, docId, record, nowMillis) {
335
- const sql = `INSERT OR REPLACE INTO ${quoteDOIdent(table)} (
336
- doc_id, a_type, a_uid, axb_type, b_type, b_uid, data, v, created_at, updated_at
337
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
338
- const params = [
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
- return { sql, params };
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.dataFields && Object.keys(update.dataFields).length > 0) {
359
- const entries = Object.entries(update.dataFields);
360
- const pathArgs = entries.map(() => `?, ?`).join(", ");
361
- setClauses.push(`"data" = json_set(COALESCE("data", '{}'), ${pathArgs})`);
362
- for (const [k, v] of entries) {
363
- validateJsonPathKey(k);
364
- params.push(`$.${k}`);
365
- params.push(bindValue(v));
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
- createSiblingClient
1399
+ createMergedRegistry,
1400
+ createRegistry,
1401
+ createSiblingClient,
1402
+ deleteField,
1403
+ generateId,
1404
+ intersectCapabilities
946
1405
  };
947
1406
  //# sourceMappingURL=index.js.map