@isaijs/core 0.1.5 → 0.1.8

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-BHdX68Ng.js → create-auth-BCvMwlVD.js} +18 -10
  9. package/dist/{data-layer-C5oJZ8V1.js → data-layer-CVCFjPyv.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-D0FEnMhn.d.ts → index-Bb9Px6SR.d.ts} +116 -22
  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 +169 -79
  17. package/dist/{make-ByA-84h8.js → make-jMJABOWI.js} +21 -3
  18. package/dist/{makers-P9kg68C8.js → makers-A0Pj6Tda.js} +3 -3
  19. package/dist/{migrator-B25prc9n.js → migrator-DxTpns_-.js} +174 -30
  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-BIMAiOre.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
@@ -9,6 +9,7 @@ export interface R2Binding {
9
9
  interface R2PutOptions {
10
10
  readonly httpMetadata?: {
11
11
  readonly contentType?: string;
12
+ readonly cacheControl?: string;
12
13
  };
13
14
  }
14
15
  interface R2ObjectBodyLike {
@@ -18,6 +19,8 @@ interface R2ObjectBodyLike {
18
19
  readonly contentType?: string;
19
20
  };
20
21
  }
22
+ /** What every object is served with: its key never holds anything else. */
23
+ export declare const CACHE_CONTROL = "public, max-age=31536000, immutable";
21
24
  export interface R2Options {
22
25
  /** Where the bucket serves its files (`https://media.example.com`), to make it a public storage. */
23
26
  readonly publicUrl?: string;
@@ -4,14 +4,25 @@ import { a as publicUrlOf, n as bytesOf, r as checkedStream } from "../../../sto
4
4
  * `r2`: file storage on Cloudflare R2, by binding (no S3 keys), streamed through the Worker. With a
5
5
  * `publicUrl` (the bucket's custom domain, or its r2.dev URL) it's a public storage, and the bucket
6
6
  * serves its files; without one it's private, and the app serves them through signed links.
7
+ *
8
+ * Every object is stored with `Cache-Control: public, max-age=31536000, immutable`: isai never
9
+ * writes a key twice (a file's key is its unguessable id), so browsers and Cloudflare's cache can
10
+ * keep a file for good, and a page of photos doesn't read the bucket again for each viewer (R2
11
+ * bills each read). The bucket's custom domain still needs a cache rule to cache files whose keys
12
+ * have no extension.
7
13
  */
14
+ /** What every object is served with: its key never holds anything else. */
15
+ const CACHE_CONTROL = "public, max-age=31536000, immutable";
8
16
  function r2(binding, options = {}) {
9
17
  const publicUrl = publicUrlOf(options.publicUrl);
10
18
  return {
11
19
  kind: "storage",
12
20
  ...publicUrl ? { publicUrl } : {},
13
21
  put: async (key, body, { size, contentType }) => {
14
- const httpMetadata = { contentType };
22
+ const httpMetadata = {
23
+ contentType,
24
+ cacheControl: CACHE_CONTROL
25
+ };
15
26
  if (!(body instanceof ReadableStream)) {
16
27
  await binding.put(key, await bytesOf(body, size), { httpMetadata });
17
28
  return;
@@ -32,4 +43,4 @@ function r2(binding, options = {}) {
32
43
  };
33
44
  }
34
45
  //#endregion
35
- export { r2 };
46
+ export { CACHE_CONTROL, r2 };
@@ -1,3 +1,3 @@
1
- import { r as AuthTablesOptions } from "../data-layer-43pme0Gy.js";
2
- import { A as Permissions, F as Policy, I as Rules, L as StandardAction, M as Bound, N as DefinePolicy, P as Perms, a as OAuthMethod, c as Roles, i as Methods, j as AnyPolicy, l as SignInMethod, n as AuthConfig, o as Provider, r as AuthDefinition, s as RoleDefinition, t as Auth, u as createAuth } from "../index-D0FEnMhn.js";
1
+ import { r as AuthTablesOptions } from "../data-layer-nvm_ivGQ.js";
2
+ import { B as Rules, F as AnyPolicy, I as Bound, L as DefinePolicy, P as Permissions, R as Perms, V as StandardAction, a as OAuthMethod, c as Roles, i as Methods, l as SignInMethod, n as AuthConfig, o as Provider, r as AuthDefinition, s as RoleDefinition, t as Auth, u as createAuth, z as Policy } from "../index-Bb9Px6SR.js";
3
3
  export { type AnyPolicy, Auth, type AuthConfig, type AuthDefinition, type AuthTablesOptions, type Bound, type DefinePolicy, type Methods, type OAuthMethod, type Permissions, type Perms, Policy, type Provider, type RoleDefinition, type Roles, type Rules, type SignInMethod, type StandardAction, createAuth };
@@ -1,3 +1,3 @@
1
- import { t as Policy } from "../policies-KsqmlX67.js";
2
- import { a as createAuth, t as Auth } from "../create-auth-BHdX68Ng.js";
1
+ import { t as Policy } from "../policies-C_oiSgmY.js";
2
+ import { a as createAuth, t as Auth } from "../create-auth-BCvMwlVD.js";
3
3
  export { Auth, Policy, createAuth };
package/dist/cli/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { V as toKind, z as object } from "../validation-Cppmd0RV.js";
3
- import { F as dialectFor, m as emptySnapshot, r as sync, t as migrate, y as MigrationError } from "../migrator-B25prc9n.js";
4
- import { a as lookupsOf, i as filterableOf, n as isRequired, t as Resource, u as Route } from "../resource-BQK7bkPQ.js";
5
- import { o as fieldOf, p as sensitivity, t as Model } from "../model-Dc3HunKb.js";
2
+ import { U as toKind, V as object } from "../validation-C3MCRBQO.js";
3
+ import { I as dialectFor, h as snapshotOf, m as emptySnapshot, r as sync, t as migrate, y as MigrationError } from "../migrator-DxTpns_-.js";
4
+ import { a as lookupsOf, i as filterableOf, n as isRequired, t as Resource, u as Route } from "../resource-BIMAiOre.js";
5
+ import { o as fieldOf, p as sensitivity, r as columnName, t as Model } from "../model-6pJBqqGd.js";
6
6
  import { t as kebabCase } from "../case-DpXlJCL9.js";
7
- import { i as makeMigrations, n as checkSteps, t as checkMigrations } from "../make-ByA-84h8.js";
7
+ import { i as makeMigrations, n as checkSteps, t as checkMigrations } from "../make-jMJABOWI.js";
8
8
  import { d1 } from "../adapters/database/d1/index.js";
9
9
  import { spawn } from "node:child_process";
10
10
  import { parseArgs } from "node:util";
@@ -87,8 +87,22 @@ function generateClient(source) {
87
87
  for (const { name, route } of [...source.routes].sort(byName)) {
88
88
  const output = outputs.routes[name] ?? "unknown";
89
89
  const args = routeArgs(route);
90
- members.push(` ${name}(${args}): Promise<${output === "undefined" ? "void" : output}>`);
91
- table.push(` ${name}: { route: [${JSON.stringify(route.method)}, ${JSON.stringify(route.path)}] },`);
90
+ const returns = `Promise<${output === "undefined" ? "void" : output}>`;
91
+ const files = route.input === "own" ? [] : Object.entries(route.input).filter(([, shape]) => toKind(shape).spec.file);
92
+ const uploads = files.map(([key]) => key);
93
+ const images = files.filter(([, shape]) => toKind(shape).spec.file?.image).map(([key]) => key);
94
+ if (uploads.length === 0) members.push(` ${name}(${args}): ${returns}`);
95
+ else {
96
+ used.add("FileJSON");
97
+ const fields = uploads.map((key) => JSON.stringify(key)).join(" | ");
98
+ members.push(` ${name}: {\n (${args}): ${returns}\n upload(field: ${fields}, file: Blob): Promise<FileJSON>\n }`);
99
+ }
100
+ const endpoint = [
101
+ `route: [${JSON.stringify(route.method)}, ${JSON.stringify(route.path)}]`,
102
+ ...uploads.length > 0 ? [`uploads: ${JSON.stringify(uploads)}`] : [],
103
+ ...images.length > 0 ? [`images: ${JSON.stringify(images)}`] : []
104
+ ];
105
+ table.push(` ${name}: { ${endpoint.join(", ")} },`);
92
106
  }
93
107
  const body = [
94
108
  ...types,
@@ -550,7 +564,8 @@ const CLIENT_FILE = ".isai/client.ts";
550
564
  function appModules(root) {
551
565
  return globSync("apps/**/*.ts", { cwd: root }).filter((file) => !/\.(test|d)\.ts$/.test(file)).sort().map((file) => join(root, file));
552
566
  }
553
- async function generate$1(project) {
567
+ /** The app modules' resources and routes, loaded as the app loads them, and which file has which. */
568
+ async function appExports(project) {
554
569
  const resources = [];
555
570
  const routes = [];
556
571
  const modules = [];
@@ -581,6 +596,14 @@ async function generate$1(project) {
581
596
  }
582
597
  if (found.routes.length + found.resources.length > 0) modules.push(found);
583
598
  }
599
+ return {
600
+ resources,
601
+ routes,
602
+ modules
603
+ };
604
+ }
605
+ async function generate$1(project) {
606
+ const { resources, routes, modules } = await appExports(project);
584
607
  const user = (await loadModels(project)).find((found) => found.model.spec.name === "User")?.model;
585
608
  return {
586
609
  text: generateClient({
@@ -663,6 +686,42 @@ function watchClient(root, regenerate, settleMs = 200) {
663
686
  watcher.close();
664
687
  };
665
688
  }
689
+ //#endregion
690
+ //#region src/cli/indexes.ts
691
+ /**
692
+ * `isai check`'s warning about lists without an index: a resource that sorts by a field no index
693
+ * leads with makes the database read the whole table, and sort it, for every page of the list
694
+ * (and for every refresh of a screen that keeps it open). A warning, never a failure: a small
695
+ * table never needs one, and only the app knows which tables grow. The fix is on the model:
696
+ * `.index(...)` (`db/schema/model.ts`).
697
+ */
698
+ /** One line per resource field sorted without an index, naming the `.index()` to add. */
699
+ function unindexedSorts(resources, models) {
700
+ const snapshots = /* @__PURE__ */ new Map();
701
+ const snapshotFor = (database) => {
702
+ let snapshot = snapshots.get(database);
703
+ if (!snapshot) {
704
+ snapshot = snapshotOf(models.filter((m) => m.database === database).map((m) => m.model));
705
+ snapshots.set(database, snapshot);
706
+ }
707
+ return snapshot;
708
+ };
709
+ const warnings = [];
710
+ for (const { name, resource } of resources) {
711
+ const spec = resource.model.spec;
712
+ const found = models.find((m) => m.model.spec.name === spec.name);
713
+ if (!found) continue;
714
+ const indexes = snapshotFor(found.database).tables[spec.table]?.indexes ?? [];
715
+ for (const field of resource.sortable) {
716
+ const fieldSpec = spec.fields[field];
717
+ if (!fieldSpec || fieldSpec.computed) continue;
718
+ const column = columnName(field, fieldSpec);
719
+ if (column === "id" || indexes.some((index) => index.columns[0] === column)) continue;
720
+ warnings.push(`${name} sorts by ${spec.name}.${field}, which no index leads with: each page reads the whole table. Add .index("${field}") to ${spec.name}${found.file ? ` (db/schema/${found.file}.ts)` : ""}`);
721
+ }
722
+ }
723
+ return warnings;
724
+ }
666
725
  const PLATFORM = "platform";
667
726
  /** One history per database: `migrations/<database>/` (`migrations/tenant/`, `migrations/main/`). */
668
727
  const migrationsDir = (root, database) => join(root, "migrations", database);
@@ -1109,11 +1168,13 @@ const commands = {
1109
1168
  await writeClient(project, log);
1110
1169
  return 0;
1111
1170
  },
1112
- /** Fails when something generated is out of date with the app (CI). */
1171
+ /** Fails when something generated is out of date with the app (CI); warns about lists without an index. */
1113
1172
  async check(project) {
1114
1173
  await checkClient(project);
1115
1174
  checkDeployable(project.root);
1116
- log("No problems");
1175
+ const warnings = await indexWarnings(project);
1176
+ for (const warning of warnings) log(`⚠ ${warning}`);
1177
+ log(warnings.length === 0 ? "No problems" : `No problems; ${warnings.length} warning${warnings.length === 1 ? "" : "s"}`);
1117
1178
  return 0;
1118
1179
  },
1119
1180
  /**
@@ -1140,6 +1201,7 @@ const commands = {
1140
1201
  await writeClient(project, log).catch((error) => {
1141
1202
  log(`\n⚠ The typed client wasn't written: ${error instanceof Error ? error.message : error}\n`);
1142
1203
  });
1204
+ for (const warning of await indexWarnings(project).catch(() => [])) log(`⚠ ${warning}`);
1143
1205
  const token = crypto.randomUUID();
1144
1206
  const served = wranglerDev(project.root, [
1145
1207
  ...wrangler,
@@ -1160,6 +1222,11 @@ const commands = {
1160
1222
  return served;
1161
1223
  }
1162
1224
  };
1225
+ /** Resources sorted by a field no index leads with (`./indexes`). */
1226
+ async function indexWarnings(project) {
1227
+ const { resources } = await appExports(project);
1228
+ return unindexedSorts(resources, await loadModels(project));
1229
+ }
1163
1230
  /**
1164
1231
  * `isai client` in a new process: this one has the app's modules cached as they were on start, and
1165
1232
  * ESM can't unload them. A failure is printed by the child; the server keeps running.
@@ -1196,7 +1263,8 @@ const usage = `usage: isai <command>
1196
1263
  makemigrations [--check] write migration steps for model changes (--check: fail if one is missing)
1197
1264
  migrate [--remote] apply pending migrations to the local databases (--remote: the deployed ones)
1198
1265
  client write .isai/client.ts, the typed client for the app's frontends
1199
- check fail if .isai/client.ts is stale, or a D1 database has no database_id
1266
+ check fail if .isai/client.ts is stale, or a D1 database has no database_id;
1267
+ warn when a resource sorts by a field no index leads with
1200
1268
  dev [--seed <name>] [wrangler args]
1201
1269
  migrate, then run wrangler dev, rewriting the typed client on each change
1202
1270
  (--seed: fill the local databases from a seed)`;
@@ -165,6 +165,10 @@ interface ResourceEndpoint {
165
165
  /** A route in the endpoint table. */
166
166
  interface RouteEndpoint {
167
167
  readonly route: readonly [Method, string];
168
+ /** Its file inputs: `upload(field, file)` sends a file to name by id in its input. */
169
+ readonly uploads?: readonly string[];
170
+ /** Those of them that are images: resized in the browser before they're sent. */
171
+ readonly images?: readonly string[];
168
172
  }
169
173
  type Endpoints = Readonly<Record<string, ResourceEndpoint | RouteEndpoint>>;
170
174
  type FetchLike = (input: Request | string | URL, init?: RequestInit) => Promise<Response>;
@@ -159,6 +159,7 @@ async function encoded(canvas, name, clear) {
159
159
  * · <custom action>(key, input?) · upload(field, file) (an image is resized first)
160
160
  * (a singleton's calls take no key)
161
161
  * routes api.<name>(args) (path params and input in one object)
162
+ * · api.<name>.upload(field, file) (a route with file inputs)
162
163
  * auth api.auth.me() · signIn() · signOut() · users · invites
163
164
  *
164
165
  * Writes survive a dropping connection. Every write (any call but a GET) carries an
@@ -237,7 +238,7 @@ function createClient(endpoints, options = {}) {
237
238
  };
238
239
  for (const [name, endpoint] of Object.entries(endpoints)) {
239
240
  if (Object.hasOwn(api, name)) throw new TypeError(`isai: the client's ${name} is taken`);
240
- api[name] = "route" in endpoint ? routeCall(call, endpoint) : resourceCalls(call, endpoint);
241
+ api[name] = "route" in endpoint ? routeCall(call, endpoint, name) : resourceCalls(call, endpoint);
241
242
  }
242
243
  return api;
243
244
  }
@@ -350,18 +351,20 @@ function resourceCalls(call, endpoint) {
350
351
  default: calls[action] = actionCall(call, keyed, action);
351
352
  }
352
353
  for (const action of endpoint.custom ?? []) calls[action] = actionCall(call, keyed, action);
353
- if (endpoint.uploads?.length) {
354
- const fields = new Set(endpoint.uploads);
355
- const images = new Set(endpoint.images);
356
- calls.upload = (field, file) => {
357
- if (!fields.has(field)) throw new TypeError(`isai: ${base} takes no upload for ${field}`);
358
- const path = `/uploads${base}/${kebabCase(field)}`;
359
- if (!images.has(field)) return call("POST", path, { body: file });
360
- return uploadImage(call, path, file);
361
- };
362
- }
354
+ if (endpoint.uploads?.length) calls.upload = uploadCall(call, base, endpoint);
363
355
  return calls;
364
356
  }
357
+ /** `upload(field, file)` to `/uploads<base>/<field>`: an image field's file is resized first. */
358
+ function uploadCall(call, base, endpoint) {
359
+ const fields = new Set(endpoint.uploads);
360
+ const images = new Set(endpoint.images);
361
+ return (field, file) => {
362
+ if (!fields.has(field)) throw new TypeError(`isai: ${base} takes no upload for ${field}`);
363
+ const path = `/uploads${base}/${kebabCase(field)}`;
364
+ if (!images.has(field)) return call("POST", path, { body: file });
365
+ return uploadImage(call, path, file);
366
+ };
367
+ }
365
368
  /** An image, resized: the full one, then its smaller copy with `X-Size-Of`; the answer is the image. */
366
369
  async function uploadImage(call, path, file) {
367
370
  const sizes = await resized(file, file.name ?? "image");
@@ -377,9 +380,9 @@ function actionCall(call, keyed, action) {
377
380
  return keyed((path, input) => call("POST", `${path}/${kebabCase(action)}`, input === void 0 ? {} : { body: input }));
378
381
  }
379
382
  /** A route: `:params` filled from the argument, the rest its input (the body, or the query string on GET / DELETE). */
380
- function routeCall(call, endpoint) {
383
+ function routeCall(call, endpoint, name) {
381
384
  const [method, template] = endpoint.route;
382
- return (args = {}) => {
385
+ const run = (args = {}) => {
383
386
  const input = { ...args };
384
387
  const path = template.replace(/:([A-Za-z0-9_]+)/g, (_, name) => {
385
388
  const value = input[name];
@@ -392,6 +395,8 @@ function routeCall(call, endpoint) {
392
395
  for (const [key, value] of Object.entries(input)) if (value !== void 0) query.set(key, typeof value === "string" ? value : JSON.stringify(value));
393
396
  return call(method, path, { query });
394
397
  };
398
+ if (endpoint.uploads?.length) return Object.assign(run, { upload: uploadCall(call, `/${kebabCase(name)}`, endpoint) });
399
+ return run;
395
400
  }
396
401
  /** A value in a list's query string: text as it is, a list comma-separated. */
397
402
  function queryValue(value) {
@@ -1,6 +1,6 @@
1
- import { I as fromBase64url } from "./model-Dc3HunKb.js";
2
- import { n as PROVIDERS } from "./tables-DgSwge7s.js";
3
- import { a as byRole, c as tenant, i as anyone, l as user, n as PolicyRegistry, o as owner, s as readsTenant } from "./policies-KsqmlX67.js";
1
+ import { I as fromBase64url } from "./model-6pJBqqGd.js";
2
+ import { n as PROVIDERS } from "./tables-DjW96Ubl.js";
3
+ import { a as byRole, c as tenant, i as anyone, l as user, n as PolicyRegistry, o as owner, s as readsTenant } from "./policies-C_oiSgmY.js";
4
4
  //#region src/auth/create-auth.ts
5
5
  const methods = { oauth: (provider, options) => ({
6
6
  kind: "oauth",
@@ -23,8 +23,8 @@ var Auth = class {
23
23
  perms = authPerms;
24
24
  /** The policies made with this instance's `policy()`. */
25
25
  policies = new PolicyRegistry();
26
- /** Set by the app's core when it starts: `invite` on its current database. */
27
- invites;
26
+ /** Set by the app's core when it starts: `invite` and `revoke` on its current database. */
27
+ people;
28
28
  constructor(define) {
29
29
  this.define = define;
30
30
  }
@@ -34,12 +34,20 @@ var Auth = class {
34
34
  * `held: "platform"` puts the assignment beyond the tenant's reach.
35
35
  */
36
36
  invite(input) {
37
- if (!this.invites) return Promise.reject(/* @__PURE__ */ new Error("isai: auth.invite is used before the runtime started the app"));
38
- return this.invites(input);
37
+ if (!this.people) return Promise.reject(/* @__PURE__ */ new Error("isai: auth.invite is used before the runtime started the app"));
38
+ return this.people.invite(input);
39
39
  }
40
- /** The core's: where `invite` writes. */
41
- attach(invites) {
42
- this.invites = invites;
40
+ /**
41
+ * Takes a role from `email`, from code (replacing a tenant's owner): its invite to the role is
42
+ * deleted, and the role taken away if it has an account in the current database. Safe to repeat.
43
+ */
44
+ revoke(input) {
45
+ if (!this.people) return Promise.reject(/* @__PURE__ */ new Error("isai: auth.revoke is used before the runtime started the app"));
46
+ return this.people.revoke(input);
47
+ }
48
+ /** The core's: where `invite` and `revoke` write. */
49
+ attach(people) {
50
+ this.people = people;
43
51
  }
44
52
  /** The config, for this env. Checked here for itself; against the databases by the core. */
45
53
  resolve(env) {
@@ -1,7 +1,7 @@
1
- import { F as quantity, k as datetime, m as host, r as isPhoneRegion } from "./validation-Cppmd0RV.js";
2
- import "./migrator-B25prc9n.js";
3
- import { D as now, E as not, S as any, T as count, a as defineModel, c as rowOf, d as fieldKinds, u as field, x as all, y as Relation } from "./model-Dc3HunKb.js";
4
- import { a as TableSet, i as platformAuthDefinitions, o as defineBuiltins, r as authTables } from "./tables-DgSwge7s.js";
1
+ import { L as quantity, g as host, j as datetime, r as isPhoneRegion } from "./validation-C3MCRBQO.js";
2
+ import "./migrator-DxTpns_-.js";
3
+ import { D as now, E as not, S as any, T as count, a as defineModel, c as rowOf, d as fieldKinds, u as field, x as all, y as Relation } from "./model-6pJBqqGd.js";
4
+ import { a as TableSet, i as platformAuthDefinitions, o as defineBuiltins, r as authTables } from "./tables-DjW96Ubl.js";
5
5
  import { n as seed } from "./seed-BObmdKdP.js";
6
6
  //#region src/db/fields/slot.ts
7
7
  var Slot = class {
@@ -77,6 +77,8 @@ function uploadTables(f, options) {
77
77
  fileId: f.line().unique(),
78
78
  /** The file as stored, written into the field that attaches it. */
79
79
  file: f.file(),
80
+ /** Bytes stored for it, its smaller copies' too: what a cap counts as pending. */
81
+ size: f.quantity().default(0),
80
82
  /** Where it may be attached: `<resource or route>/<field>` (`manage-products/images`). */
81
83
  target: f.line(),
82
84
  /** Who uploaded it (a user's id); empty for an anonymous upload. */
@@ -173,8 +175,8 @@ var ModelRegistry = class {
173
175
  /**
174
176
  * Traits: behaviour a model takes whole, written before its fields (`model(name, ...traits,
175
177
  * fields)`), Laravel's traits typed. A trait adds fields (so the model's own fields already see
176
- * them; declaring one of them again is an error), computed fields, and a name the framework reads
177
- * (`singleton`). Always written `traits.x()`, never imported bare: a bare `publishable()` reads like
178
+ * them; declaring one of them again is an error), computed fields, the indexes its behaviour reads
179
+ * by, and a name the framework reads (`singleton`). Always written `traits.x()`, never imported bare: a bare `publishable()` reads like
178
180
  * an action.
179
181
  */
180
182
  var Trait = class {
@@ -188,10 +190,11 @@ var Trait = class {
188
190
  *
189
191
  * - `ordered({ within })`: a `position` the database keeps: a new row goes last in its group (the
190
192
  * rows sharing `within`, or the whole table), reads are in position order unless they name
191
- * their own, and `db.reorder(Model, ids)` sets a group's order.
193
+ * their own, and `db.reorder(Model, ids)` sets a group's order. Indexed by its group, then
194
+ * `position` (which also serves the group's ref).
192
195
  * - `singleton()`: one row, with the id `"singleton"`; read it with `findMany(Model).first()`.
193
196
  * - `publishable()`: `publishedAt` (empty: a draft; in the future: scheduled) and `published`,
194
- * true once it has passed.
197
+ * true once it has passed. Indexed by `publishedAt`, for lists of the newest.
195
198
  */
196
199
  function builtinTraits() {
197
200
  return {
@@ -201,7 +204,8 @@ function builtinTraits() {
201
204
  by: "position",
202
205
  within: options.within
203
206
  } }) },
204
- computed: {}
207
+ computed: {},
208
+ indexes: [options.within === void 0 ? ["position"] : [options.within, "position"]]
205
209
  }),
206
210
  singleton: () => new Trait({
207
211
  name: "singleton",
@@ -212,7 +216,8 @@ function builtinTraits() {
212
216
  return new Trait({
213
217
  name: "publishable",
214
218
  fields: { publishedAt: field(datetime(), "datetime").optional() },
215
- computed: { published: (row) => all(row.publishedAt.isNull().not(), row.publishedAt.lte(now)) }
219
+ computed: { published: (row) => all(row.publishedAt.isNull().not(), row.publishedAt.lte(now)) },
220
+ indexes: [["publishedAt"]]
216
221
  });
217
222
  }
218
223
  };
@@ -1,13 +1,15 @@
1
1
  import { a as DatabaseDriver, o as DialectName } from "./index-Ce5p2-EB.js";
2
2
  import { t as Issue } from "./issues-CKto95IW.js";
3
- import { M as IdInput, Y as StoredFile } from "./index-CYcklQ1l.js";
4
- import { $ as Expr, A as Dialect, G as Model, H as Columns, I as Clock, K as ModelSpec, M as RetiredSnapshot, N as SchemaSnapshot, U as ComputedFields, V as AnyModel, W as Fields, X as Condition, at as count, b as RowOf, dt as Kinds, et as Now, f as seed, ft as SchemaDefaults, ht as Field, i as Row, it as any, mt as AnyField, n as Patch, ot as not, p as Database, q as ModelRegistry, rt as all, yt as Slot } from "./types-BgOkJy5d.js";
3
+ import { M as IdInput, Y as StoredFile } from "./index-CaR3hb0i.js";
4
+ import { $ as Expr, A as Dialect, G as Model, H as Columns, I as Clock, K as ModelSpec, M as RetiredSnapshot, N as SchemaSnapshot, U as ComputedFields, V as AnyModel, W as Fields, X as Condition, at as count, b as RowOf, dt as Kinds, et as Now, f as seed, ft as SchemaDefaults, ht as Field, i as Row, it as any, mt as AnyField, n as Patch, ot as not, p as Database, q as ModelRegistry, rt as all, yt as Slot } from "./types-D5SIVeej.js";
5
5
  //#region src/db/schema/traits.d.ts
6
6
  type ComputedDefinitions = Record<string, (row: any) => Condition>;
7
7
  interface TraitSpec {
8
8
  readonly name: string;
9
9
  readonly fields: Fields;
10
10
  readonly computed: ComputedDefinitions;
11
+ /** Indexes the model gets, by field names: what the trait's own reads sort or group by. */
12
+ readonly indexes?: readonly (readonly string[])[];
11
13
  }
12
14
  declare class Trait<F extends Fields = Fields, C extends ComputedDefinitions = ComputedDefinitions> {
13
15
  readonly spec: TraitSpec;
@@ -741,6 +743,8 @@ declare function uploadTables(f: Kinds, options: UploadTablesOptions): TableSet<
741
743
  }>;
742
744
  /** The file as stored, written into the field that attaches it. */
743
745
  file: Field<StoredFile, StoredFile, false, false, {}>;
746
+ /** Bytes stored for it, its smaller copies' too: what a cap counts as pending. */
747
+ size: Field<number, number, false, true, {}>;
744
748
  /** Where it may be attached: `<resource or route>/<field>` (`manage-products/images`). */
745
749
  target: Field<string, string, false, false, {}>;
746
750
  /** Who uploaded it (a user's id); empty for an anonymous upload. */
@@ -1,4 +1,4 @@
1
1
  import { a as ValidationError } from "../issues-CKto95IW.js";
2
- import { $ as Expr, A as Dialect, B as newSecretKey, F as snapshotOf, G as Model, I as Clock, J as Models, K as ModelSpec, L as FixedClock, M as RetiredSnapshot, N as SchemaSnapshot, O as Write, P as emptySnapshot, R as fixedClock, V as AnyModel, X as Condition, Y as Column, _ as database, _t as Storage, a as Visible, at as count, bt as slot, c as Many, d as SeedContext, dt as Kinds, et as Now, ft as SchemaDefaults, g as Transaction, gt as FieldSpec, h as RawValue, ht as Field, i as Row, it as any, j as Rule, k as WriteListener, l as Overrides, lt as ConditionNode, m as DatabaseOptions, n as Patch, nt as Total, o as Make, ot as not, p as Database, pt as fieldKinds, r as Planned, rt as all, st as now, t as Insert, tt as Relation, u as Seed, v as OrderKey, vt as sensitivity, y as Query, yt as Slot, z as systemClock } from "../types-BgOkJy5d.js";
3
- import { A as MigrateOptions, B as NotFound, C as MigrationErrorCode, D as Cleanup, E as UnsafeMigration, F as DataStep, H as UniqueFailed, I as TODO, L as dataStep, M as migrate, N as migrateFresh, O as cleanupMigration, P as sync, R as CheckFailed, S as MigrationError, T as UnsafeChange, U as dialectFor, V as ReferenceFailed, W as sqlite, _ as Generated, b as manifest, c as acquireLock, d as setReleaseStatus, f as startRelease, g as makeMigrations, h as makeCleanup, j as Migration, k as Manifest, l as releaseLock, m as checkMigrations, n as row, o as Release, p as applyRules, s as ReleaseStatus, t as createDataLayer, u as rollback, v as generateMigration, w as SuspectedRename, x as structureStep, y as cleanupStep, z as DatabaseError } from "../data-layer-43pme0Gy.js";
2
+ import { $ as Expr, A as Dialect, B as newSecretKey, F as snapshotOf, G as Model, I as Clock, J as Models, K as ModelSpec, L as FixedClock, M as RetiredSnapshot, N as SchemaSnapshot, O as Write, P as emptySnapshot, R as fixedClock, V as AnyModel, X as Condition, Y as Column, _ as database, _t as Storage, a as Visible, at as count, bt as slot, c as Many, d as SeedContext, dt as Kinds, et as Now, ft as SchemaDefaults, g as Transaction, gt as FieldSpec, h as RawValue, ht as Field, i as Row, it as any, j as Rule, k as WriteListener, l as Overrides, lt as ConditionNode, m as DatabaseOptions, n as Patch, nt as Total, o as Make, ot as not, p as Database, pt as fieldKinds, r as Planned, rt as all, st as now, t as Insert, tt as Relation, u as Seed, v as OrderKey, vt as sensitivity, y as Query, yt as Slot, z as systemClock } from "../types-D5SIVeej.js";
3
+ import { A as MigrateOptions, B as NotFound, C as MigrationErrorCode, D as Cleanup, E as UnsafeMigration, F as DataStep, H as UniqueFailed, I as TODO, L as dataStep, M as migrate, N as migrateFresh, O as cleanupMigration, P as sync, R as CheckFailed, S as MigrationError, T as UnsafeChange, U as dialectFor, V as ReferenceFailed, W as sqlite, _ as Generated, b as manifest, c as acquireLock, d as setReleaseStatus, f as startRelease, g as makeMigrations, h as makeCleanup, j as Migration, k as Manifest, l as releaseLock, m as checkMigrations, n as row, o as Release, p as applyRules, s as ReleaseStatus, t as createDataLayer, u as rollback, v as generateMigration, w as SuspectedRename, x as structureStep, y as cleanupStep, z as DatabaseError } from "../data-layer-nvm_ivGQ.js";
4
4
  export { type AnyModel, CheckFailed, type Cleanup, type Clock, Column, Condition, type ConditionNode, type DataStep, Database, DatabaseError, type DatabaseOptions, type Dialect, Expr, Field, type FieldSpec, type FixedClock, type Generated, type Insert, type Kinds, type Make, type Manifest, type Many, type MigrateOptions, type Migration, MigrationError, type MigrationErrorCode, Model, type ModelSpec, type Models, NotFound, Now, type OrderKey, type Overrides, type Patch, type Planned, Query, type RawValue, ReferenceFailed, Relation, type Release, type ReleaseStatus, type RetiredSnapshot, type Row, type Rule, type SchemaDefaults, type SchemaSnapshot, Seed, type SeedContext, Slot, type Storage, type SuspectedRename, TODO, Total, Transaction, UniqueFailed, type UnsafeChange, UnsafeMigration, ValidationError, type Visible, type Write, type WriteListener, acquireLock, all, any, applyRules, checkMigrations, cleanupMigration, cleanupStep, count, createDataLayer, dataStep, database, dialectFor, emptySnapshot, fieldKinds, fixedClock, generateMigration, makeCleanup, makeMigrations, manifest, migrate, migrateFresh, newSecretKey, not, now, releaseLock, rollback, row, sensitivity, setReleaseStatus, slot, snapshotOf, sqlite, startRelease, structureStep, sync, systemClock };
package/dist/db/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { G as ValidationError } from "../validation-Cppmd0RV.js";
2
- import { C as database, F as dialectFor, G as newSecretKey, H as fixedClock, I as sqlite, J as NotFound, K as CheckFailed, M as Query, U as systemClock, X as UniqueFailed, Y as ReferenceFailed, _ as TODO, b as Database, c as rollback, d as applyRules, g as UnsafeMigration, h as snapshotOf, i as acquireLock, l as setReleaseStatus, m as emptySnapshot, n as migrateFresh, p as generateMigration, q as DatabaseError, r as sync, s as releaseLock, t as migrate, u as startRelease, v as dataStep, x as Transaction, y as MigrationError } from "../migrator-B25prc9n.js";
3
- import { D as now, E as not, S as any, T as count, _ as Expr, b as Total, d as fieldKinds, f as Field, h as Condition, m as Column, p as sensitivity, t as Model, v as Now, x as all, y as Relation } from "../model-Dc3HunKb.js";
4
- import { i as slot, n as row, r as Slot, t as createDataLayer } from "../data-layer-C5oJZ8V1.js";
1
+ import { q as ValidationError } from "../validation-C3MCRBQO.js";
2
+ import { C as database, I as dialectFor, J as DatabaseError, K as newSecretKey, L as sqlite, M as Query, U as fixedClock, W as systemClock, X as ReferenceFailed, Y as NotFound, Z as UniqueFailed, _ as TODO, b as Database, c as rollback, d as applyRules, g as UnsafeMigration, h as snapshotOf, i as acquireLock, l as setReleaseStatus, m as emptySnapshot, n as migrateFresh, p as generateMigration, q as CheckFailed, r as sync, s as releaseLock, t as migrate, u as startRelease, v as dataStep, x as Transaction, y as MigrationError } from "../migrator-DxTpns_-.js";
3
+ import { D as now, E as not, S as any, T as count, _ as Expr, b as Total, d as fieldKinds, f as Field, h as Condition, m as Column, p as sensitivity, t as Model, v as Now, x as all, y as Relation } from "../model-6pJBqqGd.js";
4
+ import { i as slot, n as row, r as Slot, t as createDataLayer } from "../data-layer-CVCFjPyv.js";
5
5
  import { t as Seed } from "../seed-BObmdKdP.js";
6
- import { a as cleanupMigration, i as makeMigrations, r as makeCleanup, t as checkMigrations } from "../make-ByA-84h8.js";
6
+ import { a as cleanupMigration, i as makeMigrations, r as makeCleanup, t as checkMigrations } from "../make-jMJABOWI.js";
7
7
  //#region src/db/migrations/files.ts
8
8
  /** A structure step file: adds tables and columns. Applied once, never edited. */
9
9
  function structureStep(options) {