@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,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 {
|
|
@@ -16,15 +16,32 @@ var CheckFailed = class extends DatabaseError {
|
|
|
16
16
|
this.check = check;
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* A unique field (or combination) already has this value. `fields` are the unique's own; `issues`
|
|
21
|
+
* put one on each for a form, except that a field made from another (a slug from its title) puts
|
|
22
|
+
* it on that one, the field a person typed.
|
|
23
|
+
*/
|
|
20
24
|
var UniqueFailed = class extends DatabaseError {
|
|
21
25
|
model;
|
|
22
26
|
fields;
|
|
23
27
|
name = "UniqueFailed";
|
|
28
|
+
issues;
|
|
24
29
|
constructor(model, fields, options) {
|
|
25
30
|
super(`${model}: ${fields.join(", ")} must be unique`, options);
|
|
26
31
|
this.model = model;
|
|
27
32
|
this.fields = fields;
|
|
33
|
+
this.issues = fields.map((field) => {
|
|
34
|
+
const source = options?.from?.[field];
|
|
35
|
+
return source === void 0 ? {
|
|
36
|
+
path: [field],
|
|
37
|
+
code: "taken",
|
|
38
|
+
message: "Already taken"
|
|
39
|
+
} : {
|
|
40
|
+
path: [source],
|
|
41
|
+
code: "taken",
|
|
42
|
+
message: "Already taken, or too like one that is"
|
|
43
|
+
};
|
|
44
|
+
});
|
|
28
45
|
}
|
|
29
46
|
};
|
|
30
47
|
/** A ref points at a row that doesn't exist, or a delete is restricted by rows that reference it. */
|
|
@@ -261,6 +278,39 @@ function evaluate(node, row, now, zone = utc) {
|
|
|
261
278
|
}
|
|
262
279
|
}
|
|
263
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
|
|
264
314
|
//#region src/internal/secret-box.ts
|
|
265
315
|
/**
|
|
266
316
|
* Encrypted values for `secret` fields: AES-256-GCM through Web Crypto, on every runtime.
|
|
@@ -947,6 +997,20 @@ const audit = {
|
|
|
947
997
|
* Migration operations and rules as SQLite DDL. Tables hold only what can't live elsewhere:
|
|
948
998
|
* columns (nullable; "required" is a rule in code), the primary key and foreign keys. Uniques
|
|
949
999
|
* are separate indexes and guards are triggers, so changing either never rebuilds a table.
|
|
1000
|
+
*
|
|
1001
|
+
* Where each rule runs (one-field rules in code, everything else in the database):
|
|
1002
|
+
*
|
|
1003
|
+
* required, a kind's limits and format, choices JS only, in the ORM's write path
|
|
1004
|
+
* unique a unique index (concurrent writes both see "free")
|
|
1005
|
+
* ref a foreign key, and an index
|
|
1006
|
+
* a number's limits (≥ 0, a narrowed min / max) a guard, on updates only: `stock - 2` is SQL's
|
|
1007
|
+
* rules over counts and `.from()` copies a guard: the database changes them
|
|
1008
|
+
* a `.check()` over several columns a guard: a partial update needs the stored values
|
|
1009
|
+
*
|
|
1010
|
+
* A guard is a trigger that raises (`RAISE(ABORT, 'isai:check:<model>.<rule>')`, parsed into the
|
|
1011
|
+
* same `CheckFailed` JS gives), because a D1 batch rolls back only on an error. One insert trigger
|
|
1012
|
+
* per table; update triggers are scoped to the columns their rules read (`BEFORE UPDATE OF …`).
|
|
1013
|
+
* Writers outside the ORM (raw SQL, the D1 console) skip the one-field rules; guards still apply.
|
|
950
1014
|
*/
|
|
951
1015
|
const TYPES = {
|
|
952
1016
|
text: "TEXT",
|
|
@@ -1293,7 +1357,15 @@ function translate(error, dialect, models) {
|
|
|
1293
1357
|
case "unique": {
|
|
1294
1358
|
const model = models.find((m) => m.table === violation.table);
|
|
1295
1359
|
const fields = model ? violation.columns.map((c) => fieldAt(model, c)) : violation.columns;
|
|
1296
|
-
|
|
1360
|
+
const from = {};
|
|
1361
|
+
for (const field of fields) {
|
|
1362
|
+
const source = model && fieldOf(model, field)?.generate?.from;
|
|
1363
|
+
if (source) from[field] = source;
|
|
1364
|
+
}
|
|
1365
|
+
return new UniqueFailed(model?.name ?? violation.table, fields, {
|
|
1366
|
+
cause: error,
|
|
1367
|
+
from
|
|
1368
|
+
});
|
|
1297
1369
|
}
|
|
1298
1370
|
case "reference": return new ReferenceFailed("A referenced row doesn't exist, or other rows still refer to this one", { cause: error });
|
|
1299
1371
|
default: return error;
|
|
@@ -1925,7 +1997,7 @@ var FindOne = class FindOne extends Maybe {
|
|
|
1925
1997
|
* The write pipeline's first half, in JS, before any SQL: fill implicit and generated fields,
|
|
1926
1998
|
* read every value through its kind (defaults, normalisation, one-field rules), then run the
|
|
1927
1999
|
* checks the write can decide. Rules over values the database holds are guards, enforced in
|
|
1928
|
-
* SQL
|
|
2000
|
+
* SQL.
|
|
1929
2001
|
*/
|
|
1930
2002
|
const MANAGED$1 = new Set(IMPLICIT_FIELDS);
|
|
1931
2003
|
/** The one row of a `traits.singleton()` model has this id, so a second create fails as a duplicate. */
|
|
@@ -2198,9 +2270,29 @@ var DeleteMany = class DeleteMany extends Conditions {
|
|
|
2198
2270
|
//#endregion
|
|
2199
2271
|
//#region src/db/orm/database.ts
|
|
2200
2272
|
/**
|
|
2201
|
-
* The database handle (`app.db` in services). Async everywhere, one API on every driver
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2273
|
+
* The database handle (`app.db` in services). Async everywhere, one API on every driver:
|
|
2274
|
+
* `…One` by a key (one of the model's declared uniques: `{ id }`, `{ email }`, `{ event, user }`),
|
|
2275
|
+
* `…Many` by a condition (chained `.where()`, so policies, filters and client queries combine) or a
|
|
2276
|
+
* list. Rows are plain data: no identity map, no change tracking.
|
|
2277
|
+
*
|
|
2278
|
+
* await db.findOne(Event, { id }).mustExist() // Row, or NotFound (a 404)
|
|
2279
|
+
* await db.findMany(Event).where((e) => e.startsAt.after(now)).order("startsAt").limit(20)
|
|
2280
|
+
* await db.updateOne(Variant, { id }, { stock: (v) => v.stock.minus(2) }) // an SQL expression
|
|
2281
|
+
* await db.upsertOne(Registration, { event, user }, { create: { … }, update: { … } })
|
|
2282
|
+
* await app.db.atomic((tx) => { const order = tx.createOne(Order, { … }); tx.createOne(Line, { order, … }) })
|
|
2283
|
+
*
|
|
2284
|
+
* - Absence isn't an error: `findOne`, `updateOne`, `deleteOne` give `undefined` unless
|
|
2285
|
+
* `.mustExist()`. A refused write always throws its typed error.
|
|
2286
|
+
* - Conflicts belong to `upsert…`: `createOne` / `createMany` always fail on a duplicate.
|
|
2287
|
+
* - `updateMany` / `deleteMany` refuse to run without a `.where()` (every row is
|
|
2288
|
+
* `.where(() => all())`), and take no order or limit (SQLite has no `UPDATE … LIMIT`).
|
|
2289
|
+
* - No `.select()`: a row is always the model's full row. Shaping output is a resource's job,
|
|
2290
|
+
* hiding sensitive values the output layer's.
|
|
2291
|
+
* - `now` (and `now.plus("8d")`) is a value in writes, resolved once per write or `atomic`, from the
|
|
2292
|
+
* framework's clock (which tests set), so every row in it gets the same instant.
|
|
2293
|
+
* - `atomic` is write-only on every driver: D1 can't run an interactive transaction, and a rule
|
|
2294
|
+
* that depended on the driver would make app code depend on the target. Read before it; enforce
|
|
2295
|
+
* rules with constraints (`quantity` ≥ 0), counters (`f.count`) or conditional writes.
|
|
2204
2296
|
*/
|
|
2205
2297
|
function isRawValue(value) {
|
|
2206
2298
|
return value === null || typeof value === "string" || typeof value === "number" || value instanceof Uint8Array;
|
|
@@ -2484,7 +2576,7 @@ function forUpkeep(db) {
|
|
|
2484
2576
|
return db[UPKEEP]();
|
|
2485
2577
|
}
|
|
2486
2578
|
/**
|
|
2487
|
-
* An action on a row, in the audit log (`markPaid`, `signIn
|
|
2579
|
+
* An action on a row, in the audit log (`markPaid`, `signIn`): who took it, from
|
|
2488
2580
|
* the request, unless `user` says (a sign-in, before the request has a user). Nothing when the
|
|
2489
2581
|
* model isn't recorded. Internal, like `forUpkeep`.
|
|
2490
2582
|
*/
|
|
@@ -3469,4 +3561,4 @@ async function migrateFresh(driver, models, options = {}) {
|
|
|
3469
3561
|
await sync(driver, manifest, options);
|
|
3470
3562
|
}
|
|
3471
3563
|
//#endregion
|
|
3472
|
-
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,7 +1,7 @@
|
|
|
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
|
-
* Checking a policy
|
|
4
|
+
* Checking a policy: its action's condition, with the
|
|
5
5
|
* placeholders bound for this request, then simplified so constant parts drop out:
|
|
6
6
|
*
|
|
7
7
|
* anyone → true
|
|
@@ -236,7 +236,7 @@ async function bindFor(node, policies, model, action, asker) {
|
|
|
236
236
|
//#endregion
|
|
237
237
|
//#region src/auth/policies.ts
|
|
238
238
|
/**
|
|
239
|
-
* Policies
|
|
239
|
+
* Policies: who may do what to a model's rows, one policy per model, each
|
|
240
240
|
* action a condition. Every action is a permission, keyed `<model in camelCase>.<action>`
|
|
241
241
|
* (`product.publish`), which roles in `app.ts` list. A model's policy always has `read`,
|
|
242
242
|
* `create`, `update` and `delete`, `byRole` unless it says otherwise.
|
package/dist/ports/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as StoredObject, a as DatabaseDriver, b as TenantDatabases, c as MailMessage, d as RateLimiter, f as Row, g as Storage, h as Sql, i as DatabaseCapabilities, l as PdfRenderer, m as ServiceRef, n as Body, o as DialectName, p as Runtime, r as Cache, s as MailDriver, t as AppCore, u as Queue, v as TaskMessage, y as TenantCore } from "../index-
|
|
1
|
+
import { _ as StoredObject, a as DatabaseDriver, b as TenantDatabases, c as MailMessage, d as RateLimiter, f as Row, g as Storage, h as Sql, i as DatabaseCapabilities, l as PdfRenderer, m as ServiceRef, n as Body, o as DialectName, p as Runtime, r as Cache, s as MailDriver, t as AppCore, u as Queue, v as TaskMessage, y as TenantCore } from "../index-Ce5p2-EB.js";
|
|
2
2
|
export { AppCore, Body, Cache, DatabaseCapabilities, DatabaseDriver, DialectName, MailDriver, MailMessage, PdfRenderer, Queue, RateLimiter, Row, Runtime, ServiceRef, Sql, Storage, StoredObject, TaskMessage, TenantCore, TenantDatabases };
|
|
@@ -1,12 +1,12 @@
|
|
|
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
|
/**
|
|
8
8
|
* `fail(code, message)`: the app's own business rules, for what can't be a `.check()`, a flow or a
|
|
9
|
-
* unique. Always a 409; app code never picks a status
|
|
9
|
+
* unique. Always a 409; app code never picks a status.
|
|
10
10
|
*/
|
|
11
11
|
var Rejected = class extends Error {
|
|
12
12
|
code;
|
|
@@ -24,8 +24,25 @@ function fail(code, message) {
|
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/api/errors.ts
|
|
26
26
|
/**
|
|
27
|
-
* The one error format
|
|
28
|
-
*
|
|
27
|
+
* The one error format: `{ error: { code, message, … }, requestId }`. `code` is stable and
|
|
28
|
+
* machine-readable, `message` is for people, and typed errors add their own fields (`issues`,
|
|
29
|
+
* `model`, `check`, `fields`, `action`). `requestId` is also the `X-Request-Id` header.
|
|
30
|
+
*
|
|
31
|
+
* The status comes from the error; app code never picks one:
|
|
32
|
+
*
|
|
33
|
+
* 400 ValidationError input, or a write, fails its kinds (`issues`, each with its path)
|
|
34
|
+
* 401 SignInRequired no session, or an anonymous request a policy denies
|
|
35
|
+
* 403 NotAllowed a row the user can read, without the action
|
|
36
|
+
* 403 CrossSiteRequest a cookie session's write from a site the app doesn't trust
|
|
37
|
+
* 404 NotFound `.mustExist()`, a row the user can't read, an unknown path or host
|
|
38
|
+
* 405 MethodNotAllowed a known path with another method (`Allow`)
|
|
39
|
+
* 409 UniqueFailed (`fields`, and `issues` for a form), CheckFailed, ReferenceFailed, or the
|
|
40
|
+
* app's own code from `fail`
|
|
41
|
+
* 411/413/415 uploads without a size, too large, of a refused type; a body over
|
|
42
|
+
* the JSON limit (413) or not JSON (415)
|
|
43
|
+
* 429 TooManyRequests anonymous writes past the per-IP limit (`Retry-After`)
|
|
44
|
+
* 503 TenantSuspended a suspended tenant
|
|
45
|
+
* 500 InternalError anything else: logged with its stack as one JSON line, nothing sent
|
|
29
46
|
*/
|
|
30
47
|
/** An error the pipeline raises itself, already carrying its status and code. */
|
|
31
48
|
var HttpError = class extends Error {
|
|
@@ -47,7 +64,7 @@ const methodNotAllowed = (allowed) => new HttpError(405, "MethodNotAllowed", "Th
|
|
|
47
64
|
const tooLarge = (limit) => new HttpError(413, "TooLarge", `The body must be at most ${limit / 1024 / 1024} MB`);
|
|
48
65
|
/** Over the per-IP limit for anonymous writes. */
|
|
49
66
|
const tooManyRequests = () => new HttpError(429, "TooManyRequests", "Too many requests; try again in a minute", { "Retry-After": "60" });
|
|
50
|
-
/** A tenant a platform admin suspended: its data is kept, and it answers nothing
|
|
67
|
+
/** A tenant a platform admin suspended: its data is kept, and it answers nothing. */
|
|
51
68
|
const tenantSuspended = () => new HttpError(503, "TenantSuspended", "This site is suspended");
|
|
52
69
|
const lengthRequired = () => new HttpError(411, "LengthRequired", "Send the file's size (Content-Length)");
|
|
53
70
|
const fileTooLarge = (maxBytes) => new HttpError(413, "TooLarge", `The file must be at most ${maxBytes / 1024 / 1024} MB`);
|
|
@@ -85,7 +102,8 @@ function describe(error) {
|
|
|
85
102
|
code: "UniqueFailed",
|
|
86
103
|
message: error.message,
|
|
87
104
|
model: error.model,
|
|
88
|
-
fields: error.fields
|
|
105
|
+
fields: error.fields,
|
|
106
|
+
issues: error.issues
|
|
89
107
|
}
|
|
90
108
|
};
|
|
91
109
|
if (error instanceof CheckFailed) return {
|
|
@@ -170,7 +188,7 @@ function failure(error, requestId, place) {
|
|
|
170
188
|
const QUERY_METHODS = /* @__PURE__ */ new Set(["GET", "DELETE"]);
|
|
171
189
|
/**
|
|
172
190
|
* One declared route. The pipeline finds these among an app module's exports. `O` is what its
|
|
173
|
-
* handler returns, awaited: the typed client reads it
|
|
191
|
+
* handler returns, awaited: the typed client reads it.
|
|
174
192
|
*/
|
|
175
193
|
var Route = class {
|
|
176
194
|
method;
|
|
@@ -209,8 +227,12 @@ const route = {
|
|
|
209
227
|
//#endregion
|
|
210
228
|
//#region src/api/input.ts
|
|
211
229
|
/**
|
|
212
|
-
* `input
|
|
213
|
-
*
|
|
230
|
+
* `input`: the JSON body on writes, the query string on GET / DELETE, validated by the endpoint's
|
|
231
|
+
* kinds in their JSON forms (money in minor units, dates in ISO 8601, refs by id). Unknown keys are
|
|
232
|
+
* issues, never dropped, so a client's typo doesn't pass silently.
|
|
233
|
+
*
|
|
234
|
+
* One source per method: on writes the query string belongs to the framework, never `input`.
|
|
235
|
+
* `params` are strings typed from the path and aren't validated: a malformed id finds no row (404).
|
|
214
236
|
*/
|
|
215
237
|
/** The largest JSON body a request may send. Uploads have their own path. */
|
|
216
238
|
const BODY_LIMIT = 1048576;
|
|
@@ -295,7 +317,7 @@ async function readText(request) {
|
|
|
295
317
|
//#endregion
|
|
296
318
|
//#region src/resources/query.ts
|
|
297
319
|
/**
|
|
298
|
-
* A resource list's query syntax
|
|
320
|
+
* A resource list's query syntax: Django's lookups,
|
|
299
321
|
* as Django REST framework and django-filter take them.
|
|
300
322
|
*
|
|
301
323
|
* GET /api/products?category__slug=rice&price__gte=1000&price__lt=5000&onSale&search=basmati
|
|
@@ -513,7 +535,7 @@ function filterFor(field, op, raw) {
|
|
|
513
535
|
//#endregion
|
|
514
536
|
//#region src/resources/resource.ts
|
|
515
537
|
/**
|
|
516
|
-
* `resource(Model, options)
|
|
538
|
+
* `resource(Model, options)`: what the API exposes over a model, a named
|
|
517
539
|
* shape the model's policy always applies to and that can only narrow it. Each resource exported
|
|
518
540
|
* from an app module is mounted at `/api/<export name in kebab-case>` (mount.ts).
|
|
519
541
|
*
|
|
@@ -525,6 +547,11 @@ function filterFor(field, op, raw) {
|
|
|
525
547
|
* custom: { archive },
|
|
526
548
|
* })
|
|
527
549
|
*
|
|
550
|
+
* Models are never exposed directly: adding a model never publishes an API. A resource over a model
|
|
551
|
+
* without a policy denies every request, so declaring one isn't enough to open it. A role-dependent
|
|
552
|
+
* view of the same rows is two resources (`products`, `manageProducts`), never one with hidden
|
|
553
|
+
* fields; the resource *is* the shape, so there's no `?fields=` or `?include=`.
|
|
554
|
+
*
|
|
528
555
|
* A resource is checked when it's declared, against its model: its columns are the model's,
|
|
529
556
|
* its key is unique, `create` covers the required fields, the owner column is never writable, a
|
|
530
557
|
* singleton has no key. Its actions are checked against the model's policy when the app loads.
|
|
@@ -552,7 +579,7 @@ function fieldAt(model, node) {
|
|
|
552
579
|
const NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
553
580
|
/**
|
|
554
581
|
* A declared resource, with what the engine reads from it. `C` is its custom actions, as declared:
|
|
555
|
-
* the typed client reads what each returns
|
|
582
|
+
* the typed client reads what each returns.
|
|
556
583
|
*/
|
|
557
584
|
var Resource = class {
|
|
558
585
|
model;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { r as Model, s as defineModel } from "./base64url-
|
|
1
|
+
import { r as Model, s as defineModel } from "./base64url-C5GTYIL5.js";
|
|
2
2
|
//#region src/db/schema/builtins.ts
|
|
3
3
|
/**
|
|
4
|
-
* Built-in tables
|
|
4
|
+
* Built-in tables: sets of models a part of isai owns, switched on
|
|
5
5
|
* in `db/index.ts` with `use: [base.tenancy({ database: "platform" })]`. They're defined in the
|
|
6
6
|
* schema's registry by `createDataLayer`, so they exist without being declared, and refs reach them
|
|
7
7
|
* by name (`f.ref("Tenant")`). An app adds columns to one with `builtins.Tenant.extend({ … })`,
|
|
@@ -77,7 +77,7 @@ function authTables(f, options) {
|
|
|
77
77
|
return new TableSet("auth", options.database, authDefinitions(f, "", false));
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
* The platform's own copy
|
|
80
|
+
* The platform's own copy, in the platform database with
|
|
81
81
|
* `base.tenancy()`: the same tables, prefixed (`PlatformUser`, `PlatformSession`…), since model
|
|
82
82
|
* names are one namespace across the app's databases. Sealed: the platform's are never extended.
|
|
83
83
|
*/
|
|
@@ -164,7 +164,7 @@ function authDefinitions(f, prefix, sealAll) {
|
|
|
164
164
|
user: f.ref(named("User"), { related: "roles" }).cascade(),
|
|
165
165
|
role: f.ref(named("Role"), { related: "holders" }).cascade(),
|
|
166
166
|
/**
|
|
167
|
-
* Held by the platform
|
|
167
|
+
* Held by the platform: the tenant's people, the owner
|
|
168
168
|
* included, can't take it away or suspend its holder.
|
|
169
169
|
*/
|
|
170
170
|
held: f.choice(HELD).optional()
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as DatabaseDriver, g as Storage } from "../index-
|
|
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-
|
|
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-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
|
/**
|
|
@@ -720,7 +720,7 @@ var conformance_exports = /* @__PURE__ */ __exportAll({
|
|
|
720
720
|
//#endregion
|
|
721
721
|
//#region src/testing/database.ts
|
|
722
722
|
/**
|
|
723
|
-
* A database per test
|
|
723
|
+
* A database per test: migrated fresh from the
|
|
724
724
|
* models, filled from a seed, with `make` and `many` for the test's own rows.
|
|
725
725
|
*
|
|
726
726
|
* let t: TestDatabase
|
|
@@ -842,7 +842,7 @@ async function fakeGoogle(client) {
|
|
|
842
842
|
//#endregion
|
|
843
843
|
//#region src/testing/tenant.ts
|
|
844
844
|
/**
|
|
845
|
-
* A tenant per test
|
|
845
|
+
* A tenant per test: a fresh platform database with
|
|
846
846
|
* the tenant's row, and the tenant's own database migrated fresh from the app's models and filled
|
|
847
847
|
* from a seed that runs in it (`tenant` in its context), as `inTenant` runs one.
|
|
848
848
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as DatabaseDriver, f as Row$1, h as Sql, o as DialectName } from "./index-
|
|
2
|
-
import { E as list, G as FileType,
|
|
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, 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`. */
|
|
@@ -168,8 +168,11 @@ declare class Field<Output, Input = Output, Optional extends boolean = false, Ha
|
|
|
168
168
|
flow(this: Field<Output, Input, Optional, HasDefault, Meta>, transitions: { readonly [From in Output & string]?: readonly (Output & string)[]; }): Field<Output, Input, Optional, HasDefault, Meta>;
|
|
169
169
|
/**
|
|
170
170
|
* A copy of a related row's value (`.from((s) => s.criterion.maxScore)`), so the row's checks and
|
|
171
|
-
* uniques can use it. The database writes it on create and keeps it equal to its source
|
|
172
|
-
* never written. The path's
|
|
171
|
+
* uniques can use it. The database writes it on create and keeps it equal to its source (triggers
|
|
172
|
+
* on each table along the path, so raw SQL writes are seen too); it's never written. The path's
|
|
173
|
+
* first ref must be `.fixed()`; when every step and the final field are fixed, it's written once
|
|
174
|
+
* and no trigger is made. A plain path to one column only: a derived value is `.computed()`, and a
|
|
175
|
+
* value frozen as it was is a snapshot, not a copy.
|
|
173
176
|
*/
|
|
174
177
|
from(build: (row: any) => Expr<unknown, unknown>): Field<Output, Input, Optional, HasDefault, Meta & {
|
|
175
178
|
readonly maintained: true;
|
|
@@ -276,7 +279,7 @@ declare function fieldKinds(defaults: () => SchemaDefaults): {
|
|
|
276
279
|
}) => Field<Output<S>, Input$2<S>, false, false, {}>;
|
|
277
280
|
/** A list, for shapes (`f.json({ shape: f.list(...) })`, route inputs). Not a column itself. */
|
|
278
281
|
list: typeof list;
|
|
279
|
-
/** A file; `{ storage }` names a storage other than `default
|
|
282
|
+
/** A file; `{ storage }` names a storage other than `default`. */
|
|
280
283
|
file: (options?: {
|
|
281
284
|
readonly types?: readonly FileType[];
|
|
282
285
|
readonly storage?: string;
|
|
@@ -636,7 +639,7 @@ interface RelationTarget {
|
|
|
636
639
|
readonly field: string;
|
|
637
640
|
readonly spec: FieldSpec;
|
|
638
641
|
}
|
|
639
|
-
/** The audit log's settings, when `base.audit` is in the `use` list
|
|
642
|
+
/** The audit log's settings, when `base.audit` is in the `use` list. */
|
|
640
643
|
interface AuditConfig {
|
|
641
644
|
/** The database the log lives in; only writes to it are recorded. */
|
|
642
645
|
readonly database: string;
|
|
@@ -690,7 +693,7 @@ interface ModelSpec {
|
|
|
690
693
|
/** The built-in table set it comes from (`tenancy`); undefined for the app's own models. */
|
|
691
694
|
readonly builtin?: string;
|
|
692
695
|
/**
|
|
693
|
-
* Whether the audit log records its writes
|
|
696
|
+
* Whether the audit log records its writes: its own `.audit(…)`, else the
|
|
694
697
|
* module's `byDefault`; `"never"` for built-ins that are never recorded (Session, the log itself).
|
|
695
698
|
*/
|
|
696
699
|
readonly audit?: boolean | "never";
|
|
@@ -774,7 +777,7 @@ declare class Model<Name extends string = string, F extends Fields = Fields, Uni
|
|
|
774
777
|
table(name: string): Model<Name, F, Uniques>;
|
|
775
778
|
/**
|
|
776
779
|
* A rule over the row, written as a condition. It runs in JS on every write; when it reads
|
|
777
|
-
* more than one field it is also a database guard
|
|
780
|
+
* more than one field it is also a database guard.
|
|
778
781
|
*/
|
|
779
782
|
check(name: string, build: (row: OwnColumns<ImplicitFields & F>) => Condition): Model<Name, F, Uniques>;
|
|
780
783
|
/** The spec with these computed fields added (`computed`, `reading`). */
|
|
@@ -880,7 +883,7 @@ interface IndexSnapshot {
|
|
|
880
883
|
readonly unique: boolean;
|
|
881
884
|
}
|
|
882
885
|
/**
|
|
883
|
-
* A rule the database enforces itself
|
|
886
|
+
* A rule the database enforces itself: a `.check()` over more than one
|
|
884
887
|
* column, or a number's limit (`min` / `max`), which an SQL expression could otherwise break.
|
|
885
888
|
* For a limit, `name` is the field.
|
|
886
889
|
*/
|
|
@@ -1060,7 +1063,7 @@ interface TotalsQuery {
|
|
|
1060
1063
|
/** What a write returns: the stored rows, or only their ids (when the caller just counts them). */
|
|
1061
1064
|
type Returning = "rows" | "ids";
|
|
1062
1065
|
/**
|
|
1063
|
-
* One audited statement's entries
|
|
1066
|
+
* One audited statement's entries: each entry's id is `stamp.<row id>`, so a
|
|
1064
1067
|
* later statement in the batch finds this statement's entries by the stamp. The rest is who and
|
|
1065
1068
|
* where, the same on every entry.
|
|
1066
1069
|
*/
|
|
@@ -1119,7 +1122,7 @@ interface Dialect {
|
|
|
1119
1122
|
*/
|
|
1120
1123
|
reorder(model: ModelSpec, ids: readonly string[], context: SqlContext): Sql;
|
|
1121
1124
|
/**
|
|
1122
|
-
* The audit log's statements, placed around a write in its batch
|
|
1125
|
+
* The audit log's statements, placed around a write in its batch. `log` is
|
|
1123
1126
|
* the `AuditEntry` model; `model` the one written.
|
|
1124
1127
|
*/
|
|
1125
1128
|
readonly audit: {
|
|
@@ -1550,7 +1553,7 @@ interface Makers {
|
|
|
1550
1553
|
}
|
|
1551
1554
|
/**
|
|
1552
1555
|
* What a seed's function is given: `make` and `many` on its database and, with tenancy, the tenant
|
|
1553
|
-
* it runs in and `inTenant` to fill one
|
|
1556
|
+
* it runs in and `inTenant` to fill one.
|
|
1554
1557
|
*/
|
|
1555
1558
|
interface SeedContext extends Makers {
|
|
1556
1559
|
/** The tenant a seed runs in (its `Tenant` row), when `inTenant` ran it; undefined otherwise. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as ValidationError, i as Result, n as IssueCode, r as Path, t as Issue } from "../issues-
|
|
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 };
|
|
1
|
+
import { a as ValidationError, i as Result, n as IssueCode, r as Path, t as Issue } from "../issues-CKto95IW.js";
|
|
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";
|
|
@@ -904,9 +904,8 @@ const RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
|
904
904
|
"system"
|
|
905
905
|
]);
|
|
906
906
|
/**
|
|
907
|
-
* One DNS label for a tenant's host on the platform (`demo` in `demo.thiraiangadi.shop
|
|
908
|
-
*
|
|
909
|
-
* none of the reserved names.
|
|
907
|
+
* One DNS label for a tenant's host on the platform (`demo` in `demo.thiraiangadi.shop`):
|
|
908
|
+
* lowercase letters, digits and inner dashes, 3–63 characters, none of the reserved names.
|
|
910
909
|
*/
|
|
911
910
|
const subdomain = () => textKind("subdomain", {
|
|
912
911
|
min: 3,
|
|
@@ -1079,6 +1078,8 @@ function define(definition) {
|
|
|
1079
1078
|
}
|
|
1080
1079
|
//#endregion
|
|
1081
1080
|
//#region src/validation/kinds/files.ts
|
|
1081
|
+
/** At most this many smaller copies of one image. */
|
|
1082
|
+
const SIZES_MAX = 4;
|
|
1082
1083
|
/** File types by their usual name, with the media types each covers. */
|
|
1083
1084
|
const FILE_TYPES = {
|
|
1084
1085
|
pdf: ["application/pdf"],
|
|
@@ -1140,7 +1141,21 @@ const reference = object({
|
|
|
1140
1141
|
fake: () => "text/plain"
|
|
1141
1142
|
}),
|
|
1142
1143
|
width: int().min(1).optional(),
|
|
1143
|
-
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()
|
|
1144
1159
|
});
|
|
1145
1160
|
function fileKind(name, spec) {
|
|
1146
1161
|
const { types, maxBytes } = spec;
|
|
@@ -1151,11 +1166,12 @@ function fileKind(name, spec) {
|
|
|
1151
1166
|
read: (value, path) => {
|
|
1152
1167
|
const read = reference.parse(value, path);
|
|
1153
1168
|
if (!read.ok) return read;
|
|
1154
|
-
const { width, height, ...rest } = read.value;
|
|
1169
|
+
const { width, height, sizes, ...rest } = read.value;
|
|
1155
1170
|
const file = {
|
|
1156
1171
|
...rest,
|
|
1157
1172
|
...width ? { width } : {},
|
|
1158
|
-
...height ? { height } : {}
|
|
1173
|
+
...height ? { height } : {},
|
|
1174
|
+
...sizes?.length ? { sizes } : {}
|
|
1159
1175
|
};
|
|
1160
1176
|
const issues = [];
|
|
1161
1177
|
if (!types.has(file.type)) issues.push({
|
|
@@ -1297,4 +1313,4 @@ function phone(options = {}) {
|
|
|
1297
1313
|
return phoneKind(() => region);
|
|
1298
1314
|
}
|
|
1299
1315
|
//#endregion
|
|
1300
|
-
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 };
|