@isaijs/core 0.1.1 → 0.1.3

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,3 +1,3 @@
1
- import { r as AuthTablesOptions } from "../data-layer-ubPfZvFV.js";
2
- import { A as Permissions, F as DefinePolicy, I as Perms, L as Policy, N as AnyPolicy, P as Bound, R as Rules, 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 StandardAction } from "../index-B6ipokoP.js";
1
+ import { r as AuthTablesOptions } from "../data-layer-D1BFo3wO.js";
2
+ import { A as Permissions, F as DefinePolicy, I as Perms, L as Policy, N as AnyPolicy, P as Bound, R as Rules, 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 StandardAction } from "../index-DlgVg3D8.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-pFzslmSB.js";
2
- import { a as createAuth, t as Auth } from "../create-auth-y8E8cxkn.js";
1
+ import { t as Policy } from "../policies-CKIdBFKt.js";
2
+ import { a as createAuth, t as Auth } from "../create-auth-Ci0PaBoX.js";
3
3
  export { Auth, Policy, createAuth };
@@ -1,4 +1,4 @@
1
- import { A as flag, C as textKind, D as date, E as choice, F as sequence, L as list, M as int, N as money, O as datetime, P as quantity, S as text, T as url, U as kind, _ as personName, b as slugify, c as images, d as urls, f as email, g as newToken, h as markdown, i as file, j as id, k as fakeUlid, l as define, m as line, n as phoneKind, o as files, s as image, u as json, v as secret, w as token, x as subdomain, y as slug } from "./validation-FyOpSZqE.js";
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
2
  import { n as snakeCase } from "./case-BnpcvhdW.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  //#region src/internal/scope.ts
package/dist/cli/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { B as toKind, R as object } from "../validation-FyOpSZqE.js";
3
- import { M as dialectFor, m as emptySnapshot, r as sync, t as migrate, y as MigrationError } from "../migrator-6oWUefLA.js";
4
- import { a as lookupsOf, i as filterableOf, n as isRequired, t as Resource, u as Route } from "../resource-C17NvNa3.js";
5
- import { F as sensitivity, c as fieldOf, r as Model } from "../base64url-K_wCmvxR.js";
2
+ import { V as toKind, z as object } from "../validation-Cppmd0RV.js";
3
+ import { M as dialectFor, m as emptySnapshot, r as sync, t as migrate, y as MigrationError } from "../migrator-BU3_PHk8.js";
4
+ import { a as lookupsOf, i as filterableOf, n as isRequired, t as Resource, u as Route } from "../resource-CsYksjKc.js";
5
+ import { F as sensitivity, c as fieldOf, r as Model } from "../base64url-C5GTYIL5.js";
6
6
  import { t as kebabCase } from "../case-BnpcvhdW.js";
7
- import { i as makeMigrations, n as checkSteps, t as checkMigrations } from "../make-WQpkYW8N.js";
7
+ import { i as makeMigrations, n as checkSteps, t as checkMigrations } from "../make-DKBn79cH.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";
@@ -198,13 +198,15 @@ function resourceMembers(named, custom, used) {
198
198
  }
199
199
  const spec = resource.model.spec;
200
200
  const uploads = resource.writable.filter((field) => spec.fields[field]?.kind.spec.file);
201
+ const images = uploads.filter((field) => spec.fields[field]?.kind.spec.file?.image);
201
202
  if (uploads.length > 0) calls.push(`upload(field: ${uploads.map((f) => JSON.stringify(f)).join(" | ")}, file: Blob): Promise<FileJSON>`);
202
203
  const endpoint = [
203
204
  `resource: ${JSON.stringify(`/${kebabCase(name)}`)}`,
204
205
  ...resource.singleton ? ["singleton: true"] : [],
205
206
  `actions: ${JSON.stringify(resource.actions)}`,
206
207
  ...Object.keys(resource.custom).length > 0 ? [`custom: ${JSON.stringify(Object.keys(resource.custom))}`] : [],
207
- ...uploads.length > 0 ? [`uploads: ${JSON.stringify(uploads)}`] : []
208
+ ...uploads.length > 0 ? [`uploads: ${JSON.stringify(uploads)}`] : [],
209
+ ...images.length > 0 ? [`images: ${JSON.stringify(images)}`] : []
208
210
  ];
209
211
  used.add("Page");
210
212
  return {
@@ -350,8 +352,9 @@ async function loadModels(project) {
350
352
  const name = value.spec.name;
351
353
  const other = found.get(name);
352
354
  if (other && other.model !== value) throw new Error(`isai: two models are named ${name} (db/schema/${other.file}.ts and ${file})`);
353
- const builtin = value.spec.database;
354
- if (builtin !== void 0 && builtin !== database) throw new Error(`isai: ${name} is a built-in table of the ${builtin} database, so it's extended in ${folderOf(builtin)}/ (not db/schema/${file})`);
355
+ const said = value.spec.database;
356
+ if (said !== void 0 && said !== database) throw new Error(value.spec.builtin !== void 0 ? `isai: ${name} is a built-in table of the ${said} database, so it's extended in ${folderOf(said)}/ (not db/schema/${file})` : `isai: ${name} says { database: "${said}" }, so it goes in ${folderOf(said)}/ (not db/schema/${file})`);
357
+ if (said === void 0 && database === "platform") throw new Error(`isai: ${name} is in db/schema/${file}, so it says so: model("${name}", { database: "platform" }, …)`);
355
358
  found.set(name, {
356
359
  model: value,
357
360
  file: file.slice(0, -3),
@@ -82,6 +82,12 @@ interface FileJSON {
82
82
  readonly type: string;
83
83
  readonly width?: number;
84
84
  readonly height?: number;
85
+ /** An image's smaller copies, widest first: `srcset(file)` lists them for an `<img>`. */
86
+ readonly sizes?: readonly {
87
+ readonly width: number;
88
+ readonly height: number;
89
+ readonly url: string;
90
+ }[];
85
91
  }
86
92
  /** `GET /api/auth/me`: the user, their roles, and their permission keys (to show or hide what the UI offers). */
87
93
  interface Me<U> {
@@ -153,6 +159,8 @@ interface ResourceEndpoint {
153
159
  readonly custom?: readonly string[];
154
160
  /** Its writable file fields: `upload(field, file)` sends a file to attach by id. */
155
161
  readonly uploads?: readonly string[];
162
+ /** Those of them that are images: resized in the browser before they're sent (`./resize`). */
163
+ readonly images?: readonly string[];
156
164
  }
157
165
  /** A route in the endpoint table. */
158
166
  interface RouteEndpoint {
@@ -205,4 +213,37 @@ export declare class ApiError extends Error {
205
213
  get fields(): Readonly<Record<string, string>>;
206
214
  }
207
215
  //#endregion
216
+ //#region src/client/files.d.ts
217
+ /**
218
+ * An image's `srcset`: the image and its smaller copies, each with its width, so the browser loads
219
+ * the one the screen needs (`<img src={file.url} srcset={srcset(file)} sizes="50vw">`). Undefined
220
+ * for a file without copies: its `url` is its only size.
221
+ */
222
+ export declare function srcset(file: Pick<FileJSON, "url" | "width" | "sizes">): string | undefined;
223
+ /** The smallest copy at least `width` wide (or the widest there is), else the file itself: a preview's image. */
224
+ export declare function sizeFor(file: Pick<FileJSON, "url" | "width" | "height" | "sizes">, width: number): {
225
+ readonly url: string;
226
+ readonly width?: number | undefined;
227
+ readonly height?: number | undefined;
228
+ };
229
+ //#endregion
230
+ //#region src/client/resize.d.ts
231
+ /**
232
+ * Images are made smaller in the browser before they're uploaded: the full image at most 1080 px
233
+ * wide, and a copy at most 540 px wide (a list's card, a thumbnail, a link's preview), both kept
234
+ * when the image is larger than that; never made larger. Phones are the screens that matter: 1080
235
+ * is sharp across one at 3×, and the original (often 4000 px and several MB) isn't kept.
236
+ *
237
+ * Each is encoded again, which also drops the photo's metadata (a phone's GPS position): WebP where
238
+ * the browser can encode it, else JPEG; PNG only for an image with transparent pixels, which JPEG
239
+ * can't keep. No library: the browser decodes (turned the way the camera says), draws and encodes.
240
+ * A large step down is drawn in halves, which keeps it sharp. Where it can't (no `createImageBitmap`,
241
+ * or a format the browser can't decode), the file is uploaded as it is.
242
+ */
243
+ /** The widths an image is kept at, at most. */
244
+ export declare const IMAGE_WIDTHS: {
245
+ readonly full: 1080;
246
+ readonly small: 540;
247
+ };
248
+ //#endregion
208
249
  export type { AuthApi, ChoiceFilter, ClientBase, ClientOptions, Endpoints, FetchLike, FileJSON, FlagFilter, HistoryEntry, HistoryQuery, Invitation, InviteJSON, ListQuery, Me, Page, Person, RangeFilter, ResourceEndpoint, RouteEndpoint, SignInMethod, TextFilter };
@@ -33,6 +33,121 @@ async function errorOf(response) {
33
33
  return new ApiError(response.status, typeof code === "string" ? code : "HttpError", typeof message === "string" ? message : `The request failed (${response.status})`, Array.isArray(issues) ? issues : [], details, typeof body.requestId === "string" ? body.requestId : void 0);
34
34
  }
35
35
  //#endregion
36
+ //#region src/client/resize.ts
37
+ /**
38
+ * Images are made smaller in the browser before they're uploaded: the full image at most 1080 px
39
+ * wide, and a copy at most 540 px wide (a list's card, a thumbnail, a link's preview), both kept
40
+ * when the image is larger than that; never made larger. Phones are the screens that matter: 1080
41
+ * is sharp across one at 3×, and the original (often 4000 px and several MB) isn't kept.
42
+ *
43
+ * Each is encoded again, which also drops the photo's metadata (a phone's GPS position): WebP where
44
+ * the browser can encode it, else JPEG; PNG only for an image with transparent pixels, which JPEG
45
+ * can't keep. No library: the browser decodes (turned the way the camera says), draws and encodes.
46
+ * A large step down is drawn in halves, which keeps it sharp. Where it can't (no `createImageBitmap`,
47
+ * or a format the browser can't decode), the file is uploaded as it is.
48
+ */
49
+ /** The widths an image is kept at, at most. */
50
+ const IMAGE_WIDTHS = {
51
+ full: 1080,
52
+ small: 540
53
+ };
54
+ /** How tall an image may be, for each width: a very tall one is held to twice its width's limit. */
55
+ const TALLEST = 2;
56
+ /** JPEG and WebP quality: no visible loss in a photo, at a fraction of its size. */
57
+ const QUALITY = .82;
58
+ /** `width` × `height` made to fit within `max` wide (and twice that tall), never larger. */
59
+ function fitWithin(width, height, max) {
60
+ const scale = Math.min(1, max / width, max * TALLEST / height);
61
+ return {
62
+ width: Math.max(1, Math.round(width * scale)),
63
+ height: Math.max(1, Math.round(height * scale))
64
+ };
65
+ }
66
+ /** The sizes it's drawn at on the way down to `to`, halving each time, ending at `to`. */
67
+ function stepsDown(from, to) {
68
+ const steps = [];
69
+ let { width, height } = from;
70
+ while (width / 2 > to.width) {
71
+ width = Math.round(width / 2);
72
+ height = Math.round(height / 2);
73
+ steps.push({
74
+ width,
75
+ height
76
+ });
77
+ }
78
+ steps.push(to);
79
+ return steps;
80
+ }
81
+ /** The image's sizes, or undefined when the browser can't make them (it's uploaded as it is). */
82
+ async function resized(file, name) {
83
+ if (typeof createImageBitmap === "undefined") return void 0;
84
+ let bitmap;
85
+ try {
86
+ bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
87
+ } catch {
88
+ return;
89
+ }
90
+ try {
91
+ const source = {
92
+ width: bitmap.width,
93
+ height: bitmap.height
94
+ };
95
+ const full = drawn(bitmap, source, fitWithin(source.width, source.height, IMAGE_WIDTHS.full));
96
+ const clear = file.type !== "image/jpeg" && hasTransparency(full);
97
+ const fullFile = await encoded(full, name, clear);
98
+ if (source.width <= IMAGE_WIDTHS.small) return { full: fullFile };
99
+ return {
100
+ full: fullFile,
101
+ small: await encoded(drawn(full, full, fitWithin(source.width, source.height, IMAGE_WIDTHS.small)), name, clear)
102
+ };
103
+ } finally {
104
+ bitmap.close();
105
+ }
106
+ }
107
+ function canvasOf(width, height) {
108
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(width, height);
109
+ const canvas = document.createElement("canvas");
110
+ canvas.width = width;
111
+ canvas.height = height;
112
+ return canvas;
113
+ }
114
+ function context2d(canvas) {
115
+ const context = canvas.getContext("2d");
116
+ if (!context) throw new Error("isai: this browser can't draw images");
117
+ context.imageSmoothingEnabled = true;
118
+ context.imageSmoothingQuality = "high";
119
+ return context;
120
+ }
121
+ /** `image` drawn at `to`, halving from `from` on the way. */
122
+ function drawn(image, from, to) {
123
+ let current = image;
124
+ let canvas;
125
+ for (const step of stepsDown(from, to)) {
126
+ canvas = canvasOf(step.width, step.height);
127
+ context2d(canvas).drawImage(current, 0, 0, step.width, step.height);
128
+ current = canvas;
129
+ }
130
+ return canvas;
131
+ }
132
+ /** Whether any pixel isn't fully opaque. */
133
+ function hasTransparency(canvas) {
134
+ const { data } = context2d(canvas).getImageData(0, 0, canvas.width, canvas.height);
135
+ for (let at = 3; at < data.length; at += 4) if (data[at] < 255) return true;
136
+ return false;
137
+ }
138
+ const blobOf = (canvas, type) => "convertToBlob" in canvas ? canvas.convertToBlob({
139
+ type,
140
+ quality: QUALITY
141
+ }) : new Promise((resolve) => canvas.toBlob(resolve, type, QUALITY));
142
+ /** WebP where the browser encodes it (it answers PNG where it can't), else JPEG, or PNG to keep transparency. */
143
+ async function encoded(canvas, name, clear) {
144
+ let blob = await blobOf(canvas, "image/webp");
145
+ if (blob?.type !== "image/webp") blob = await blobOf(canvas, clear ? "image/png" : "image/jpeg");
146
+ if (!blob) throw new Error("isai: this browser couldn't encode the image");
147
+ const extension = blob.type === "image/webp" ? "webp" : blob.type === "image/png" ? "png" : "jpg";
148
+ return new File([blob], `${name.replace(/\.[^.]*$/, "") || "image"}.${extension}`, { type: blob.type });
149
+ }
150
+ //#endregion
36
151
  //#region src/client/client.ts
37
152
  /**
38
153
  * The client's runtime: plain async calls over `fetch`, built from the endpoint
@@ -41,7 +156,7 @@ async function errorOf(response) {
41
156
  *
42
157
  * resources api.<name>.list(query) · get(key) · create(input) · update(key, input) · delete(key)
43
158
  * · publish(key) · unpublish(key) · reorder(keys) · history(key, query)
44
- * · <custom action>(key, input?) · upload(field, file)
159
+ * · <custom action>(key, input?) · upload(field, file) (an image is resized first)
45
160
  * (a singleton's calls take no key)
46
161
  * routes api.<name>(args) (path params and input in one object)
47
162
  * auth api.auth.me() · signIn() · signOut() · users · invites
@@ -52,11 +167,12 @@ function createClient(endpoints, options = {}) {
52
167
  const base = (options.baseUrl ?? "/api").replace(/\/$/, "");
53
168
  const send = options.fetch ?? ((input, init) => fetch(input, init));
54
169
  const listeners = /* @__PURE__ */ new Set();
55
- async function call(method, path, { query, body, signedOutIsAnswer = false } = {}) {
170
+ async function call(method, path, { query, body, headers: extra, signedOutIsAnswer = false } = {}) {
56
171
  const search = query && query.size > 0 ? `?${query.toString().replace(/=(?=&|$)/g, "")}` : "";
57
172
  const headers = {
58
173
  Accept: "application/json",
59
- ...options.headers
174
+ ...options.headers,
175
+ ...extra
60
176
  };
61
177
  const file = body instanceof Blob ? body : void 0;
62
178
  if (file) {
@@ -122,13 +238,27 @@ function resourceCalls(call, endpoint) {
122
238
  for (const action of endpoint.custom ?? []) calls[action] = actionCall(call, keyed, action);
123
239
  if (endpoint.uploads?.length) {
124
240
  const fields = new Set(endpoint.uploads);
241
+ const images = new Set(endpoint.images);
125
242
  calls.upload = (field, file) => {
126
243
  if (!fields.has(field)) throw new TypeError(`isai: ${base} takes no upload for ${field}`);
127
- return call("POST", `/uploads${base}/${kebabCase(field)}`, { body: file });
244
+ const path = `/uploads${base}/${kebabCase(field)}`;
245
+ if (!images.has(field)) return call("POST", path, { body: file });
246
+ return uploadImage(call, path, file);
128
247
  };
129
248
  }
130
249
  return calls;
131
250
  }
251
+ /** An image, resized: the full one, then its smaller copy with `X-Size-Of`; the answer is the image. */
252
+ async function uploadImage(call, path, file) {
253
+ const sizes = await resized(file, file.name ?? "image");
254
+ if (!sizes) return call("POST", path, { body: file });
255
+ const image = await call("POST", path, { body: sizes.full });
256
+ if (!sizes.small) return image;
257
+ return call("POST", path, {
258
+ body: sizes.small,
259
+ headers: { "X-Size-Of": image.id }
260
+ });
261
+ }
132
262
  function actionCall(call, keyed, action) {
133
263
  return keyed((path, input) => call("POST", `${path}/${kebabCase(action)}`, input === void 0 ? {} : { body: input }));
134
264
  }
@@ -211,4 +341,22 @@ function browser() {
211
341
  return { location };
212
342
  }
213
343
  //#endregion
214
- export { ApiError, createClient, listQuery };
344
+ //#region src/client/files.ts
345
+ /**
346
+ * An image's `srcset`: the image and its smaller copies, each with its width, so the browser loads
347
+ * the one the screen needs (`<img src={file.url} srcset={srcset(file)} sizes="50vw">`). Undefined
348
+ * for a file without copies: its `url` is its only size.
349
+ */
350
+ function srcset(file) {
351
+ if (!file.sizes?.length || !file.width) return void 0;
352
+ return [...file.sizes, {
353
+ url: file.url,
354
+ width: file.width
355
+ }].map((one) => `${one.url} ${one.width}w`).join(", ");
356
+ }
357
+ /** The smallest copy at least `width` wide (or the widest there is), else the file itself: a preview's image. */
358
+ function sizeFor(file, width) {
359
+ return [...file.sizes ?? []].sort((a, b) => a.width - b.width).find((one) => one.width >= width) ?? file;
360
+ }
361
+ //#endregion
362
+ export { ApiError, IMAGE_WIDTHS, createClient, listQuery, sizeFor, srcset };
@@ -1,6 +1,6 @@
1
- import { t as fromBase64url } from "./base64url-K_wCmvxR.js";
2
- import { n as PROVIDERS } from "./tables-C4ipMFjg.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-pFzslmSB.js";
1
+ import { t as fromBase64url } from "./base64url-C5GTYIL5.js";
2
+ import { n as PROVIDERS } from "./tables-IrLjsomf.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-CKIdBFKt.js";
4
4
  //#region src/auth/create-auth.ts
5
5
  const methods = { oauth: (provider, options) => ({
6
6
  kind: "oauth",
@@ -1,7 +1,7 @@
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, q as StoredFile } from "./index-C-WmIKxT.js";
4
- import { 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-DpOcIuDl.js";
3
+ import { M as IdInput, Y as StoredFile } from "./index-CYcklQ1l.js";
4
+ import { 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-S6zF5IXV.js";
5
5
  //#region src/db/schema/traits.d.ts
6
6
  type ComputedDefinitions = Record<string, (row: any) => Condition>;
7
7
  interface TraitSpec {
@@ -920,6 +920,11 @@ declare function tenancyTables(f: Kinds, options: TenancyOptions): TableSet<{
920
920
  //#region src/db/schema/data-layer.d.ts
921
921
  /** A project kind: a preset field (`base.line().max(120)`), or a factory for parameterized kinds. */
922
922
  type ProjectKind = AnyField | ((...args: any[]) => AnyField);
923
+ /** What a model may say before its traits and fields. */
924
+ interface ModelOptions {
925
+ /** It lives in the platform's database (with tenancy), as its folder does: `db/schema/platform/`. */
926
+ readonly database: "platform";
927
+ }
923
928
  interface DataLayerDefinition<P extends Record<string, ProjectKind>, U extends readonly AnyTableSet[]> {
924
929
  /**
925
930
  * Built-in tables to switch on (`[base.authTables({ database: "tenant" }), base.tenancy({ database: "platform" })]`):
@@ -944,7 +949,10 @@ type ProjectKinds<P> = { readonly [K in keyof P]: P[K] extends AnyField ? () =>
944
949
  /** The placeholder for conditions over a model's rows: `const event = row(Event)`. */
945
950
  declare function row<M extends AnyModel>(target: M): Columns<M["~fields"], M["name"]>;
946
951
  declare function createDataLayer<P extends Record<string, ProjectKind> = {}, const U extends readonly AnyTableSet[] = []>(define?: (base: Base) => DataLayerDefinition<P, U>): {
947
- model: <const Name extends string, const T extends readonly AnyTrait[], F extends Fields>(name: Name, ...rest: [...traits: T, fields: F]) => Model<Name, TraitFields<T> & F>;
952
+ model: {
953
+ <const Name extends string, const T extends readonly AnyTrait[], F extends Fields>(name: Name, ...rest: [...traits: T, fields: F]): Model<Name, TraitFields<T> & F>;
954
+ <const Name extends string, const T extends readonly AnyTrait[], F extends Fields>(name: Name, options: ModelOptions, ...rest: [...traits: T, fields: F]): Model<Name, TraitFields<T> & F>;
955
+ };
948
956
  f: Kinds & ProjectKinds<P>;
949
957
  builtins: Builtins<U>;
950
958
  slots: {
@@ -1,6 +1,7 @@
1
- import { O as datetime, P as quantity, p as host, r as isPhoneRegion } from "./validation-FyOpSZqE.js";
2
- import { C as not, M as field, N as fieldKinds, S as count, s as defineModel, u as rowOf, v as all, w as now, y as any } from "./base64url-K_wCmvxR.js";
3
- import { a as TableSet, i as platformAuthDefinitions, o as defineBuiltins, r as authTables } from "./tables-C4ipMFjg.js";
1
+ import { F as quantity, k as datetime, m as host, r as isPhoneRegion } from "./validation-Cppmd0RV.js";
2
+ import "./migrator-BU3_PHk8.js";
3
+ import { C as not, M as field, N as fieldKinds, S as count, s as defineModel, u as rowOf, v as all, w as now, y as any } from "./base64url-C5GTYIL5.js";
4
+ import { a as TableSet, i as platformAuthDefinitions, o as defineBuiltins, r as authTables } from "./tables-IrLjsomf.js";
4
5
  import { n as seed } from "./seed-BObmdKdP.js";
5
6
  //#region src/db/fields/slot.ts
6
7
  var Slot = class {
@@ -226,6 +227,8 @@ function builtinTraits() {
226
227
  * `slots`, `builtins`, the condition language and `seed`. One call serves every database the app
227
228
  * has (with isolated tenancy, the tenants' and the platform's).
228
229
  */
230
+ /** A model's options: `{ database: "…" }`, never fields (a field is a kind, not a string). */
231
+ const isOptions = (value) => typeof value === "object" && value !== null && !(value instanceof Trait) && Object.keys(value).length === 1 && typeof value.database === "string";
229
232
  /** The placeholder for conditions over a model's rows: `const event = row(Event)`. */
230
233
  function row(target) {
231
234
  return rowOf(target.spec, target.registry);
@@ -260,15 +263,18 @@ function createDataLayer(define) {
260
263
  byDefault
261
264
  };
262
265
  }
263
- /** `model(name, fields)`, or with traits first: `model(name, traits.ordered({ within: "event" }), fields)`. */
264
- const model = (name, ...rest) => {
266
+ function model(name, ...given) {
265
267
  if (Object.hasOwn(builtins, name)) throw new TypeError(`isai: ${name} is a built-in table; add columns with builtins.${name}.extend({ … })`);
268
+ const first = given[0];
269
+ const options = given.length > 1 && isOptions(first) ? first : void 0;
270
+ const rest = options ? given.slice(1) : given;
271
+ if (options && options.database !== "platform") throw new TypeError(`isai: model("${name}") takes { database: "platform" } only`);
266
272
  const fields = rest.at(-1);
267
273
  if (!fields || fields instanceof Trait) throw new TypeError(`isai: model("${name}") needs its fields last`);
268
274
  const traits = rest.slice(0, -1);
269
275
  if (!traits.every((trait) => trait instanceof Trait)) throw new TypeError(`isai: model("${name}") takes traits, then its fields`);
270
- return defineModel(registry, name, fields, traits);
271
- };
276
+ return defineModel(registry, name, fields, traits, { ...options ? { database: options.database } : {} });
277
+ }
272
278
  return {
273
279
  model,
274
280
  f: {
@@ -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-DpOcIuDl.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-ubPfZvFV.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-S6zF5IXV.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-D1BFo3wO.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 { W as ValidationError } from "../validation-FyOpSZqE.js";
2
- import { A as Query, C as database, F as systemClock, G as UniqueFailed, H as DatabaseError, I as newSecretKey, M as dialectFor, N as sqlite, P as fixedClock, U as NotFound, V as CheckFailed, W 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, r as sync, s as releaseLock, t as migrate, u as startRelease, v as dataStep, x as Transaction, y as MigrationError } from "../migrator-6oWUefLA.js";
3
- import { C as not, F as sensitivity, N as fieldKinds, P as Field, S as count, _ as Total, d as Column, f as Condition, g as Relation, h as Now, m as Expr, r as Model, v as all, w as now, y as any } from "../base64url-K_wCmvxR.js";
4
- import { i as slot, n as row, r as Slot, t as createDataLayer } from "../data-layer-D9UPOK6L.js";
1
+ import { G as ValidationError } from "../validation-Cppmd0RV.js";
2
+ import { A as Query, C as database, F as systemClock, G as DatabaseError, I as newSecretKey, J as UniqueFailed, K as NotFound, M as dialectFor, N as sqlite, P as fixedClock, W as CheckFailed, _ 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 ReferenceFailed, r as sync, s as releaseLock, t as migrate, u as startRelease, v as dataStep, x as Transaction, y as MigrationError } from "../migrator-BU3_PHk8.js";
3
+ import { C as not, F as sensitivity, N as fieldKinds, P as Field, S as count, _ as Total, d as Column, f as Condition, g as Relation, h as Now, m as Expr, r as Model, v as all, w as now, y as any } from "../base64url-C5GTYIL5.js";
4
+ import { i as slot, n as row, r as Slot, t as createDataLayer } from "../data-layer-_TZtuy-F.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-WQpkYW8N.js";
6
+ import { a as cleanupMigration, i as makeMigrations, r as makeCleanup, t as checkMigrations } from "../make-DKBn79cH.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) {
@@ -176,7 +176,22 @@ interface StoredFile {
176
176
  /** An image's size in pixels, when its format says. */
177
177
  readonly width?: number;
178
178
  readonly height?: number;
179
+ /**
180
+ * An image's smaller copies, each in its own key: the client makes them before uploading (`srcset`
181
+ * picks one). None means the file itself serves every size.
182
+ */
183
+ readonly sizes?: readonly ImageSize[];
184
+ }
185
+ /** One smaller copy of an image: where it's stored, and what it is. */
186
+ interface ImageSize {
187
+ readonly key: string;
188
+ readonly size: number;
189
+ readonly type: string;
190
+ readonly width: number;
191
+ readonly height: number;
179
192
  }
193
+ /** At most this many smaller copies of one image. */
194
+ declare const SIZES_MAX = 4;
180
195
  /** File types by their usual name, with the media types each covers. */
181
196
  declare const FILE_TYPES: {
182
197
  readonly pdf: readonly ["application/pdf"];
@@ -367,4 +382,4 @@ declare const email: () => Kind<string, string, false, false>;
367
382
  /** An http(s) URL, up to 2,048 characters. */
368
383
  declare const url: () => Kind<string, string, false, false>;
369
384
  //#endregion
370
- export { FakeContext as $, validate as A, money as B, ObjectShape as C, object as D, list as E, datetime as F, FileType as G, sequence as H, fakeUlid as I, file as J, ImageType as K, flag as L, IdInput as M, choice as N, serialize as O, date as P, Fake as Q, id as R, LIST_MAX as S, Shape as T, phone as U, quantity as V, phoneKind as W, image as X, files as Y, images as Z, define as _, newToken as a, Measure as at, CarriesKind as b, slug as c, kind as ct, text as d, StandardResult as dt, AnyKind as et, textKind as f, StandardSchemaV1 as ft, KindDefinition as g, Definable as h, markdown as i, KindTypes as it, CalendarDate as j, toKind as k, slugify as l, StandardIssue as lt, url as m, host as n, Kind as nt, personName as o, Sensitivity as ot, token as p, StoredFile as q, line as r, KindSpec as rt, secret as s, TypeText as st, email as t, FileSpec as tt, subdomain as u, StandardProps as ut, json as v, Output as w, Input as x, urls as y, int as z };
385
+ export { images as $, validate as A, money as B, ObjectShape as C, object as D, list as E, datetime as F, FileType as G, sequence as H, fakeUlid as I, SIZES_MAX as J, ImageSize as K, flag as L, IdInput as M, choice as N, serialize as O, date as P, image as Q, id as R, LIST_MAX as S, Shape as T, phone as U, quantity as V, phoneKind as W, file as X, StoredFile as Y, files as Z, define as _, newToken as a, KindSpec as at, CarriesKind as b, slug as c, Sensitivity as ct, text as d, StandardIssue as dt, Fake as et, textKind as f, StandardProps as ft, KindDefinition as g, Definable as h, markdown as i, Kind as it, CalendarDate as j, toKind as k, slugify as l, TypeText as lt, url as m, StandardSchemaV1 as mt, host as n, AnyKind as nt, personName as o, KindTypes as ot, token as p, StandardResult as pt, ImageType as q, line as r, FileSpec as rt, secret as s, Measure as st, email as t, FakeContext as tt, subdomain as u, kind as ut, json as v, Output as w, Input as x, urls as y, int as z };
@@ -1,7 +1,7 @@
1
1
  import { a as DatabaseDriver, b as TenantDatabases, d as RateLimiter, g as Storage, l as PdfRenderer, n as Body, p as Runtime, r as Cache, s as MailDriver, u as Queue } from "./index-Ce5p2-EB.js";
2
- import { C as ObjectShape, q as StoredFile, w as Output } from "./index-C-WmIKxT.js";
3
- import { $ as Expr, C as WithIncludes, D as PageOptions, E as Page, J as Models, Q as Embed, S as Key, T as NumberedPageOptions, V as AnyModel, X as Condition, Z as Conditional, b as RowOf, ct as ColumnNode, gt as FieldSpec, i as Row, lt as ConditionNode, n as Patch, p as Database, t as Insert, ut as Operand, v as OrderKey, w as NumberedPage, x as IncludePath } from "./types-DpOcIuDl.js";
4
- import { a as PROVIDERS, i as HELD, k as Manifest } from "./data-layer-ubPfZvFV.js";
2
+ import { C as ObjectShape, Y as StoredFile, w as Output } from "./index-CYcklQ1l.js";
3
+ import { $ as Expr, C as WithIncludes, D as PageOptions, E as Page, J as Models, Q as Embed, S as Key, T as NumberedPageOptions, V as AnyModel, X as Condition, Z as Conditional, b as RowOf, ct as ColumnNode, gt as FieldSpec, i as Row, lt as ConditionNode, n as Patch, p as Database, t as Insert, ut as Operand, v as OrderKey, w as NumberedPage, x as IncludePath } from "./types-S6zF5IXV.js";
4
+ import { a as PROVIDERS, i as HELD, k as Manifest } from "./data-layer-D1BFo3wO.js";
5
5
  //#region src/auth/policies.d.ts
6
6
  /** The actions every model's policy has. */
7
7
  declare const STANDARD_ACTIONS: readonly ["read", "create", "update", "delete"];
@@ -404,6 +404,8 @@ interface PutOptions {
404
404
  readonly storage?: string;
405
405
  /** The body's length in bytes: needed for a stream, read from bytes and text. */
406
406
  readonly size?: number;
407
+ /** An image's smaller copies, as bytes (each smaller than it): `srcset` picks one. */
408
+ readonly sizes?: readonly (ArrayBuffer | Uint8Array)[];
407
409
  }
408
410
  interface AppFiles {
409
411
  /** Stores a file (its type read from its bytes) in the current tenant's space, and returns it for a write. */
@@ -417,7 +419,7 @@ interface AppFiles {
417
419
  url(file: StoredFile, options?: {
418
420
  readonly origin?: string;
419
421
  }): Promise<string>;
420
- /** Removes the file from its storage. */
422
+ /** Removes the file (and its sizes) from its storage. */
421
423
  delete(file: StoredFile): Promise<void>;
422
424
  }
423
425
  //#endregion
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { p as Runtime, t as AppCore } from "./index-Ce5p2-EB.js";
2
- import { C as Params, D as RouteHandler, E as RouteContext, M as fail, O as RouteOptions, S as Method, T as RouteBuilder, _ as CustomAction, b as AppConfig, d as NotImplementedError, f as AppModules, g as createApp, h as CreatedApp, j as Rejected, k as User, m as AppOptions, p as App, v as Resource, w as Route, x as Named, y as ResourceOptions } from "./index-B6ipokoP.js";
2
+ import { C as Params, D as RouteHandler, E as RouteContext, M as fail, O as RouteOptions, S as Method, T as RouteBuilder, _ as CustomAction, b as AppConfig, d as NotImplementedError, f as AppModules, g as createApp, h as CreatedApp, j as Rejected, k as User, m as AppOptions, p as App, v as Resource, w as Route, x as Named, y as ResourceOptions } from "./index-DlgVg3D8.js";
3
3
  export { type App, type AppConfig, type AppCore, type AppModules, type AppOptions, type CreatedApp, type CustomAction, type Method, type Named, NotImplementedError, type Params, Rejected, type Resource, type ResourceOptions, Route, type RouteBuilder, type RouteContext, type RouteHandler, type RouteOptions, type Runtime, type User, createApp, fail };
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { B as toKind, R as object, V as validate, W as ValidationError, a as fileTypeNames, f as email, m as line } from "./validation-FyOpSZqE.js";
2
- import { B as modelOfRow, C as database, D as planInsert, E as recordAction, F as systemClock, G as UniqueFailed, L as NeedsDatabase, O as planUpdate, R as evaluate, S as clockOf, U as NotFound, j as conditionOf, k as keyCondition, r as sync, t as migrate, w as decodeStored } from "./migrator-6oWUefLA.js";
3
- import { C as tooLarge, E as fail, S as tenantSuspended, T as Rejected, _ as json, b as routeNotFound, c as fromBody, d as route, f as HttpError, g as fileTypeRefused, h as fileTooLarge, l as readInput, m as errorResponse, o as parseListQuery, p as crossSiteRequest, r as resource, s as BODY_LIMIT, t as Resource, u as Route, v as lengthRequired, w as tooManyRequests, x as signInRequired, y as methodNotAllowed } from "./resource-C17NvNa3.js";
4
- import { F as sensitivity, G as ulid, K as Scope, V as asActor, a as columnName, b as asCondition$1, c as fieldOf, f as Condition, m as Expr, n as toBase64url, r as Model, t as fromBase64url, u as rowOf } from "./base64url-K_wCmvxR.js";
1
+ import { G as ValidationError, H as validate, V as toKind, h as line, o as fileTypeNames, p as email, z as object } from "./validation-Cppmd0RV.js";
2
+ import { B as NeedsDatabase, C as database, D as planInsert, E as recordAction, F as systemClock, J as UniqueFailed, K as NotFound, L as PLATFORM, O as planUpdate, R as TENANT, S as clockOf, U as modelOfRow, V as evaluate, j as conditionOf, k as keyCondition, r as sync, t as migrate, w as decodeStored, z as checkTenancy } from "./migrator-BU3_PHk8.js";
3
+ import { C as tooLarge, E as fail, S as tenantSuspended, T as Rejected, _ as json, b as routeNotFound, c as fromBody, d as route, f as HttpError, g as fileTypeRefused, h as fileTooLarge, l as readInput, m as errorResponse, o as parseListQuery, p as crossSiteRequest, r as resource, s as BODY_LIMIT, t as Resource, u as Route, v as lengthRequired, w as tooManyRequests, x as signInRequired, y as methodNotAllowed } from "./resource-CsYksjKc.js";
4
+ import { F as sensitivity, G as ulid, K as Scope, V as asActor, a as columnName, b as asCondition$1, c as fieldOf, f as Condition, m as Expr, n as toBase64url, r as Model, t as fromBase64url, u as rowOf } from "./base64url-C5GTYIL5.js";
5
5
  import { t as kebabCase } from "./case-BnpcvhdW.js";
6
- import { t as PLATFORM_PREFIX } from "./tables-C4ipMFjg.js";
6
+ import { t as PLATFORM_PREFIX } from "./tables-IrLjsomf.js";
7
7
  import { t as Seed } from "./seed-BObmdKdP.js";
8
8
  import { n as notImplemented, t as NotImplementedError } from "./errors-mCNLlXg9.js";
9
- import { d as afterUpdate, f as bindFor, h as ownerFieldOf, m as isFalse, p as check, u as FALSE } from "./policies-pFzslmSB.js";
10
- import { i as checkRoles, n as authPerms, r as checkAuth } from "./create-auth-y8E8cxkn.js";
11
- import { C as syncRoles, S as seedAs, _ as cookieOf, a as authModels, b as permissionsOf, c as hashOf, d as GoogleError, f as consentUrl, g as cookie, h as verifyIdToken, i as makers, l as sessions, n as TENANT, p as exchangeCode, r as checkTenancy, s as clearedCookie, t as PLATFORM, v as redirect, x as roleKeys, y as safeReturn } from "./config-0PGFbJh6.js";
9
+ import { d as afterUpdate, f as bindFor, h as ownerFieldOf, m as isFalse, p as check, u as FALSE } from "./policies-CKIdBFKt.js";
10
+ import { i as checkRoles, n as authPerms, r as checkAuth } from "./create-auth-Ci0PaBoX.js";
11
+ import { _ as permissionsOf, a as hashOf, b as syncRoles, c as GoogleError, f as verifyIdToken, g as safeReturn, h as redirect, i as clearedCookie, l as consentUrl, m as cookieOf, n as authModels, o as sessions, p as cookie, t as makers, u as exchangeCode, v as roleKeys, y as seedAs } from "./makers-DeSNM0Nd.js";
12
12
  import { t as WrongSize } from "./storage-body-7dWyMszh.js";
13
13
  import { RegExpRouter } from "hono/router/reg-exp-router";
14
14
  import { SmartRouter } from "hono/router/smart-router";
@@ -109,20 +109,39 @@ function rowJSON(row, model, files) {
109
109
  }
110
110
  return out;
111
111
  }
112
- /** A file as a client sees it: its id, its `url`, then what's shown of it; never its storage or key. */
112
+ /**
113
+ * A file as a client sees it: its id, its `url`, then what's shown of it, and an image's `sizes`
114
+ * (each `{ width, height, url }`, widest first); never its storage or keys.
115
+ */
113
116
  function fileJSON(file, files) {
114
- const { id, storage: _storage, key: _key, ...shown } = file;
117
+ const { id, storage: _storage, key: _key, sizes, ...shown } = file;
115
118
  const out = {
116
119
  id,
117
120
  ...files ? { url: null } : {},
118
121
  ...shown
119
122
  };
120
- if (!files) return out;
121
- const url = files.urlOf(file);
122
- if (typeof url === "string") out.url = url;
123
- else files.pending.push(url.then((made) => {
124
- out.url = made;
125
- }));
123
+ const linked = (into, of) => {
124
+ if (!files) return;
125
+ const url = files.urlOf(of);
126
+ if (typeof url === "string") into.url = url;
127
+ else files.pending.push(url.then((made) => {
128
+ into.url = made;
129
+ }));
130
+ };
131
+ linked(out, file);
132
+ if (sizes?.length) out.sizes = sizes.map(({ key, type, width, height }) => {
133
+ const one = {
134
+ width,
135
+ height,
136
+ ...files ? { url: null } : {}
137
+ };
138
+ linked(one, {
139
+ ...file,
140
+ key,
141
+ type
142
+ });
143
+ return one;
144
+ });
126
145
  return out;
127
146
  }
128
147
  //#endregion
@@ -1179,6 +1198,8 @@ function filesOf(model, row) {
1179
1198
  if (!row) return [];
1180
1199
  return Object.entries(model.spec.fields).flatMap(([field, spec]) => spec.kind.spec.file ? listOf(row[field]) : []);
1181
1200
  }
1201
+ /** Every key a file is stored at: its own, and its sizes'. */
1202
+ const keysOf = (file) => [file.key, ...(file.sizes ?? []).map((one) => one.key)];
1182
1203
  /** The files a write dropped: held before, held no longer. */
1183
1204
  function droppedFiles(before, after) {
1184
1205
  const kept = new Set(after.map((file) => file.id));
@@ -1264,13 +1285,13 @@ function fileWrites(upload, storages) {
1264
1285
  },
1265
1286
  settle: async (db, uploads, dropped = []) => {
1266
1287
  if (uploads.length > 0) await db.deleteMany(upload).where((u) => u.fileId.in(uploads));
1267
- await Promise.all(dropped.map(async (file) => {
1288
+ await Promise.all(dropped.flatMap((file) => keysOf(file).map(async (key) => {
1268
1289
  try {
1269
- await storages[file.storage]?.delete(file.key);
1290
+ await storages[file.storage]?.delete(key);
1270
1291
  } catch (error) {
1271
- console.error(`isai: couldn't remove ${file.key} from storage "${file.storage}"`, error);
1292
+ console.error(`isai: couldn't remove ${key} from storage "${file.storage}"`, error);
1272
1293
  }
1273
- }));
1294
+ })));
1274
1295
  }
1275
1296
  };
1276
1297
  }
@@ -2542,6 +2563,11 @@ async function headOf(stream, bytes) {
2542
2563
  * 5. an `Upload` row: pending until a write names the file's id, for a day
2543
2564
  *
2544
2565
  * It answers 201 with the file as a client sees it, its `url` included.
2566
+ *
2567
+ * An image's smaller copy (the client makes them: `srcset` picks one) is sent the same way, with
2568
+ * `X-Size-Of: <the image's id>`. It's kept with that image while its upload is still pending, from
2569
+ * the same user to the same field, and must be smaller than it: the answer is the image, its
2570
+ * `sizes` included. A write then names only the image.
2545
2571
  */
2546
2572
  /** How long an upload waits to be attached. */
2547
2573
  const UPLOAD_TTL_MS = 864e5;
@@ -2579,17 +2605,50 @@ function uploadRoute(options) {
2579
2605
  await body.cancel().catch(() => {});
2580
2606
  throw fileTypeRefused(fileTypeNames(spec.types));
2581
2607
  }
2608
+ const db = accessOf(context.db).trusted;
2609
+ const user = context.user;
2610
+ const put = async (key) => {
2611
+ try {
2612
+ await storage.put(key, body, {
2613
+ size,
2614
+ contentType: sniffed.type
2615
+ });
2616
+ } catch (error) {
2617
+ if (error instanceof WrongSize) throw new HttpError(400, "WrongSize", "The file's bytes don't match its Content-Length");
2618
+ throw error;
2619
+ }
2620
+ };
2621
+ const sizeOf = request.headers.get("X-Size-Of");
2622
+ if (sizeOf !== null) {
2623
+ const pending = await db.findOne(options.upload, { fileId: sizeOf });
2624
+ const image = pending?.file;
2625
+ const refused = (code, message) => {
2626
+ body.cancel().catch(() => {});
2627
+ return new HttpError(400, code, message);
2628
+ };
2629
+ if (!pending || !image || pending.target !== name || (pending.user ?? void 0) !== user?.id || pending.expiresAt.getTime() <= clockOf(db).now().getTime()) throw refused("SizeOfUnknown", "X-Size-Of must name an image uploaded here, before it's attached");
2630
+ const { width, height } = sniffed;
2631
+ const sizes = image.sizes ?? [];
2632
+ if (!width || !height || !image.width || !image.height || width >= image.width || height > image.height) throw refused("NotSmaller", "A size must be an image smaller than the one it's of");
2633
+ if (sizes.length >= 4 || sizes.some((one) => one.width === width)) throw refused("TooManySizes", `An image keeps at most 4 sizes, each of its own width`);
2634
+ const key = `${image.key}.${width}`;
2635
+ await put(key);
2636
+ const file = {
2637
+ ...image,
2638
+ sizes: [...sizes, {
2639
+ key,
2640
+ size,
2641
+ type: sniffed.type,
2642
+ width,
2643
+ height
2644
+ }].sort((a, b) => b.width - a.width)
2645
+ };
2646
+ await db.updateOne(options.upload, { fileId: sizeOf }, { file });
2647
+ return json(201, await options.answer(file, new URL(request.url).origin));
2648
+ }
2582
2649
  const id = newFileId();
2583
2650
  const key = keyOf(id, context.tenant);
2584
- try {
2585
- await storage.put(key, body, {
2586
- size,
2587
- contentType: sniffed.type
2588
- });
2589
- } catch (error) {
2590
- if (error instanceof WrongSize) throw new HttpError(400, "WrongSize", "The file's bytes don't match its Content-Length");
2591
- throw error;
2592
- }
2651
+ await put(key);
2593
2652
  const file = {
2594
2653
  id,
2595
2654
  storage: spec.storage,
@@ -2598,8 +2657,6 @@ function uploadRoute(options) {
2598
2657
  size,
2599
2658
  ...sniffed
2600
2659
  };
2601
- const db = accessOf(context.db).trusted;
2602
- const user = context.user;
2603
2660
  await db.createOne(options.upload, {
2604
2661
  fileId: id,
2605
2662
  file,
@@ -2720,7 +2777,7 @@ function appFiles(options) {
2720
2777
  return storage;
2721
2778
  };
2722
2779
  return {
2723
- put: async (body, { name, storage = "default", size: given }) => {
2780
+ put: async (body, { name, storage = "default", size: given, sizes: copies = [] }) => {
2724
2781
  const where = storageOf(storage);
2725
2782
  const bytes = body instanceof ReadableStream ? void 0 : asBytes(body);
2726
2783
  const size = bytes?.byteLength ?? given;
@@ -2740,18 +2797,40 @@ function appFiles(options) {
2740
2797
  size,
2741
2798
  contentType: sniffed.type
2742
2799
  });
2800
+ const sizes = [];
2801
+ for (const copy of copies) {
2802
+ const bytes = new Uint8Array(copy);
2803
+ const one = sniff(bytes.subarray(0, SNIFF_BYTES), name);
2804
+ if (!one?.width || !one.height || !sniffed.width || !sniffed.height || one.width >= sniffed.width || one.height > sniffed.height) throw new TypeError(`isai: a size of ${name} must be an image smaller than it`);
2805
+ const at = `${key}.${one.width}`;
2806
+ await where.put(at, bytes, {
2807
+ size: bytes.byteLength,
2808
+ contentType: one.type
2809
+ });
2810
+ sizes.push({
2811
+ key: at,
2812
+ size: bytes.byteLength,
2813
+ type: one.type,
2814
+ width: one.width,
2815
+ height: one.height
2816
+ });
2817
+ }
2818
+ sizes.sort((a, b) => b.width - a.width);
2743
2819
  return {
2744
2820
  id,
2745
2821
  storage,
2746
2822
  key,
2747
2823
  name,
2748
2824
  size,
2749
- ...sniffed
2825
+ ...sniffed,
2826
+ ...sizes.length ? { sizes } : {}
2750
2827
  };
2751
2828
  },
2752
2829
  get: async (file) => (await storageOf(file.storage).get(file.key))?.body,
2753
2830
  url: async (file, { origin = "" } = {}) => url(file, origin),
2754
- delete: (file) => storageOf(file.storage).delete(file.key)
2831
+ delete: async (file) => {
2832
+ await Promise.all(keysOf(file).map((key) => storageOf(file.storage).delete(key)));
2833
+ }
2755
2834
  };
2756
2835
  }
2757
2836
  function asBytes(body) {
@@ -1,4 +1,4 @@
1
- import { F as systemClock, f as dayOf, h as snapshotOf, p as generateMigration, y as MigrationError } from "./migrator-6oWUefLA.js";
1
+ import { F as systemClock, f as dayOf, h as snapshotOf, p as generateMigration, y as MigrationError } from "./migrator-BU3_PHk8.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) {
@@ -1,6 +1,6 @@
1
- import { G as Fake, K as fakeValue } from "./validation-FyOpSZqE.js";
2
- import { D as planInsert, E as recordAction, S as clockOf, T as forUpkeep, V as CheckFailed } from "./migrator-6oWUefLA.js";
3
- import { j as IMPLICIT_FIELDS, n as toBase64url, r as Model, t as fromBase64url } from "./base64url-K_wCmvxR.js";
1
+ import { K as Fake, q as fakeValue } from "./validation-Cppmd0RV.js";
2
+ import { D as planInsert, E as recordAction, S as clockOf, T as forUpkeep, W as CheckFailed } from "./migrator-BU3_PHk8.js";
3
+ import { j as IMPLICIT_FIELDS, n as toBase64url, r as Model, t as fromBase64url } from "./base64url-C5GTYIL5.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. */
@@ -410,37 +410,4 @@ function failedCheck(model, input, now) {
410
410
  }
411
411
  }
412
412
  //#endregion
413
- //#region src/tenancy/config.ts
414
- /**
415
- * The two database names that mean something, and only with tenancy: each tenant's own database,
416
- * and the platform's. Every other name is just a name.
417
- */
418
- const TENANT = "tenant";
419
- const PLATFORM = "platform";
420
- /** A host without a scheme, path or trailing dot; a port is allowed (local development). */
421
- const HOST = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::\d{1,5})?$/;
422
- /**
423
- * Checks the tenancy key against the databases, when the app starts. With tenancy the databases
424
- * are `tenant` (each tenant's) and `platform`; without it, the app has one database, named freely.
425
- */
426
- function checkTenancy(tenancy, databases) {
427
- const names = Object.keys(databases);
428
- if (!tenancy) {
429
- const reserved = names.find((name) => name === "tenant" || name === "platform");
430
- if (reserved) throw new TypeError(`isai: databases.${reserved} is ${reserved === "tenant" ? "each tenant's database" : "the platform's database"}, which only an app with a \`tenancy\` key has; name the app's database something else (main)`);
431
- if (names.length !== 1) throw new TypeError(`isai: an app without tenancy has one database (named as you like); it has ${names.length}${names.length > 1 ? `: ${names.join(", ")}. Several aren't supported yet` : ""}`);
432
- if (Object.values(databases)[0]?.kind !== "database") throw new TypeError("isai: every tenant's own database (durableObject(env.TENANT)) needs a `tenancy` key");
433
- return;
434
- }
435
- if (tenancy.mode !== "isolated") throw new TypeError(`isai: tenancy.mode "${String(tenancy.mode)}" isn't built yet; use "isolated"`);
436
- if (tenancy.by !== "host") throw new TypeError(`isai: tenancy.by "${String(tenancy.by)}" isn't built yet; use "host"`);
437
- if (typeof tenancy.platform !== "string" || !HOST.test(tenancy.platform.toLowerCase())) throw new TypeError(`isai: tenancy.platform must be a host, like app.example.com (no https:// or path); got "${String(tenancy.platform)}"`);
438
- const model = tenancy.tenant;
439
- if (model?.spec?.name !== "Tenant" || model.spec.builtin !== "tenancy") throw new TypeError("isai: tenancy.tenant is the Tenant model: builtins.Tenant, or its extension (import { Tenant } from \"#db/platform/tenants\")");
440
- if (databases["tenant"]?.kind !== "tenantDatabases") throw new TypeError("isai: with isolated tenancy, databases.tenant is each tenant's database: durableObject(env.TENANT)");
441
- if (databases["platform"]?.kind !== "database") throw new TypeError("isai: an app with tenancy needs databases.platform, where the Tenant and Domain tables live");
442
- const other = names.find((name) => name !== "tenant" && name !== "platform");
443
- if (other) throw new TypeError(`isai: with tenancy, the databases are tenant and platform; databases.${other} isn't supported yet`);
444
- }
445
- //#endregion
446
- export { syncRoles as C, seedAs as S, cookieOf as _, authModels as a, permissionsOf as b, hashOf as c, GoogleError as d, consentUrl as f, cookie as g, verifyIdToken as h, makers as i, sessions as l, forgetGoogleKeys as m, TENANT as n, authOf as o, exchangeCode as p, checkTenancy as r, clearedCookie as s, PLATFORM as t, GOOGLE as u, redirect as v, roleKeys as x, safeReturn as y };
413
+ export { permissionsOf as _, hashOf as a, syncRoles as b, GoogleError as c, forgetGoogleKeys as d, verifyIdToken as f, safeReturn as g, redirect as h, clearedCookie as i, consentUrl as l, cookieOf as m, authModels as n, sessions as o, cookie as p, authOf as r, GOOGLE as s, makers as t, exchangeCode as u, roleKeys as v, seedAs as y };
@@ -1,5 +1,5 @@
1
- import { W as ValidationError, j as id } from "./validation-FyOpSZqE.js";
2
- import { A as unbound, B as aroundUpsert, E as fieldsOf, G as ulid, H as assertNotLog, I as actionEntry, L as aroundCreate, O as readsNow, R as aroundDelete, T as storageOf, W as wrap, a as columnName, c as fieldOf, f as Condition, h as Now, i as assertNotEncrypted, j as IMPLICIT_FIELDS, k as readsOtherRows, m as Expr, n as toBase64url, o as columnNode, t as fromBase64url$1, u as rowOf$1, x as conditionNode, z as aroundUpdate } from "./base64url-K_wCmvxR.js";
1
+ import { G as ValidationError, M as id } from "./validation-Cppmd0RV.js";
2
+ import { A as unbound, B as aroundUpsert, E as fieldsOf, G as ulid, H as assertNotLog, I as actionEntry, L as aroundCreate, O as readsNow, R as aroundDelete, T as storageOf, W as wrap, a as columnName, c as fieldOf, f as Condition, h as Now, i as assertNotEncrypted, j as IMPLICIT_FIELDS, k as readsOtherRows, m as Expr, n as toBase64url, o as columnNode, t as fromBase64url$1, u as rowOf$1, x as conditionNode, z as aroundUpdate } from "./base64url-C5GTYIL5.js";
3
3
  import { t as NotImplementedError } from "./errors-mCNLlXg9.js";
4
4
  //#region src/db/errors.ts
5
5
  var DatabaseError = class extends Error {
@@ -278,6 +278,39 @@ function evaluate(node, row, now, zone = utc) {
278
278
  }
279
279
  }
280
280
  //#endregion
281
+ //#region src/tenancy/config.ts
282
+ /**
283
+ * The two database names that mean something, and only with tenancy: each tenant's own database,
284
+ * and the platform's. Every other name is just a name.
285
+ */
286
+ const TENANT = "tenant";
287
+ const PLATFORM = "platform";
288
+ /** A host without a scheme, path or trailing dot; a port is allowed (local development). */
289
+ const HOST = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::\d{1,5})?$/;
290
+ /**
291
+ * Checks the tenancy key against the databases, when the app starts. With tenancy the databases
292
+ * are `tenant` (each tenant's) and `platform`; without it, the app has one database, named freely.
293
+ */
294
+ function checkTenancy(tenancy, databases) {
295
+ const names = Object.keys(databases);
296
+ if (!tenancy) {
297
+ const reserved = names.find((name) => name === "tenant" || name === "platform");
298
+ if (reserved) throw new TypeError(`isai: databases.${reserved} is ${reserved === "tenant" ? "each tenant's database" : "the platform's database"}, which only an app with a \`tenancy\` key has; name the app's database something else (main)`);
299
+ if (names.length !== 1) throw new TypeError(`isai: an app without tenancy has one database (named as you like); it has ${names.length}${names.length > 1 ? `: ${names.join(", ")}. Several aren't supported yet` : ""}`);
300
+ if (Object.values(databases)[0]?.kind !== "database") throw new TypeError("isai: every tenant's own database (durableObject(env.TENANT)) needs a `tenancy` key");
301
+ return;
302
+ }
303
+ if (tenancy.mode !== "isolated") throw new TypeError(`isai: tenancy.mode "${String(tenancy.mode)}" isn't built yet; use "isolated"`);
304
+ if (tenancy.by !== "host") throw new TypeError(`isai: tenancy.by "${String(tenancy.by)}" isn't built yet; use "host"`);
305
+ if (typeof tenancy.platform !== "string" || !HOST.test(tenancy.platform.toLowerCase())) throw new TypeError(`isai: tenancy.platform must be a host, like app.example.com (no https:// or path); got "${String(tenancy.platform)}"`);
306
+ const model = tenancy.tenant;
307
+ if (model?.spec?.name !== "Tenant" || model.spec.builtin !== "tenancy") throw new TypeError("isai: tenancy.tenant is the Tenant model: builtins.Tenant, or its extension (import { Tenant } from \"#db/platform/tenants\")");
308
+ if (databases["tenant"]?.kind !== "tenantDatabases") throw new TypeError("isai: with isolated tenancy, databases.tenant is each tenant's database: durableObject(env.TENANT)");
309
+ if (databases["platform"]?.kind !== "database") throw new TypeError("isai: an app with tenancy needs databases.platform, where the Tenant and Domain tables live");
310
+ const other = names.find((name) => name !== "tenant" && name !== "platform");
311
+ if (other) throw new TypeError(`isai: with tenancy, the databases are tenant and platform; databases.${other} isn't supported yet`);
312
+ }
313
+ //#endregion
281
314
  //#region src/internal/secret-box.ts
282
315
  /**
283
316
  * Encrypted values for `secret` fields: AES-256-GCM through Web Crypto, on every runtime.
@@ -3528,4 +3561,4 @@ async function migrateFresh(driver, models, options = {}) {
3528
3561
  await sync(driver, manifest, options);
3529
3562
  }
3530
3563
  //#endregion
3531
- export { Query as A, modelOfRow as B, database as C, planInsert as D, recordAction as E, systemClock as F, UniqueFailed as G, DatabaseError as H, newSecretKey as I, NeedsDatabase as L, dialectFor as M, sqlite as N, planUpdate as O, fixedClock as P, evaluate as R, clockOf as S, forUpkeep as T, NotFound as U, CheckFailed as V, ReferenceFailed as W, 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, conditionOf as j, keyCondition as k, setReleaseStatus as l, emptySnapshot as m, migrateFresh as n, holdOf as o, generateMigration as p, sync as r, releaseLock as s, migrate as t, startRelease as u, dataStep as v, decodeStored as w, Transaction as x, MigrationError as y, utc as z };
3564
+ export { Query as A, NeedsDatabase as B, database as C, planInsert as D, recordAction as E, systemClock as F, DatabaseError as G, utc as H, newSecretKey as I, UniqueFailed as J, NotFound as K, PLATFORM as L, dialectFor as M, sqlite as N, planUpdate as O, fixedClock as P, TENANT as R, clockOf as S, forUpkeep as T, modelOfRow as U, evaluate as V, CheckFailed as W, 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, conditionOf as j, keyCondition as k, setReleaseStatus as l, emptySnapshot as m, migrateFresh as n, holdOf as o, generateMigration as p, ReferenceFailed as q, sync as r, releaseLock as s, migrate as t, startRelease as u, dataStep as v, decodeStored as w, Transaction as x, MigrationError as y, checkTenancy as z };
@@ -1,4 +1,4 @@
1
- import { D as nodesOf, U as isRecorded, a as columnName, f as Condition, m as Expr, u as rowOf, x as conditionNode } from "./base64url-K_wCmvxR.js";
1
+ import { D as nodesOf, U as isRecorded, a as columnName, f as Condition, m as Expr, u as rowOf, x as conditionNode } from "./base64url-C5GTYIL5.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 { B as toKind, R as object, V as validate, W as ValidationError } from "./validation-FyOpSZqE.js";
2
- import { G as UniqueFailed, U as NotFound, V as CheckFailed, W as ReferenceFailed } from "./migrator-6oWUefLA.js";
3
- import { a as columnName, b as asCondition, j as IMPLICIT_FIELDS, l as computedFieldOf, m as Expr, p as Embed } from "./base64url-K_wCmvxR.js";
4
- import { r as actionsOf } from "./policies-pFzslmSB.js";
1
+ import { G as ValidationError, H as validate, V as toKind, z as object } from "./validation-Cppmd0RV.js";
2
+ import { J as UniqueFailed, K as NotFound, W as CheckFailed, q as ReferenceFailed } from "./migrator-BU3_PHk8.js";
3
+ import { a as columnName, b as asCondition, j as IMPLICIT_FIELDS, l as computedFieldOf, m as Expr, p as Embed } from "./base64url-C5GTYIL5.js";
4
+ import { r as actionsOf } from "./policies-CKIdBFKt.js";
5
5
  import { n as RESERVED, t as LOOKUPS } from "./syntax-CrUZFrS7.js";
6
6
  //#region src/api/fail.ts
7
7
  /**
@@ -1,4 +1,4 @@
1
- import { r as Model, s as defineModel } from "./base64url-K_wCmvxR.js";
1
+ import { r as Model, s as defineModel } from "./base64url-C5GTYIL5.js";
2
2
  //#region src/db/schema/builtins.ts
3
3
  /**
4
4
  * Built-in tables: sets of models a part of isai owns, switched on
@@ -1,5 +1,5 @@
1
1
  import { a as DatabaseDriver, g as Storage } from "../index-Ce5p2-EB.js";
2
- import { I as Clock, L as FixedClock, R as fixedClock, V as AnyModel, i as Row, l as Overrides, p as Database, s as Makers, u as Seed } from "../types-DpOcIuDl.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-S6zF5IXV.js";
3
3
  //#region src/testing/conformance/database.d.ts
4
4
  /**
5
5
  * Makes the driver under test. Every call must return a driver on an **empty** database:
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.js";
2
- import { M as dialectFor, P as fixedClock, a as currentRelease, b as Database, c as rollback, i as acquireLock, k 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 utc } from "../migrator-6oWUefLA.js";
3
- import { n as toBase64url, r as Model } from "../base64url-K_wCmvxR.js";
4
- import { t as createDataLayer } from "../data-layer-D9UPOK6L.js";
5
- import { S as seedAs, i as makers, m as forgetGoogleKeys, n as TENANT, o as authOf, t as PLATFORM, u as GOOGLE } from "../config-0PGFbJh6.js";
2
+ import { H as utc, L as PLATFORM, M as dialectFor, P as fixedClock, R as TENANT, a as currentRelease, b as Database, c as rollback, i as acquireLock, k as keyCondition, l as setReleaseStatus, m as emptySnapshot, n as migrateFresh, o as holdOf, p as generateMigration, r as sync, s as releaseLock, t as migrate, u as startRelease, v as dataStep, y as MigrationError } from "../migrator-BU3_PHk8.js";
3
+ import { n as toBase64url, r as Model } from "../base64url-C5GTYIL5.js";
4
+ import { t as createDataLayer } from "../data-layer-_TZtuy-F.js";
5
+ import { d as forgetGoogleKeys, r as authOf, s as GOOGLE, t as makers, y as seedAs } from "../makers-DeSNM0Nd.js";
6
6
  import { describe, expect, it } from "vitest";
7
7
  //#region src/testing/conformance/database.ts
8
8
  /**
@@ -1,5 +1,5 @@
1
1
  import { a as DatabaseDriver, f as Row$1, h as Sql, o as DialectName } from "./index-Ce5p2-EB.js";
2
- import { E as list, G as FileType, K as ImageType, M as IdInput, T as Shape, at as Measure, et as AnyKind, g as KindDefinition, h as Definable, it as KindTypes, ot as Sensitivity, q as StoredFile, ut as StandardProps, w as Output, x as Input$2 } from "./index-C-WmIKxT.js";
2
+ import { E as list, G as FileType, M as IdInput, T as Shape, Y as StoredFile, ct as Sensitivity, ft as StandardProps, g as KindDefinition, h as Definable, nt as AnyKind, ot as KindTypes, q as ImageType, st as Measure, w as Output, x as Input$2 } from "./index-CYcklQ1l.js";
3
3
  //#region src/db/fields/slot.d.ts
4
4
  interface SlotSpec {
5
5
  /** Namespaced by module: `auth.owner`. */
@@ -1,3 +1,3 @@
1
1
  import { a as ValidationError, i as Result, n as IssueCode, r as Path, t as Issue } from "../issues-CKto95IW.js";
2
- import { $ as FakeContext, A as validate, B as money, C as ObjectShape, D as object, E as list, F as datetime, G as FileType, H as sequence, I as fakeUlid, J as file, K as ImageType, L as flag, M as IdInput, N as choice, O as serialize, P as date, Q as Fake, R as id, S as LIST_MAX, T as Shape, U as phone, V as quantity, W as phoneKind, X as image, Y as files, Z as images, _ as define, a as newToken, at as Measure, b as CarriesKind, c as slug, ct as kind, d as text, dt as StandardResult, et as AnyKind, f as textKind, ft as StandardSchemaV1, g as KindDefinition, h as Definable, i as markdown, it as KindTypes, j as CalendarDate, k as toKind, l as slugify, lt as StandardIssue, m as url, n as host, nt as Kind, o as personName, ot as Sensitivity, p as token, q as StoredFile, r as line, rt as KindSpec, s as secret, st as TypeText, t as email, tt as FileSpec, u as subdomain, ut as StandardProps, v as json, w as Output, x as Input, y as urls, z as int } from "../index-C-WmIKxT.js";
3
- export { type AnyKind, type CalendarDate, type CarriesKind, type Definable, Fake, type FakeContext, type FileSpec, type FileType, type IdInput, type ImageType, type Input, type Issue, type IssueCode, Kind, type KindDefinition, type KindSpec, type KindTypes, LIST_MAX, type Measure, type ObjectShape, type Output, type Path, type Result, type Sensitivity, type Shape, type StandardIssue, type StandardProps, type StandardResult, type StandardSchemaV1, type StoredFile, type TypeText, ValidationError, choice, date, datetime, define, email, fakeUlid, file, files, flag, host, id, image, images, int, json, kind, line, list, markdown, money, newToken, object, personName, phone, phoneKind, quantity, secret, sequence, serialize, slug, slugify, subdomain, text, textKind, toKind, token, url, urls, validate };
2
+ import { $ as images, A as validate, B as money, C as ObjectShape, D as object, E as list, F as datetime, G as FileType, H as sequence, I as fakeUlid, J as SIZES_MAX, K as ImageSize, L as flag, M as IdInput, N as choice, O as serialize, P as date, Q as image, R as id, S as LIST_MAX, T as Shape, U as phone, V as quantity, W as phoneKind, X as file, Y as StoredFile, Z as files, _ as define, a as newToken, at as KindSpec, b as CarriesKind, c as slug, ct as Sensitivity, d as text, dt as StandardIssue, et as Fake, f as textKind, ft as StandardProps, g as KindDefinition, h as Definable, i as markdown, it as Kind, j as CalendarDate, k as toKind, l as slugify, lt as TypeText, m as url, mt as StandardSchemaV1, n as host, nt as AnyKind, o as personName, ot as KindTypes, p as token, pt as StandardResult, q as ImageType, r as line, rt as FileSpec, s as secret, st as Measure, t as email, tt as FakeContext, u as subdomain, ut as kind, v as json, w as Output, x as Input, y as urls, z as int } from "../index-CYcklQ1l.js";
3
+ export { type AnyKind, type CalendarDate, type CarriesKind, type Definable, Fake, type FakeContext, type FileSpec, type FileType, type IdInput, type ImageSize, type ImageType, type Input, type Issue, type IssueCode, Kind, type KindDefinition, type KindSpec, type KindTypes, LIST_MAX, type Measure, type ObjectShape, type Output, type Path, type Result, SIZES_MAX, type Sensitivity, type Shape, type StandardIssue, type StandardProps, type StandardResult, type StandardSchemaV1, type StoredFile, type TypeText, ValidationError, choice, date, datetime, define, email, fakeUlid, file, files, flag, host, id, image, images, int, json, kind, line, list, markdown, money, newToken, object, personName, phone, phoneKind, quantity, secret, sequence, serialize, slug, slugify, subdomain, text, textKind, toKind, token, url, urls, validate };
@@ -1 +1 @@
1
- export { G as Fake, H as Kind, I as LIST_MAX, W as ValidationError, E as choice, D as date, O as datetime, l as define, f as email, k as fakeUlid, i as file, o as files, A as flag, p as host, j as id, s as image, c as images, M as int, u as json, U as kind, m as line, L as list, h as markdown, N as money, g as newToken, R as object, _ as personName, t as phone, n as phoneKind, P as quantity, v as secret, F as sequence, z as serialize, y as slug, b as slugify, x as subdomain, S as text, C as textKind, B as toKind, w as token, T as url, d as urls, V as validate } from "../validation-FyOpSZqE.js";
1
+ export { K as Fake, U as Kind, L as LIST_MAX, i as SIZES_MAX, G as ValidationError, D as choice, O as date, k as datetime, u as define, p as email, A as fakeUlid, a as file, s as files, j as flag, m as host, M as id, c as image, l as images, N as int, d as json, W as kind, h as line, R as list, g as markdown, P as money, _ as newToken, z as object, v as personName, t as phone, n as phoneKind, F as quantity, y as secret, I as sequence, B as serialize, b as slug, x as slugify, S as subdomain, C as text, w as textKind, V as toKind, T as token, E as url, f as urls, H as validate } from "../validation-Cppmd0RV.js";
@@ -1078,6 +1078,8 @@ function define(definition) {
1078
1078
  }
1079
1079
  //#endregion
1080
1080
  //#region src/validation/kinds/files.ts
1081
+ /** At most this many smaller copies of one image. */
1082
+ const SIZES_MAX = 4;
1081
1083
  /** File types by their usual name, with the media types each covers. */
1082
1084
  const FILE_TYPES = {
1083
1085
  pdf: ["application/pdf"],
@@ -1139,7 +1141,21 @@ const reference = object({
1139
1141
  fake: () => "text/plain"
1140
1142
  }),
1141
1143
  width: int().min(1).optional(),
1142
- height: int().min(1).optional()
1144
+ height: int().min(1).optional(),
1145
+ sizes: list(object({
1146
+ key: textKind("fileKey", {
1147
+ max: 1024,
1148
+ fake: (fake) => `fake/${fakeUlid(fake)}`
1149
+ }),
1150
+ size: quantity(),
1151
+ type: textKind("mediaType", {
1152
+ max: 255,
1153
+ normalize: (value) => value.toLowerCase(),
1154
+ fake: () => "image/webp"
1155
+ }),
1156
+ width: int().min(1),
1157
+ height: int().min(1)
1158
+ })).max(4).optional()
1143
1159
  });
1144
1160
  function fileKind(name, spec) {
1145
1161
  const { types, maxBytes } = spec;
@@ -1150,11 +1166,12 @@ function fileKind(name, spec) {
1150
1166
  read: (value, path) => {
1151
1167
  const read = reference.parse(value, path);
1152
1168
  if (!read.ok) return read;
1153
- const { width, height, ...rest } = read.value;
1169
+ const { width, height, sizes, ...rest } = read.value;
1154
1170
  const file = {
1155
1171
  ...rest,
1156
1172
  ...width ? { width } : {},
1157
- ...height ? { height } : {}
1173
+ ...height ? { height } : {},
1174
+ ...sizes?.length ? { sizes } : {}
1158
1175
  };
1159
1176
  const issues = [];
1160
1177
  if (!types.has(file.type)) issues.push({
@@ -1296,4 +1313,4 @@ function phone(options = {}) {
1296
1313
  return phoneKind(() => region);
1297
1314
  }
1298
1315
  //#endregion
1299
- export { flag as A, toKind as B, textKind as C, date as D, choice as E, sequence as F, Fake as G, Kind as H, LIST_MAX as I, fakeValue as K, list as L, int as M, money as N, datetime as O, quantity as P, object as R, text as S, url as T, kind as U, validate as V, ValidationError as W, personName as _, fileTypeNames as a, slugify as b, images as c, urls as d, email as f, newToken as g, markdown as h, file as i, id as j, fakeUlid as k, define as l, line as m, phoneKind as n, files as o, host as p, isPhoneRegion as r, image as s, phone as t, json as u, secret as v, token as w, subdomain as x, slug as y, serialize as z };
1316
+ export { fakeUlid as A, serialize as B, text as C, choice as D, url as E, quantity as F, ValidationError as G, validate as H, sequence as I, Fake as K, LIST_MAX as L, id as M, int as N, date as O, money as P, list as R, subdomain as S, token as T, Kind as U, toKind as V, kind as W, newToken as _, file as a, slug as b, image as c, json as d, urls as f, markdown as g, line as h, SIZES_MAX as i, flag as j, datetime as k, images as l, host as m, phoneKind as n, fileTypeNames as o, email as p, fakeValue as q, isPhoneRegion as r, files as s, phone as t, define as u, personName as v, textKind as w, slugify as x, secret as y, object as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isaijs/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A batteries-included, type-safe TypeScript backend framework. Cloudflare first, no lock-in.",
5
5
  "type": "module",
6
6
  "license": "MIT",