@isaijs/core 0.1.4 → 0.1.6

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.
@@ -1,5 +1,5 @@
1
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";
2
- import { n as snakeCase } from "./case-BnpcvhdW.js";
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
5
5
  /**
@@ -19,58 +19,6 @@ var Scope = class {
19
19
  }
20
20
  };
21
21
  //#endregion
22
- //#region src/internal/ulid.ts
23
- /**
24
- * ULIDs: 48 bits of milliseconds + 80 random bits, Crockford base32, 26 characters.
25
- * Generated in JS (works inside a D1 batch, the same on every driver) and sortable by time.
26
- *
27
- * Monotonic: ids made in the same millisecond (or after the clock stepped back) reuse the
28
- * last time and add one to the last random part, so ids always sort in the order they were
29
- * made within an isolate or process.
30
- */
31
- const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
32
- const RANDOM_LENGTH = 16;
33
- function encodeTime(time) {
34
- let rest = time;
35
- let text = "";
36
- for (let i = 0; i < 10; i++) {
37
- text = ALPHABET.charAt(rest % 32) + text;
38
- rest = Math.floor(rest / 32);
39
- }
40
- return text;
41
- }
42
- function randomDigits() {
43
- return Array.from(crypto.getRandomValues(new Uint8Array(RANDOM_LENGTH)), (byte) => byte % 32);
44
- }
45
- /** Adds one to base-32 digits in place; false when they were all at the maximum. */
46
- function increment(digits) {
47
- for (let i = digits.length - 1; i >= 0; i--) {
48
- if ((digits[i] ?? 0) < 31) {
49
- digits[i] = (digits[i] ?? 0) + 1;
50
- return true;
51
- }
52
- digits[i] = 0;
53
- }
54
- return false;
55
- }
56
- function monotonicUlid() {
57
- let lastTime = -1;
58
- let lastRandom = [];
59
- return (now) => {
60
- const time = now.getTime();
61
- if (time > lastTime) {
62
- lastTime = time;
63
- lastRandom = randomDigits();
64
- } else if (!increment(lastRandom)) {
65
- lastTime += 1;
66
- lastRandom = randomDigits();
67
- }
68
- return encodeTime(lastTime) + lastRandom.map((digit) => ALPHABET.charAt(digit)).join("");
69
- };
70
- }
71
- /** The process-wide generator every write uses. */
72
- const ulid = monotonicUlid();
73
- //#endregion
74
22
  //#region src/db/orm/audit.ts
75
23
  /**
76
24
  * The audit log in the ORM's write path: which writes are recorded, who makes them, and the
@@ -196,287 +144,21 @@ function actionEntry(engine, model, rowId, action, context, user) {
196
144
  return engine.dialect.insert(log, [entry], context);
197
145
  }
198
146
  //#endregion
199
- //#region src/db/fields/field.ts
200
- var Field = class Field {
201
- spec;
202
- constructor(spec) {
203
- this.spec = spec;
204
- }
205
- /** The kind behind the field, so a field works anywhere a shape does (route inputs, `validate`). */
206
- get "~kind"() {
207
- return this.spec.kind;
208
- }
209
- get "~standard"() {
210
- return this.spec.kind["~standard"];
211
- }
212
- /** The field may be empty (`null`). Fields are required by default. */
213
- optional() {
214
- return new Field({
215
- ...this.spec,
216
- kind: this.spec.kind.optional()
217
- });
218
- }
219
- /** The value written when a create leaves the field out. */
220
- default(value) {
221
- return new Field({
222
- ...this.spec,
223
- kind: this.spec.kind.default(value)
224
- });
225
- }
226
- /** Narrows the kind's lower limit, for a business rule. */
227
- min(limit) {
228
- return new Field({
229
- ...this.spec,
230
- kind: this.spec.kind.min(limit)
231
- });
232
- }
233
- /** Narrows the kind's upper limit, for a business rule ("at most 8 images"). */
234
- max(limit) {
235
- return new Field({
236
- ...this.spec,
237
- kind: this.spec.kind.max(limit)
238
- });
239
- }
240
- unique() {
241
- return new Field({
242
- ...this.spec,
243
- unique: true
244
- });
245
- }
246
- /** Stores the field in a column with another name (a field renamed in code keeps its column). */
247
- column(name) {
248
- return new Field({
249
- ...this.spec,
250
- column: name
251
- });
252
- }
253
- /** Set on create and never changed: updates can't name it (a type error, and a `fixed` issue). */
254
- fixed() {
255
- return new Field({
256
- ...this.spec,
257
- fixed: true
258
- });
259
- }
260
- /**
261
- * The changes a choice allows, from each value to the ones it may become; any other change of a
262
- * stored value fails (`notAllowed`), whoever writes it. A value missing from the keys is final.
263
- *
264
- * f.choice(["pending", "confirmed", "cancelled"]).flow({ pending: ["confirmed", "cancelled"], confirmed: ["cancelled"] })
265
- */
266
- flow(transitions) {
267
- const choices = this.spec.kind.spec.choices;
268
- if (!choices) throw new TypeError("isai: .flow() is for a choice");
269
- for (const [from, to] of Object.entries(transitions)) for (const value of [from, ...to]) if (!choices.includes(value)) throw new TypeError(`isai: .flow() names "${value}", which isn't a choice`);
270
- return new Field({
271
- ...this.spec,
272
- flow: transitions
273
- });
274
- }
275
- /**
276
- * A copy of a related row's value (`.from((s) => s.criterion.maxScore)`), so the row's checks and
277
- * uniques can use it. The database writes it on create and keeps it equal to its source (triggers
278
- * on each table along the path, so raw SQL writes are seen too); it's never written. The path's
279
- * first ref must be `.fixed()`; when every step and the final field are fixed, it's written once
280
- * and no trigger is made. A plain path to one column only: a derived value is `.computed()`, and a
281
- * value frozen as it was is a snapshot, not a copy.
282
- */
283
- from(build) {
284
- if (this.spec.maintained || this.spec.computed || this.spec.generate) throw new TypeError("isai: .from() is for a plain field (not one the database or the framework makes)");
285
- return new Field({
286
- ...this.spec,
287
- maintained: {
288
- by: "copy",
289
- build,
290
- node: void 0
291
- }
292
- });
293
- }
294
- /** Fills a module's slot. The field's values must be the slot's (a `string` slot takes no `f.int()`). */
295
- slot(slot) {
296
- if (this.spec.slots.some((filled) => filled.name === slot.spec.name)) throw new TypeError(`isai: the field already fills the slot ${slot.spec.name}`);
297
- return new Field({
298
- ...this.spec,
299
- slots: [...this.spec.slots, slot.spec]
300
- });
301
- }
302
- /** On a ref: the row this one belongs to. At most one per model. */
303
- parent() {
304
- this.refSpec("parent");
305
- return new Field({
306
- ...this.spec,
307
- parent: true
308
- });
309
- }
310
- /** On a ref: deleting the referenced row deletes this one. */
311
- cascade() {
312
- return new Field({
313
- ...this.spec,
314
- ref: {
315
- ...this.refSpec("cascade"),
316
- onDelete: "cascade"
317
- }
318
- });
319
- }
320
- /** On an optional ref: deleting the referenced row empties this field. */
321
- setNull() {
322
- return new Field({
323
- ...this.spec,
324
- ref: {
325
- ...this.refSpec("setNull"),
326
- onDelete: "setNull"
327
- }
328
- });
329
- }
330
- refSpec(modifier) {
331
- if (!this.spec.ref) throw new TypeError(`isai: ${modifier}() is only for ref fields`);
332
- return this.spec.ref;
333
- }
334
- };
335
- /**
336
- * How sensitive a field's values are, from its kind and the slots it fills; `hidden` wins over
337
- * `personal`. Rows in services stay complete: the output layer applies it.
338
- */
339
- function sensitivity(spec) {
340
- const marks = [spec.kind.spec.sensitive, ...spec.slots.map((slot) => slot.sensitive)];
341
- if (marks.includes("hidden")) return "hidden";
342
- if (marks.includes("personal")) return "personal";
343
- }
344
- //#endregion
345
- //#region src/db/fields/kinds.ts
346
- /**
347
- * The built-in field kinds: each is a kind from `isai/validation` with its storage. Kinds own
348
- * their limits and rules; columns never repeat them.
349
- */
350
- function fieldSpec(kind, storage, options) {
351
- return {
352
- kind,
353
- storage,
354
- unique: false,
355
- column: void 0,
356
- ref: void 0,
357
- fixed: false,
358
- parent: false,
359
- generate: void 0,
360
- computed: void 0,
361
- flow: void 0,
362
- maintained: void 0,
363
- encrypted: false,
364
- slots: [],
365
- ...options
366
- };
367
- }
368
- /** A field of a kind, stored as `storage`. */
369
- function field(kind, storage, options = {}) {
370
- return new Field(fieldSpec(kind, storage, options));
371
- }
372
- /** A field the framework fills when a create leaves it out, so writes may omit it. */
373
- function filled(kind, storage, options = {}) {
374
- return new Field(fieldSpec(kind, storage, options));
147
+ //#region src/internal/base64url.ts
148
+ /** base64url without padding: how isai writes random bytes as text (keys, tokens, signatures). */
149
+ function toBase64url(bytes) {
150
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
375
151
  }
376
- const STORAGE_OF = {
377
- text: "text",
378
- integer: "integer",
379
- number: "real",
380
- boolean: "boolean"
381
- };
382
- /** The kinds every project gets as `f`. `defaults` is read when a value is checked. */
383
- function fieldKinds(defaults) {
384
- function slug$1(options = {}) {
385
- const { from } = options;
386
- if (from === void 0) return field(slug(), "text", { unique: true });
387
- return filled(slug(), "text", {
388
- unique: true,
389
- generate: {
390
- from,
391
- make: (values) => slugify(String(values[from] ?? "")) || void 0
392
- }
393
- });
152
+ /** The bytes, or undefined when `text` isn't base64url. */
153
+ function fromBase64url(text) {
154
+ if (!/^[A-Za-z0-9_-]*$/.test(text)) return void 0;
155
+ try {
156
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
157
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
158
+ } catch {
159
+ return;
394
160
  }
395
- return {
396
- line: () => field(line(), "text"),
397
- text: () => field(text(), "text"),
398
- markdown: () => field(markdown(), "text"),
399
- personName: () => field(personName(), "text"),
400
- email: () => field(email(), "text"),
401
- phone: () => field(phoneKind(() => defaults().phoneRegion), "text"),
402
- url: () => field(url(), "text"),
403
- urls: () => field(urls(), "json"),
404
- slug: slug$1,
405
- /** A tenant's subdomain on the platform: one DNS label, 3–63, not a reserved name. */
406
- subdomain: () => field(subdomain(), "text"),
407
- /** Encrypted at rest with the database's `secretKey`, and hidden. Never compared, ordered or unique. */
408
- secret: () => field(secret(), "text", { encrypted: true }),
409
- /**
410
- * A human-friendly number made by the database on create (`f.sequence({ prefix: "ORD-" })`):
411
- * `ORD-0042` in code and JSON, stored as its number, so it orders as one.
412
- */
413
- sequence: (options = {}) => field(sequence(options), "integer", {
414
- unique: true,
415
- maintained: { by: "sequence" }
416
- }),
417
- /**
418
- * How many rows of a reverse relation there are (`f.count({ of: "members" })`), or how many match
419
- * `where` (over their own fields). Kept by the database as they're created, changed and deleted,
420
- * so checks can use it (`e.confirmedCount.lte(e.capacity)`).
421
- */
422
- count: (options) => field(quantity(), "integer", { maintained: {
423
- by: "count",
424
- of: options.of,
425
- where: options.where
426
- } }),
427
- /** Random and unique, made on create. */
428
- token: () => filled(token(), "text", {
429
- unique: true,
430
- generate: {
431
- from: void 0,
432
- make: newToken
433
- }
434
- }),
435
- int: () => field(int(), "integer"),
436
- quantity: () => field(quantity(), "integer"),
437
- money: () => field(money(), "integer"),
438
- flag: () => field(flag(), "boolean"),
439
- datetime: () => field(datetime(), "datetime"),
440
- date: () => field(date(), "date"),
441
- choice: (values) => field(choice(values), "text"),
442
- /** A JSON value of a shape (`{ shape: { name: f.line() } }`, or `f.list(...)`). */
443
- json: (options) => field(json(options.shape), "json"),
444
- /** A list, for shapes (`f.json({ shape: f.list(...) })`, route inputs). Not a column itself. */
445
- list,
446
- /** A file; `{ storage }` names a storage other than `default`. */
447
- file: (options = {}) => field(file(options), "json"),
448
- image: (options = {}) => field(image(options), "json"),
449
- files: (options = {}) => field(files(options), "json"),
450
- images: (options = {}) => field(images(options), "json"),
451
- /** A row of another model, by its id. Restricts deletes of that row unless `.cascade()` / `.setNull()`. */
452
- ref: (model, options = {}) => new Field(fieldSpec(id(`a ${model} id`), "text", { ref: {
453
- model,
454
- onDelete: "restrict",
455
- related: options.related
456
- } })),
457
- /** A project kind: a base type, a normalisation and a rule. */
458
- define: (definition) => field(define(definition), STORAGE_OF[definition.type])
459
- };
460
161
  }
461
- /**
462
- * The fields every model has without declaring them. The framework fills them on every write
463
- * (a ULID, the clock's time), so their type says "defaulted" and writes may omit them.
464
- */
465
- function implicitFields() {
466
- return {
467
- id: filled(textKind("id", {
468
- max: 255,
469
- fake: fakeUlid
470
- }), "text"),
471
- createdAt: filled(datetime(), "datetime"),
472
- updatedAt: filled(datetime(), "datetime")
473
- };
474
- }
475
- const IMPLICIT_FIELDS = [
476
- "id",
477
- "createdAt",
478
- "updatedAt"
479
- ];
480
162
  //#endregion
481
163
  //#region src/db/conditions/ast.ts
482
164
  /** Every node in a tree, depth first. */
@@ -529,6 +211,64 @@ function* nodesOf(node) {
529
211
  case "placeholder": return;
530
212
  }
531
213
  }
214
+ /**
215
+ * The condition with each operand `swap` replaces (it returns the operand itself to keep it),
216
+ * looking inside arithmetic and conditions used as values. A relation's own rows (an aggregate's
217
+ * `where`, a `some`) are left as they are.
218
+ */
219
+ function mapOperands(node, swap) {
220
+ const operand = (o) => {
221
+ const swapped = swap(o);
222
+ if (swapped !== o) return swapped;
223
+ switch (o.t) {
224
+ case "math": return {
225
+ ...o,
226
+ left: operand(o.left),
227
+ right: operand(o.right)
228
+ };
229
+ case "coalesce": return {
230
+ ...o,
231
+ items: o.items.map(operand)
232
+ };
233
+ case "local": return {
234
+ ...o,
235
+ operand: operand(o.operand)
236
+ };
237
+ case "truth": return {
238
+ ...o,
239
+ condition: mapOperands(o.condition, swap)
240
+ };
241
+ default: return o;
242
+ }
243
+ };
244
+ switch (node.t) {
245
+ case "compare": return {
246
+ ...node,
247
+ left: operand(node.left),
248
+ right: operand(node.right)
249
+ };
250
+ case "isNull":
251
+ case "in":
252
+ case "text": return {
253
+ ...node,
254
+ operand: operand(node.operand)
255
+ };
256
+ case "and":
257
+ case "or": return {
258
+ ...node,
259
+ items: node.items.map((item) => mapOperands(item, swap))
260
+ };
261
+ case "not": return {
262
+ ...node,
263
+ item: mapOperands(node.item, swap)
264
+ };
265
+ default: return node;
266
+ }
267
+ }
268
+ /** A condition's totals over a whole table (`rows(Product).count()`): aggregates with no owner. */
269
+ function tableTotals(node) {
270
+ return [...nodesOf(node)].filter((n) => n.t === "aggregate" && !n.relation.owner);
271
+ }
532
272
  /** The row's own fields a condition reads, in first-seen order (refs and relations excluded). */
533
273
  function fieldsOf(node) {
534
274
  const fields = /* @__PURE__ */ new Set();
@@ -845,88 +585,370 @@ var Relation = class {
845
585
  sum(build, where) {
846
586
  return this.aggregate("sum", build, where, void 0);
847
587
  }
848
- /** The smallest value over the related rows, or those that match (null when there are none). */
849
- min(build, where) {
850
- return this.aggregate("min", build, where, void 0);
588
+ /** The smallest value over the related rows, or those that match (null when there are none). */
589
+ min(build, where) {
590
+ return this.aggregate("min", build, where, void 0);
591
+ }
592
+ /** The largest value over the related rows, or those that match (null when there are none). */
593
+ max(build, where) {
594
+ return this.aggregate("max", build, where, void 0);
595
+ }
596
+ aggregate(fn, value, where, storage) {
597
+ const scope = newScope();
598
+ const rows = this.rows(scope);
599
+ const valueNode = value?.(rows).node;
600
+ return new Expr({
601
+ t: "aggregate",
602
+ fn,
603
+ relation: {
604
+ ...this.relation,
605
+ scope
606
+ },
607
+ value: valueNode,
608
+ where: where?.(rows).node,
609
+ storage: storage ?? (valueNode ? storageOf(valueNode) : void 0) ?? "real"
610
+ });
611
+ }
612
+ };
613
+ /** The storage of an operand's value, when it's known (for encoding the literals compared with it). */
614
+ function storageOf(node) {
615
+ switch (node.t) {
616
+ case "column":
617
+ case "aggregate": return node.storage;
618
+ case "now": return "datetime";
619
+ case "math": return node.op === "/" ? "real" : storageOf(node.left) ?? storageOf(node.right);
620
+ case "coalesce": return node.items.map(storageOf).find((storage) => storage !== void 0);
621
+ case "local": return node.unit === "date" ? "date" : "text";
622
+ case "truth": return "boolean";
623
+ case "total": return node.storage;
624
+ case "value":
625
+ case "bound": return;
626
+ }
627
+ }
628
+ /** A computed value's node: an expression, or a condition used as a value. */
629
+ function valueNode(value) {
630
+ if (value instanceof Condition) return {
631
+ t: "truth",
632
+ condition: value.node
633
+ };
634
+ if (value instanceof Expr) return value.node;
635
+ throw new TypeError("isai: a computed field must be an expression or a condition");
636
+ }
637
+ /** A condition's node, from a condition or a boolean computed field. */
638
+ function conditionNode(value) {
639
+ if (value instanceof Condition) return value.node;
640
+ if (value instanceof Expr && value.node.t === "truth") return value.node.condition;
641
+ if (value instanceof Expr) return {
642
+ t: "compare",
643
+ op: "=",
644
+ left: value.node,
645
+ right: {
646
+ t: "value",
647
+ value: true
648
+ }
649
+ };
650
+ throw new TypeError("isai: a condition is expected here (row(Model).field.is(…), any(…), byRole)");
651
+ }
652
+ /** As a condition: a condition, or a boolean computed field. */
653
+ const asCondition = (value) => value instanceof Condition ? value : new Condition(conditionNode(value));
654
+ function all(...conditions) {
655
+ return new Condition({
656
+ t: "and",
657
+ items: conditions.map(conditionNode)
658
+ });
659
+ }
660
+ /** Any of them (OR). */
661
+ function any(...conditions) {
662
+ return new Condition({
663
+ t: "or",
664
+ items: conditions.map(conditionNode)
665
+ });
666
+ }
667
+ function not(condition) {
668
+ return asCondition(condition).not();
669
+ }
670
+ //#endregion
671
+ //#region src/db/fields/field.ts
672
+ var Field = class Field {
673
+ spec;
674
+ constructor(spec) {
675
+ this.spec = spec;
676
+ }
677
+ /** The kind behind the field, so a field works anywhere a shape does (route inputs, `validate`). */
678
+ get "~kind"() {
679
+ return this.spec.kind;
680
+ }
681
+ get "~standard"() {
682
+ return this.spec.kind["~standard"];
683
+ }
684
+ /** The field may be empty (`null`). Fields are required by default. */
685
+ optional() {
686
+ return new Field({
687
+ ...this.spec,
688
+ kind: this.spec.kind.optional()
689
+ });
690
+ }
691
+ /** The value written when a create leaves the field out. */
692
+ default(value) {
693
+ return new Field({
694
+ ...this.spec,
695
+ kind: this.spec.kind.default(value)
696
+ });
697
+ }
698
+ /** Narrows the kind's lower limit, for a business rule. */
699
+ min(limit) {
700
+ return new Field({
701
+ ...this.spec,
702
+ kind: this.spec.kind.min(limit)
703
+ });
704
+ }
705
+ /** Narrows the kind's upper limit, for a business rule ("at most 8 images"). */
706
+ max(limit) {
707
+ return new Field({
708
+ ...this.spec,
709
+ kind: this.spec.kind.max(limit)
710
+ });
711
+ }
712
+ unique() {
713
+ return new Field({
714
+ ...this.spec,
715
+ unique: true
716
+ });
717
+ }
718
+ /** Stores the field in a column with another name (a field renamed in code keeps its column). */
719
+ column(name) {
720
+ return new Field({
721
+ ...this.spec,
722
+ column: name
723
+ });
724
+ }
725
+ /** Set on create and never changed: updates can't name it (a type error, and a `fixed` issue). */
726
+ fixed() {
727
+ return new Field({
728
+ ...this.spec,
729
+ fixed: true
730
+ });
731
+ }
732
+ /**
733
+ * The changes a choice allows, from each value to the ones it may become; any other change of a
734
+ * stored value fails (`notAllowed`), whoever writes it. A value missing from the keys is final.
735
+ *
736
+ * f.choice(["pending", "confirmed", "cancelled"]).flow({ pending: ["confirmed", "cancelled"], confirmed: ["cancelled"] })
737
+ */
738
+ flow(transitions) {
739
+ const choices = this.spec.kind.spec.choices;
740
+ if (!choices) throw new TypeError("isai: .flow() is for a choice");
741
+ for (const [from, to] of Object.entries(transitions)) for (const value of [from, ...to]) if (!choices.includes(value)) throw new TypeError(`isai: .flow() names "${value}", which isn't a choice`);
742
+ return new Field({
743
+ ...this.spec,
744
+ flow: transitions
745
+ });
746
+ }
747
+ /**
748
+ * A copy of a related row's value (`.from((s) => s.criterion.maxScore)`), so the row's checks and
749
+ * uniques can use it. The database writes it on create and keeps it equal to its source (triggers
750
+ * on each table along the path, so raw SQL writes are seen too); it's never written. The path's
751
+ * first ref must be `.fixed()`; when every step and the final field are fixed, it's written once
752
+ * and no trigger is made. A plain path to one column only: a derived value is `.computed()`, and a
753
+ * value frozen as it was is a snapshot, not a copy.
754
+ */
755
+ from(build) {
756
+ if (this.spec.maintained || this.spec.computed || this.spec.generate) throw new TypeError("isai: .from() is for a plain field (not one the database or the framework makes)");
757
+ return new Field({
758
+ ...this.spec,
759
+ maintained: {
760
+ by: "copy",
761
+ build,
762
+ node: void 0
763
+ }
764
+ });
765
+ }
766
+ /** Fills a module's slot. The field's values must be the slot's (a `string` slot takes no `f.int()`). */
767
+ slot(slot) {
768
+ if (this.spec.slots.some((filled) => filled.name === slot.spec.name)) throw new TypeError(`isai: the field already fills the slot ${slot.spec.name}`);
769
+ return new Field({
770
+ ...this.spec,
771
+ slots: [...this.spec.slots, slot.spec]
772
+ });
773
+ }
774
+ /** On a ref: the row this one belongs to. At most one per model. */
775
+ parent() {
776
+ this.refSpec("parent");
777
+ return new Field({
778
+ ...this.spec,
779
+ parent: true
780
+ });
851
781
  }
852
- /** The largest value over the related rows, or those that match (null when there are none). */
853
- max(build, where) {
854
- return this.aggregate("max", build, where, void 0);
782
+ /** On a ref: deleting the referenced row deletes this one. */
783
+ cascade() {
784
+ return new Field({
785
+ ...this.spec,
786
+ ref: {
787
+ ...this.refSpec("cascade"),
788
+ onDelete: "cascade"
789
+ }
790
+ });
855
791
  }
856
- aggregate(fn, value, where, storage) {
857
- const scope = newScope();
858
- const rows = this.rows(scope);
859
- const valueNode = value?.(rows).node;
860
- return new Expr({
861
- t: "aggregate",
862
- fn,
863
- relation: {
864
- ...this.relation,
865
- scope
866
- },
867
- value: valueNode,
868
- where: where?.(rows).node,
869
- storage: storage ?? (valueNode ? storageOf(valueNode) : void 0) ?? "real"
792
+ /** On an optional ref: deleting the referenced row empties this field. */
793
+ setNull() {
794
+ return new Field({
795
+ ...this.spec,
796
+ ref: {
797
+ ...this.refSpec("setNull"),
798
+ onDelete: "setNull"
799
+ }
870
800
  });
871
801
  }
872
- };
873
- /** The storage of an operand's value, when it's known (for encoding the literals compared with it). */
874
- function storageOf(node) {
875
- switch (node.t) {
876
- case "column":
877
- case "aggregate": return node.storage;
878
- case "now": return "datetime";
879
- case "math": return node.op === "/" ? "real" : storageOf(node.left) ?? storageOf(node.right);
880
- case "coalesce": return node.items.map(storageOf).find((storage) => storage !== void 0);
881
- case "local": return node.unit === "date" ? "date" : "text";
882
- case "truth": return "boolean";
883
- case "total": return node.storage;
884
- case "value":
885
- case "bound": return;
802
+ refSpec(modifier) {
803
+ if (!this.spec.ref) throw new TypeError(`isai: ${modifier}() is only for ref fields`);
804
+ return this.spec.ref;
886
805
  }
806
+ };
807
+ /**
808
+ * How sensitive a field's values are, from its kind and the slots it fills; `hidden` wins over
809
+ * `personal`. Rows in services stay complete: the output layer applies it.
810
+ */
811
+ function sensitivity(spec) {
812
+ const marks = [spec.kind.spec.sensitive, ...spec.slots.map((slot) => slot.sensitive)];
813
+ if (marks.includes("hidden")) return "hidden";
814
+ if (marks.includes("personal")) return "personal";
887
815
  }
888
- /** A computed value's node: an expression, or a condition used as a value. */
889
- function valueNode(value) {
890
- if (value instanceof Condition) return {
891
- t: "truth",
892
- condition: value.node
816
+ //#endregion
817
+ //#region src/db/fields/kinds.ts
818
+ /**
819
+ * The built-in field kinds: each is a kind from `isai/validation` with its storage. Kinds own
820
+ * their limits and rules; columns never repeat them.
821
+ */
822
+ function fieldSpec(kind, storage, options) {
823
+ return {
824
+ kind,
825
+ storage,
826
+ unique: false,
827
+ column: void 0,
828
+ ref: void 0,
829
+ fixed: false,
830
+ parent: false,
831
+ generate: void 0,
832
+ computed: void 0,
833
+ flow: void 0,
834
+ maintained: void 0,
835
+ encrypted: false,
836
+ slots: [],
837
+ ...options
893
838
  };
894
- if (value instanceof Expr) return value.node;
895
- throw new TypeError("isai: a computed field must be an expression or a condition");
896
839
  }
897
- /** A condition's node, from a condition or a boolean computed field. */
898
- function conditionNode(value) {
899
- if (value instanceof Condition) return value.node;
900
- if (value instanceof Expr && value.node.t === "truth") return value.node.condition;
901
- if (value instanceof Expr) return {
902
- t: "compare",
903
- op: "=",
904
- left: value.node,
905
- right: {
906
- t: "value",
907
- value: true
908
- }
909
- };
910
- throw new TypeError("isai: a condition is expected here (row(Model).field.is(…), any(…), byRole)");
840
+ /** A field of a kind, stored as `storage`. */
841
+ function field(kind, storage, options = {}) {
842
+ return new Field(fieldSpec(kind, storage, options));
911
843
  }
912
- /** As a condition: a condition, or a boolean computed field. */
913
- const asCondition = (value) => value instanceof Condition ? value : new Condition(conditionNode(value));
914
- function all(...conditions) {
915
- return new Condition({
916
- t: "and",
917
- items: conditions.map(conditionNode)
918
- });
844
+ /** A field the framework fills when a create leaves it out, so writes may omit it. */
845
+ function filled(kind, storage, options = {}) {
846
+ return new Field(fieldSpec(kind, storage, options));
919
847
  }
920
- /** Any of them (OR). */
921
- function any(...conditions) {
922
- return new Condition({
923
- t: "or",
924
- items: conditions.map(conditionNode)
925
- });
848
+ const STORAGE_OF = {
849
+ text: "text",
850
+ integer: "integer",
851
+ number: "real",
852
+ boolean: "boolean"
853
+ };
854
+ /** The kinds every project gets as `f`. `defaults` is read when a value is checked. */
855
+ function fieldKinds(defaults) {
856
+ function slug$1(options = {}) {
857
+ const { from } = options;
858
+ if (from === void 0) return field(slug(), "text", { unique: true });
859
+ return filled(slug(), "text", {
860
+ unique: true,
861
+ generate: {
862
+ from,
863
+ make: (values) => slugify(String(values[from] ?? "")) || void 0
864
+ }
865
+ });
866
+ }
867
+ return {
868
+ line: () => field(line(), "text"),
869
+ text: () => field(text(), "text"),
870
+ markdown: () => field(markdown(), "text"),
871
+ personName: () => field(personName(), "text"),
872
+ email: () => field(email(), "text"),
873
+ phone: () => field(phoneKind(() => defaults().phoneRegion), "text"),
874
+ url: () => field(url(), "text"),
875
+ urls: () => field(urls(), "json"),
876
+ slug: slug$1,
877
+ /** A tenant's subdomain on the platform: one DNS label, 3–63, not a reserved name. */
878
+ subdomain: () => field(subdomain(), "text"),
879
+ /** Encrypted at rest with the database's `secretKey`, and hidden. Never compared, ordered or unique. */
880
+ secret: () => field(secret(), "text", { encrypted: true }),
881
+ /**
882
+ * A human-friendly number made by the database on create (`f.sequence({ prefix: "ORD-" })`):
883
+ * `ORD-0042` in code and JSON, stored as its number, so it orders as one.
884
+ */
885
+ sequence: (options = {}) => field(sequence(options), "integer", {
886
+ unique: true,
887
+ maintained: { by: "sequence" }
888
+ }),
889
+ /**
890
+ * How many rows of a reverse relation there are (`f.count({ of: "members" })`), or how many match
891
+ * `where` (over their own fields). Kept by the database as they're created, changed and deleted,
892
+ * so checks can use it (`e.confirmedCount.lte(e.capacity)`).
893
+ */
894
+ count: (options) => field(quantity(), "integer", { maintained: {
895
+ by: "count",
896
+ of: options.of,
897
+ where: options.where
898
+ } }),
899
+ /** Random and unique, made on create. */
900
+ token: () => filled(token(), "text", {
901
+ unique: true,
902
+ generate: {
903
+ from: void 0,
904
+ make: newToken
905
+ }
906
+ }),
907
+ int: () => field(int(), "integer"),
908
+ quantity: () => field(quantity(), "integer"),
909
+ money: () => field(money(), "integer"),
910
+ flag: () => field(flag(), "boolean"),
911
+ datetime: () => field(datetime(), "datetime"),
912
+ date: () => field(date(), "date"),
913
+ choice: (values) => field(choice(values), "text"),
914
+ /** A JSON value of a shape (`{ shape: { name: f.line() } }`, or `f.list(...)`). */
915
+ json: (options) => field(json(options.shape), "json"),
916
+ /** A list, for shapes (`f.json({ shape: f.list(...) })`, route inputs). Not a column itself. */
917
+ list,
918
+ /** A file; `{ storage }` names a storage other than `default`. */
919
+ file: (options = {}) => field(file(options), "json"),
920
+ image: (options = {}) => field(image(options), "json"),
921
+ files: (options = {}) => field(files(options), "json"),
922
+ images: (options = {}) => field(images(options), "json"),
923
+ /** A row of another model, by its id. Restricts deletes of that row unless `.cascade()` / `.setNull()`. */
924
+ ref: (model, options = {}) => new Field(fieldSpec(id(`a ${model} id`), "text", { ref: {
925
+ model,
926
+ onDelete: "restrict",
927
+ related: options.related
928
+ } })),
929
+ /** A project kind: a base type, a normalisation and a rule. */
930
+ define: (definition) => field(define(definition), STORAGE_OF[definition.type])
931
+ };
926
932
  }
927
- function not(condition) {
928
- return asCondition(condition).not();
933
+ /**
934
+ * The fields every model has without declaring them. The framework fills them on every write
935
+ * (a ULID, the clock's time), so their type says "defaulted" and writes may omit them.
936
+ */
937
+ function implicitFields() {
938
+ return {
939
+ id: filled(textKind("id", {
940
+ max: 255,
941
+ fake: fakeUlid
942
+ }), "text"),
943
+ createdAt: filled(datetime(), "datetime"),
944
+ updatedAt: filled(datetime(), "datetime")
945
+ };
929
946
  }
947
+ const IMPLICIT_FIELDS = [
948
+ "id",
949
+ "createdAt",
950
+ "updatedAt"
951
+ ];
930
952
  //#endregion
931
953
  //#region src/db/schema/rows.ts
932
954
  /**
@@ -1292,20 +1314,4 @@ function defineModel(registry, name, declared, traits = [], origin = {}) {
1292
1314
  return Object.keys(computed).length > 0 ? model.computed(computed) : model;
1293
1315
  }
1294
1316
  //#endregion
1295
- //#region src/internal/base64url.ts
1296
- /** base64url without padding: how isai writes random bytes as text (keys, tokens, signatures). */
1297
- function toBase64url(bytes) {
1298
- return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1299
- }
1300
- /** The bytes, or undefined when `text` isn't base64url. */
1301
- function fromBase64url(text) {
1302
- if (!/^[A-Za-z0-9_-]*$/.test(text)) return void 0;
1303
- try {
1304
- const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
1305
- return Uint8Array.from(binary, (c) => c.charCodeAt(0));
1306
- } catch {
1307
- return;
1308
- }
1309
- }
1310
- //#endregion
1311
- export { unbound as A, aroundUpsert as B, not as C, nodesOf as D, fieldsOf as E, sensitivity as F, ulid as G, assertNotLog as H, actionEntry as I, Scope as K, aroundCreate as L, field as M, fieldKinds as N, readsNow as O, Field as P, aroundDelete as R, count as S, storageOf as T, isRecorded as U, asActor as V, wrap as W, Total as _, columnName as a, asCondition as b, fieldOf as c, Column as d, Condition as f, Relation as g, Now as h, assertNotEncrypted as i, IMPLICIT_FIELDS as j, readsOtherRows as k, computedFieldOf as l, Expr as m, toBase64url as n, columnNode as o, Embed as p, Model as r, defineModel as s, fromBase64url as t, rowOf as u, all as v, now as w, conditionNode as x, any as y, aroundUpdate as z };
1317
+ export { mapOperands as A, aroundDelete as B, asCondition as C, now as D, not as E, unbound as F, isRecorded as G, aroundUpsert as H, fromBase64url as I, wrap as K, toBase64url as L, readsNow as M, readsOtherRows as N, storageOf as O, tableTotals as P, actionEntry as R, any as S, count as T, asActor as U, aroundUpdate as V, assertNotLog as W, Expr as _, defineModel as a, Total as b, rowOf as c, fieldKinds as d, Field as f, Embed as g, Condition as h, columnNode as i, nodesOf as j, fieldsOf as k, IMPLICIT_FIELDS as l, Column as m, assertNotEncrypted as n, fieldOf as o, sensitivity as p, Scope as q, columnName as r, computedFieldOf as s, Model as t, field as u, Now as v, conditionNode as w, all as x, Relation as y, aroundCreate as z };