@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.
- package/dist/adapters/storage/r2/index.d.ts +3 -0
- package/dist/adapters/storage/r2/index.js +13 -2
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +2 -2
- package/dist/cli/index.js +79 -11
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +18 -13
- package/dist/{create-auth-BFKvQVOG.js → create-auth-BCvMwlVD.js} +3 -3
- package/dist/{data-layer-C5oJZ8V1.js → data-layer-BOVkYESH.js} +15 -10
- package/dist/{data-layer-43pme0Gy.d.ts → data-layer-nvm_ivGQ.d.ts} +6 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +5 -5
- package/dist/{index-a7ElyL3B.d.ts → index-Bb9Px6SR.d.ts} +97 -18
- package/dist/{index-CYcklQ1l.d.ts → index-CaR3hb0i.d.ts} +5 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +150 -78
- package/dist/{make-ByA-84h8.js → make-D6_caNkv.js} +21 -3
- package/dist/{makers-P9kg68C8.js → makers-DjkBP74P.js} +3 -3
- package/dist/{migrator-B25prc9n.js → migrator-vvvKa0Vk.js} +180 -36
- package/dist/{model-Dc3HunKb.js → model-6pJBqqGd.js} +31 -1
- package/dist/{policies-KsqmlX67.js → policies-C_oiSgmY.js} +1 -1
- package/dist/{resource-BQK7bkPQ.js → resource-DCg6LvMH.js} +8 -6
- package/dist/{tables-DgSwge7s.js → tables-DjW96Ubl.js} +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +4 -4
- package/dist/{types-BgOkJy5d.d.ts → types-D5SIVeej.d.ts} +59 -2
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +1 -1
- package/dist/{validation-Cppmd0RV.js → validation-C3MCRBQO.js} +26 -3
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
*
|
|
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 {
|
|
2
|
-
import { D as recordAction, E as forUpkeep,
|
|
3
|
-
import { I as fromBase64url, L as toBase64url, l as IMPLICIT_FIELDS, t as Model } from "./model-
|
|
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 {
|
|
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-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
2151
|
+
values: upkeep ? values : {
|
|
2109
2152
|
...values,
|
|
2110
2153
|
updatedAt: now
|
|
2111
|
-
}
|
|
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,
|
|
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,
|
|
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
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
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: [
|
|
3004
|
-
|
|
3005
|
-
table
|
|
3006
|
-
|
|
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
|
|
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: [
|
|
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
|
|
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,
|
|
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
|
|
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-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { C as asCondition, _ as Expr, g as Embed, l as IMPLICIT_FIELDS, r as columnName, s as computedFieldOf } from "./model-
|
|
4
|
-
import { r as actionsOf } from "./policies-
|
|
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
|
-
|
|
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
|
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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-
|
|
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:
|
package/dist/testing/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import {
|
|
3
|
-
import { L as toBase64url, t as Model } from "../model-
|
|
4
|
-
import { t as createDataLayer } from "../data-layer-
|
|
5
|
-
import { d as forgetGoogleKeys, r as authOf, s as GOOGLE, t as makers, y as seedAs } from "../makers-
|
|
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
|
/**
|