@isaijs/core 0.1.6 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/adapters/storage/r2/index.d.ts +3 -0
  2. package/dist/adapters/storage/r2/index.js +13 -2
  3. package/dist/auth/index.d.ts +2 -2
  4. package/dist/auth/index.js +2 -2
  5. package/dist/cli/index.js +79 -11
  6. package/dist/client/index.d.ts +4 -0
  7. package/dist/client/index.js +18 -13
  8. package/dist/{create-auth-BFKvQVOG.js → create-auth-BCvMwlVD.js} +3 -3
  9. package/dist/{data-layer-C5oJZ8V1.js → data-layer-BOVkYESH.js} +15 -10
  10. package/dist/{data-layer-43pme0Gy.d.ts → data-layer-nvm_ivGQ.d.ts} +6 -2
  11. package/dist/db/index.d.ts +2 -2
  12. package/dist/db/index.js +5 -5
  13. package/dist/{index-a7ElyL3B.d.ts → index-Bb9Px6SR.d.ts} +97 -18
  14. package/dist/{index-CYcklQ1l.d.ts → index-CaR3hb0i.d.ts} +5 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +150 -78
  17. package/dist/{make-ByA-84h8.js → make-D6_caNkv.js} +21 -3
  18. package/dist/{makers-P9kg68C8.js → makers-DjkBP74P.js} +3 -3
  19. package/dist/{migrator-B25prc9n.js → migrator-vvvKa0Vk.js} +180 -36
  20. package/dist/{model-Dc3HunKb.js → model-6pJBqqGd.js} +31 -1
  21. package/dist/{policies-KsqmlX67.js → policies-C_oiSgmY.js} +1 -1
  22. package/dist/{resource-BQK7bkPQ.js → resource-DCg6LvMH.js} +8 -6
  23. package/dist/{tables-DgSwge7s.js → tables-DjW96Ubl.js} +1 -1
  24. package/dist/testing/index.d.ts +1 -1
  25. package/dist/testing/index.js +4 -4
  26. package/dist/{types-BgOkJy5d.d.ts → types-D5SIVeej.d.ts} +59 -2
  27. package/dist/validation/index.d.ts +2 -2
  28. package/dist/validation/index.js +1 -1
  29. package/dist/{validation-Cppmd0RV.js → validation-C3MCRBQO.js} +26 -3
  30. package/package.json +1 -1
@@ -1,7 +1,7 @@
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, 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, u as Seed, ut as Operand, v as OrderKey, w as NumberedPage, x as IncludePath } from "./types-BgOkJy5d.js";
4
- import { a as PROVIDERS, i as HELD, k as Manifest } from "./data-layer-43pme0Gy.js";
1
+ import { a as DatabaseDriver, b as TenantDatabases, d as RateLimiter, f as Row, 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, Y as StoredFile, w as Output } from "./index-CaR3hb0i.js";
3
+ import { $ as Expr, C as WithIncludes, D as PageOptions, E as Page, J as Models, O as Write, 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$1, lt as ConditionNode, n as Patch, p as Database, t as Insert, u as Seed, ut as Operand, v as OrderKey, w as NumberedPage, x as IncludePath } from "./types-D5SIVeej.js";
4
+ import { a as PROVIDERS, i as HELD, k as Manifest } from "./data-layer-nvm_ivGQ.js";
5
5
  //#region src/api/fail.d.ts
6
6
  /**
7
7
  * `fail(code, message)`: the app's own business rules, for what can't be a `.check()`, a flow or a
@@ -33,10 +33,10 @@ interface BuiltinUser {
33
33
  }
34
34
  type UserRow = Models extends {
35
35
  readonly User: infer U extends AnyModel;
36
- } ? Row<U> : BuiltinUser;
36
+ } ? Row$1<U> : BuiltinUser;
37
37
  type TenantRow$1 = Models extends {
38
38
  readonly Tenant: infer T extends AnyModel;
39
- } ? Row<T> : {
39
+ } ? Row$1<T> : {
40
40
  readonly id: string;
41
41
  readonly name: string;
42
42
  };
@@ -133,12 +133,12 @@ type Simplify<T> = { [K in keyof T]: T[K]; } & {};
133
133
  type OwnedInsert<M extends AnyModel> = Simplify<Omit<Insert<M>, OwnerKeys<M>> & Partial<Pick<Insert<M>, OwnerKeys<M> & keyof Insert<M>>>>;
134
134
  /** The route context's `db`. Transactions, upserts and raw SQL are for services (`app.db`). */
135
135
  interface RouteDatabase {
136
- findOne<M extends AnyModel>(model: M, key: Key<M>): CheckedOne<M, Row<M>>;
137
- findMany<M extends AnyModel>(model: M): CheckedQuery<M, Row<M>>;
138
- createOne<M extends AnyModel>(model: M, input: OwnedInsert<M>): Ends<Row<M>>;
139
- createMany<M extends AnyModel>(model: M, inputs: readonly OwnedInsert<M>[]): Ends<Row<M>[]>;
140
- updateOne<M extends AnyModel>(model: M, key: Key<M>, patch: Patch<M>): CheckedMaybe<Row<M>>;
141
- deleteOne<M extends AnyModel>(model: M, key: Key<M>): CheckedMaybe<Row<M>>;
136
+ findOne<M extends AnyModel>(model: M, key: Key<M>): CheckedOne<M, Row$1<M>>;
137
+ findMany<M extends AnyModel>(model: M): CheckedQuery<M, Row$1<M>>;
138
+ createOne<M extends AnyModel>(model: M, input: OwnedInsert<M>): Ends<Row$1<M>>;
139
+ createMany<M extends AnyModel>(model: M, inputs: readonly OwnedInsert<M>[]): Ends<Row$1<M>[]>;
140
+ updateOne<M extends AnyModel>(model: M, key: Key<M>, patch: Patch<M>): CheckedMaybe<Row$1<M>>;
141
+ deleteOne<M extends AnyModel>(model: M, key: Key<M>): CheckedMaybe<Row$1<M>>;
142
142
  updateMany<M extends AnyModel>(model: M, patch: Patch<M>): CheckedMany<M>;
143
143
  deleteMany<M extends AnyModel>(model: M): CheckedMany<M>;
144
144
  }
@@ -153,7 +153,7 @@ type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
153
153
  */
154
154
  type User = Models extends {
155
155
  readonly User: infer U extends AnyModel;
156
- } ? Row<U> : {
156
+ } ? Row$1<U> : {
157
157
  readonly id: string;
158
158
  };
159
159
  /**
@@ -162,7 +162,7 @@ type User = Models extends {
162
162
  */
163
163
  type PlatformUser = Models extends {
164
164
  readonly PlatformUser: infer U extends AnyModel;
165
- } ? Row<U> : {
165
+ } ? Row$1<U> : {
166
166
  readonly id: string;
167
167
  };
168
168
  type ParamNames<P extends string> = P extends `${string}:${infer Name}/${infer Rest}` ? Name | ParamNames<`/${Rest}`> : P extends `${string}:${infer Name}` ? Name : never;
@@ -178,14 +178,28 @@ interface RouteOptions<S extends ObjectShape = ObjectShape, A extends boolean =
178
178
  * the platform's, `user` a platform user.
179
179
  */
180
180
  readonly platform?: Pl;
181
+ /**
182
+ * Runs before each upload to this route's file inputs is stored, once its size and type are
183
+ * known: throw (`fail`) to refuse it. `pending` is the bytes of every upload not yet attached, so
184
+ * a cap counts them: `upload: async (_, { size, pending }) => { if (used() + pending + size > cap) fail(…) }`.
185
+ */
186
+ readonly upload?: UploadCheck<A, Pl>;
181
187
  }
188
+ /** An upload about to be stored: its route input's key, bytes, media type, and the bytes pending. */
189
+ interface UploadInfo {
190
+ readonly field: string;
191
+ readonly size: number;
192
+ readonly type: string;
193
+ readonly pending: number;
194
+ }
195
+ type UploadCheck<A extends boolean = boolean, Pl extends boolean = boolean> = (context: RouteContext<string, Record<never, never>, A, Pl>, upload: UploadInfo) => unknown;
182
196
  /**
183
197
  * The current tenant's row, with tenancy: the app's `Tenant` (its own columns too) once it's in the
184
198
  * `Models` registry; `undefined` in a single-tenant app.
185
199
  */
186
200
  type TenantRow = Models extends {
187
201
  readonly Tenant: infer T extends AnyModel;
188
- } ? Row<T> : undefined;
202
+ } ? Row$1<T> : undefined;
189
203
  interface RouteContext<P extends string, S extends ObjectShape, A extends boolean, Pl extends boolean = false> {
190
204
  /**
191
205
  * The app's database at the boundary: each query ends in `.allowed(action)` (the model's policy
@@ -219,6 +233,8 @@ declare class Route<O = unknown> {
219
233
  readonly handler: (context: RouteContext<string, ObjectShape, boolean>) => unknown;
220
234
  /** Served on the platform's host instead of a tenant's. */
221
235
  readonly platform: boolean;
236
+ /** Checks each upload to its file inputs before it's stored (`upload` in its options). */
237
+ readonly upload?: UploadCheck | undefined;
222
238
  /** Type only: what the handler returns. */
223
239
  readonly "~output": O;
224
240
  constructor(method: Method, path: string,
@@ -228,7 +244,9 @@ declare class Route<O = unknown> {
228
244
  */
229
245
  input: ObjectShape | "own", anonymous: boolean, handler: (context: RouteContext<string, ObjectShape, boolean>) => unknown,
230
246
  /** Served on the platform's host instead of a tenant's. */
231
- platform?: boolean);
247
+ platform?: boolean,
248
+ /** Checks each upload to its file inputs before it's stored (`upload` in its options). */
249
+ upload?: UploadCheck | undefined);
232
250
  }
233
251
  /** A route that takes no input. */
234
252
  type NoInput = Record<never, never>;
@@ -285,6 +303,16 @@ interface TenancyConfig {
285
303
  type Named<T> = {
286
304
  readonly default: T;
287
305
  } & Readonly<Record<string, T>>;
306
+ /** Where a committed write happened, for `onWrite`. */
307
+ interface WriteContext {
308
+ /** The database's name (`tenant`, `platform`, or the one database's). */
309
+ readonly database: string;
310
+ /**
311
+ * The tenant whose request or service made it (isolated tenancy). None on the platform's
312
+ * database, nor for what a tenant's database writes as it wakes (the roles from `app.ts`).
313
+ */
314
+ readonly tenant?: Row;
315
+ }
288
316
  /** The application layer: what `app.ts` returns from `createApp`'s configure callback. */
289
317
  interface AppConfig {
290
318
  /**
@@ -314,6 +342,14 @@ interface AppConfig {
314
342
  readonly language: string;
315
343
  readonly timezone: string;
316
344
  };
345
+ /**
346
+ * Called after every committed write on any of the app's databases, with what it created,
347
+ * changed and deleted (an `atomic`'s writes in one call): for what has to follow a change outside
348
+ * the database, such as purging a cache of pages built from it. Rows the database changes
349
+ * itself (cascade deletes, counts) aren't listed, nor are migrations' writes. It runs before the
350
+ * write returns; a failure is logged and never fails the write, which is already committed.
351
+ */
352
+ readonly onWrite?: (writes: readonly Write[], context: WriteContext) => void | Promise<void>;
317
353
  readonly api?: {
318
354
  /**
319
355
  * Anonymous writes (POST, PUT, PATCH, DELETE with no session) are counted per client IP:
@@ -430,6 +466,13 @@ interface AppFiles {
430
466
  }): Promise<string>;
431
467
  /** Removes the file (and its sizes) from its storage. */
432
468
  delete(file: StoredFile): Promise<void>;
469
+ /**
470
+ * Removes the uploads no write attached before their expiry, from storage and the `Upload` table,
471
+ * in the current database (the tenant's, inside one). Run it from a schedule.
472
+ */
473
+ sweep(): Promise<{
474
+ readonly removed: number;
475
+ }>;
433
476
  }
434
477
  //#endregion
435
478
  //#region src/tenancy/services.d.ts
@@ -437,6 +480,40 @@ interface AppFiles {
437
480
  type AppServices = () => readonly Promise<object>[];
438
481
  type Service = (...args: any[]) => unknown;
439
482
  //#endregion
483
+ //#region src/tasks/schedule.d.ts
484
+ /**
485
+ * Schedules: work the app runs on the clock, each a named export of an app module listed in `apps`
486
+ * (`apps/<name>/tasks.ts`), found like routes.
487
+ *
488
+ * export const nightly = schedule({ daily: "03:00" }, async ({ now }) => { … })
489
+ * export const hourly = schedule({ hourly: true }, async () => { … })
490
+ *
491
+ * Times are in the app's timezone (`fallbackLocale.timezone`, else UTC), on the hour. The runtime
492
+ * calls `app.tick(now)` from one hourly timer (on `workers`, the cron line `"0 * * * *"` in
493
+ * `wrangler.jsonc`), and each tick runs the schedules due in that hour. A tick that's missed is
494
+ * skipped, never caught up, so a schedule's work is safe to leave to the next time it runs.
495
+ *
496
+ * A schedule runs outside any tenant: with isolated tenancy it reaches a tenant through
497
+ * `app.tenant(id).run(service)`, and the platform through `app.platform.db`.
498
+ */
499
+ /** When a schedule runs: every hour, or once a day at a whole hour (`"03:00"`). */
500
+ type When = {
501
+ readonly hourly: true;
502
+ } | {
503
+ readonly daily: `${number}:00`;
504
+ };
505
+ interface ScheduleContext {
506
+ /** The tick's time: the hour it's for. */
507
+ readonly now: Date;
508
+ }
509
+ declare class Schedule {
510
+ readonly when: When;
511
+ readonly run: (context: ScheduleContext) => unknown;
512
+ constructor(when: When, run: (context: ScheduleContext) => unknown);
513
+ }
514
+ /** `schedule(when, run)` in `#app`. */
515
+ declare function schedule(when: When, run: (context: ScheduleContext) => unknown): Schedule;
516
+ //#endregion
440
517
  //#region src/app/create-app.d.ts
441
518
  /**
442
519
  * The app instance services import from `#app`. It's usable at module scope; what it
@@ -468,7 +545,7 @@ interface App {
468
545
  }
469
546
  interface AppOptions {
470
547
  /**
471
- * Every `apps/<name>/routes.ts` and `resources.ts`, each as a lazy import, like Django's
548
+ * Every `apps/<name>/routes.ts`, `resources.ts` and `tasks.ts`, each as a lazy import, like Django's
472
549
  * INSTALLED_APPS. Lazy, because those modules import `#app` themselves.
473
550
  */
474
551
  readonly apps?: AppModules;
@@ -502,6 +579,8 @@ interface CreatedApp<Served, TenantHost = undefined> {
502
579
  /** `resource(Model, options)`: what the API exposes over a model. */
503
580
  readonly resource: typeof resource;
504
581
  readonly fail: typeof fail;
582
+ /** `schedule({ daily: "03:00" }, run)`: work on the clock, exported from an app module. */
583
+ readonly schedule: typeof schedule;
505
584
  }
506
585
  /**
507
586
  * `configure` runs once, the first time the runtime starts the core with an env.
@@ -663,4 +742,4 @@ interface CreatedAuth<Env> {
663
742
  /** The auth instance, from `auth.ts`, with `policy()` and the placeholders policies are written with. */
664
743
  declare function createAuth<Env = unknown>(define: (env: Env) => AuthDefinition): CreatedAuth<Env>;
665
744
  //#endregion
666
- export { Permissions as A, Params as C, RouteHandler as D, RouteContext as E, Policy as F, Rules as I, StandardAction as L, Bound as M, DefinePolicy as N, RouteOptions as O, Perms as P, Rejected as R, Method as S, RouteBuilder as T, CustomAction as _, OAuthMethod as a, AppConfig as b, Roles as c, NotImplementedError as d, AppModules as f, createApp as g, CreatedApp as h, Methods as i, AnyPolicy as j, User as k, SignInMethod as l, AppOptions as m, AuthConfig as n, Provider as o, App as p, AuthDefinition as r, RoleDefinition as s, Auth as t, createAuth as u, Resource as v, Route as w, Named as x, ResourceOptions as y, fail as z };
745
+ export { RouteContext as A, Rules as B, AppConfig as C, Params as D, Method as E, AnyPolicy as F, Rejected as H, Bound as I, DefinePolicy as L, RouteOptions as M, User as N, Route as O, Permissions as P, Perms as R, ResourceOptions as S, WriteContext as T, fail as U, StandardAction as V, Schedule as _, OAuthMethod as a, CustomAction as b, Roles as c, NotImplementedError as d, AppModules as f, createApp as g, CreatedApp as h, Methods as i, RouteHandler as j, RouteBuilder as k, SignInMethod as l, AppOptions as m, AuthConfig as n, Provider as o, App as p, AuthDefinition as r, RoleDefinition as s, Auth as t, createAuth as u, ScheduleContext as v, Named as w, Resource as x, When as y, Policy as z };
@@ -222,6 +222,8 @@ declare function file(options?: FileOptions & {
222
222
  declare function image(options?: FileOptions & {
223
223
  readonly types?: readonly ImageType[];
224
224
  }): Kind<StoredFile>;
225
+ /** A PDF or an image to print or read whole (never resized), up to 100 MB. */
226
+ declare function document(options?: FileOptions): Kind<StoredFile>;
225
227
  /** Up to 10 files. */
226
228
  declare const files: (options?: FileOptions & {
227
229
  readonly types?: readonly FileType[];
@@ -230,6 +232,8 @@ declare const files: (options?: FileOptions & {
230
232
  declare const images: (options?: FileOptions & {
231
233
  readonly types?: readonly ImageType[];
232
234
  }) => Kind<StoredFile[], StoredFile[], false, false>;
235
+ /** Up to 100 documents (PDFs and images, each up to 100 MB). */
236
+ declare const documents: (options?: FileOptions) => Kind<StoredFile[], StoredFile[], false, false>;
233
237
  //#endregion
234
238
  //#region src/validation/kinds/phone.d.ts
235
239
  /**
@@ -382,4 +386,4 @@ declare const email: () => Kind<string, string, false, false>;
382
386
  /** An http(s) URL, up to 2,048 characters. */
383
387
  declare const url: () => Kind<string, string, false, false>;
384
388
  //#endregion
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 };
389
+ export { files 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, file as Q, id as R, LIST_MAX as S, Shape as T, phone as U, quantity as V, phoneKind as W, document as X, StoredFile as Y, documents as Z, define as _, newToken as a, FileSpec as at, CarriesKind as b, slug as c, KindTypes as ct, text as d, TypeText as dt, image as et, textKind as f, kind as ft, KindDefinition as g, StandardSchemaV1 as gt, Definable as h, StandardResult as ht, markdown as i, AnyKind as it, CalendarDate as j, toKind as k, slugify as l, Measure as lt, url as m, StandardProps as mt, host as n, Fake as nt, personName as o, Kind as ot, token as p, StandardIssue as pt, ImageType as q, line as r, FakeContext as rt, secret as s, KindSpec as st, email as t, images as tt, subdomain as u, Sensitivity as ut, json as v, Output as w, Input as x, urls as y, int as z };
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, O as RouteOptions, R as Rejected, S as Method, T as RouteBuilder, _ as CustomAction, b as AppConfig, d as NotImplementedError, f as AppModules, g as createApp, h as CreatedApp, k as User, m as AppOptions, p as App, v as Resource, w as Route, x as Named, y as ResourceOptions, z as fail } from "./index-a7ElyL3B.js";
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 };
2
+ import { A as RouteContext, C as AppConfig, D as Params, E as Method, H as Rejected, M as RouteOptions, N as User, O as Route, S as ResourceOptions, T as WriteContext, U as fail, _ as Schedule, b as CustomAction, d as NotImplementedError, f as AppModules, g as createApp, h as CreatedApp, j as RouteHandler, k as RouteBuilder, m as AppOptions, p as App, v as ScheduleContext, w as Named, x as Resource, y as When } from "./index-Bb9Px6SR.js";
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, Schedule, type ScheduleContext, type User, type When, type WriteContext, createApp, fail };
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
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 { A as planUpdate, B as NeedsDatabase, C as database, D as recordAction, J as NotFound, L as PLATFORM, N as conditionOf, O as withTableTotals, P as modelOfRow, R as TENANT, S as clockOf, T as driverOf, U as systemClock, V as evaluate, X as UniqueFailed, j as keyCondition, k as planInsert, r as sync, t as migrate, w as decodeStored, z as checkTenancy } from "./migrator-B25prc9n.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-BQK7bkPQ.js";
4
- import { C as asCondition$1, I as fromBase64url, L as toBase64url, U as asActor, _ as Expr, c as rowOf, h as Condition, o as fieldOf, p as sensitivity, q as Scope, r as columnName, t as Model } from "./model-Dc3HunKb.js";
1
+ import { U as toKind, V as object, W as validate, _ as line, c as fileTypeNames, h as email, q as ValidationError } from "./validation-C3MCRBQO.js";
2
+ import { A as planUpdate, B as checkTenancy, C as database, D as recordAction, F as modelOfRow, H as evaluate, N as conditionOf, O as withTableTotals, P as withIncludeRule, R as PLATFORM, S as clockOf, T as driverOf, V as NeedsDatabase, W as systemClock, Y as NotFound, Z as UniqueFailed, j as keyCondition, k as planInsert, r as sync, t as migrate, w as decodeStored, z as TENANT } from "./migrator-vvvKa0Vk.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-DCg6LvMH.js";
4
+ import { C as asCondition$1, I as fromBase64url, L as toBase64url, U as asActor, _ as Expr, c as rowOf, h as Condition, o as fieldOf, p as sensitivity, q as Scope, r as columnName, t as Model } from "./model-6pJBqqGd.js";
5
5
  import { r as ulid, t as kebabCase } from "./case-DpXlJCL9.js";
6
6
  import { n as notImplemented, t as NotImplementedError } from "./errors-mCNLlXg9.js";
7
- import { t as PLATFORM_PREFIX } from "./tables-DgSwge7s.js";
7
+ import { t as PLATFORM_PREFIX } from "./tables-DjW96Ubl.js";
8
8
  import { t as Seed } from "./seed-BObmdKdP.js";
9
- import { d as afterUpdate, f as bindFor, h as ownerFieldOf, m as isFalse, p as check, u as FALSE } from "./policies-KsqmlX67.js";
10
- import { i as checkRoles, n as authPerms, r as checkAuth } from "./create-auth-BFKvQVOG.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-P9kg68C8.js";
9
+ import { d as afterUpdate, f as bindFor, h as ownerFieldOf, m as isFalse, p as check, u as FALSE } from "./policies-C_oiSgmY.js";
10
+ import { i as checkRoles, n as authPerms, r as checkAuth } from "./create-auth-BCvMwlVD.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-DjkBP74P.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";
@@ -503,18 +503,6 @@ function accessOf(db) {
503
503
  return access;
504
504
  }
505
505
  const asCondition = (node) => () => new Condition(node);
506
- /** The paths a chain's `.include()` calls loaded. */
507
- const includesOf$1 = (steps) => steps.flatMap(([method, args]) => method === "include" ? args : []);
508
- /** The rows in a query's result: a list, a page, or one row (`first()`); none in a count. */
509
- function rowsIn(result) {
510
- if (Array.isArray(result)) return result;
511
- if (result && typeof result === "object") {
512
- const rows = result.rows;
513
- if (Array.isArray(rows)) return rows;
514
- if (modelOfRow(result)) return [result];
515
- }
516
- return [];
517
- }
518
506
  /** The route `db` and `can` for one request. */
519
507
  function boundary(options) {
520
508
  const { db, policies, asker } = options;
@@ -543,7 +531,7 @@ function boundary(options) {
543
531
  const readable = async (model, key, steps) => {
544
532
  const read = await filter(model, "read");
545
533
  if (!read) return void 0;
546
- const query = db.findMany(model).where(asCondition(keyCondition(model.spec, key))).where(asCondition(read));
534
+ const query = db.findMany(model)[withIncludeRule](includeRule(model)).where(asCondition(keyCondition(model.spec, key))).where(asCondition(read));
547
535
  return replay(query, steps.filter(([method]) => method === "include")).first();
548
536
  };
549
537
  /** Whether the row with this id meets the action's condition. */
@@ -569,48 +557,20 @@ function boundary(options) {
569
557
  };
570
558
  };
571
559
  /**
572
- * Related rows loaded with `.include()` keep only what their own model's `read` rule allows: a
573
- * ref the user can't read becomes `null`, a list keeps its readable rows. Level by level, one
574
- * query per model and level, for every row at once.
560
+ * Related rows loaded with `.include()` keep only what their own model's `read` rule allows: the
561
+ * rule is in each level's query (`db/orm/include.ts`), so a ref the user can't read is `null` and
562
+ * a list keeps its readable rows, with nothing read twice. One verdict per model, per query.
575
563
  */
576
- const underTheirPolicies = async (registry, rows, paths) => {
577
- const byFirst = /* @__PURE__ */ new Map();
578
- for (const path of paths) {
579
- const [first, ...rest] = path.split(".");
580
- if (first === void 0) continue;
581
- byFirst.set(first, [...byFirst.get(first) ?? [], ...rest.length > 0 ? [rest.join(".")] : []]);
582
- }
583
- for (const [field, deeper] of byFirst) {
584
- const holders = rows;
585
- const related = holders.flatMap((row) => {
586
- const value = row[field];
587
- return Array.isArray(value) ? value : value && typeof value === "object" ? [value] : [];
588
- });
589
- if (related.length === 0) continue;
590
- const spec = modelOfRow(related[0]);
591
- if (!spec) continue;
592
- const target = new Model(spec, registry);
593
- const read = await readableIds(target, related.map((r) => r.id));
594
- for (const row of holders) {
595
- const value = row[field];
596
- if (Array.isArray(value)) row[field] = value.filter((r) => read.has(r.id));
597
- else if (value && typeof value === "object") {
598
- if (!read.has(value.id)) row[field] = null;
599
- }
564
+ const includeRule = (model) => {
565
+ const verdicts = /* @__PURE__ */ new Map();
566
+ return (spec) => {
567
+ let found = verdicts.get(spec.name);
568
+ if (!found) {
569
+ found = verdict(new Model(spec, model.registry), "read").then((v) => v.kind === "denied" ? false : v.node);
570
+ verdicts.set(spec.name, found);
600
571
  }
601
- const kept = holders.flatMap((row) => {
602
- const value = row[field];
603
- return Array.isArray(value) ? value : value && typeof value === "object" ? [value] : [];
604
- });
605
- if (deeper.length > 0) await underTheirPolicies(registry, kept, deeper);
606
- }
607
- };
608
- /** The ids, among these, of rows of `target` its `read` rule lets this request see. */
609
- const readableIds = async (target, ids) => {
610
- const verdictOf = await verdict(target, "read");
611
- if (verdictOf.kind === "denied") return /* @__PURE__ */ new Set();
612
- const found = await db.findMany(target).where((r) => r.id.in([...new Set(ids)])).where(asCondition(verdictOf.node));
613
- return new Set(found.map((r) => r.id));
572
+ return found;
573
+ };
614
574
  };
615
575
  /**
616
576
  * The plan with the placeholders in its own `where`s bound (`byRole`, `user`, `tenant`), as a
@@ -657,9 +617,7 @@ function boundary(options) {
657
617
  t: "or",
658
618
  items: []
659
619
  };
660
- const result = await replay(db.findMany(model).where(asCondition(node)), plan.steps);
661
- await underTheirPolicies(model.registry, rowsIn(result), includesOf$1(plan.steps));
662
- return result;
620
+ return replay(db.findMany(model)[withIncludeRule](includeRule(model)).where(asCondition(node)), plan.steps);
663
621
  }
664
622
  case "updateMany":
665
623
  case "deleteMany": {
@@ -674,7 +632,6 @@ function boundary(options) {
674
632
  case "findOne": {
675
633
  const key = plan.args[0];
676
634
  const row = await readable(model, key, plan.steps);
677
- if (row !== void 0) await underTheirPolicies(model.registry, [row], includesOf$1(plan.steps));
678
635
  if (row !== void 0 && action !== "read") {
679
636
  const node = required(await verdict(model, action));
680
637
  if (!await holdsOn(model, row.id, node)) throw deny();
@@ -1692,6 +1649,53 @@ const engine = (resource, action, context, files) => {
1692
1649
  }
1693
1650
  };
1694
1651
  //#endregion
1652
+ //#region src/tasks/schedule.ts
1653
+ var Schedule = class {
1654
+ when;
1655
+ run;
1656
+ constructor(when, run) {
1657
+ this.when = when;
1658
+ this.run = run;
1659
+ }
1660
+ };
1661
+ const DAILY = /^([01]\d|2[0-3]):00$/;
1662
+ /** `schedule(when, run)` in `#app`. */
1663
+ function schedule(when, run) {
1664
+ if ("daily" in when && !DAILY.test(when.daily)) throw new TypeError(`isai: a daily schedule runs at a whole hour ("03:00"), not "${when.daily}"`);
1665
+ if (!("daily" in when) && when.hourly !== true) throw new TypeError("isai: a schedule is { hourly: true } or { daily: \"HH:00\" }");
1666
+ return new Schedule(when, run);
1667
+ }
1668
+ /** Every schedule among the app modules' exports. */
1669
+ function schedulesIn(modules) {
1670
+ const found = /* @__PURE__ */ new Set();
1671
+ for (const module of modules) for (const value of Object.values(module)) if (value instanceof Schedule) found.add(value);
1672
+ return [...found];
1673
+ }
1674
+ /** The hour (0–23) `now` falls in, in `timezone`. */
1675
+ function hourIn(now, timezone = "UTC") {
1676
+ const hour = new Intl.DateTimeFormat("en-GB", {
1677
+ timeZone: timezone,
1678
+ hour: "2-digit",
1679
+ hourCycle: "h23"
1680
+ }).format(now);
1681
+ return Number(hour);
1682
+ }
1683
+ /** Whether `when` is due in the hour `now` falls in. */
1684
+ function isDue(when, now, timezone) {
1685
+ return "daily" in when ? Number(when.daily.slice(0, 2)) === hourIn(now, timezone) : true;
1686
+ }
1687
+ /**
1688
+ * The timer check: runs every schedule due in `now`'s hour, one after another. One that fails is
1689
+ * logged and the rest still run.
1690
+ */
1691
+ async function tick(schedules, now, timezone) {
1692
+ for (const due of schedules.filter((one) => isDue(one.when, now, timezone))) try {
1693
+ await due.run({ now });
1694
+ } catch (error) {
1695
+ console.error(`isai: a schedule (${JSON.stringify(due.when)}) failed`, error);
1696
+ }
1697
+ }
1698
+ //#endregion
1695
1699
  //#region src/audit/routes.ts
1696
1700
  /**
1697
1701
  * The whole log, a built-in route:
@@ -2493,6 +2497,7 @@ function uploadTargets(modules, resources) {
2493
2497
  route,
2494
2498
  key
2495
2499
  },
2500
+ ...route.upload ? { check: route.upload } : {},
2496
2501
  allow: async (context) => {
2497
2502
  if (!route.anonymous && !context.user) throw signInRequired();
2498
2503
  }
@@ -2720,6 +2725,20 @@ function uploadRoute(options) {
2720
2725
  throw fileTypeRefused(fileTypeNames(spec.types));
2721
2726
  }
2722
2727
  const db = accessOf(context.db).trusted;
2728
+ if (target.check) {
2729
+ const { bytes } = await db.findMany(options.upload).totals((u) => ({ bytes: u.size.sum() }));
2730
+ try {
2731
+ await target.check(context, {
2732
+ field: target.route?.key ?? field,
2733
+ size,
2734
+ type: sniffed.type,
2735
+ pending: bytes ?? 0
2736
+ });
2737
+ } catch (error) {
2738
+ await body.cancel().catch(() => {});
2739
+ throw error;
2740
+ }
2741
+ }
2723
2742
  const user = context.user;
2724
2743
  const put = async (key) => {
2725
2744
  try {
@@ -2757,7 +2776,10 @@ function uploadRoute(options) {
2757
2776
  height
2758
2777
  }].sort((a, b) => b.width - a.width)
2759
2778
  };
2760
- await db.updateOne(options.upload, { fileId: sizeOf }, { file });
2779
+ await db.updateOne(options.upload, { fileId: sizeOf }, {
2780
+ file,
2781
+ size: Number(pending.size ?? 0) + size
2782
+ });
2761
2783
  return json(201, await options.answer(file, new URL(request.url).origin));
2762
2784
  }
2763
2785
  const id = newFileId();
@@ -2774,6 +2796,7 @@ function uploadRoute(options) {
2774
2796
  await db.createOne(options.upload, {
2775
2797
  fileId: id,
2776
2798
  file,
2799
+ size,
2777
2800
  target: name,
2778
2801
  user: user?.id ?? null,
2779
2802
  expiresAt: new Date(clockOf(db).now().getTime() + UPLOAD_TTL_MS)
@@ -2883,8 +2906,10 @@ function dispositionOf(type, name) {
2883
2906
  }
2884
2907
  //#endregion
2885
2908
  //#region src/files/service.ts
2909
+ /** How many expired uploads one sweep reads at a time. */
2910
+ const SWEEP_BATCH = 100;
2886
2911
  function appFiles(options) {
2887
- const { storages, url, tenant } = options;
2912
+ const { storages, url, tenant, db: dbOf, upload: uploadOf } = options;
2888
2913
  const storageOf = (name) => {
2889
2914
  const storage = storages[name];
2890
2915
  if (!storage) throw new TypeError(`isai: there's no storage "${name}" in app.ts`);
@@ -2944,6 +2969,30 @@ function appFiles(options) {
2944
2969
  url: async (file, { origin = "" } = {}) => url(file, origin),
2945
2970
  delete: async (file) => {
2946
2971
  await Promise.all(keysOf(file).map((key) => storageOf(file.storage).delete(key)));
2972
+ },
2973
+ sweep: async () => {
2974
+ const upload = uploadOf();
2975
+ if (!upload) throw new TypeError("isai: app.files.sweep needs base.uploads({ database }) in db/index.ts");
2976
+ const db = dbOf();
2977
+ const now = clockOf(db).now();
2978
+ let removed = 0;
2979
+ for (;;) {
2980
+ const expired = await db.findMany(upload).where((u) => u.expiresAt.lte(now)).limit(SWEEP_BATCH).all();
2981
+ if (expired.length === 0) return { removed };
2982
+ await Promise.all(expired.flatMap((row) => {
2983
+ const file = row.file;
2984
+ return keysOf(file).map(async (key) => {
2985
+ try {
2986
+ await storageOf(file.storage).delete(key);
2987
+ } catch (error) {
2988
+ console.error(`isai: couldn't remove ${key} from storage "${file.storage}"`, error);
2989
+ }
2990
+ });
2991
+ }));
2992
+ const ids = expired.map((row) => String(row.fileId));
2993
+ await db.deleteMany(upload).where((u) => u.fileId.in(ids));
2994
+ removed += ids.length;
2995
+ }
2947
2996
  }
2948
2997
  };
2949
2998
  }
@@ -3158,12 +3207,28 @@ function createCore(config, env, options = {}) {
3158
3207
  invite: (input) => invite(currentDb(), models, input),
3159
3208
  revoke: (input) => revoke(currentDb(), models, input)
3160
3209
  });
3161
- const single = named.kind === "database" ? database({
3210
+ const scope = new Scope();
3211
+ /** Tells `onWrite` what each committed write on `db` touched, and in which tenant. */
3212
+ const watched = (db, name) => {
3213
+ const { onWrite } = config;
3214
+ if (onWrite) db.subscribe(async (writes) => {
3215
+ const tenant = scope.current()?.tenant;
3216
+ try {
3217
+ await onWrite(writes, {
3218
+ database: name,
3219
+ ...tenant ? { tenant } : {}
3220
+ });
3221
+ } catch (error) {
3222
+ console.error(`isai: onWrite failed after a write to the ${name} database`, error);
3223
+ }
3224
+ });
3225
+ return db;
3226
+ };
3227
+ const single = named.kind === "database" ? watched(database({
3162
3228
  driver: named,
3163
3229
  name,
3164
3230
  ...zoned
3165
- }) : void 0;
3166
- const scope = new Scope();
3231
+ }), name) : void 0;
3167
3232
  const registry = serviceRegistry(services, seeds);
3168
3233
  /** The app's database: with tenancy, the current tenant's (inside one), and nothing outside. */
3169
3234
  const currentDb = () => {
@@ -3265,11 +3330,11 @@ function createCore(config, env, options = {}) {
3265
3330
  });
3266
3331
  return pipeline;
3267
3332
  };
3268
- const platform = named.kind === "tenantDatabases" ? database({
3333
+ const platform = named.kind === "tenantDatabases" ? watched(database({
3269
3334
  driver: config.databases[PLATFORM],
3270
3335
  name: PLATFORM,
3271
3336
  ...zoned
3272
- }) : void 0;
3337
+ }), PLATFORM) : void 0;
3273
3338
  const platformModels = auth && platform && config.tenancy ? authModels(new Model(config.tenancy.tenant.registry.get(`${PLATFORM_PREFIX}User`), config.tenancy.tenant.registry), PLATFORM_PREFIX) : void 0;
3274
3339
  const platformDb = () => platform;
3275
3340
  let platformReady;
@@ -3341,6 +3406,7 @@ function createCore(config, env, options = {}) {
3341
3406
  registry,
3342
3407
  models,
3343
3408
  woken: syncedRoles,
3409
+ watched: (db) => watched(db, TENANT),
3344
3410
  run: (db, tenant, fn) => scope.run({
3345
3411
  db,
3346
3412
  tenant
@@ -3376,7 +3442,12 @@ function createCore(config, env, options = {}) {
3376
3442
  files: appFiles({
3377
3443
  storages,
3378
3444
  url,
3379
- tenant: () => scope.current()?.tenant
3445
+ tenant: () => scope.current()?.tenant,
3446
+ db: currentDb,
3447
+ upload: () => {
3448
+ const models = auth?.user.registry ?? config.tenancy?.tenant.registry;
3449
+ return models?.has("Upload") ? new Model(models.get("Upload"), models) : void 0;
3450
+ }
3380
3451
  }),
3381
3452
  handle,
3382
3453
  openTenant,
@@ -3411,7 +3482,7 @@ function createCore(config, env, options = {}) {
3411
3482
  return await named.call(id, ref, args, tenant);
3412
3483
  } }),
3413
3484
  runTask: async () => notImplemented("app.runTask"),
3414
- tick: async () => notImplemented("app.tick")
3485
+ tick: async (now) => tick(schedulesIn((await loadModules()).modules), now, timezone)
3415
3486
  };
3416
3487
  }
3417
3488
  /**
@@ -3422,11 +3493,11 @@ function createCore(config, env, options = {}) {
3422
3493
  */
3423
3494
  function tenantCore(databases, host, options) {
3424
3495
  const driver = databases.open(host);
3425
- const db = database({
3496
+ const db = options.watched(database({
3426
3497
  driver,
3427
3498
  name: TENANT,
3428
3499
  ...options.zoned
3429
- });
3500
+ }));
3430
3501
  let woken;
3431
3502
  let recorded = false;
3432
3503
  const wake = () => {
@@ -3587,8 +3658,9 @@ function createApp(runtime, configure, options = {}) {
3587
3658
  Tenant: runtime.tenant?.(start),
3588
3659
  route,
3589
3660
  resource,
3590
- fail
3661
+ fail,
3662
+ schedule
3591
3663
  };
3592
3664
  }
3593
3665
  //#endregion
3594
- export { NotImplementedError, Rejected, Route, createApp, fail };
3666
+ export { NotImplementedError, Rejected, Route, Schedule, createApp, fail };