@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.
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +2 -2
- package/dist/{base64url-K_wCmvxR.js → base64url-C5GTYIL5.js} +1 -1
- package/dist/cli/index.js +11 -8
- package/dist/client/index.d.ts +41 -0
- package/dist/client/index.js +153 -5
- package/dist/{create-auth-y8E8cxkn.js → create-auth-Ci0PaBoX.js} +3 -3
- package/dist/{data-layer-ubPfZvFV.d.ts → data-layer-D1BFo3wO.d.ts} +11 -3
- package/dist/{data-layer-D9UPOK6L.js → data-layer-_TZtuy-F.js} +13 -7
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +5 -5
- package/dist/{index-C-WmIKxT.d.ts → index-CYcklQ1l.d.ts} +16 -1
- package/dist/{index-B6ipokoP.d.ts → index-DlgVg3D8.d.ts} +6 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +113 -34
- package/dist/{make-WQpkYW8N.js → make-DKBn79cH.js} +1 -1
- package/dist/{config-0PGFbJh6.js → makers-DeSNM0Nd.js} +4 -37
- package/dist/{migrator-6oWUefLA.js → migrator-BU3_PHk8.js} +36 -3
- package/dist/{policies-pFzslmSB.js → policies-CKIdBFKt.js} +1 -1
- package/dist/{resource-C17NvNa3.js → resource-CsYksjKc.js} +4 -4
- package/dist/{tables-C4ipMFjg.js → tables-IrLjsomf.js} +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +4 -4
- package/dist/{types-DpOcIuDl.d.ts → types-S6zF5IXV.d.ts} +1 -1
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +1 -1
- package/dist/{validation-FyOpSZqE.js → validation-Cppmd0RV.js} +21 -4
- package/package.json +1 -1
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { r as AuthTablesOptions } from "../data-layer-
|
|
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-
|
|
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 };
|
package/dist/auth/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as Policy } from "../policies-
|
|
2
|
-
import { a as createAuth, t as Auth } from "../create-auth-
|
|
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
|
|
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 {
|
|
3
|
-
import { M as dialectFor, m as emptySnapshot, r as sync, t as migrate, y as MigrationError } from "../migrator-
|
|
4
|
-
import { a as lookupsOf, i as filterableOf, n as isRequired, t as Resource, u as Route } from "../resource-
|
|
5
|
-
import { F as sensitivity, c as fieldOf, r as Model } from "../base64url-
|
|
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-
|
|
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
|
|
354
|
-
if (
|
|
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),
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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-
|
|
2
|
-
import { n as PROVIDERS } from "./tables-
|
|
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-
|
|
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,
|
|
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-
|
|
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:
|
|
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 {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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
|
-
|
|
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: {
|
package/dist/db/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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 {
|
|
2
|
-
import { A as Query, C as database, F as systemClock, G as
|
|
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-
|
|
4
|
-
import { i as slot, n as row, r as Slot, t as createDataLayer } from "../data-layer-
|
|
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-
|
|
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 {
|
|
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,
|
|
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-
|
|
4
|
-
import { a as PROVIDERS, i as HELD, k as Manifest } from "./data-layer-
|
|
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-
|
|
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 {
|
|
2
|
-
import { B as
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
10
|
-
import { i as checkRoles, n as authPerms, r as checkAuth } from "./create-auth-
|
|
11
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 (
|
|
1288
|
+
await Promise.all(dropped.flatMap((file) => keysOf(file).map(async (key) => {
|
|
1268
1289
|
try {
|
|
1269
|
-
await storages[file.storage]?.delete(
|
|
1290
|
+
await storages[file.storage]?.delete(key);
|
|
1270
1291
|
} catch (error) {
|
|
1271
|
-
console.error(`isai: couldn't remove ${
|
|
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
|
-
|
|
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) =>
|
|
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-
|
|
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 {
|
|
2
|
-
import { D as planInsert, E as recordAction, S as clockOf, T as forUpkeep,
|
|
3
|
-
import { j as IMPLICIT_FIELDS, n as toBase64url, r as Model, t as fromBase64url } from "./base64url-
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
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,
|
|
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-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { a as columnName, b as asCondition, j as IMPLICIT_FIELDS, l as computedFieldOf, m as Expr, p as Embed } from "./base64url-
|
|
4
|
-
import { r as actionsOf } from "./policies-
|
|
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
|
/**
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as DatabaseDriver, g as Storage } from "../index-Ce5p2-EB.js";
|
|
2
|
-
import { I as Clock, L as FixedClock, R as fixedClock, V as AnyModel, i as Row, l as Overrides, p as Database, s as Makers, u as Seed } from "../types-
|
|
2
|
+
import { I as Clock, L as FixedClock, R as fixedClock, V as AnyModel, i as Row, l as Overrides, p as Database, s as Makers, u as Seed } from "../types-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:
|
package/dist/testing/index.js
CHANGED
|
@@ -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
|
|
3
|
-
import { n as toBase64url, r as Model } from "../base64url-
|
|
4
|
-
import { t as createDataLayer } from "../data-layer-
|
|
5
|
-
import {
|
|
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,
|
|
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
|
|
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 };
|
package/dist/validation/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
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 {
|
|
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 };
|