@isaijs/core 0.1.5 → 0.1.8
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-BHdX68Ng.js → create-auth-BCvMwlVD.js} +18 -10
- package/dist/{data-layer-C5oJZ8V1.js → data-layer-CVCFjPyv.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-D0FEnMhn.d.ts → index-Bb9Px6SR.d.ts} +116 -22
- package/dist/{index-CYcklQ1l.d.ts → index-CaR3hb0i.d.ts} +5 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +169 -79
- package/dist/{make-ByA-84h8.js → make-jMJABOWI.js} +21 -3
- package/dist/{makers-P9kg68C8.js → makers-A0Pj6Tda.js} +3 -3
- package/dist/{migrator-B25prc9n.js → migrator-DxTpns_-.js} +174 -30
- 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-BIMAiOre.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
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-DxTpns_-.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-BIMAiOre.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-A0Pj6Tda.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:
|
|
@@ -2393,6 +2397,21 @@ async function invite(db, models, input) {
|
|
|
2393
2397
|
}
|
|
2394
2398
|
});
|
|
2395
2399
|
}
|
|
2400
|
+
/**
|
|
2401
|
+
* `auth.revoke` from code, undoing `auth.invite`: the email's invite to the role is deleted, and if
|
|
2402
|
+
* it has an account here, the role is taken away (held by the platform or not). Its sessions stay,
|
|
2403
|
+
* with what its other roles allow. Safe to repeat. No permission or last-owner check: services are
|
|
2404
|
+
* trusted, so give the role to someone else first.
|
|
2405
|
+
*/
|
|
2406
|
+
async function revoke(db, models, input) {
|
|
2407
|
+
const { User, Role, UserRole, Invite } = models;
|
|
2408
|
+
const role = await db.findOne(Role, { key: input.role });
|
|
2409
|
+
if (!role) throw new TypeError(`isai: auth.revoke takes the role "${input.role}", which app.ts's roles don't have`);
|
|
2410
|
+
const invite = await db.findOne(Invite, { email: input.email });
|
|
2411
|
+
if (invite?.role === role.id) await db.deleteOne(Invite, { id: invite.id });
|
|
2412
|
+
const user = await db.findOne(User, { email: input.email });
|
|
2413
|
+
if (user) await db.deleteMany(UserRole).where((h) => h.user.is(user.id)).where((h) => h.role.is(role.id));
|
|
2414
|
+
}
|
|
2396
2415
|
//#endregion
|
|
2397
2416
|
//#region src/auth/routes.ts
|
|
2398
2417
|
/**
|
|
@@ -2478,6 +2497,7 @@ function uploadTargets(modules, resources) {
|
|
|
2478
2497
|
route,
|
|
2479
2498
|
key
|
|
2480
2499
|
},
|
|
2500
|
+
...route.upload ? { check: route.upload } : {},
|
|
2481
2501
|
allow: async (context) => {
|
|
2482
2502
|
if (!route.anonymous && !context.user) throw signInRequired();
|
|
2483
2503
|
}
|
|
@@ -2705,6 +2725,20 @@ function uploadRoute(options) {
|
|
|
2705
2725
|
throw fileTypeRefused(fileTypeNames(spec.types));
|
|
2706
2726
|
}
|
|
2707
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
|
+
}
|
|
2708
2742
|
const user = context.user;
|
|
2709
2743
|
const put = async (key) => {
|
|
2710
2744
|
try {
|
|
@@ -2742,7 +2776,10 @@ function uploadRoute(options) {
|
|
|
2742
2776
|
height
|
|
2743
2777
|
}].sort((a, b) => b.width - a.width)
|
|
2744
2778
|
};
|
|
2745
|
-
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
|
+
});
|
|
2746
2783
|
return json(201, await options.answer(file, new URL(request.url).origin));
|
|
2747
2784
|
}
|
|
2748
2785
|
const id = newFileId();
|
|
@@ -2759,6 +2796,7 @@ function uploadRoute(options) {
|
|
|
2759
2796
|
await db.createOne(options.upload, {
|
|
2760
2797
|
fileId: id,
|
|
2761
2798
|
file,
|
|
2799
|
+
size,
|
|
2762
2800
|
target: name,
|
|
2763
2801
|
user: user?.id ?? null,
|
|
2764
2802
|
expiresAt: new Date(clockOf(db).now().getTime() + UPLOAD_TTL_MS)
|
|
@@ -2868,8 +2906,10 @@ function dispositionOf(type, name) {
|
|
|
2868
2906
|
}
|
|
2869
2907
|
//#endregion
|
|
2870
2908
|
//#region src/files/service.ts
|
|
2909
|
+
/** How many expired uploads one sweep reads at a time. */
|
|
2910
|
+
const SWEEP_BATCH = 100;
|
|
2871
2911
|
function appFiles(options) {
|
|
2872
|
-
const { storages, url, tenant } = options;
|
|
2912
|
+
const { storages, url, tenant, db: dbOf, upload: uploadOf } = options;
|
|
2873
2913
|
const storageOf = (name) => {
|
|
2874
2914
|
const storage = storages[name];
|
|
2875
2915
|
if (!storage) throw new TypeError(`isai: there's no storage "${name}" in app.ts`);
|
|
@@ -2929,6 +2969,30 @@ function appFiles(options) {
|
|
|
2929
2969
|
url: async (file, { origin = "" } = {}) => url(file, origin),
|
|
2930
2970
|
delete: async (file) => {
|
|
2931
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
|
+
}
|
|
2932
2996
|
}
|
|
2933
2997
|
};
|
|
2934
2998
|
}
|
|
@@ -3139,13 +3203,32 @@ function createCore(config, env, options = {}) {
|
|
|
3139
3203
|
checkRoles(auth, config.tenancy.roles, "tenancy.roles");
|
|
3140
3204
|
}
|
|
3141
3205
|
const models = auth ? authModels(auth.user) : void 0;
|
|
3142
|
-
if (models) config.auth?.attach(
|
|
3143
|
-
|
|
3206
|
+
if (models) config.auth?.attach({
|
|
3207
|
+
invite: (input) => invite(currentDb(), models, input),
|
|
3208
|
+
revoke: (input) => revoke(currentDb(), models, input)
|
|
3209
|
+
});
|
|
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({
|
|
3144
3228
|
driver: named,
|
|
3145
3229
|
name,
|
|
3146
3230
|
...zoned
|
|
3147
|
-
}) : void 0;
|
|
3148
|
-
const scope = new Scope();
|
|
3231
|
+
}), name) : void 0;
|
|
3149
3232
|
const registry = serviceRegistry(services, seeds);
|
|
3150
3233
|
/** The app's database: with tenancy, the current tenant's (inside one), and nothing outside. */
|
|
3151
3234
|
const currentDb = () => {
|
|
@@ -3247,11 +3330,11 @@ function createCore(config, env, options = {}) {
|
|
|
3247
3330
|
});
|
|
3248
3331
|
return pipeline;
|
|
3249
3332
|
};
|
|
3250
|
-
const platform = named.kind === "tenantDatabases" ? database({
|
|
3333
|
+
const platform = named.kind === "tenantDatabases" ? watched(database({
|
|
3251
3334
|
driver: config.databases[PLATFORM],
|
|
3252
3335
|
name: PLATFORM,
|
|
3253
3336
|
...zoned
|
|
3254
|
-
}) : void 0;
|
|
3337
|
+
}), PLATFORM) : void 0;
|
|
3255
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;
|
|
3256
3339
|
const platformDb = () => platform;
|
|
3257
3340
|
let platformReady;
|
|
@@ -3323,6 +3406,7 @@ function createCore(config, env, options = {}) {
|
|
|
3323
3406
|
registry,
|
|
3324
3407
|
models,
|
|
3325
3408
|
woken: syncedRoles,
|
|
3409
|
+
watched: (db) => watched(db, TENANT),
|
|
3326
3410
|
run: (db, tenant, fn) => scope.run({
|
|
3327
3411
|
db,
|
|
3328
3412
|
tenant
|
|
@@ -3358,7 +3442,12 @@ function createCore(config, env, options = {}) {
|
|
|
3358
3442
|
files: appFiles({
|
|
3359
3443
|
storages,
|
|
3360
3444
|
url,
|
|
3361
|
-
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
|
+
}
|
|
3362
3451
|
}),
|
|
3363
3452
|
handle,
|
|
3364
3453
|
openTenant,
|
|
@@ -3393,7 +3482,7 @@ function createCore(config, env, options = {}) {
|
|
|
3393
3482
|
return await named.call(id, ref, args, tenant);
|
|
3394
3483
|
} }),
|
|
3395
3484
|
runTask: async () => notImplemented("app.runTask"),
|
|
3396
|
-
tick: async () =>
|
|
3485
|
+
tick: async (now) => tick(schedulesIn((await loadModules()).modules), now, timezone)
|
|
3397
3486
|
};
|
|
3398
3487
|
}
|
|
3399
3488
|
/**
|
|
@@ -3404,11 +3493,11 @@ function createCore(config, env, options = {}) {
|
|
|
3404
3493
|
*/
|
|
3405
3494
|
function tenantCore(databases, host, options) {
|
|
3406
3495
|
const driver = databases.open(host);
|
|
3407
|
-
const db = database({
|
|
3496
|
+
const db = options.watched(database({
|
|
3408
3497
|
driver,
|
|
3409
3498
|
name: TENANT,
|
|
3410
3499
|
...options.zoned
|
|
3411
|
-
});
|
|
3500
|
+
}));
|
|
3412
3501
|
let woken;
|
|
3413
3502
|
let recorded = false;
|
|
3414
3503
|
const wake = () => {
|
|
@@ -3569,8 +3658,9 @@ function createApp(runtime, configure, options = {}) {
|
|
|
3569
3658
|
Tenant: runtime.tenant?.(start),
|
|
3570
3659
|
route,
|
|
3571
3660
|
resource,
|
|
3572
|
-
fail
|
|
3661
|
+
fail,
|
|
3662
|
+
schedule
|
|
3573
3663
|
};
|
|
3574
3664
|
}
|
|
3575
3665
|
//#endregion
|
|
3576
|
-
export { NotImplementedError, Rejected, Route, createApp, fail };
|
|
3666
|
+
export { NotImplementedError, Rejected, Route, Schedule, createApp, fail };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { W as systemClock, f as dayOf, h as snapshotOf, p as generateMigration, y as MigrationError } from "./migrator-DxTpns_-.js";
|
|
2
2
|
//#region src/db/migrations/cleanup.ts
|
|
3
3
|
/** Dropped tables, those that refer to others first, so no row is left pointing at a dropped one. */
|
|
4
4
|
function dropOrder(tables) {
|
|
@@ -88,10 +88,12 @@ function cleanupMigration(options) {
|
|
|
88
88
|
* tighten one, or add a check a check data step: passes when no row breaks it, else a `fix`
|
|
89
89
|
* add a unique a resolution step before the index (duplicates as chosen)
|
|
90
90
|
* remove a unique retired until cleanup (an earlier release may rely on it)
|
|
91
|
+
* change a ref's onDelete or a rebuild: the table and every table referencing it, copied
|
|
92
|
+
* table, or a table's storage whole in one step (`operations.ts` → rebuild)
|
|
91
93
|
*
|
|
92
94
|
* Refused, each with the safe sequence: an incompatible kind change (or toggling `secret`), a field
|
|
93
95
|
* becoming or ceasing to be kept by the database, a new required unique field over existing rows,
|
|
94
|
-
*
|
|
96
|
+
* reusing a retired name, a rebuild of tables that reference each other.
|
|
95
97
|
*/
|
|
96
98
|
const pad = (n) => String(n).padStart(4, "0");
|
|
97
99
|
const json = (value) => JSON.stringify(value);
|
|
@@ -299,13 +301,17 @@ function describeOperation(operation, tables) {
|
|
|
299
301
|
what: `add ${tables[operation.table]?.model ?? operation.table}.${operation.column.field}`,
|
|
300
302
|
how: "ADD COLUMN"
|
|
301
303
|
};
|
|
304
|
+
case "rebuild": return {
|
|
305
|
+
what: `rebuild ${operation.tables.map((table) => table.model).join(", ")}`,
|
|
306
|
+
how: "copies every row"
|
|
307
|
+
};
|
|
302
308
|
default: return {
|
|
303
309
|
what: operation.op,
|
|
304
310
|
how: ""
|
|
305
311
|
};
|
|
306
312
|
}
|
|
307
313
|
}
|
|
308
|
-
const slugOf = (operation) => operation.op === "createTable" ? `create_${operation.table.name}` : operation.op === "addColumn" ? `add_${operation.table}_${operation.column.name}` : operation.op;
|
|
314
|
+
const slugOf = (operation) => operation.op === "createTable" ? `create_${operation.table.name}` : operation.op === "addColumn" ? `add_${operation.table}_${operation.column.name}` : operation.op === "rebuild" ? `rebuild_${operation.tables[0]?.name ?? "tables"}` : operation.op;
|
|
309
315
|
/** A template literal holding SQL (no backtick or `${` is left live). */
|
|
310
316
|
const literal = (text) => `\`${text.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")}\``;
|
|
311
317
|
function structureSource(name, after, statements, day, framework) {
|
|
@@ -482,6 +488,18 @@ function makeMigrations(options) {
|
|
|
482
488
|
/** The structure operations between two snapshots, for naming the step and the plan. */
|
|
483
489
|
function snapshotOperations(previous, next) {
|
|
484
490
|
const operations = [];
|
|
491
|
+
const rebuilt = Object.values(next.tables).filter((table) => {
|
|
492
|
+
const before = previous.tables[table.name];
|
|
493
|
+
if (!before) return false;
|
|
494
|
+
const references = new Map(before.columns.map((c) => [c.name, JSON.stringify(c.references)]));
|
|
495
|
+
return Boolean(before.withoutRowid) !== Boolean(table.withoutRowid) || table.columns.some((c) => references.has(c.name) && references.get(c.name) !== JSON.stringify(c.references));
|
|
496
|
+
});
|
|
497
|
+
if (rebuilt.length > 0) operations.push({
|
|
498
|
+
op: "rebuild",
|
|
499
|
+
tables: rebuilt,
|
|
500
|
+
copy: {},
|
|
501
|
+
triggers: []
|
|
502
|
+
});
|
|
485
503
|
for (const table of Object.values(next.tables)) {
|
|
486
504
|
const before = previous.tables[table.name];
|
|
487
505
|
if (!before) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { D as recordAction, E as forUpkeep,
|
|
3
|
-
import { I as fromBase64url, L as toBase64url, l as IMPLICIT_FIELDS, t as Model } from "./model-
|
|
1
|
+
import { J as Fake, Y as fakeValue } from "./validation-C3MCRBQO.js";
|
|
2
|
+
import { D as recordAction, E as forUpkeep, S as clockOf, k as planInsert, q as CheckFailed } from "./migrator-DxTpns_-.js";
|
|
3
|
+
import { I as fromBase64url, L as toBase64url, l as IMPLICIT_FIELDS, t as Model } from "./model-6pJBqqGd.js";
|
|
4
4
|
//#region src/auth/roles.ts
|
|
5
5
|
const same = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
6
6
|
/** What a role in `app.ts` is stored as. */
|