@fonderie/admin 0.8.3 → 0.9.0

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/index.d.cts CHANGED
@@ -1,11 +1,23 @@
1
1
  import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem, Middleware } from '@fonderie/core';
2
- import { IStoreAdapter } from '@fonderie/store';
2
+ import { IStoreAdapter, MigrationImpact } from '@fonderie/store';
3
+ import { IRequestSchema } from '@fonderie/core/middlewares';
3
4
 
4
5
  interface IAdminOptions {
5
6
  adminToken?: string;
6
7
  path?: string;
7
8
  checks?: IAdminCheck[];
8
9
  checkTimeoutMs?: number;
10
+ /**
11
+ * The ordered migration sets this deployment applies, exactly as its own
12
+ * applier runs them — brick directories and the app's own, interleaved.
13
+ * Unset ⇒ the migrations routes are not registered at all.
14
+ *
15
+ * Pass the SAME constant the applier reads. The order is the app's to
16
+ * declare and this surface must never infer it: bricks and app migrations
17
+ * depend on each other across the list, and a panel guessing that order
18
+ * would eventually guess wrong in a way that only shows on a fresh database.
19
+ */
20
+ migrations?: ReadonlyArray<IMigrationSet>;
9
21
  ui?: boolean;
10
22
  /**
11
23
  * Answer only for requests addressed to this hostname (or one of these);
@@ -131,6 +143,22 @@ interface IAdminTokensReport {
131
143
  legacy: IAdminTokenEntry[];
132
144
  issued: IAdminTokenRecord[] | null;
133
145
  }
146
+ type IMigrationSet = readonly [name: string, dir: string];
147
+ interface IAdminPendingMigration {
148
+ file: string;
149
+ impact: MigrationImpact;
150
+ destructive: string[];
151
+ }
152
+ interface IAdminMigrationModule {
153
+ name: string;
154
+ pending: IAdminPendingMigration[];
155
+ blockedBy: string | null;
156
+ appliable: boolean;
157
+ }
158
+ interface IAdminMigrationsReport {
159
+ everApplied: boolean;
160
+ modules: IAdminMigrationModule[];
161
+ }
134
162
 
135
163
  declare const DEFAULT_ADMIN_PATH = "/_admin";
136
164
  declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
@@ -171,6 +199,62 @@ declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigRep
171
199
  declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
172
200
  declare function tokensReport(app: IFonderieApp, adminModule: string, issued: IAdminTokenRecord[] | null): IAdminTokensReport;
173
201
 
202
+ /**
203
+ * `expect` is the exact pending list the operator was shown, in order.
204
+ *
205
+ * It is not ceremony. Between rendering the page and clicking the button a
206
+ * deploy can change what is pending — and the thing that changed is precisely
207
+ * the thing nobody reviewed. Sending it back makes the server refuse rather
208
+ * than apply a set the operator never saw. Same shape as versionedWrite's
209
+ * if_version, and it also gives this POST the body schema audit:validation
210
+ * requires.
211
+ */
212
+ declare const applyMigrationsSchema: IRequestSchema;
213
+ /**
214
+ * What every module is waiting on, with each pending file's impact.
215
+ *
216
+ * `blockedBy` names the first EARLIER module that is behind. Order is
217
+ * load-bearing and declared by the app, so a module cannot be applied while
218
+ * something it may depend on is unapplied — the panel offers one button at a
219
+ * time and the operator walks the list in the declared order.
220
+ */
221
+ declare function migrationsReport(store: IStoreAdapter, sets: ReadonlyArray<IMigrationSet>): Promise<IAdminMigrationsReport>;
222
+ type ApplyOutcome = {
223
+ ok: true;
224
+ reason: 'MIGRATIONS_APPLIED' | 'MIGRATIONS_UP_TO_DATE';
225
+ module: IAdminMigrationModule;
226
+ } | {
227
+ ok: false;
228
+ reason: 'NOT_FOUND';
229
+ } | {
230
+ ok: false;
231
+ reason: 'MIGRATIONS_OUT_OF_ORDER';
232
+ blockedBy: string;
233
+ } | {
234
+ ok: false;
235
+ reason: 'MIGRATIONS_CHANGED';
236
+ expected: string[];
237
+ actual: string[];
238
+ } | {
239
+ ok: false;
240
+ reason: 'MIGRATION_DESTRUCTIVE';
241
+ files: IAdminMigrationModule['pending'];
242
+ };
243
+ /**
244
+ * Apply one module's pending migrations — all of them, or none.
245
+ *
246
+ * There is no partial apply, deliberately: files run in lexicographic order, so
247
+ * an additive migration sorting AFTER a destructive one may depend on it.
248
+ * Cherry-picking around a DROP would apply 901 against a schema that never saw
249
+ * 900. Refusing the whole module at the first destructive file is the only safe
250
+ * reading, and happens to be the only one MigrationRunner.run() supports.
251
+ *
252
+ * `expect` is the file list the operator was looking at. Between the render and
253
+ * the click a deploy can change what is pending; without this the button would
254
+ * apply something nobody reviewed.
255
+ */
256
+ declare function applyModuleMigrations(store: IStoreAdapter, sets: ReadonlyArray<IMigrationSet>, module: string, expect: readonly string[]): Promise<ApplyOutcome>;
257
+
174
258
  declare const SCOPES: readonly AdminScope[];
175
259
  declare function scopeFor(method: string, path: string): AdminScope;
176
260
  declare function grants(held: readonly AdminScope[], needed: AdminScope): boolean;
@@ -188,4 +272,4 @@ declare function issueToken(store: IStoreAdapter, input: {
188
272
  declare function revokeToken(store: IStoreAdapter, id: string): Promise<boolean>;
189
273
  declare function requireAdminScope(bootstrap: string, store: IStoreAdapter | undefined, needed: AdminScope | 'root'): Middleware;
190
274
 
191
- export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, MAX_PAGE, SCOPES, adminLog, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
275
+ export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminMigrationModule, type IAdminMigrationsReport, type IAdminModuleEntry, type IAdminOptions, type IAdminPendingMigration, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, type IMigrationSet, MAX_PAGE, SCOPES, adminLog, applyMigrationsSchema, applyModuleMigrations, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, migrationsReport, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,23 @@
1
1
  import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem, Middleware } from '@fonderie/core';
2
- import { IStoreAdapter } from '@fonderie/store';
2
+ import { IStoreAdapter, MigrationImpact } from '@fonderie/store';
3
+ import { IRequestSchema } from '@fonderie/core/middlewares';
3
4
 
4
5
  interface IAdminOptions {
5
6
  adminToken?: string;
6
7
  path?: string;
7
8
  checks?: IAdminCheck[];
8
9
  checkTimeoutMs?: number;
10
+ /**
11
+ * The ordered migration sets this deployment applies, exactly as its own
12
+ * applier runs them — brick directories and the app's own, interleaved.
13
+ * Unset ⇒ the migrations routes are not registered at all.
14
+ *
15
+ * Pass the SAME constant the applier reads. The order is the app's to
16
+ * declare and this surface must never infer it: bricks and app migrations
17
+ * depend on each other across the list, and a panel guessing that order
18
+ * would eventually guess wrong in a way that only shows on a fresh database.
19
+ */
20
+ migrations?: ReadonlyArray<IMigrationSet>;
9
21
  ui?: boolean;
10
22
  /**
11
23
  * Answer only for requests addressed to this hostname (or one of these);
@@ -131,6 +143,22 @@ interface IAdminTokensReport {
131
143
  legacy: IAdminTokenEntry[];
132
144
  issued: IAdminTokenRecord[] | null;
133
145
  }
146
+ type IMigrationSet = readonly [name: string, dir: string];
147
+ interface IAdminPendingMigration {
148
+ file: string;
149
+ impact: MigrationImpact;
150
+ destructive: string[];
151
+ }
152
+ interface IAdminMigrationModule {
153
+ name: string;
154
+ pending: IAdminPendingMigration[];
155
+ blockedBy: string | null;
156
+ appliable: boolean;
157
+ }
158
+ interface IAdminMigrationsReport {
159
+ everApplied: boolean;
160
+ modules: IAdminMigrationModule[];
161
+ }
134
162
 
135
163
  declare const DEFAULT_ADMIN_PATH = "/_admin";
136
164
  declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
@@ -171,6 +199,62 @@ declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigRep
171
199
  declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
172
200
  declare function tokensReport(app: IFonderieApp, adminModule: string, issued: IAdminTokenRecord[] | null): IAdminTokensReport;
173
201
 
202
+ /**
203
+ * `expect` is the exact pending list the operator was shown, in order.
204
+ *
205
+ * It is not ceremony. Between rendering the page and clicking the button a
206
+ * deploy can change what is pending — and the thing that changed is precisely
207
+ * the thing nobody reviewed. Sending it back makes the server refuse rather
208
+ * than apply a set the operator never saw. Same shape as versionedWrite's
209
+ * if_version, and it also gives this POST the body schema audit:validation
210
+ * requires.
211
+ */
212
+ declare const applyMigrationsSchema: IRequestSchema;
213
+ /**
214
+ * What every module is waiting on, with each pending file's impact.
215
+ *
216
+ * `blockedBy` names the first EARLIER module that is behind. Order is
217
+ * load-bearing and declared by the app, so a module cannot be applied while
218
+ * something it may depend on is unapplied — the panel offers one button at a
219
+ * time and the operator walks the list in the declared order.
220
+ */
221
+ declare function migrationsReport(store: IStoreAdapter, sets: ReadonlyArray<IMigrationSet>): Promise<IAdminMigrationsReport>;
222
+ type ApplyOutcome = {
223
+ ok: true;
224
+ reason: 'MIGRATIONS_APPLIED' | 'MIGRATIONS_UP_TO_DATE';
225
+ module: IAdminMigrationModule;
226
+ } | {
227
+ ok: false;
228
+ reason: 'NOT_FOUND';
229
+ } | {
230
+ ok: false;
231
+ reason: 'MIGRATIONS_OUT_OF_ORDER';
232
+ blockedBy: string;
233
+ } | {
234
+ ok: false;
235
+ reason: 'MIGRATIONS_CHANGED';
236
+ expected: string[];
237
+ actual: string[];
238
+ } | {
239
+ ok: false;
240
+ reason: 'MIGRATION_DESTRUCTIVE';
241
+ files: IAdminMigrationModule['pending'];
242
+ };
243
+ /**
244
+ * Apply one module's pending migrations — all of them, or none.
245
+ *
246
+ * There is no partial apply, deliberately: files run in lexicographic order, so
247
+ * an additive migration sorting AFTER a destructive one may depend on it.
248
+ * Cherry-picking around a DROP would apply 901 against a schema that never saw
249
+ * 900. Refusing the whole module at the first destructive file is the only safe
250
+ * reading, and happens to be the only one MigrationRunner.run() supports.
251
+ *
252
+ * `expect` is the file list the operator was looking at. Between the render and
253
+ * the click a deploy can change what is pending; without this the button would
254
+ * apply something nobody reviewed.
255
+ */
256
+ declare function applyModuleMigrations(store: IStoreAdapter, sets: ReadonlyArray<IMigrationSet>, module: string, expect: readonly string[]): Promise<ApplyOutcome>;
257
+
174
258
  declare const SCOPES: readonly AdminScope[];
175
259
  declare function scopeFor(method: string, path: string): AdminScope;
176
260
  declare function grants(held: readonly AdminScope[], needed: AdminScope): boolean;
@@ -188,4 +272,4 @@ declare function issueToken(store: IStoreAdapter, input: {
188
272
  declare function revokeToken(store: IStoreAdapter, id: string): Promise<boolean>;
189
273
  declare function requireAdminScope(bootstrap: string, store: IStoreAdapter | undefined, needed: AdminScope | 'root'): Middleware;
190
274
 
191
- export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, MAX_PAGE, SCOPES, adminLog, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
275
+ export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminMigrationModule, type IAdminMigrationsReport, type IAdminModuleEntry, type IAdminOptions, type IAdminPendingMigration, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, type IMigrationSet, MAX_PAGE, SCOPES, adminLog, applyMigrationsSchema, applyModuleMigrations, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, migrationsReport, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/module.ts
2
- import { readFileSync } from "fs";
2
+ import { readFileSync as readFileSync2 } from "fs";
3
3
  import { HTTP as HTTP2, normalizeMountPath, normalizeRequestPath as normalizeRequestPath2, setApiResponse as setApiResponse2 } from "@fonderie/core";
4
4
  import { validate, validateAdminToken } from "@fonderie/core/middlewares";
5
5
 
@@ -142,6 +142,91 @@ function decodeCursor(c) {
142
142
  return at && id ? { at, id } : null;
143
143
  }
144
144
 
145
+ // src/migrate.ts
146
+ import { readFileSync } from "fs";
147
+ import { join } from "path";
148
+ import { classifyMigration, InternalMigrationRunner } from "@fonderie/store";
149
+ var applyMigrationsSchema = {
150
+ safeParse(input) {
151
+ const fail = (path, message) => ({
152
+ success: false,
153
+ error: { issues: [{ path: [path], message }] }
154
+ });
155
+ const b = input ?? {};
156
+ const expect = b["expect"];
157
+ if (!Array.isArray(expect) || !expect.every((f) => typeof f === "string"))
158
+ return fail("expect", "expect must be the list of pending filenames you were shown");
159
+ return { success: true, data: { expect } };
160
+ }
161
+ };
162
+ async function hasMigrationHistory(store) {
163
+ try {
164
+ const rows = await store.query(
165
+ "SELECT count(*)::int AS n FROM fonderie_migrations"
166
+ );
167
+ return (rows[0]?.n ?? 0) > 0;
168
+ } catch {
169
+ return false;
170
+ }
171
+ }
172
+ function classifyPending(dir, files) {
173
+ return files.map((file) => {
174
+ const { impact, destructive } = classifyMigration(readFileSync(join(dir, file), "utf8"));
175
+ return { file, impact, destructive };
176
+ });
177
+ }
178
+ async function migrationsReport(store, sets) {
179
+ const everApplied = await hasMigrationHistory(store);
180
+ const modules = [];
181
+ let firstBehind = null;
182
+ for (const [name, dir] of sets) {
183
+ const pending = await new InternalMigrationRunner(store, dir).pending();
184
+ const files = classifyPending(dir, pending);
185
+ modules.push({
186
+ name,
187
+ pending: files,
188
+ // Only a module BEFORE this one blocks it. The first module with
189
+ // pending work blocks every module after it, and nothing blocks it.
190
+ blockedBy: firstBehind,
191
+ // A file that deletes data on a database with nothing in it yet is not
192
+ // destroying anything. Same rule the CLI's --check applies.
193
+ appliable: files.length > 0 && firstBehind === null && (!everApplied || files.every((f) => f.impact === "additive"))
194
+ });
195
+ if (files.length > 0 && firstBehind === null) firstBehind = name;
196
+ }
197
+ return { everApplied, modules };
198
+ }
199
+ async function applyModuleMigrations(store, sets, module, expect) {
200
+ const set = sets.find(([name]) => name === module);
201
+ if (!set) return { ok: false, reason: "NOT_FOUND" };
202
+ const before = await migrationsReport(store, sets);
203
+ const target = before.modules.find((m) => m.name === module);
204
+ if (!target) return { ok: false, reason: "NOT_FOUND" };
205
+ if (target.blockedBy) {
206
+ return { ok: false, reason: "MIGRATIONS_OUT_OF_ORDER", blockedBy: target.blockedBy };
207
+ }
208
+ const actual = target.pending.map((p) => p.file);
209
+ const same = actual.length === expect.length && actual.every((f, i) => f === expect[i]);
210
+ if (!same) {
211
+ return { ok: false, reason: "MIGRATIONS_CHANGED", expected: [...expect], actual };
212
+ }
213
+ if (actual.length === 0) {
214
+ return { ok: true, reason: "MIGRATIONS_UP_TO_DATE", module: target };
215
+ }
216
+ const destructive = target.pending.filter((p) => p.impact === "destructive");
217
+ if (destructive.length > 0 && before.everApplied) {
218
+ return { ok: false, reason: "MIGRATION_DESTRUCTIVE", files: destructive };
219
+ }
220
+ await new InternalMigrationRunner(store, set[1]).run();
221
+ const after = await migrationsReport(store, sets);
222
+ const now = after.modules.find((m) => m.name === module);
223
+ return {
224
+ ok: true,
225
+ reason: "MIGRATIONS_APPLIED",
226
+ module: now ?? { name: module, pending: [], blockedBy: null, appliable: false }
227
+ };
228
+ }
229
+
145
230
  // src/manifest.ts
146
231
  function buildManifest(app, admin) {
147
232
  const report = app.securityReport();
@@ -332,7 +417,7 @@ var uiHtml = (scriptPath) => `<!doctype html>
332
417
  // src/module.ts
333
418
  var DEFAULT_ADMIN_PATH = "/_admin";
334
419
  var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
335
- var ADMIN_VERSION = "0.8.3";
420
+ var ADMIN_VERSION = "0.9.0";
336
421
  function normalizeHostPattern(host) {
337
422
  return host.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
338
423
  }
@@ -465,6 +550,70 @@ var AdminModule = class {
465
550
  ]
466
551
  ]
467
552
  ] : [];
553
+ const migrationSets = this.options.migrations;
554
+ const scoped = store && migrationSets ? [
555
+ [
556
+ "GET",
557
+ "/_admin/migrations",
558
+ [
559
+ async () => setApiResponse2(
560
+ HTTP2.OK,
561
+ "MIGRATIONS",
562
+ "Pending migrations by module",
563
+ await migrationsReport(store, migrationSets)
564
+ )
565
+ ]
566
+ ],
567
+ [
568
+ "POST",
569
+ "/_admin/migrations/:module/apply",
570
+ [
571
+ validate(applyMigrationsSchema),
572
+ async (ctx) => {
573
+ const { expect } = ctx.meta["body"];
574
+ const name = ctx.meta.params?.["module"] ?? "";
575
+ const out = await applyModuleMigrations(store, migrationSets, name, expect);
576
+ if (out.ok) {
577
+ return setApiResponse2(
578
+ HTTP2.OK,
579
+ out.reason,
580
+ out.reason === "MIGRATIONS_APPLIED" ? `Applied. ${out.module.pending.length} still pending in ${name}.` : `${name} is already up to date`,
581
+ out.module
582
+ );
583
+ }
584
+ switch (out.reason) {
585
+ case "NOT_FOUND":
586
+ return setApiResponse2(
587
+ HTTP2.NOT_FOUND,
588
+ "NOT_FOUND",
589
+ `No migration set named "${name}"`
590
+ );
591
+ case "MIGRATIONS_OUT_OF_ORDER":
592
+ return setApiResponse2(
593
+ HTTP2.CONFLICT,
594
+ out.reason,
595
+ `Apply "${out.blockedBy}" first \u2014 it runs before "${name}" and is behind.`,
596
+ { blockedBy: out.blockedBy }
597
+ );
598
+ case "MIGRATIONS_CHANGED":
599
+ return setApiResponse2(
600
+ HTTP2.CONFLICT,
601
+ out.reason,
602
+ "What is pending changed since you looked. Refresh and read it again.",
603
+ { expected: out.expected, actual: out.actual }
604
+ );
605
+ default:
606
+ return setApiResponse2(
607
+ HTTP2.UNPROCESSABLE,
608
+ out.reason,
609
+ "A pending migration deletes data. No down-migration brings it back \u2014 apply it through CI or `npm run migrate`, not from here.",
610
+ { files: out.files }
611
+ );
612
+ }
613
+ }
614
+ ]
615
+ ]
616
+ ] : [];
468
617
  const mounted = /* @__PURE__ */ new Map();
469
618
  const mount = (module, method, path, handlers, needed) => {
470
619
  const key = `${method.toUpperCase()} ${path}`;
@@ -492,7 +641,7 @@ var AdminModule = class {
492
641
  const loadJs = () => {
493
642
  if (js === null) {
494
643
  try {
495
- js = readFileSync(new URL("./ui/app.global.js", import.meta.url), "utf8");
644
+ js = readFileSync2(new URL("./ui/app.global.js", import.meta.url), "utf8");
496
645
  } catch {
497
646
  js = "";
498
647
  }
@@ -531,6 +680,7 @@ var AdminModule = class {
531
680
  for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
532
681
  for (const [method, path, handlers] of rootOnly)
533
682
  mount(this.name, method, at(path), handlers, "root");
683
+ for (const [method, path, handlers] of scoped) mount(this.name, method, at(path), handlers);
534
684
  for (const { module, description } of app.adminDescriptions()) {
535
685
  for (const r of description.routes ?? [])
536
686
  mount(module, r.method, this.path + r.path, r.handlers);
@@ -549,6 +699,8 @@ export {
549
699
  MAX_PAGE,
550
700
  SCOPES,
551
701
  adminLog,
702
+ applyMigrationsSchema,
703
+ applyModuleMigrations,
552
704
  attention,
553
705
  buildManifest,
554
706
  collectChecks,
@@ -557,6 +709,7 @@ export {
557
709
  hashToken,
558
710
  issueToken,
559
711
  listTokens,
712
+ migrationsReport,
560
713
  readAdminLog,
561
714
  requireAdminScope,
562
715
  revokeToken,