@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.
- package/dist/adapters/storage/r2/index.d.ts +3 -0
- package/dist/adapters/storage/r2/index.js +13 -2
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +2 -2
- package/dist/cli/index.js +79 -11
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +18 -13
- package/dist/{create-auth-BFKvQVOG.js → create-auth-BCvMwlVD.js} +3 -3
- package/dist/{data-layer-C5oJZ8V1.js → data-layer-BOVkYESH.js} +15 -10
- package/dist/{data-layer-43pme0Gy.d.ts → data-layer-nvm_ivGQ.d.ts} +6 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +5 -5
- package/dist/{index-a7ElyL3B.d.ts → index-Bb9Px6SR.d.ts} +97 -18
- package/dist/{index-CYcklQ1l.d.ts → index-CaR3hb0i.d.ts} +5 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +150 -78
- package/dist/{make-ByA-84h8.js → make-D6_caNkv.js} +21 -3
- package/dist/{makers-P9kg68C8.js → makers-DjkBP74P.js} +3 -3
- package/dist/{migrator-B25prc9n.js → migrator-vvvKa0Vk.js} +180 -36
- package/dist/{model-Dc3HunKb.js → model-6pJBqqGd.js} +31 -1
- package/dist/{policies-KsqmlX67.js → policies-C_oiSgmY.js} +1 -1
- package/dist/{resource-BQK7bkPQ.js → resource-DCg6LvMH.js} +8 -6
- package/dist/{tables-DgSwge7s.js → tables-DjW96Ubl.js} +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +4 -4
- package/dist/{types-BgOkJy5d.d.ts → types-D5SIVeej.d.ts} +59 -2
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +1 -1
- package/dist/{validation-Cppmd0RV.js → validation-C3MCRBQO.js} +26 -3
- 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-
|
|
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-
|
|
4
|
-
import { a as PROVIDERS, i as HELD, k as Manifest } from "./data-layer-
|
|
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 `
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
2
|
-
import { A as planUpdate, B as
|
|
3
|
-
import { C as tooLarge, E as fail, S as tenantSuspended, T as Rejected, _ as json, b as routeNotFound, c as fromBody, d as route, f as HttpError, g as fileTypeRefused, h as fileTooLarge, l as readInput, m as errorResponse, o as parseListQuery, p as crossSiteRequest, r as resource, s as BODY_LIMIT, t as Resource, u as Route, v as lengthRequired, w as tooManyRequests, x as signInRequired, y as methodNotAllowed } from "./resource-
|
|
4
|
-
import { 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-
|
|
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-
|
|
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-
|
|
10
|
-
import { i as checkRoles, n as authPerms, r as checkAuth } from "./create-auth-
|
|
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-
|
|
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:
|
|
573
|
-
*
|
|
574
|
-
*
|
|
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
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
-
|
|
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 }, {
|
|
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
|
|
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 () =>
|
|
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 };
|