@typicalday/firegraph 0.10.0 → 0.11.1

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 (40) hide show
  1. package/README.md +93 -90
  2. package/bin/firegraph.mjs +21 -7
  3. package/dist/{backend-BrqFkbid.d.ts → backend-U-MLShlg.d.ts} +1 -1
  4. package/dist/{backend-73p5Blx7.d.cts → backend-np4gEVhB.d.cts} +1 -1
  5. package/dist/backend.d.cts +3 -3
  6. package/dist/backend.d.ts +3 -3
  7. package/dist/{chunk-LZOIQHYN.js → chunk-6SB34IPQ.js} +20 -7
  8. package/dist/chunk-6SB34IPQ.js.map +1 -0
  9. package/dist/{chunk-SU4FNLC3.js → chunk-EEKWRX5E.js} +1 -1
  10. package/dist/{chunk-SU4FNLC3.js.map → chunk-EEKWRX5E.js.map} +1 -1
  11. package/dist/{chunk-YLGXLEUE.js → chunk-GJVVRTQT.js} +5 -14
  12. package/dist/chunk-GJVVRTQT.js.map +1 -0
  13. package/dist/cloudflare/index.cjs +151 -27
  14. package/dist/cloudflare/index.cjs.map +1 -1
  15. package/dist/cloudflare/index.d.cts +78 -3
  16. package/dist/cloudflare/index.d.ts +78 -3
  17. package/dist/cloudflare/index.js +135 -23
  18. package/dist/cloudflare/index.js.map +1 -1
  19. package/dist/codegen/index.cjs +4 -13
  20. package/dist/codegen/index.cjs.map +1 -1
  21. package/dist/codegen/index.d.cts +1 -1
  22. package/dist/codegen/index.d.ts +1 -1
  23. package/dist/codegen/index.js +1 -1
  24. package/dist/index.cjs +89 -132
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +106 -21
  27. package/dist/index.d.ts +106 -21
  28. package/dist/index.js +70 -116
  29. package/dist/index.js.map +1 -1
  30. package/dist/query-client/index.cjs.map +1 -1
  31. package/dist/query-client/index.js +1 -1
  32. package/dist/{types-DOemdlVA.d.ts → types-BGWxcpI_.d.cts} +75 -1
  33. package/dist/{types-DOemdlVA.d.cts → types-BGWxcpI_.d.ts} +75 -1
  34. package/package.json +35 -27
  35. package/dist/chunk-LZOIQHYN.js.map +0 -1
  36. package/dist/chunk-YLGXLEUE.js.map +0 -1
  37. package/dist/editor/client/assets/index-Bq2bfzeY.js +0 -411
  38. package/dist/editor/client/assets/index-CJ4m_EOL.css +0 -1
  39. package/dist/editor/client/index.html +0 -16
  40. package/dist/editor/server/index.mjs +0 -51566
@@ -159,6 +159,7 @@ var cloudflare_exports = {};
159
159
  __export(cloudflare_exports, {
160
160
  DORPCBackend: () => DORPCBackend,
161
161
  FiregraphDO: () => FiregraphDO,
162
+ buildDOSchemaStatements: () => buildDOSchemaStatements,
162
163
  createDOClient: () => createDOClient,
163
164
  createSiblingClient: () => createSiblingClient
164
165
  });
@@ -258,6 +259,116 @@ var GraphTimestampImpl = class _GraphTimestampImpl {
258
259
  }
259
260
  };
260
261
 
262
+ // src/default-indexes.ts
263
+ var DEFAULT_CORE_INDEXES = Object.freeze([
264
+ { fields: ["aUid"] },
265
+ { fields: ["bUid"] },
266
+ { fields: ["aType"] },
267
+ { fields: ["bType"] },
268
+ { fields: ["aUid", "axbType"] },
269
+ { fields: ["axbType", "bUid"] },
270
+ { fields: ["aType", "axbType"] },
271
+ { fields: ["axbType", "bType"] }
272
+ ]);
273
+
274
+ // src/internal/sqlite-index-ddl.ts
275
+ var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
276
+ var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
277
+ function quoteIdent(name) {
278
+ if (!IDENT_RE.test(name)) {
279
+ throw new FiregraphError(
280
+ `Invalid SQL identifier in index DDL: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
281
+ "INVALID_INDEX"
282
+ );
283
+ }
284
+ return `"${name}"`;
285
+ }
286
+ function fnv1a32(str) {
287
+ let h = 2166136261;
288
+ for (let i = 0; i < str.length; i++) {
289
+ h ^= str.charCodeAt(i);
290
+ h = Math.imul(h, 16777619);
291
+ }
292
+ return (h >>> 0).toString(16).padStart(8, "0");
293
+ }
294
+ function normalizeFields(fields) {
295
+ return fields.map((f) => {
296
+ if (typeof f === "string") return { path: f, desc: false };
297
+ if (!f.path || typeof f.path !== "string") {
298
+ throw new FiregraphError(
299
+ `IndexSpec field must be a string or { path: string, desc?: boolean }; got ${JSON.stringify(f)}`,
300
+ "INVALID_INDEX"
301
+ );
302
+ }
303
+ return { path: f.path, desc: !!f.desc };
304
+ });
305
+ }
306
+ function specFingerprint(spec, leadingColumns) {
307
+ const normalized = {
308
+ lead: leadingColumns,
309
+ fields: normalizeFields(spec.fields),
310
+ where: spec.where ?? ""
311
+ };
312
+ return fnv1a32(JSON.stringify(normalized));
313
+ }
314
+ function compileFieldExpr(path, fieldToColumn) {
315
+ const col = fieldToColumn[path];
316
+ if (col) return quoteIdent(col);
317
+ if (path === "data") {
318
+ return `json_extract("data", '$')`;
319
+ }
320
+ if (path.startsWith("data.")) {
321
+ const suffix = path.slice(5);
322
+ const parts = suffix.split(".");
323
+ for (const part of parts) {
324
+ if (!JSON_PATH_KEY_RE.test(part)) {
325
+ throw new FiregraphError(
326
+ `IndexSpec data path "${path}" has invalid component "${part}". Each component must match /^[A-Za-z_][A-Za-z0-9_-]*$/.`,
327
+ "INVALID_INDEX"
328
+ );
329
+ }
330
+ }
331
+ return `json_extract("data", '$.${suffix}')`;
332
+ }
333
+ throw new FiregraphError(
334
+ `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'.`,
335
+ "INVALID_INDEX"
336
+ );
337
+ }
338
+ function buildIndexDDL(spec, options) {
339
+ const { table, fieldToColumn, leadingColumns = [] } = options;
340
+ if (!spec.fields || spec.fields.length === 0) {
341
+ throw new FiregraphError("IndexSpec.fields must be a non-empty array", "INVALID_INDEX");
342
+ }
343
+ const normalized = normalizeFields(spec.fields);
344
+ const hash = specFingerprint(spec, leadingColumns);
345
+ const indexName = `${table}_idx_${hash}`;
346
+ const cols = [];
347
+ for (const col of leadingColumns) {
348
+ cols.push(quoteIdent(col));
349
+ }
350
+ for (const f of normalized) {
351
+ const expr = compileFieldExpr(f.path, fieldToColumn);
352
+ cols.push(f.desc ? `${expr} DESC` : expr);
353
+ }
354
+ let ddl = `CREATE INDEX IF NOT EXISTS ${quoteIdent(indexName)} ON ${quoteIdent(table)}(${cols.join(", ")})`;
355
+ if (spec.where) {
356
+ ddl += ` WHERE ${spec.where}`;
357
+ }
358
+ return ddl;
359
+ }
360
+ function dedupeIndexSpecs(specs, leadingColumns = []) {
361
+ const seen = /* @__PURE__ */ new Set();
362
+ const out = [];
363
+ for (const spec of specs) {
364
+ const fp = specFingerprint(spec, leadingColumns);
365
+ if (seen.has(fp)) continue;
366
+ seen.add(fp);
367
+ out.push(spec);
368
+ }
369
+ return out;
370
+ }
371
+
261
372
  // src/cloudflare/schema.ts
262
373
  var DO_FIELD_TO_COLUMN = {
263
374
  aType: "a_type",
@@ -269,9 +380,9 @@ var DO_FIELD_TO_COLUMN = {
269
380
  createdAt: "created_at",
270
381
  updatedAt: "updated_at"
271
382
  };
272
- var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
383
+ var IDENT_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
273
384
  function validateDOTableName(name) {
274
- if (!IDENT_RE.test(name)) {
385
+ if (!IDENT_RE2.test(name)) {
275
386
  throw new Error(`Invalid SQL identifier: ${name}. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
276
387
  }
277
388
  }
@@ -279,9 +390,9 @@ function quoteDOIdent(name) {
279
390
  validateDOTableName(name);
280
391
  return `"${name}"`;
281
392
  }
282
- function buildDOSchemaStatements(table) {
393
+ function buildDOSchemaStatements(table, options = {}) {
283
394
  const t = quoteDOIdent(table);
284
- return [
395
+ const statements = [
285
396
  `CREATE TABLE IF NOT EXISTS ${t} (
286
397
  doc_id TEXT NOT NULL PRIMARY KEY,
287
398
  a_type TEXT NOT NULL,
@@ -293,13 +404,15 @@ function buildDOSchemaStatements(table) {
293
404
  v INTEGER,
294
405
  created_at INTEGER NOT NULL,
295
406
  updated_at INTEGER NOT NULL
296
- )`,
297
- `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_a_uid`)} ON ${t}(a_uid)`,
298
- `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_b_uid`)} ON ${t}(b_uid)`,
299
- `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_axb_type_b_uid`)} ON ${t}(axb_type, b_uid)`,
300
- `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_a_type`)} ON ${t}(a_type)`,
301
- `CREATE INDEX IF NOT EXISTS ${quoteDOIdent(`${table}_idx_b_type`)} ON ${t}(b_type)`
407
+ )`
302
408
  ];
409
+ const core = options.coreIndexes ?? [...DEFAULT_CORE_INDEXES];
410
+ const fromRegistry = options.registry?.entries().flatMap((e) => e.indexes ?? []) ?? [];
411
+ const deduped = dedupeIndexSpecs([...core, ...fromRegistry]);
412
+ for (const spec of deduped) {
413
+ statements.push(buildIndexDDL(spec, { table, fieldToColumn: DO_FIELD_TO_COLUMN }));
414
+ }
415
+ return statements;
303
416
  }
304
417
 
305
418
  // src/cloudflare/sql.ts
@@ -309,12 +422,14 @@ function compileFieldRef(field) {
309
422
  return { expr: quoteDOIdent(column) };
310
423
  }
311
424
  if (field.startsWith("data.")) {
312
- const key = field.slice(5);
313
- validateJsonPathKey(key);
314
- return { expr: 'json_extract("data", ?)', pathParam: `$.${key}` };
425
+ const suffix = field.slice(5);
426
+ for (const part of suffix.split(".")) {
427
+ validateJsonPathKey(part);
428
+ }
429
+ return { expr: `json_extract("data", '$.${suffix}')` };
315
430
  }
316
431
  if (field === "data") {
317
- return { expr: 'json_extract("data", ?)', pathParam: "$" };
432
+ return { expr: `json_extract("data", '$')` };
318
433
  }
319
434
  throw new FiregraphError(
320
435
  `DO SQLite backend cannot resolve filter field: ${field}`,
@@ -351,7 +466,7 @@ function bindValue(value) {
351
466
  }
352
467
  return String(value);
353
468
  }
354
- var JSON_PATH_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
469
+ var JSON_PATH_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
355
470
  function validateJsonPathKey(key) {
356
471
  if (key.length === 0) {
357
472
  throw new FiregraphError(
@@ -359,7 +474,7 @@ function validateJsonPathKey(key) {
359
474
  "INVALID_QUERY"
360
475
  );
361
476
  }
362
- if (!JSON_PATH_KEY_RE.test(key)) {
477
+ if (!JSON_PATH_KEY_RE2.test(key)) {
363
478
  throw new FiregraphError(
364
479
  `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.).`,
365
480
  "INVALID_QUERY"
@@ -367,8 +482,7 @@ function validateJsonPathKey(key) {
367
482
  }
368
483
  }
369
484
  function compileFilter(filter, params) {
370
- const { expr, pathParam } = compileFieldRef(filter.field);
371
- if (pathParam !== void 0) params.push(pathParam);
485
+ const { expr } = compileFieldRef(filter.field);
372
486
  switch (filter.op) {
373
487
  case "==":
374
488
  params.push(bindValue(filter.value));
@@ -423,11 +537,10 @@ function asArray(value, op) {
423
537
  }
424
538
  return value;
425
539
  }
426
- function compileOrderBy(options, params) {
540
+ function compileOrderBy(options, _params) {
427
541
  if (!options?.orderBy) return "";
428
542
  const { field, direction } = options.orderBy;
429
- const { expr, pathParam } = compileFieldRef(field);
430
- if (pathParam !== void 0) params.push(pathParam);
543
+ const { expr } = compileFieldRef(field);
431
544
  const dir = direction === "desc" ? "DESC" : "ASC";
432
545
  return ` ORDER BY ${expr} ${dir}`;
433
546
  }
@@ -915,9 +1028,7 @@ async function migrateRecord(record, registry, globalWriteBack = "off") {
915
1028
  };
916
1029
  }
917
1030
  async function migrateRecords(records, registry, globalWriteBack = "off") {
918
- return Promise.all(
919
- records.map((r) => migrateRecord(r, registry, globalWriteBack))
920
- );
1031
+ return Promise.all(records.map((r) => migrateRecord(r, registry, globalWriteBack)));
921
1032
  }
922
1033
 
923
1034
  // src/scope.ts
@@ -1125,7 +1236,8 @@ function discoveryToEntries(discovery) {
1125
1236
  subtitleField: entity.subtitleField,
1126
1237
  allowedIn: entity.allowedIn,
1127
1238
  migrations: entity.migrations,
1128
- migrationWriteBack: entity.migrationWriteBack
1239
+ migrationWriteBack: entity.migrationWriteBack,
1240
+ indexes: entity.indexes
1129
1241
  });
1130
1242
  }
1131
1243
  for (const [axbType, entity] of discovery.edges) {
@@ -1153,7 +1265,8 @@ function discoveryToEntries(discovery) {
1153
1265
  allowedIn: entity.allowedIn,
1154
1266
  targetGraph: resolvedTargetGraph,
1155
1267
  migrations: entity.migrations,
1156
- migrationWriteBack: entity.migrationWriteBack
1268
+ migrationWriteBack: entity.migrationWriteBack,
1269
+ indexes: entity.indexes
1157
1270
  });
1158
1271
  }
1159
1272
  }
@@ -2321,12 +2434,18 @@ var FiregraphDO = class {
2321
2434
  env;
2322
2435
  /** @internal — table name used by every compiled statement. */
2323
2436
  table;
2437
+ /** @internal — registry consulted by `runSchema` for per-entry indexes. */
2438
+ registry;
2439
+ /** @internal — overrides `DEFAULT_CORE_INDEXES` when set. */
2440
+ coreIndexes;
2324
2441
  constructor(ctx, env, options = {}) {
2325
2442
  this.ctx = ctx;
2326
2443
  this.env = env;
2327
2444
  const table = options.table ?? DEFAULT_OPTIONS.table;
2328
2445
  validateDOTableName(table);
2329
2446
  this.table = table;
2447
+ this.registry = options.registry;
2448
+ this.coreIndexes = options.coreIndexes;
2330
2449
  const autoMigrate = options.autoMigrate ?? DEFAULT_OPTIONS.autoMigrate;
2331
2450
  if (autoMigrate) {
2332
2451
  void this.ctx.blockConcurrencyWhile(async () => {
@@ -2516,7 +2635,11 @@ var FiregraphDO = class {
2516
2635
  // Internals
2517
2636
  // ---------------------------------------------------------------------------
2518
2637
  runSchema() {
2519
- for (const sql of buildDOSchemaStatements(this.table)) {
2638
+ const statements = buildDOSchemaStatements(this.table, {
2639
+ coreIndexes: this.coreIndexes,
2640
+ registry: this.registry
2641
+ });
2642
+ for (const sql of statements) {
2520
2643
  this.ctx.storage.sql.exec(sql).toArray();
2521
2644
  }
2522
2645
  }
@@ -2531,6 +2654,7 @@ var FiregraphDO = class {
2531
2654
  0 && (module.exports = {
2532
2655
  DORPCBackend,
2533
2656
  FiregraphDO,
2657
+ buildDOSchemaStatements,
2534
2658
  createDOClient,
2535
2659
  createSiblingClient
2536
2660
  });