@isaijs/core 0.1.0 → 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/adapters/cache/cloudflare-kv/index.d.ts +1 -1
- package/dist/adapters/database/d1/index.d.ts +1 -1
- package/dist/adapters/database/durable-object/index.d.ts +1 -1
- package/dist/adapters/database/sqlite-node/index.d.ts +1 -1
- package/dist/adapters/mail/gmail/index.d.ts +1 -1
- package/dist/adapters/pdf/cloudflare-browser/index.d.ts +1 -1
- package/dist/adapters/queue/cloudflare/index.d.ts +1 -1
- package/dist/adapters/ratelimit/cloudflare/index.d.ts +1 -1
- package/dist/adapters/ratelimit/memory/index.d.ts +1 -1
- package/dist/adapters/runtime/cloudflare/index.d.ts +1 -1
- package/dist/adapters/runtime/cloudflare/index.js +1 -1
- package/dist/adapters/storage/disk/index.d.ts +1 -1
- package/dist/adapters/storage/memory/index.d.ts +1 -1
- package/dist/adapters/storage/r2/index.d.ts +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +2 -2
- package/dist/{base64url-DwWPqoOT.js → base64url-C5GTYIL5.js} +23 -9
- package/dist/cli/index.js +18 -15
- package/dist/client/index.d.ts +46 -5
- package/dist/client/index.js +156 -8
- package/dist/{create-auth-Byf1-VJw.js → create-auth-Ci0PaBoX.js} +5 -5
- package/dist/{data-layer-BMviRJaW.d.ts → data-layer-D1BFo3wO.d.ts} +29 -12
- package/dist/{data-layer-BNtz40PV.js → data-layer-_TZtuy-F.js} +19 -11
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.js +5 -5
- package/dist/{index-BVCrbMav.d.ts → index-CYcklQ1l.d.ts} +21 -7
- package/dist/{index-BKwTOLop.d.ts → index-Ce5p2-EB.d.ts} +2 -2
- package/dist/{index-CBWFoL3s.d.ts → index-DlgVg3D8.d.ts} +38 -26
- package/dist/index.d.ts +2 -2
- package/dist/index.js +152 -56
- package/dist/{issues-Bxq4JExX.d.ts → issues-CKto95IW.d.ts} +3 -1
- package/dist/{make-DKzPd2b_.js → make-DKBn79cH.js} +17 -2
- package/dist/{config-D6PexpSC.js → makers-DeSNM0Nd.js} +13 -39
- package/dist/{migrator-C65RM_K-.js → migrator-BU3_PHk8.js} +102 -10
- package/dist/{policies-yVzFCjxU.js → policies-CKIdBFKt.js} +3 -3
- package/dist/ports/index.d.ts +1 -1
- package/dist/{resource-9fjxJgmz.js → resource-CsYksjKc.js} +42 -15
- package/dist/{tables-C3Uza4qK.js → tables-IrLjsomf.js} +4 -4
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +6 -6
- package/dist/{types-BFc_NEx8.d.ts → types-S6zF5IXV.d.ts} +15 -12
- package/dist/validation/index.d.ts +3 -3
- package/dist/validation/index.js +1 -1
- package/dist/{validation-l5JG-PFn.js → validation-Cppmd0RV.js} +23 -7
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as DatabaseDriver, f as Row } from "../../../index-
|
|
1
|
+
import { a as DatabaseDriver, f as Row } from "../../../index-Ce5p2-EB.js";
|
|
2
2
|
//#region src/adapters/database/d1/index.d.ts
|
|
3
3
|
/** The parts of a D1 prepared statement this driver uses. */
|
|
4
4
|
export interface D1Statement {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as DatabaseDriver, b as TenantDatabases, f as Row } from "../../../index-
|
|
1
|
+
import { a as DatabaseDriver, b as TenantDatabases, f as Row } from "../../../index-Ce5p2-EB.js";
|
|
2
2
|
//#region src/adapters/database/durable-object/index.d.ts
|
|
3
3
|
/** The parts of `ctx.storage` this driver uses (structurally compatible with `wrangler types`). */
|
|
4
4
|
export interface DurableObjectStorageBinding {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as PdfRenderer } from "../../../index-
|
|
1
|
+
import { l as PdfRenderer } from "../../../index-Ce5p2-EB.js";
|
|
2
2
|
//#region src/adapters/pdf/cloudflare-browser/index.d.ts
|
|
3
3
|
/** A Browser Rendering binding (opaque here; used through @cloudflare/puppeteer). */
|
|
4
4
|
export type BrowserBinding = object;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as RateLimiter } from "../../../index-
|
|
1
|
+
import { d as RateLimiter } from "../../../index-Ce5p2-EB.js";
|
|
2
2
|
//#region src/adapters/ratelimit/cloudflare/index.d.ts
|
|
3
3
|
/** The part of a Rate Limiting binding this uses. */
|
|
4
4
|
export interface RateLimitBinding {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as Row, m as ServiceRef, p as Runtime, v as TaskMessage } from "../../../index-
|
|
1
|
+
import { f as Row, m as ServiceRef, p as Runtime, v as TaskMessage } from "../../../index-Ce5p2-EB.js";
|
|
2
2
|
import { DurableObject } from "cloudflare:workers";
|
|
3
3
|
//#region src/adapters/runtime/cloudflare/index.d.ts
|
|
4
4
|
/** The parts of the Workers platform this runtime uses (structurally compatible with `wrangler types`). */
|
|
@@ -11,7 +11,7 @@ import { DurableObject } from "cloudflare:workers";
|
|
|
11
11
|
*
|
|
12
12
|
* With isolated tenancy, `Tenant` is the Durable Object class each tenant lives in: it opens the
|
|
13
13
|
* tenant from its own storage, migrates it before any request (`blockConcurrencyWhile`), and
|
|
14
|
-
* handles what the front door forwards to it over RPC
|
|
14
|
+
* handles what the front door forwards to it over RPC.
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
17
|
* One failing task never holds back the rest of its batch: each message is acked on
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as Storage } from "../../../index-
|
|
1
|
+
import { g as Storage } from "../../../index-Ce5p2-EB.js";
|
|
2
2
|
//#region src/adapters/storage/memory/index.d.ts
|
|
3
3
|
export interface MemoryStorageOptions {
|
|
4
4
|
/** Where its files would be served from, to make it a public storage. */
|
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
|
|
@@ -73,9 +73,20 @@ const ulid = monotonicUlid();
|
|
|
73
73
|
//#endregion
|
|
74
74
|
//#region src/db/orm/audit.ts
|
|
75
75
|
/**
|
|
76
|
-
* The audit log in the ORM's write path
|
|
77
|
-
*
|
|
78
|
-
*
|
|
76
|
+
* The audit log in the ORM's write path: which writes are recorded, who makes them, and the
|
|
77
|
+
* statements placed around each one in its batch, so a change and its entries commit together (or
|
|
78
|
+
* neither does).
|
|
79
|
+
*
|
|
80
|
+
* Recorded: every ORM write (`…One`, `…Many`, upserts, inside `atomic` too), from routes,
|
|
81
|
+
* resources and services alike; a custom action's entry beside the writes it made (one request id
|
|
82
|
+
* ties them); sign-ins and sign-outs. Not recorded: data steps and the roles synced from `app.ts`
|
|
83
|
+
* (upkeep, nobody edited them), `db.raw`, `reorder` (positions are maintained), rows the database
|
|
84
|
+
* changes itself (cascades, counts, copies follow from a recorded write), reads, and the tables
|
|
85
|
+
* marked `unaudited` (Session, Verification, Upload, the log itself).
|
|
86
|
+
*
|
|
87
|
+
* Why in the batch, not from the post-commit write stream: a second write after commit can be lost
|
|
88
|
+
* while the change stays, and once committed the before values are gone. Why not triggers: a
|
|
89
|
+
* trigger can't see who wrote, and would record the database's own count and copy updates as noise.
|
|
79
90
|
*/
|
|
80
91
|
const actors = new Scope();
|
|
81
92
|
/** Runs `fn` with `actor` as the one every write inside it is recorded as made by. */
|
|
@@ -263,8 +274,11 @@ var Field = class Field {
|
|
|
263
274
|
}
|
|
264
275
|
/**
|
|
265
276
|
* A copy of a related row's value (`.from((s) => s.criterion.maxScore)`), so the row's checks and
|
|
266
|
-
* uniques can use it. The database writes it on create and keeps it equal to its source
|
|
267
|
-
* never written. The path's
|
|
277
|
+
* uniques can use it. The database writes it on create and keeps it equal to its source (triggers
|
|
278
|
+
* on each table along the path, so raw SQL writes are seen too); it's never written. The path's
|
|
279
|
+
* first ref must be `.fixed()`; when every step and the final field are fixed, it's written once
|
|
280
|
+
* and no trigger is made. A plain path to one column only: a derived value is `.computed()`, and a
|
|
281
|
+
* value frozen as it was is a snapshot, not a copy.
|
|
268
282
|
*/
|
|
269
283
|
from(build) {
|
|
270
284
|
if (this.spec.maintained || this.spec.computed || this.spec.generate) throw new TypeError("isai: .from() is for a plain field (not one the database or the framework makes)");
|
|
@@ -331,7 +345,7 @@ function sensitivity(spec) {
|
|
|
331
345
|
//#region src/db/fields/kinds.ts
|
|
332
346
|
/**
|
|
333
347
|
* The built-in field kinds: each is a kind from `isai/validation` with its storage. Kinds own
|
|
334
|
-
* their limits and rules; columns never repeat them
|
|
348
|
+
* their limits and rules; columns never repeat them.
|
|
335
349
|
*/
|
|
336
350
|
function fieldSpec(kind, storage, options) {
|
|
337
351
|
return {
|
|
@@ -429,7 +443,7 @@ function fieldKinds(defaults) {
|
|
|
429
443
|
json: (options) => field(json(options.shape), "json"),
|
|
430
444
|
/** A list, for shapes (`f.json({ shape: f.list(...) })`, route inputs). Not a column itself. */
|
|
431
445
|
list,
|
|
432
|
-
/** A file; `{ storage }` names a storage other than `default
|
|
446
|
+
/** A file; `{ storage }` names a storage other than `default`. */
|
|
433
447
|
file: (options = {}) => field(file(options), "json"),
|
|
434
448
|
image: (options = {}) => field(image(options), "json"),
|
|
435
449
|
files: (options = {}) => field(files(options), "json"),
|
|
@@ -1067,7 +1081,7 @@ var Model = class Model {
|
|
|
1067
1081
|
}
|
|
1068
1082
|
/**
|
|
1069
1083
|
* A rule over the row, written as a condition. It runs in JS on every write; when it reads
|
|
1070
|
-
* more than one field it is also a database guard
|
|
1084
|
+
* more than one field it is also a database guard.
|
|
1071
1085
|
*/
|
|
1072
1086
|
check(name, build) {
|
|
1073
1087
|
assertIdentifier("a check name", name);
|
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 {
|
|
@@ -325,7 +327,7 @@ function json(text, side) {
|
|
|
325
327
|
/**
|
|
326
328
|
* The app's models: every export of every file in `db/schema/` that is a model, plus the built-in
|
|
327
329
|
* tables `db/index.ts` switches on (`use`) that the app doesn't extend. `db/schema/` holds models
|
|
328
|
-
* only
|
|
330
|
+
* only, so there's no list to keep. A model's database is its folder:
|
|
329
331
|
* `db/schema/<database>/` (`tenant/` and `platform/` with tenancy, `main/` or any one name without).
|
|
330
332
|
*/
|
|
331
333
|
const SCHEMA_DIR = "db/schema";
|
|
@@ -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),
|
|
@@ -385,7 +388,7 @@ function databasesOf(models) {
|
|
|
385
388
|
//#endregion
|
|
386
389
|
//#region src/cli/outputs.ts
|
|
387
390
|
/**
|
|
388
|
-
* What handlers return, for the typed client
|
|
391
|
+
* What handlers return, for the typed client: each route's and custom
|
|
389
392
|
* action's TypeScript return type, read with the compiler over the app's own tsconfig, and printed
|
|
390
393
|
* as the JSON the output layer sends: dates (anything with `toJSON`) as what `toJSON` returns,
|
|
391
394
|
* files as `FileJSON`, functions and symbol keys left out, `undefined` keys optional.
|
|
@@ -534,7 +537,7 @@ const key = (name) => IDENTIFIER.test(name) ? name : JSON.stringify(name);
|
|
|
534
537
|
//#region src/cli/client.ts
|
|
535
538
|
/**
|
|
536
539
|
* `isai client` (and `isai dev`, on start and on each change): writes `.isai/client.ts`, the app's
|
|
537
|
-
* typed client
|
|
540
|
+
* typed client; `isai check` fails on a stale one. The app modules (`apps/**`)
|
|
538
541
|
* are loaded as the app loads them, for the exact endpoints, inputs and rows; what their handlers
|
|
539
542
|
* return is read with the TypeScript compiler.
|
|
540
543
|
*/
|
|
@@ -762,7 +765,7 @@ function printPlan(plan, log) {
|
|
|
762
765
|
}
|
|
763
766
|
/**
|
|
764
767
|
* Applies every pending step to the local databases at once: structure, rules, then data steps.
|
|
765
|
-
* No older code is running locally, so nothing waits for a release
|
|
768
|
+
* No older code is running locally, so nothing waits for a release.
|
|
766
769
|
* Returns the steps applied, as `<database>/<step>`.
|
|
767
770
|
*/
|
|
768
771
|
async function migrateLocal(project, databaseFor, log) {
|
|
@@ -792,7 +795,7 @@ async function migrateLocal(project, databaseFor, log) {
|
|
|
792
795
|
//#region src/cli/project.ts
|
|
793
796
|
/**
|
|
794
797
|
* The app the CLI works on: its root, and how its TypeScript is imported: models (`db/schema/`),
|
|
795
|
-
* migration manifests, and the app modules for the typed client
|
|
798
|
+
* migration manifests, and the app modules for the typed client.
|
|
796
799
|
*/
|
|
797
800
|
/** The nearest directory upward from `from` with a `package.json`. */
|
|
798
801
|
function findRoot(from) {
|
|
@@ -838,7 +841,7 @@ const PLATFORM_STUB = [
|
|
|
838
841
|
].join("\n");
|
|
839
842
|
//#endregion
|
|
840
843
|
//#region src/cli/prompt.ts
|
|
841
|
-
/** Asks `makemigrations`' questions in a terminal
|
|
844
|
+
/** Asks `makemigrations`' questions in a terminal. */
|
|
842
845
|
/** A value typed at the prompt: JSON when it reads as JSON (`3`, `true`), the text otherwise. */
|
|
843
846
|
function typedValue(text) {
|
|
844
847
|
try {
|
|
@@ -898,7 +901,7 @@ async function answer(question, prompt) {
|
|
|
898
901
|
/**
|
|
899
902
|
* The `workers` target, from the CLI: the app's `wrangler.jsonc`, its local D1 (the database
|
|
900
903
|
* `wrangler dev` uses), and `wrangler dev` itself. The CLI never imports `app.ts`: the database
|
|
901
|
-
* is the D1 binding whose `migrations_dir` is `migrations/<database
|
|
904
|
+
* is the D1 binding whose `migrations_dir` is `migrations/<database>`.
|
|
902
905
|
*/
|
|
903
906
|
const CONFIG_FILES = ["wrangler.jsonc", "wrangler.json"];
|
|
904
907
|
function configPath(root) {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { t as Issue } from "../issues-
|
|
1
|
+
import { t as Issue } from "../issues-CKto95IW.js";
|
|
2
2
|
//#region src/client/types.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The types a generated client (`.isai/client.ts`) is written in: pages, list queries with the
|
|
5
5
|
* lookups each kind takes, files as JSON, and the auth module's endpoints. Values are their JSON
|
|
6
6
|
* forms: dates are ISO strings, money integer minor units, refs ids.
|
|
7
7
|
*/
|
|
8
|
-
/** A list's page
|
|
8
|
+
/** A list's page; `page`, `pages` and `count` when asked. */
|
|
9
9
|
interface Page<T> {
|
|
10
10
|
readonly data: T[];
|
|
11
11
|
readonly next: string | null;
|
|
@@ -62,7 +62,7 @@ interface HistoryQuery {
|
|
|
62
62
|
readonly after?: string;
|
|
63
63
|
readonly before?: string;
|
|
64
64
|
}
|
|
65
|
-
/** One audit entry on a row
|
|
65
|
+
/** One audit entry on a row. */
|
|
66
66
|
interface HistoryEntry {
|
|
67
67
|
readonly id: string;
|
|
68
68
|
readonly createdAt: string;
|
|
@@ -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> {
|
|
@@ -151,8 +157,10 @@ interface ResourceEndpoint {
|
|
|
151
157
|
/** `read`, `create`, `update`, `delete`, trait actions (`publish`, `reorder`), `history`. */
|
|
152
158
|
readonly actions: readonly string[];
|
|
153
159
|
readonly custom?: readonly string[];
|
|
154
|
-
/** Its writable file fields: `upload(field, file)` sends a file to attach by id
|
|
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 {
|
|
@@ -181,7 +189,7 @@ export declare function listQuery(query: ListQuery<Readonly<Record<string, unkno
|
|
|
181
189
|
//#endregion
|
|
182
190
|
//#region src/client/errors.d.ts
|
|
183
191
|
/**
|
|
184
|
-
* A failed call: the API's one error shape
|
|
192
|
+
* A failed call: the API's one error shape,
|
|
185
193
|
* `{ error: { code, message, … }, requestId }`, with its status.
|
|
186
194
|
*/
|
|
187
195
|
export declare class ApiError extends Error {
|
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as kebabCase } from "../case-BnpcvhdW.js";
|
|
2
2
|
//#region src/client/errors.ts
|
|
3
3
|
/**
|
|
4
|
-
* A failed call: the API's one error shape
|
|
4
|
+
* A failed call: the API's one error shape,
|
|
5
5
|
* `{ error: { code, message, … }, requestId }`, with its status.
|
|
6
6
|
*/
|
|
7
7
|
var ApiError = class extends Error {
|
|
@@ -33,15 +33,130 @@ 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
|
-
* The client's runtime
|
|
153
|
+
* The client's runtime: plain async calls over `fetch`, built from the endpoint
|
|
39
154
|
* table a generated client (`.isai/client.ts`) hands it. It holds no cache: a frontend wraps the
|
|
40
155
|
* calls in what it uses (TanStack Query, nanostores).
|
|
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
|
}
|
|
@@ -179,7 +309,7 @@ function pageQuery(query) {
|
|
|
179
309
|
...query.before === void 0 ? {} : { before: query.before }
|
|
180
310
|
};
|
|
181
311
|
}
|
|
182
|
-
/** The auth module's endpoints
|
|
312
|
+
/** The auth module's endpoints. */
|
|
183
313
|
function authApi(call, base) {
|
|
184
314
|
const person = (id) => `/auth/users/${segment(id)}`;
|
|
185
315
|
return {
|
|
@@ -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",
|
|
@@ -15,7 +15,7 @@ const authPerms = {
|
|
|
15
15
|
invite: "auth.invite",
|
|
16
16
|
/** See the people with accounts, give and take their roles, suspend them. */
|
|
17
17
|
manageUsers: "auth.manageUsers",
|
|
18
|
-
/** Read the whole audit log (`GET /api/audit`), with the log on
|
|
18
|
+
/** Read the whole audit log (`GET /api/audit`), with the log on. */
|
|
19
19
|
readAudit: "audit.read"
|
|
20
20
|
};
|
|
21
21
|
var Auth = class {
|
|
@@ -31,7 +31,7 @@ var Auth = class {
|
|
|
31
31
|
/**
|
|
32
32
|
* Gives `email` a role from `app.ts`, from code (a tenant's setup service, a seed): now, if it has
|
|
33
33
|
* an account in the current database, else when it first signs in. Safe to repeat.
|
|
34
|
-
* `held: "platform"` puts the assignment beyond the tenant's reach
|
|
34
|
+
* `held: "platform"` puts the assignment beyond the tenant's reach.
|
|
35
35
|
*/
|
|
36
36
|
invite(input) {
|
|
37
37
|
if (!this.invites) return Promise.reject(/* @__PURE__ */ new Error("isai: auth.invite is used before the runtime started the app"));
|