@isaijs/core 0.1.6 → 0.1.9

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 (30) hide show
  1. package/dist/adapters/storage/r2/index.d.ts +3 -0
  2. package/dist/adapters/storage/r2/index.js +13 -2
  3. package/dist/auth/index.d.ts +2 -2
  4. package/dist/auth/index.js +2 -2
  5. package/dist/cli/index.js +79 -11
  6. package/dist/client/index.d.ts +4 -0
  7. package/dist/client/index.js +18 -13
  8. package/dist/{create-auth-BFKvQVOG.js → create-auth-BCvMwlVD.js} +3 -3
  9. package/dist/{data-layer-C5oJZ8V1.js → data-layer-BOVkYESH.js} +15 -10
  10. package/dist/{data-layer-43pme0Gy.d.ts → data-layer-nvm_ivGQ.d.ts} +6 -2
  11. package/dist/db/index.d.ts +2 -2
  12. package/dist/db/index.js +5 -5
  13. package/dist/{index-a7ElyL3B.d.ts → index-Bb9Px6SR.d.ts} +97 -18
  14. package/dist/{index-CYcklQ1l.d.ts → index-CaR3hb0i.d.ts} +5 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +150 -78
  17. package/dist/{make-ByA-84h8.js → make-D6_caNkv.js} +21 -3
  18. package/dist/{makers-P9kg68C8.js → makers-DjkBP74P.js} +3 -3
  19. package/dist/{migrator-B25prc9n.js → migrator-vvvKa0Vk.js} +180 -36
  20. package/dist/{model-Dc3HunKb.js → model-6pJBqqGd.js} +31 -1
  21. package/dist/{policies-KsqmlX67.js → policies-C_oiSgmY.js} +1 -1
  22. package/dist/{resource-BQK7bkPQ.js → resource-DCg6LvMH.js} +8 -6
  23. package/dist/{tables-DgSwge7s.js → tables-DjW96Ubl.js} +1 -1
  24. package/dist/testing/index.d.ts +1 -1
  25. package/dist/testing/index.js +4 -4
  26. package/dist/{types-BgOkJy5d.d.ts → types-D5SIVeej.d.ts} +59 -2
  27. package/dist/validation/index.d.ts +2 -2
  28. package/dist/validation/index.js +1 -1
  29. package/dist/{validation-Cppmd0RV.js → validation-C3MCRBQO.js} +26 -3
  30. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { U as systemClock, f as dayOf, h as snapshotOf, p as generateMigration, y as MigrationError } from "./migrator-B25prc9n.js";
1
+ import { W as systemClock, f as dayOf, h as snapshotOf, p as generateMigration, y as MigrationError } from "./migrator-vvvKa0Vk.js";
2
2
  //#region src/db/migrations/cleanup.ts
3
3
  /** Dropped tables, those that refer to others first, so no row is left pointing at a dropped one. */
4
4
  function dropOrder(tables) {
@@ -88,10 +88,12 @@ function cleanupMigration(options) {
88
88
  * tighten one, or add a check a check data step: passes when no row breaks it, else a `fix`
89
89
  * add a unique a resolution step before the index (duplicates as chosen)
90
90
  * remove a unique retired until cleanup (an earlier release may rely on it)
91
+ * change a ref's onDelete or a rebuild: the table and every table referencing it, copied
92
+ * table, or a table's storage whole in one step (`operations.ts` → rebuild)
91
93
  *
92
94
  * Refused, each with the safe sequence: an incompatible kind change (or toggling `secret`), a field
93
95
  * becoming or ceasing to be kept by the database, a new required unique field over existing rows,
94
- * a rebuild (a ref's `onDelete`, an FK on an existing column), reusing a retired name.
96
+ * reusing a retired name, a rebuild of tables that reference each other.
95
97
  */
96
98
  const pad = (n) => String(n).padStart(4, "0");
97
99
  const json = (value) => JSON.stringify(value);
@@ -299,13 +301,17 @@ function describeOperation(operation, tables) {
299
301
  what: `add ${tables[operation.table]?.model ?? operation.table}.${operation.column.field}`,
300
302
  how: "ADD COLUMN"
301
303
  };
304
+ case "rebuild": return {
305
+ what: `rebuild ${operation.tables.map((table) => table.model).join(", ")}`,
306
+ how: "copies every row"
307
+ };
302
308
  default: return {
303
309
  what: operation.op,
304
310
  how: ""
305
311
  };
306
312
  }
307
313
  }
308
- const slugOf = (operation) => operation.op === "createTable" ? `create_${operation.table.name}` : operation.op === "addColumn" ? `add_${operation.table}_${operation.column.name}` : operation.op;
314
+ const slugOf = (operation) => operation.op === "createTable" ? `create_${operation.table.name}` : operation.op === "addColumn" ? `add_${operation.table}_${operation.column.name}` : operation.op === "rebuild" ? `rebuild_${operation.tables[0]?.name ?? "tables"}` : operation.op;
309
315
  /** A template literal holding SQL (no backtick or `${` is left live). */
310
316
  const literal = (text) => `\`${text.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")}\``;
311
317
  function structureSource(name, after, statements, day, framework) {
@@ -482,6 +488,18 @@ function makeMigrations(options) {
482
488
  /** The structure operations between two snapshots, for naming the step and the plan. */
483
489
  function snapshotOperations(previous, next) {
484
490
  const operations = [];
491
+ const rebuilt = Object.values(next.tables).filter((table) => {
492
+ const before = previous.tables[table.name];
493
+ if (!before) return false;
494
+ const references = new Map(before.columns.map((c) => [c.name, JSON.stringify(c.references)]));
495
+ return Boolean(before.withoutRowid) !== Boolean(table.withoutRowid) || table.columns.some((c) => references.has(c.name) && references.get(c.name) !== JSON.stringify(c.references));
496
+ });
497
+ if (rebuilt.length > 0) operations.push({
498
+ op: "rebuild",
499
+ tables: rebuilt,
500
+ copy: {},
501
+ triggers: []
502
+ });
485
503
  for (const table of Object.values(next.tables)) {
486
504
  const before = previous.tables[table.name];
487
505
  if (!before) {
@@ -1,6 +1,6 @@
1
- import { K as Fake, q as fakeValue } from "./validation-Cppmd0RV.js";
2
- import { D as recordAction, E as forUpkeep, K as CheckFailed, S as clockOf, k as planInsert } from "./migrator-B25prc9n.js";
3
- import { I as fromBase64url, L as toBase64url, l as IMPLICIT_FIELDS, t as Model } from "./model-Dc3HunKb.js";
1
+ import { J as Fake, Y as fakeValue } from "./validation-C3MCRBQO.js";
2
+ import { D as recordAction, E as forUpkeep, S as clockOf, k as planInsert, q as CheckFailed } from "./migrator-vvvKa0Vk.js";
3
+ import { I as fromBase64url, L as toBase64url, l as IMPLICIT_FIELDS, t as Model } from "./model-6pJBqqGd.js";
4
4
  //#region src/auth/roles.ts
5
5
  const same = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
6
6
  /** What a role in `app.ts` is stored as. */
@@ -1,5 +1,5 @@
1
- import { G as ValidationError, M as id } from "./validation-Cppmd0RV.js";
2
- import { A as mapOperands, B as aroundDelete, F as unbound, H as aroundUpsert, I as fromBase64url$1, K as wrap, L as toBase64url, M as readsNow, N as readsOtherRows, O as storageOf, P as tableTotals, R as actionEntry, V as aroundUpdate, W as assertNotLog, _ as Expr, c as rowOf$1, h as Condition, i as columnNode, k as fieldsOf, l as IMPLICIT_FIELDS, n as assertNotEncrypted, o as fieldOf, r as columnName, v as Now, w as conditionNode, z as aroundCreate } from "./model-Dc3HunKb.js";
1
+ import { P as id, q as ValidationError } from "./validation-C3MCRBQO.js";
2
+ import { A as mapOperands, B as aroundDelete, F as unbound, H as aroundUpsert, I as fromBase64url$1, K as wrap, L as toBase64url, M as readsNow, N as readsOtherRows, O as storageOf, P as tableTotals, R as actionEntry, V as aroundUpdate, W as assertNotLog, _ as Expr, c as rowOf$1, h as Condition, i as columnNode, k as fieldsOf, l as IMPLICIT_FIELDS, n as assertNotEncrypted, o as fieldOf, r as columnName, v as Now, w as conditionNode, z as aroundCreate } from "./model-6pJBqqGd.js";
3
3
  import { r as ulid } from "./case-DpXlJCL9.js";
4
4
  import { t as NotImplementedError } from "./errors-mCNLlXg9.js";
5
5
  //#region src/db/errors.ts
@@ -814,13 +814,28 @@ function onConflict(model, conflict, writer) {
814
814
  if ("fromInsert" in action) return `${target} DO UPDATE SET ${action.fromInsert.map((field) => `${column(model, field)} = excluded.${column(model, field)}`).join(", ")}`;
815
815
  return `${target} DO UPDATE SET ${writer.set(action.set)}`;
816
816
  }
817
+ /**
818
+ * A select. With a limit, computed fields are worked out for the rows it returns only: an inner
819
+ * query picks them (filtered, sorted, limited), and the outer one adds the computed values. SQLite
820
+ * otherwise works out every select-list subquery for every matching row before it sorts (a page of
821
+ * 24 products would price all 800 in their category), and each of those reads is billed. Not when
822
+ * the order is by a computed value: the inner query couldn't sort by it.
823
+ */
817
824
  function select(model, query, context) {
818
825
  return withinLimit(context, (mode) => {
819
826
  const writer = new Writer(model, mode, context);
820
- let text = `SELECT ${writer.selection()} FROM ${quote(model.table)}${writer.where(query.where)}`;
821
- if (query.order.length > 0) text += ` ORDER BY ${query.order.map(({ field, direction }) => `${column(model, field)} ${direction.toUpperCase()}`).join(", ")}`;
822
- if (query.limit !== void 0 || query.offset !== void 0) text += ` LIMIT ${writer.value(query.limit ?? -1)}`;
827
+ const table = quote(model.table);
828
+ const order = query.order.length > 0 ? ` ORDER BY ${query.order.map(({ field, direction }) => `${column(model, field)} ${direction.toUpperCase()}`).join(", ")}` : "";
829
+ const limited = query.limit !== void 0 || query.offset !== void 0;
830
+ const computed = Object.values(model.fields).some((spec) => spec.computed);
831
+ const sortsByComputed = query.order.some(({ field }) => model.fields[field]?.computed);
832
+ const selection = writer.selection();
833
+ const inner = limited && computed && !sortsByComputed;
834
+ let text = inner ? `SELECT * FROM ${table}` : `SELECT ${selection} FROM ${table}`;
835
+ text += writer.where(query.where) + order;
836
+ if (limited) text += ` LIMIT ${writer.value(query.limit ?? -1)}`;
823
837
  if (query.offset !== void 0) text += ` OFFSET ${writer.value(query.offset)}`;
838
+ if (inner) text = `SELECT ${selection} FROM (${text}) AS ${table}${order}`;
824
839
  return writer.sql(text);
825
840
  });
826
841
  }
@@ -1037,7 +1052,23 @@ function columnDefinition(column) {
1037
1052
  return definition;
1038
1053
  }
1039
1054
  function createTable(table) {
1040
- return `CREATE TABLE ${quote(table.name)} (${table.columns.map(columnDefinition).join(", ")})`;
1055
+ const columns = table.columns.map(columnDefinition).join(", ");
1056
+ return `CREATE TABLE ${quote(table.name)} (${columns})${table.withoutRowid ? " WITHOUT ROWID" : ""}`;
1057
+ }
1058
+ /** Where a rebuilt table's old rows wait while the new table is filled. */
1059
+ const aside = (table) => quote(`_isai_rebuild_${table}`);
1060
+ /** `operations.ts` → rebuild, in SQLite: one atomic step, its foreign keys checked at the end. */
1061
+ function rebuild(operation) {
1062
+ const { tables, copy, triggers } = operation;
1063
+ const columns = (table) => (copy[table] ?? []).map(quote).join(", ");
1064
+ return [
1065
+ "PRAGMA defer_foreign_keys = ON",
1066
+ ...triggers.map((name) => `DROP TRIGGER IF EXISTS ${quote(name)}`),
1067
+ ...tables.map((table) => `ALTER TABLE ${quote(table.name)} RENAME TO ${aside(table.name)}`),
1068
+ ...tables.map(createTable),
1069
+ ...tables.filter((table) => (copy[table.name] ?? []).length > 0).map((table) => `INSERT INTO ${quote(table.name)} (${columns(table.name)}) SELECT ${columns(table.name)} FROM ${aside(table.name)}`),
1070
+ ...[...tables].reverse().map((table) => `DROP TABLE ${aside(table.name)}`)
1071
+ ];
1041
1072
  }
1042
1073
  function createIndex(table, index) {
1043
1074
  const columns = index.columns.map(quote).join(", ");
@@ -1229,6 +1260,7 @@ function render(operation) {
1229
1260
  case "dropRule": return [`DROP ${operation.rule.kind.toUpperCase()} IF EXISTS ${quote(operation.rule.name)}`];
1230
1261
  case "dropColumn": return [`ALTER TABLE ${quote(operation.table)} DROP COLUMN ${quote(operation.column.name)}`, ...dropCounter(operation.table, operation.column)];
1231
1262
  case "dropTable": return [`DROP TABLE ${quote(operation.table.name)}`, ...operation.table.columns.flatMap((column) => dropCounter(operation.table.name, column))];
1263
+ case "rebuild": return rebuild(operation);
1232
1264
  }
1233
1265
  }
1234
1266
  //#endregion
@@ -1500,10 +1532,15 @@ var Engine = class Engine {
1500
1532
  };
1501
1533
  //#endregion
1502
1534
  //#region src/db/orm/include.ts
1535
+ /** How the boundary gives a query its `IncludeRule`: internal, so the app's API never shows it. */
1536
+ const withIncludeRule = Symbol("isai.withIncludeRule");
1503
1537
  /** The rows of `model` whose `field` is one of `values`. */
1504
- async function rowsWhereIn(engine, model, field, values) {
1538
+ async function rowsWhereIn(engine, model, field, values, rule) {
1505
1539
  const spec = fieldOf(model, field);
1506
1540
  if (!spec) throw new TypeError(`isai: ${model.name} has no field "${field}"`);
1541
+ if (values.length === 0) return [];
1542
+ const allowed = rule ? await rule(model) : void 0;
1543
+ if (allowed === false) return [];
1507
1544
  const chunk = engine.driver.capabilities.maxBoundParams;
1508
1545
  const found = [];
1509
1546
  for (let start = 0; start < values.length; start += chunk) {
@@ -1513,7 +1550,7 @@ async function rowsWhereIn(engine, model, field, values) {
1513
1550
  values: values.slice(start, start + chunk)
1514
1551
  };
1515
1552
  found.push(...await engine.read(model, {
1516
- where: [where],
1553
+ where: allowed ? [where, allowed] : [where],
1517
1554
  order: [{
1518
1555
  field: "id",
1519
1556
  direction: "asc"
@@ -1535,27 +1572,27 @@ function byHead(paths) {
1535
1572
  }
1536
1573
  return groups;
1537
1574
  }
1538
- async function loadIncludes(engine, model, registry, rows, paths) {
1575
+ async function loadIncludes(engine, model, registry, rows, paths, rule) {
1539
1576
  if (rows.length === 0 || paths.length === 0) return;
1540
1577
  for (const [head, tails] of byHead(paths)) {
1541
1578
  const ref = fieldOf(model, head)?.ref;
1542
1579
  if (ref) {
1543
1580
  const target = registry.get(ref.model);
1544
- const found = await rowsWhereIn(engine, target, "id", [...new Set(rows.map((row) => row[head]).filter((id) => id !== null && id !== void 0))]);
1545
- await loadIncludes(engine, target, registry, found, tails);
1581
+ const found = await rowsWhereIn(engine, target, "id", [...new Set(rows.map((row) => row[head]).filter((id) => id !== null && id !== void 0))], rule);
1582
+ await loadIncludes(engine, target, registry, found, tails, rule);
1546
1583
  const byId = new Map(found.map((row) => [row.id, row]));
1547
1584
  for (const row of rows) row[head] = row[head] === null ? null : byId.get(row[head]) ?? null;
1548
1585
  continue;
1549
1586
  }
1550
1587
  const relation = registry.relation(model.name, head);
1551
1588
  if (!relation) throw new TypeError(`isai: ${model.name} has no ref or relation named "${head}"`);
1552
- const found = await rowsWhereIn(engine, relation.model, relation.field, rows.map((row) => row.id));
1589
+ const found = await rowsWhereIn(engine, relation.model, relation.field, rows.map((row) => row.id), rule);
1553
1590
  const groups = /* @__PURE__ */ new Map();
1554
1591
  for (const row of found) {
1555
1592
  const owner = row[relation.field];
1556
1593
  groups.set(owner, [...groups.get(owner) ?? [], row]);
1557
1594
  }
1558
- await loadIncludes(engine, relation.model, registry, found, tails);
1595
+ await loadIncludes(engine, relation.model, registry, found, tails, rule);
1559
1596
  for (const row of rows) row[head] = groups.get(row.id) ?? [];
1560
1597
  }
1561
1598
  }
@@ -1707,15 +1744,21 @@ var Query = class Query {
1707
1744
  target;
1708
1745
  query;
1709
1746
  paths;
1747
+ rule;
1710
1748
  constructor(engine, target, query = {
1711
1749
  where: [],
1712
1750
  order: [],
1713
1751
  limit: void 0
1714
- }, paths = []) {
1752
+ }, paths = [], rule) {
1715
1753
  this.engine = engine;
1716
1754
  this.target = target;
1717
1755
  this.query = query;
1718
1756
  this.paths = paths;
1757
+ this.rule = rule;
1758
+ }
1759
+ /** The same query, its includes held to `rule` (the boundary's; `./include`). */
1760
+ [withIncludeRule](rule) {
1761
+ return new Query(this.engine, this.target, this.query, this.paths, rule);
1719
1762
  }
1720
1763
  /** Repeated calls are ANDed. */
1721
1764
  where(build) {
@@ -1739,7 +1782,7 @@ var Query = class Query {
1739
1782
  }
1740
1783
  /** Loads related rows: refs in place of their ids, relations as lists (`"fest"`, `"registrations.members"`). */
1741
1784
  include(...paths) {
1742
- return new Query(this.engine, this.target, this.query, [...this.paths, ...paths]);
1785
+ return new Query(this.engine, this.target, this.query, [...this.paths, ...paths], this.rule);
1743
1786
  }
1744
1787
  async all() {
1745
1788
  return this.fetch({
@@ -1843,7 +1886,7 @@ var Query = class Query {
1843
1886
  return this.withIncludes(await this.engine.read(this.target.spec, query));
1844
1887
  }
1845
1888
  async withIncludes(rows) {
1846
- await loadIncludes(this.engine, this.target.spec, this.target.registry, rows, this.paths);
1889
+ await loadIncludes(this.engine, this.target.spec, this.target.registry, rows, this.paths, this.rule);
1847
1890
  return rows;
1848
1891
  }
1849
1892
  /** The first row (by the query's order), or `undefined`. */
@@ -1870,7 +1913,7 @@ var Query = class Query {
1870
1913
  return new Query(this.engine, this.target, {
1871
1914
  ...this.query,
1872
1915
  ...change
1873
- }, this.paths);
1916
+ }, this.paths, this.rule);
1874
1917
  }
1875
1918
  };
1876
1919
  /** Kinds a group key may be: never free text. */
@@ -2077,7 +2120,7 @@ function planInsert(model, input, now) {
2077
2120
  * A patch as assignments. A value is read through its kind; a function is an SQL expression over
2078
2121
  * the stored row (`(v) => v.stock.minus(2)`), which only the database can compute (and guard).
2079
2122
  */
2080
- function planUpdate(model, registry, patch, now, touch = true) {
2123
+ function planUpdate(model, registry, patch, now, upkeep = false) {
2081
2124
  const issues = [];
2082
2125
  rejectUnknown(model, patch, /* @__PURE__ */ new Set(), issues);
2083
2126
  const values = {};
@@ -2085,7 +2128,7 @@ function planUpdate(model, registry, patch, now, touch = true) {
2085
2128
  for (const [field, raw] of Object.entries(patch)) {
2086
2129
  const spec = fieldOf(model, field);
2087
2130
  if (!spec || spec.computed || spec.maintained || MANAGED$1.has(field) || raw === void 0) continue;
2088
- if (spec.fixed) {
2131
+ if (spec.fixed && !upkeep) {
2089
2132
  issues.push({
2090
2133
  path: [field],
2091
2134
  code: "fixed",
@@ -2105,10 +2148,10 @@ function planUpdate(model, registry, patch, now, touch = true) {
2105
2148
  if (issues.length > 0) throw new ValidationError(issues, { model: model.name });
2106
2149
  runChecks(model, values, now);
2107
2150
  return {
2108
- values: touch ? {
2151
+ values: upkeep ? values : {
2109
2152
  ...values,
2110
2153
  updatedAt: now
2111
- } : values,
2154
+ },
2112
2155
  expressions
2113
2156
  };
2114
2157
  }
@@ -2164,7 +2207,7 @@ function planUpsertOne(engine, target, key, options, context) {
2164
2207
  ...options.create,
2165
2208
  ...key
2166
2209
  }, context.now);
2167
- const set = options.update && planUpdate(target.spec, target.registry, options.update, context.now, !engine.upkeep);
2210
+ const set = options.update && planUpdate(target.spec, target.registry, options.update, context.now, engine.upkeep);
2168
2211
  const statements = async () => {
2169
2212
  const action = set ? { set: {
2170
2213
  ...set,
@@ -2183,7 +2226,7 @@ function planUpsertOne(engine, target, key, options, context) {
2183
2226
  }
2184
2227
  function planUpdateWhere(engine, target, patch, where, context, returning) {
2185
2228
  assertNotLog(target.spec);
2186
- const assignments = planUpdate(target.spec, target.registry, patch, context.now, !engine.upkeep);
2229
+ const assignments = planUpdate(target.spec, target.registry, patch, context.now, engine.upkeep);
2187
2230
  return async () => {
2188
2231
  const values = await engine.seal(target.spec, assignments.values);
2189
2232
  return wrap(aroundUpdate(engine, target, where, context), [engine.dialect.update(target.spec, {
@@ -2885,7 +2928,6 @@ var UnsafeMigration = class extends Error {
2885
2928
  const COPY = "add a new field, copy the values in a data step, then remove the old one";
2886
2929
  const KEPT = "add a new field; the database fills it, then remove the old one";
2887
2930
  const PLAIN = "add a plain field, copy the values in a data step, then remove the old one";
2888
- const REBUILD = "it needs a table rebuild, which isn't supported yet; add a new field instead";
2889
2931
  const CLEANUP = "pick another name, or drop the retired one first with `isai cleanup`";
2890
2932
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2891
2933
  /** Why a column can't stay as it is when its field changes, and what to do instead; undefined when it can. */
@@ -2905,9 +2947,88 @@ function columnChange(before, after) {
2905
2947
  change: `it's no longer kept by the database (${before.maintained})`,
2906
2948
  advice: PLAIN
2907
2949
  };
2908
- if (!same(before.references, after.references)) return {
2909
- change: before.references && after.references && before.references.table === after.references.table ? `its onDelete changed from ${before.references.onDelete} to ${after.references.onDelete}` : "its reference changed",
2910
- advice: REBUILD
2950
+ }
2951
+ /** Every table in the database under a schema: live ones with their retired columns, and retired tables. */
2952
+ function physicalTables(schema, alsoRetired = []) {
2953
+ const retired = [...schema.retired, ...alsoRetired];
2954
+ const tables = /* @__PURE__ */ new Map();
2955
+ for (const table of Object.values(schema.tables)) {
2956
+ const gone = retired.flatMap((r) => r.kind === "column" && r.table === table.name ? [r.column] : []);
2957
+ tables.set(table.name, {
2958
+ table,
2959
+ columns: [...table.columns, ...gone]
2960
+ });
2961
+ }
2962
+ for (const r of retired) if (r.kind === "table" && !tables.has(r.table.name)) tables.set(r.table.name, {
2963
+ table: r.table,
2964
+ columns: r.table.columns
2965
+ });
2966
+ return tables;
2967
+ }
2968
+ const referenced = (columns) => new Set(columns.flatMap((c) => c.references ? [c.references.table] : []));
2969
+ /**
2970
+ * The rebuild these schemas need, if any: the tables whose storage or a reference changed, with
2971
+ * every table that references one of them (live or retired, before or after), so no foreign key is
2972
+ * left pointing at a table that was dropped (`operations.ts` → rebuild).
2973
+ */
2974
+ function rebuildOf(previous, next, retiredNow, unsafe) {
2975
+ const before = physicalTables(previous);
2976
+ const after = physicalTables({
2977
+ ...next,
2978
+ retired: previous.retired
2979
+ }, retiredNow);
2980
+ const roots = Object.values(next.tables).filter((table) => {
2981
+ const was = previous.tables[table.name];
2982
+ if (!was) return false;
2983
+ if (Boolean(was.withoutRowid) !== Boolean(table.withoutRowid)) return true;
2984
+ const references = new Map(was.columns.map((c) => [c.name, c.references]));
2985
+ return table.columns.some((c) => references.has(c.name) && !same(references.get(c.name), c.references));
2986
+ });
2987
+ if (roots.length === 0) return void 0;
2988
+ const members = new Set(roots.map((table) => table.name));
2989
+ for (let grew = true; grew;) {
2990
+ grew = false;
2991
+ for (const [name, table] of after) {
2992
+ if (members.has(name) || !before.has(name)) continue;
2993
+ if ([.../* @__PURE__ */ new Set([...referenced(table.columns), ...referenced(before.get(name)?.columns ?? [])])].some((ref) => members.has(ref))) {
2994
+ members.add(name);
2995
+ grew = true;
2996
+ }
2997
+ }
2998
+ }
2999
+ const ordered = [];
3000
+ const pending = new Set(members);
3001
+ while (pending.size > 0) {
3002
+ const ready = [...pending].filter((name) => [...referenced(after.get(name)?.columns ?? [])].every((ref) => ref === name || !pending.has(ref)));
3003
+ if (ready.length === 0) {
3004
+ const tables = [...pending].sort();
3005
+ unsafe.push({
3006
+ model: after.get(tables[0] ?? "")?.table.model ?? "",
3007
+ change: `its table is rebuilt, and ${tables.join(", ")} reference each other`,
3008
+ advice: "break the cycle (make one of the refs a plain field) before the change that rebuilds them"
3009
+ });
3010
+ return;
3011
+ }
3012
+ for (const name of ready.sort()) {
3013
+ ordered.push(name);
3014
+ pending.delete(name);
3015
+ }
3016
+ }
3017
+ const tables = ordered.map((name) => {
3018
+ const { table, columns } = after.get(name);
3019
+ return {
3020
+ ...table,
3021
+ columns
3022
+ };
3023
+ });
3024
+ return {
3025
+ op: "rebuild",
3026
+ tables,
3027
+ copy: Object.fromEntries(tables.map((table) => {
3028
+ const had = new Set(before.get(table.name)?.columns.map((c) => c.name));
3029
+ return [table.name, table.columns.flatMap((c) => had.has(c.name) ? [c.name] : [])];
3030
+ })),
3031
+ triggers: []
2911
3032
  };
2912
3033
  }
2913
3034
  /** A dropped column and an added one of the same kind, in one model: probably a rename. */
@@ -2998,12 +3119,18 @@ function diff(previous, next, since) {
2998
3119
  index
2999
3120
  });
3000
3121
  }
3122
+ const rebuild = rebuildOf(previous, next, retired, unsafe);
3001
3123
  if (unsafe.length > 0) throw new UnsafeMigration(unsafe);
3124
+ const rebuilt = new Set(rebuild?.tables.map((table) => table.name));
3002
3125
  return {
3003
- operations: [...created.map((table) => ({
3004
- op: "createTable",
3005
- table
3006
- })), ...addColumns],
3126
+ operations: [
3127
+ ...rebuild ? [rebuild] : [],
3128
+ ...created.map((table) => ({
3129
+ op: "createTable",
3130
+ table
3131
+ })),
3132
+ ...addColumns.filter((operation) => operation.op !== "addColumn" || !rebuilt.has(operation.table))
3133
+ ],
3007
3134
  retired,
3008
3135
  renames
3009
3136
  };
@@ -3154,7 +3281,15 @@ function tableOf(spec, registry, tables) {
3154
3281
  unique: true
3155
3282
  };
3156
3283
  });
3157
- const refIndexes = columns.filter((c) => c.references && !uniques.some((u) => u.columns[0] === c.name)).map((c) => ({
3284
+ const declared = spec.indexes.map((index) => {
3285
+ const columns = index.fields.map(column);
3286
+ return {
3287
+ name: `${spec.table}_${columns.join("_")}_index`,
3288
+ columns,
3289
+ unique: false
3290
+ };
3291
+ });
3292
+ const refIndexes = columns.filter((c) => c.references && ![...uniques, ...declared].some((index) => index.columns[0] === c.name)).map((c) => ({
3158
3293
  name: `${spec.table}_${c.name}_index`,
3159
3294
  columns: [c.name],
3160
3295
  unique: false
@@ -3169,7 +3304,11 @@ function tableOf(spec, registry, tables) {
3169
3304
  name: spec.table,
3170
3305
  model: spec.name,
3171
3306
  columns,
3172
- indexes: [...uniques, ...refIndexes],
3307
+ indexes: [
3308
+ ...uniques,
3309
+ ...declared,
3310
+ ...refIndexes
3311
+ ],
3173
3312
  guards: [...checks, ...limitGuards(spec)],
3174
3313
  counts: countsOf(spec, registry),
3175
3314
  copies: copiesOf(spec, registry),
@@ -3177,7 +3316,8 @@ function tableOf(spec, registry, tables) {
3177
3316
  field,
3178
3317
  column: columnName(field, fieldSpec),
3179
3318
  transitions: fieldSpec.flow
3180
- }] : [])
3319
+ }] : []),
3320
+ withoutRowid: true
3181
3321
  };
3182
3322
  }
3183
3323
  function snapshotOf(models) {
@@ -3209,7 +3349,11 @@ function generateMigration(options) {
3209
3349
  const { previous, dialect, name, clock = systemClock } = options;
3210
3350
  const live = snapshotOf(options.models);
3211
3351
  const { operations, retired, renames } = diff(previous, live, dayOf(clock.now()));
3212
- const statements = operations.flatMap((operation) => dialect.render(operation));
3352
+ const triggers = dialect.migrations.rules(previous).flatMap((rule) => rule.kind === "trigger" ? [rule.name] : []);
3353
+ const statements = operations.map((operation) => operation.op === "rebuild" ? {
3354
+ ...operation,
3355
+ triggers
3356
+ } : operation).flatMap((operation) => dialect.render(operation));
3213
3357
  const last = statements.length > 0 ? name : previous.last;
3214
3358
  const stillRetired = previous.retired.filter((r) => r.kind !== "index" || !live.tables[r.table]?.indexes.some((index) => index.name === r.index.name));
3215
3359
  const snapshot = {
@@ -3605,4 +3749,4 @@ async function migrateFresh(driver, models, options = {}) {
3605
3749
  await sync(driver, manifest, options);
3606
3750
  }
3607
3751
  //#endregion
3608
- export { planUpdate as A, NeedsDatabase as B, database as C, recordAction as D, forUpkeep as E, dialectFor as F, newSecretKey as G, fixedClock as H, sqlite as I, NotFound as J, CheckFailed as K, PLATFORM as L, Query as M, conditionOf as N, withTableTotals as O, modelOfRow as P, TENANT as R, clockOf as S, driverOf as T, systemClock as U, evaluate as V, utc as W, UniqueFailed as X, ReferenceFailed as Y, TODO as _, currentRelease as a, Database as b, rollback as c, applyRules as d, dayOf as f, UnsafeMigration as g, snapshotOf as h, acquireLock as i, keyCondition as j, planInsert as k, setReleaseStatus as l, emptySnapshot as m, migrateFresh as n, holdOf as o, generateMigration as p, DatabaseError as q, sync as r, releaseLock as s, migrate as t, startRelease as u, dataStep as v, decodeStored as w, Transaction as x, MigrationError as y, checkTenancy as z };
3752
+ export { planUpdate as A, checkTenancy as B, database as C, recordAction as D, forUpkeep as E, modelOfRow as F, utc as G, evaluate as H, dialectFor as I, DatabaseError as J, newSecretKey as K, sqlite as L, Query as M, conditionOf as N, withTableTotals as O, withIncludeRule as P, PLATFORM as R, clockOf as S, driverOf as T, fixedClock as U, NeedsDatabase as V, systemClock as W, ReferenceFailed as X, NotFound as Y, UniqueFailed as Z, TODO as _, currentRelease as a, Database as b, rollback as c, applyRules as d, dayOf as f, UnsafeMigration as g, snapshotOf as h, acquireLock as i, keyCondition as j, planInsert as k, setReleaseStatus as l, emptySnapshot as m, migrateFresh as n, holdOf as o, generateMigration as p, CheckFailed as q, sync as r, releaseLock as s, migrate as t, startRelease as u, dataStep as v, decodeStored as w, Transaction as x, MigrationError as y, TENANT as z };
@@ -1,4 +1,4 @@
1
- import { A as fakeUlid, C as text, D as choice, E as url, F as quantity, I as sequence, M as id, N as int, O as date, P as money, R as list, S as subdomain, T as token, W as kind, _ as newToken, a as file, b as slug, c as image, d as json, f as urls, g as markdown, h as line, j as flag, k as datetime, l as images, n as phoneKind, p as email, s as files, u as define, v as personName, w as textKind, x as slugify, y as secret } from "./validation-Cppmd0RV.js";
1
+ import { A as date, B as list, C as slugify, D as token, E as textKind, F as int, I as money, K as kind, L as quantity, M as fakeUlid, N as flag, O as url, P as id, R as sequence, S as slug, T as text, _ as line, a as document, b as personName, d as images, f as define, h as email, j as datetime, k as choice, l as files, m as urls, n as phoneKind, o as documents, p as json, s as file, u as image, v as markdown, w as subdomain, x as secret, y as newToken } from "./validation-C3MCRBQO.js";
2
2
  import { n as snakeCase, r as ulid } from "./case-DpXlJCL9.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  //#region src/internal/scope.ts
@@ -920,6 +920,10 @@ function fieldKinds(defaults) {
920
920
  image: (options = {}) => field(image(options), "json"),
921
921
  files: (options = {}) => field(files(options), "json"),
922
922
  images: (options = {}) => field(images(options), "json"),
923
+ /** A PDF or image to print or read whole (never resized), up to 100 MB; `{ storage }` as for `file`. */
924
+ document: (options = {}) => field(document(options), "json"),
925
+ /** Up to 100 documents: an order's files. */
926
+ documents: (options = {}) => field(documents(options), "json"),
923
927
  /** A row of another model, by its id. Restricts deletes of that row unless `.cascade()` / `.setNull()`. */
924
928
  ref: (model, options = {}) => new Field(fieldSpec(id(`a ${model} id`), "text", { ref: {
925
929
  model,
@@ -1091,6 +1095,31 @@ var Model = class Model {
1091
1095
  });
1092
1096
  }
1093
1097
  /**
1098
+ * An index on these fields, in this order, for the lists the app runs: a list sorted by a field
1099
+ * (`.index("createdAt")`), or filtered by one field then sorted by another
1100
+ * (`.index("status", "createdAt")`). Refs index themselves, and traits bring the indexes their
1101
+ * behaviour needs (`publishable`: `publishedAt`; `ordered`: its group, then `position`), so most
1102
+ * models never call it. `isai check` warns when a resource sorts by a field no index leads with.
1103
+ * Each index costs a row written on every insert, and on each update of its fields.
1104
+ */
1105
+ index(...fields) {
1106
+ const name = this.spec.name;
1107
+ if (fields.length === 0) throw new TypeError(`isai: ${name}.index() takes one or more fields`);
1108
+ if (new Set(fields).size !== fields.length) throw new TypeError(`isai: ${name}.index() lists a field twice`);
1109
+ for (const field of fields) {
1110
+ const spec = this.spec.fields[field];
1111
+ if (!spec || spec.computed) throw new TypeError(`isai: ${name}.index() lists "${field}", which isn't one of ${name}'s stored fields`);
1112
+ assertNotEncrypted(this.spec, field, "indexed");
1113
+ }
1114
+ const key = fields.join(",");
1115
+ if (this.spec.indexes.some((index) => index.fields.join(",") === key)) throw new TypeError(`isai: ${name} already has an index on ${fields.join(", ")}`);
1116
+ if (this.spec.uniques.some((unique) => unique.fields.join(",") === key)) throw new TypeError(`isai: ${name} already has a unique on ${fields.join(", ")}, which is an index too`);
1117
+ return this.derive({
1118
+ ...this.spec,
1119
+ indexes: [...this.spec.indexes, { fields }]
1120
+ });
1121
+ }
1122
+ /**
1094
1123
  * The table it's stored in, when that isn't its name in snake_case: after the model was renamed,
1095
1124
  * so the data stays where it is (`model("Squad", {…}).table("team")`). Physical renames never happen.
1096
1125
  */
@@ -1302,6 +1331,7 @@ function defineModel(registry, name, declared, traits = [], origin = {}) {
1302
1331
  table: snakeCase(name),
1303
1332
  fields: withCopyPaths(name, specs, registry, () => spec),
1304
1333
  uniques,
1334
+ indexes: traits.flatMap((trait) => (trait.spec.indexes ?? []).map((fields) => ({ fields }))),
1305
1335
  checks: [],
1306
1336
  traits: names,
1307
1337
  ...origin.database === void 0 ? {} : { database: origin.database },
@@ -1,4 +1,4 @@
1
- import { G as isRecorded, _ as Expr, c as rowOf, h as Condition, j as nodesOf, r as columnName, w as conditionNode } from "./model-Dc3HunKb.js";
1
+ import { G as isRecorded, _ as Expr, c as rowOf, h as Condition, j as nodesOf, r as columnName, w as conditionNode } from "./model-6pJBqqGd.js";
2
2
  //#region src/auth/enforce.ts
3
3
  /**
4
4
  * Checking a policy: its action's condition, with the
@@ -1,7 +1,7 @@
1
- import { G as ValidationError, H as validate, V as toKind, z as object } from "./validation-Cppmd0RV.js";
2
- import { J as NotFound, K as CheckFailed, X as UniqueFailed, Y as ReferenceFailed } from "./migrator-B25prc9n.js";
3
- import { C as asCondition, _ as Expr, g as Embed, l as IMPLICIT_FIELDS, r as columnName, s as computedFieldOf } from "./model-Dc3HunKb.js";
4
- import { r as actionsOf } from "./policies-KsqmlX67.js";
1
+ import { U as toKind, V as object, W as validate, q as ValidationError } from "./validation-C3MCRBQO.js";
2
+ import { X as ReferenceFailed, Y as NotFound, Z as UniqueFailed, q as CheckFailed } from "./migrator-vvvKa0Vk.js";
3
+ import { C as asCondition, _ as Expr, g as Embed, l as IMPLICIT_FIELDS, r as columnName, s as computedFieldOf } from "./model-6pJBqqGd.js";
4
+ import { r as actionsOf } from "./policies-C_oiSgmY.js";
5
5
  import { n as RESERVED, t as LOOKUPS } from "./syntax-CrUZFrS7.js";
6
6
  //#region src/api/fail.ts
7
7
  /**
@@ -199,13 +199,15 @@ var Route = class {
199
199
  anonymous;
200
200
  handler;
201
201
  platform;
202
- constructor(method, path, input, anonymous, handler, platform = false) {
202
+ upload;
203
+ constructor(method, path, input, anonymous, handler, platform = false, upload) {
203
204
  this.method = method;
204
205
  this.path = path;
205
206
  this.input = input;
206
207
  this.anonymous = anonymous;
207
208
  this.handler = handler;
208
209
  this.platform = platform;
210
+ this.upload = upload;
209
211
  }
210
212
  };
211
213
  function declare(method) {
@@ -214,7 +216,7 @@ function declare(method) {
214
216
  const handler = typeof second === "function" ? second : third;
215
217
  if (!handler) throw new TypeError(`isai: ${method} ${path} has no handler`);
216
218
  if (!path.startsWith("/")) throw new TypeError(`isai: a route's path starts with "/" (${method} ${path})`);
217
- return new Route(method, path, options.input ?? {}, options.anonymous ?? false, handler, options.platform ?? false);
219
+ return new Route(method, path, options.input ?? {}, options.anonymous ?? false, handler, options.platform ?? false, options.upload);
218
220
  }
219
221
  return define;
220
222
  }
@@ -1,4 +1,4 @@
1
- import { a as defineModel, t as Model } from "./model-Dc3HunKb.js";
1
+ import { a as defineModel, t as Model } from "./model-6pJBqqGd.js";
2
2
  //#region src/db/schema/builtins.ts
3
3
  /**
4
4
  * Built-in tables: sets of models a part of isai owns, switched on
@@ -1,5 +1,5 @@
1
1
  import { a as DatabaseDriver, g as Storage } from "../index-Ce5p2-EB.js";
2
- import { I as Clock, L as FixedClock, R as fixedClock, V as AnyModel, i as Row, l as Overrides, p as Database, s as Makers, u as Seed } from "../types-BgOkJy5d.js";
2
+ import { I as Clock, L as FixedClock, R as fixedClock, V as AnyModel, i as Row, l as Overrides, p as Database, s as Makers, u as Seed } from "../types-D5SIVeej.js";
3
3
  //#region src/testing/conformance/database.d.ts
4
4
  /**
5
5
  * Makes the driver under test. Every call must return a driver on an **empty** database:
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.js";
2
- import { F as dialectFor, H as fixedClock, L as PLATFORM, R as TENANT, W as utc, a as currentRelease, b as Database, c as rollback, i as acquireLock, j as keyCondition, l as setReleaseStatus, m as emptySnapshot, n as migrateFresh, o as holdOf, p as generateMigration, r as sync, s as releaseLock, t as migrate, u as startRelease, v as dataStep, y as MigrationError } from "../migrator-B25prc9n.js";
3
- import { L as toBase64url, t as Model } from "../model-Dc3HunKb.js";
4
- import { t as createDataLayer } from "../data-layer-C5oJZ8V1.js";
5
- import { d as forgetGoogleKeys, r as authOf, s as GOOGLE, t as makers, y as seedAs } from "../makers-P9kg68C8.js";
2
+ import { G as utc, I as dialectFor, R as PLATFORM, U as fixedClock, a as currentRelease, b as Database, c as rollback, i as acquireLock, j as keyCondition, l as setReleaseStatus, m as emptySnapshot, n as migrateFresh, o as holdOf, p as generateMigration, r as sync, s as releaseLock, t as migrate, u as startRelease, v as dataStep, y as MigrationError, z as TENANT } from "../migrator-vvvKa0Vk.js";
3
+ import { L as toBase64url, t as Model } from "../model-6pJBqqGd.js";
4
+ import { t as createDataLayer } from "../data-layer-BOVkYESH.js";
5
+ import { d as forgetGoogleKeys, r as authOf, s as GOOGLE, t as makers, y as seedAs } from "../makers-DjkBP74P.js";
6
6
  import { describe, expect, it } from "vitest";
7
7
  //#region src/testing/conformance/database.ts
8
8
  /**