@geekmidas/cli 10.0.0-alpha.3 → 10.0.0-alpha.30
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/README.md +25 -48
- package/bin/adjacent-tsconfig.mjs +97 -0
- package/bin/gkm.mjs +7 -1
- package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-BZFGYYVA.cjs} +21 -22
- package/dist/HostingerProvider-BZFGYYVA.cjs.map +1 -0
- package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-GF6NAZAK.mjs} +20 -21
- package/dist/HostingerProvider-GF6NAZAK.mjs.map +1 -0
- package/dist/{LocalStateProvider-Db9SEjyH.cjs → LocalStateProvider-7HR6SS2t.cjs} +11 -6
- package/dist/LocalStateProvider-7HR6SS2t.cjs.map +1 -0
- package/dist/{LocalStateProvider-Bn6tS-oL.mjs → LocalStateProvider-D7QyJoLM.mjs} +10 -4
- package/dist/LocalStateProvider-D7QyJoLM.mjs.map +1 -0
- package/dist/{openapi-c9bpieI5.cjs → OpenApiTsGenerator-D1nhN98m.cjs} +135 -298
- package/dist/OpenApiTsGenerator-D1nhN98m.cjs.map +1 -0
- package/dist/{openapi-CY9xFkTH.mjs → OpenApiTsGenerator-hDINScnz.mjs} +106 -250
- package/dist/OpenApiTsGenerator-hDINScnz.mjs.map +1 -0
- package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-Cvj40A8I.mjs} +12 -12
- package/dist/Route53Provider-Cvj40A8I.mjs.map +1 -0
- package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-DVILBsvV.cjs} +20 -21
- package/dist/Route53Provider-DVILBsvV.cjs.map +1 -0
- package/dist/{SSMStateProvider-BRt2py-A.mjs → SSMStateProvider-B0Hu1MKZ.mjs} +13 -4
- package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +1 -0
- package/dist/{SSMStateProvider-DyX8s9HI.cjs → SSMStateProvider-VrQ-yeqd.cjs} +17 -9
- package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +1 -0
- package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-BmhIO_Yr.cjs} +21 -19
- package/dist/backup-provisioner-BmhIO_Yr.cjs.map +1 -0
- package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DIVCbfee.mjs} +10 -7
- package/dist/backup-provisioner-DIVCbfee.mjs.map +1 -0
- package/dist/{bundler-BY1OmV1e.cjs → bundler-CYF5mWi9.cjs} +10 -28
- package/dist/bundler-CYF5mWi9.cjs.map +1 -0
- package/dist/{bundler-Be8Zg4Wd.mjs → bundler-D9IPNvDt.mjs} +9 -26
- package/dist/bundler-D9IPNvDt.mjs.map +1 -0
- package/dist/{config-CzWnfcXM.mjs → config-BH3Ki69F.mjs} +49 -89
- package/dist/config-BH3Ki69F.mjs.map +1 -0
- package/dist/{config-BholeD-0.cjs → config-URVD4bTj.cjs} +101 -148
- package/dist/config-URVD4bTj.cjs.map +1 -0
- package/dist/config.cjs +5 -5
- package/dist/config.d.cts +25 -17
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +25 -17
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -5
- package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-hooks.cjs +13 -7
- package/dist/deploy/sniffer-hooks.cjs.map +1 -1
- package/dist/deploy/sniffer-hooks.d.cts +1 -2
- package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
- package/dist/deploy/sniffer-hooks.d.mts +1 -2
- package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
- package/dist/deploy/sniffer-hooks.mjs +9 -2
- package/dist/deploy/sniffer-hooks.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +16 -7
- package/dist/deploy/sniffer-loader.cjs.map +1 -1
- package/dist/deploy/sniffer-loader.d.cts +1 -1
- package/dist/deploy/sniffer-loader.d.mts +1 -1
- package/dist/deploy/sniffer-loader.mjs +13 -2
- package/dist/deploy/sniffer-loader.mjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.cjs +11 -11
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
- package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
- package/dist/deploy/sniffer-routes-worker.mjs +3 -3
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
- package/dist/deploy/sniffer-worker.cjs +15 -6
- package/dist/deploy/sniffer-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-worker.d.cts +1 -3
- package/dist/deploy/sniffer-worker.d.cts.map +1 -1
- package/dist/deploy/sniffer-worker.d.mts +1 -3
- package/dist/deploy/sniffer-worker.d.mts.map +1 -1
- package/dist/deploy/sniffer-worker.mjs +15 -4
- package/dist/deploy/sniffer-worker.mjs.map +1 -1
- package/dist/{discover-CAlFgYec.mjs → discover-CMxesskG.mjs} +66 -22
- package/dist/discover-CMxesskG.mjs.map +1 -0
- package/dist/{discover-DTmFeloL.cjs → discover-CroTIm0K.cjs} +117 -66
- package/dist/discover-CroTIm0K.cjs.map +1 -0
- package/dist/{emit-CbcRolsa.cjs → emit-BdoPFjZf.cjs} +53 -36
- package/dist/emit-BdoPFjZf.cjs.map +1 -0
- package/dist/{emit-CiZS_5Ub.mjs → emit-Bzq7JsMP.mjs} +24 -6
- package/dist/emit-Bzq7JsMP.mjs.map +1 -0
- package/dist/github-B2fis34P.cjs +216 -0
- package/dist/github-B2fis34P.cjs.map +1 -0
- package/dist/github-BrDmlm1x.mjs +216 -0
- package/dist/github-BrDmlm1x.mjs.map +1 -0
- package/dist/{index-CHB4tSzX.d.cts → index-p89EzILa.d.mts} +4 -23
- package/dist/index-p89EzILa.d.mts.map +1 -0
- package/dist/{index-BThUzEIp.d.mts → index-rU9u761a.d.cts} +4 -23
- package/dist/index-rU9u761a.d.cts.map +1 -0
- package/dist/index.cjs +6001 -4452
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +5895 -4367
- package/dist/index.mjs.map +1 -1
- package/dist/migrate-BuCx7tIv.cjs +498 -0
- package/dist/migrate-BuCx7tIv.cjs.map +1 -0
- package/dist/migrate-CBICVMAi.mjs +493 -0
- package/dist/migrate-CBICVMAi.mjs.map +1 -0
- package/dist/openapi.cjs +197 -8
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +47 -14
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +47 -14
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +191 -5
- package/dist/openapi.mjs.map +1 -0
- package/dist/ready-CvP3JVEw.mjs +355 -0
- package/dist/ready-CvP3JVEw.mjs.map +1 -0
- package/dist/ready-DXtNfk7v.cjs +390 -0
- package/dist/ready-DXtNfk7v.cjs.map +1 -0
- package/dist/reconcile/public.cjs +4 -4
- package/dist/reconcile/public.d.cts +27 -26
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +27 -26
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +3 -4
- package/dist/reconcile-BxZTV2vl.cjs +332 -0
- package/dist/reconcile-BxZTV2vl.cjs.map +1 -0
- package/dist/reconcile-yNq6Shau.mjs +279 -0
- package/dist/reconcile-yNq6Shau.mjs.map +1 -0
- package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
- package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
- package/dist/ssm-BKN1Cg4z.mjs +60 -0
- package/dist/ssm-BKN1Cg4z.mjs.map +1 -0
- package/dist/ssm-FxeSAt35.cjs +60 -0
- package/dist/ssm-FxeSAt35.cjs.map +1 -0
- package/dist/{storage-Bi24WfoW.mjs → storage-BjTLjDDp.mjs} +22 -22
- package/dist/storage-BjTLjDDp.mjs.map +1 -0
- package/dist/{storage-BJmzYxt8.cjs → storage-C2JC31V9.cjs} +65 -84
- package/dist/storage-C2JC31V9.cjs.map +1 -0
- package/dist/types-CzK6hsXP.d.mts +6286 -0
- package/dist/types-CzK6hsXP.d.mts.map +1 -0
- package/dist/types-DOOAevjG.d.cts +6287 -0
- package/dist/types-DOOAevjG.d.cts.map +1 -0
- package/dist/vitest.cjs +61 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +28 -0
- package/dist/vitest.d.cts.map +1 -0
- package/dist/vitest.d.mts +28 -0
- package/dist/vitest.d.mts.map +1 -0
- package/dist/vitest.mjs +61 -0
- package/dist/vitest.mjs.map +1 -0
- package/dist/workspace/index.cjs +3 -4
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -3
- package/dist/workspace-Bav3TVrP.cjs +6710 -0
- package/dist/workspace-Bav3TVrP.cjs.map +1 -0
- package/dist/{credentials-DQmbRnr1.cjs → workspace-BhiMkRuA.cjs} +2076 -1963
- package/dist/workspace-BhiMkRuA.cjs.map +1 -0
- package/dist/{credentials-BZcp3CGx.mjs → workspace-C97QUCIf.mjs} +1957 -1837
- package/dist/workspace-C97QUCIf.mjs.map +1 -0
- package/dist/workspace-T4IVGSoB.mjs +6538 -0
- package/dist/workspace-T4IVGSoB.mjs.map +1 -0
- package/package.json +45 -38
- package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
- package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
- package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
- package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
- package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
- package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
- package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
- package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
- package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
- package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
- package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
- package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
- package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
- package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
- package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
- package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
- package/dist/bundler-BY1OmV1e.cjs.map +0 -1
- package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
- package/dist/chunk-Duj1WY3L.mjs +0 -7
- package/dist/config-BholeD-0.cjs.map +0 -1
- package/dist/config-CzWnfcXM.mjs.map +0 -1
- package/dist/credentials-B5zRHdAl.mjs +0 -141
- package/dist/credentials-B5zRHdAl.mjs.map +0 -1
- package/dist/credentials-BZcp3CGx.mjs.map +0 -1
- package/dist/credentials-BapzjyV5.cjs +0 -195
- package/dist/credentials-BapzjyV5.cjs.map +0 -1
- package/dist/credentials-DQmbRnr1.cjs.map +0 -1
- package/dist/discover-CAlFgYec.mjs.map +0 -1
- package/dist/discover-DTmFeloL.cjs.map +0 -1
- package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
- package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
- package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
- package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
- package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
- package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
- package/dist/emit-CbcRolsa.cjs.map +0 -1
- package/dist/emit-CiZS_5Ub.mjs.map +0 -1
- package/dist/encryption-C6jaU0hE.cjs +0 -54
- package/dist/encryption-C6jaU0hE.cjs.map +0 -1
- package/dist/encryption-CHzzeR8a.cjs +0 -4
- package/dist/encryption-CQUncu7v.mjs +0 -3
- package/dist/encryption-D-abyhZx.mjs +0 -42
- package/dist/encryption-D-abyhZx.mjs.map +0 -1
- package/dist/fullstack-secrets-C-GGohke.cjs +0 -111
- package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
- package/dist/fullstack-secrets-D1o9-G5l.mjs +0 -87
- package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
- package/dist/generator-CNzBw7g5.cjs +0 -289
- package/dist/generator-CNzBw7g5.cjs.map +0 -1
- package/dist/generator-VE2JrFlY.mjs +0 -253
- package/dist/generator-VE2JrFlY.mjs.map +0 -1
- package/dist/index-BThUzEIp.d.mts.map +0 -1
- package/dist/index-CHB4tSzX.d.cts.map +0 -1
- package/dist/openapi-CY9xFkTH.mjs.map +0 -1
- package/dist/openapi-c9bpieI5.cjs.map +0 -1
- package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
- package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
- package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
- package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
- package/dist/openapi-react-query.cjs +0 -4
- package/dist/openapi-react-query.d.cts +0 -11
- package/dist/openapi-react-query.d.cts.map +0 -1
- package/dist/openapi-react-query.d.mts +0 -11
- package/dist/openapi-react-query.d.mts.map +0 -1
- package/dist/openapi-react-query.mjs +0 -4
- package/dist/reconcile-CFs8C_1a.mjs +0 -37
- package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
- package/dist/reconcile-D0pimNiy.cjs +0 -37
- package/dist/reconcile-D0pimNiy.cjs.map +0 -1
- package/dist/setup-Bc73t0ve.mjs +0 -443
- package/dist/setup-Bc73t0ve.mjs.map +0 -1
- package/dist/setup-C48C9hto.cjs +0 -473
- package/dist/setup-C48C9hto.cjs.map +0 -1
- package/dist/setup-DCuudnCl.mjs +0 -11
- package/dist/setup-fG_ivuxO.cjs +0 -11
- package/dist/storage-BJmzYxt8.cjs.map +0 -1
- package/dist/storage-BM9hGpgv.cjs +0 -7
- package/dist/storage-Bi24WfoW.mjs.map +0 -1
- package/dist/storage-CYlGwWTj.mjs +0 -3
- package/dist/sync-CmEuNZsB.mjs +0 -76
- package/dist/sync-CmEuNZsB.mjs.map +0 -1
- package/dist/sync-D306yOWu.cjs +0 -4
- package/dist/sync-SizaChmC.cjs +0 -93
- package/dist/sync-SizaChmC.cjs.map +0 -1
- package/dist/sync-hOqAWHR6.mjs +0 -3
- package/dist/types-C-arnQ1r.d.mts +0 -2184
- package/dist/types-C-arnQ1r.d.mts.map +0 -1
- package/dist/types-abJQuk7m.d.cts +0 -2184
- package/dist/types-abJQuk7m.d.cts.map +0 -1
- package/dist/workspace-7FqprxuJ.mjs +0 -4323
- package/dist/workspace-7FqprxuJ.mjs.map +0 -1
- package/dist/workspace-BO_KWohI.cjs +0 -9
- package/dist/workspace-DK4WWJIn.mjs +0 -8
- package/dist/workspace-XvqO9jIv.cjs +0 -4472
- package/dist/workspace-XvqO9jIv.cjs.map +0 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { t as discover } from "./discover-CMxesskG.mjs";
|
|
2
|
+
import { s as loadWorkspaceConfig } from "./config-BH3Ki69F.mjs";
|
|
3
|
+
import { E as TEST_STAGE } from "./workspace-T4IVGSoB.mjs";
|
|
4
|
+
import { b as loadSecretsForApp, i as reconcileWorkspace, n as constructGlobs } from "./workspace-C97QUCIf.mjs";
|
|
5
|
+
import { i as stamp, r as slug } from "./index.mjs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
8
|
+
import { extname, join } from "node:path";
|
|
9
|
+
import { MIGRATIONS_ROOT, canonicalId, migrationTargets, provideKey } from "@geekmidas/manifest";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import pg from "pg";
|
|
12
|
+
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
13
|
+
import { serviceContext } from "@geekmidas/services";
|
|
14
|
+
//#region src/migrate/provider.ts
|
|
15
|
+
/**
|
|
16
|
+
* One construct's migrations, read from its folder.
|
|
17
|
+
*
|
|
18
|
+
* Two kinds of file, told apart by extension rather than by configuration: a
|
|
19
|
+
* script exports `up` (and may export `down`), and a `.sql` file is its own
|
|
20
|
+
* `up`. The second is what makes a folder independent of the client the app
|
|
21
|
+
* uses — Better Auth's schema is SQL whatever the application queries with.
|
|
22
|
+
*/
|
|
23
|
+
const SCRIPTS = /* @__PURE__ */ new Set([
|
|
24
|
+
".ts",
|
|
25
|
+
".mts",
|
|
26
|
+
".js",
|
|
27
|
+
".mjs"
|
|
28
|
+
]);
|
|
29
|
+
/** Two files in one folder that would be one migration. */
|
|
30
|
+
var DuplicateMigration = class extends Error {
|
|
31
|
+
folder;
|
|
32
|
+
migration;
|
|
33
|
+
constructor(folder, migration) {
|
|
34
|
+
super(`${folder} has two migrations named '${migration}' — a script and a .sql file with the same name. A migration is recorded by name, so only one of them could ever run; rename or remove the other.`);
|
|
35
|
+
this.folder = folder;
|
|
36
|
+
this.migration = migration;
|
|
37
|
+
this.name = "DuplicateMigration";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** A script in a migrations folder that exports no `up`. */
|
|
41
|
+
var MigrationHasNoUp = class extends Error {
|
|
42
|
+
file;
|
|
43
|
+
constructor(file) {
|
|
44
|
+
super(`${file} is in a migrations folder but exports no \`up\` function. Export \`async function up(db: Kysely<unknown>)\`, or move the file out of the folder if it is not a migration.`);
|
|
45
|
+
this.file = file;
|
|
46
|
+
this.name = "MigrationHasNoUp";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The migration files in a folder, by name, without reading them.
|
|
51
|
+
*
|
|
52
|
+
* Also what `gkm dev` compares against a construct's history to report what
|
|
53
|
+
* is pending, which is why it is separate from loading them.
|
|
54
|
+
*/
|
|
55
|
+
async function migrationFiles(folder) {
|
|
56
|
+
const entries = await readdir(folder, { withFileTypes: true }).catch((error) => {
|
|
57
|
+
if (error.code === "ENOENT") return [];
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
60
|
+
const files = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.isFile() || !isMigration(entry.name)) continue;
|
|
63
|
+
const name = entry.name.slice(0, -extname(entry.name).length);
|
|
64
|
+
if (files.has(name)) throw new DuplicateMigration(folder, name);
|
|
65
|
+
files.set(name, join(folder, entry.name));
|
|
66
|
+
}
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
/** A Kysely provider over one construct's folder. */
|
|
70
|
+
function folderProvider(folder, raw) {
|
|
71
|
+
return { async getMigrations() {
|
|
72
|
+
const migrations = {};
|
|
73
|
+
for (const [name, file] of await migrationFiles(folder)) migrations[name] = extname(file) === ".sql" ? await sqlMigration(file, raw) : await scriptMigration(file);
|
|
74
|
+
return migrations;
|
|
75
|
+
} };
|
|
76
|
+
}
|
|
77
|
+
function isMigration(file) {
|
|
78
|
+
if (file.endsWith(".d.ts")) return false;
|
|
79
|
+
if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
|
|
80
|
+
const extension = extname(file);
|
|
81
|
+
return extension === ".sql" || SCRIPTS.has(extension);
|
|
82
|
+
}
|
|
83
|
+
async function scriptMigration(file) {
|
|
84
|
+
const module = await import(pathToFileURL(file).href);
|
|
85
|
+
if (typeof module.up !== "function") throw new MigrationHasNoUp(file);
|
|
86
|
+
return {
|
|
87
|
+
up: module.up,
|
|
88
|
+
...typeof module.down === "function" ? { down: module.down } : {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A `.sql` file, run whole.
|
|
93
|
+
*
|
|
94
|
+
* One query rather than split on `;`: with no parameters, Postgres takes
|
|
95
|
+
* several statements in one message, and splitting by hand breaks on the first
|
|
96
|
+
* `;` inside a string or a function body. There is no `down` — a generated
|
|
97
|
+
* schema is rolled back by a migration that says how, not by guessing.
|
|
98
|
+
*/
|
|
99
|
+
async function sqlMigration(file, raw) {
|
|
100
|
+
const statements = await readFile(file, "utf-8");
|
|
101
|
+
return { up: async (db) => {
|
|
102
|
+
await raw.raw(statements).execute(db);
|
|
103
|
+
} };
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/migrate/databases.ts
|
|
107
|
+
/**
|
|
108
|
+
* Bring each construct's schema up to its migrations folder.
|
|
109
|
+
*
|
|
110
|
+
* Which constructs, in what order, from which folder, is `migrationTargets`'
|
|
111
|
+
* answer and nobody else's. What is decided here is only how one target is
|
|
112
|
+
* run: as its owner, in its own schema, with a history of its own.
|
|
113
|
+
*/
|
|
114
|
+
/** A folder under `db/` that names no construct. */
|
|
115
|
+
var UnknownMigrationFolder = class extends Error {
|
|
116
|
+
folder;
|
|
117
|
+
known;
|
|
118
|
+
constructor(folder, known) {
|
|
119
|
+
super(`${folder} names no database construct, so its migrations would never run. ${known.length > 0 ? `The folders that do: ${known.join(", ")}.` : "This project declares no database."} Rename the folder after the construct it belongs to.`);
|
|
120
|
+
this.folder = folder;
|
|
121
|
+
this.known = known;
|
|
122
|
+
this.name = "UnknownMigrationFolder";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** A construct was asked for by a name that takes no migrations. */
|
|
126
|
+
var NoSuchMigrationTarget = class extends Error {
|
|
127
|
+
construct;
|
|
128
|
+
known;
|
|
129
|
+
constructor(construct, known) {
|
|
130
|
+
super(`'${construct}' is not a database construct. ` + (known.length > 0 ? `The ones with migrations: ${known.join(", ")}.` : "This project declares no database."));
|
|
131
|
+
this.construct = construct;
|
|
132
|
+
this.known = known;
|
|
133
|
+
this.name = "NoSuchMigrationTarget";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/** A construct with roles has no owner URL to migrate with. */
|
|
137
|
+
var NoOwnerCredential = class extends Error {
|
|
138
|
+
construct;
|
|
139
|
+
key;
|
|
140
|
+
constructor(construct, key) {
|
|
141
|
+
super(`${construct} has no ${key} to migrate with. Migrations run as the owner role — the runtime one cannot create a table — and nothing resolved its URL. Is the stage reconciled?`);
|
|
142
|
+
this.construct = construct;
|
|
143
|
+
this.key = key;
|
|
144
|
+
this.name = "NoOwnerCredential";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/** A migration failed; the ones before it in the run stay applied. */
|
|
148
|
+
var MigrationFailed = class extends Error {
|
|
149
|
+
construct;
|
|
150
|
+
migration;
|
|
151
|
+
cause;
|
|
152
|
+
constructor(construct, migration, cause) {
|
|
153
|
+
super(`${construct}: migration ${migration ? `'${migration}' ` : ""}failed — ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
154
|
+
this.construct = construct;
|
|
155
|
+
this.migration = migration;
|
|
156
|
+
this.cause = cause;
|
|
157
|
+
this.name = "MigrationFailed";
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
/** The project's Kysely could not be found from where a database is declared. */
|
|
161
|
+
var KyselyNotFound = class extends Error {
|
|
162
|
+
from;
|
|
163
|
+
constructor(from) {
|
|
164
|
+
super(`Could not resolve 'kysely' from ${from}. Migrations run with the project's own Kysely — install it where the database is declared.`);
|
|
165
|
+
this.from = from;
|
|
166
|
+
this.name = "KyselyNotFound";
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
/** Apply every pending migration, construct by construct, parents first. */
|
|
170
|
+
async function migrateDatabases(options) {
|
|
171
|
+
const runs = [];
|
|
172
|
+
for (const target of await targetsFor(options)) {
|
|
173
|
+
if ((await migrationFiles(join(options.root, target.folder))).size === 0) {
|
|
174
|
+
runs.push({
|
|
175
|
+
target,
|
|
176
|
+
applied: []
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const applied = await withMigrator(options, target, async (migrator) => {
|
|
181
|
+
const { error, results = [] } = await migrator.migrateToLatest();
|
|
182
|
+
if (error) {
|
|
183
|
+
const failed = results.find((result) => result.status === "Error");
|
|
184
|
+
throw new MigrationFailed(target.id, failed?.migrationName, error);
|
|
185
|
+
}
|
|
186
|
+
return results.filter((result) => result.status === "Success").map((result) => result.migrationName);
|
|
187
|
+
});
|
|
188
|
+
runs.push({
|
|
189
|
+
target,
|
|
190
|
+
applied
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return runs;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* What each construct's folder has that its history does not — without
|
|
197
|
+
* applying any of it. What `gkm dev` reports.
|
|
198
|
+
*/
|
|
199
|
+
async function pendingMigrations(options) {
|
|
200
|
+
const found = [];
|
|
201
|
+
for (const target of await targetsFor(options)) {
|
|
202
|
+
if ((await migrationFiles(join(options.root, target.folder))).size === 0) continue;
|
|
203
|
+
const pending = await withMigrator(options, target, async (migrator) => (await migrator.getMigrations()).filter((migration) => migration.executedAt === void 0).map((migration) => migration.name));
|
|
204
|
+
if (pending.length > 0) found.push({
|
|
205
|
+
target,
|
|
206
|
+
pending
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return found;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The targets this run covers, with every folder under `db/` accounted for.
|
|
213
|
+
*
|
|
214
|
+
* A folder that names no construct is an error rather than something to skip:
|
|
215
|
+
* a typo in its name is otherwise a migration that silently never runs.
|
|
216
|
+
*/
|
|
217
|
+
async function targetsFor(options) {
|
|
218
|
+
const targets = migrationTargets(options.manifest);
|
|
219
|
+
const folders = targets.map((target) => target.folder);
|
|
220
|
+
for (const folder of await foldersIn(join(options.root, MIGRATIONS_ROOT))) {
|
|
221
|
+
const relative = `${MIGRATIONS_ROOT}/${folder}`;
|
|
222
|
+
if (!folders.includes(relative)) throw new UnknownMigrationFolder(relative, folders);
|
|
223
|
+
}
|
|
224
|
+
if (!options.only) return targets;
|
|
225
|
+
const id = canonicalId(options.only);
|
|
226
|
+
const chosen = targets.filter((target) => target.id === id);
|
|
227
|
+
if (chosen.length === 0) throw new NoSuchMigrationTarget(options.only, targets.map((target) => target.id));
|
|
228
|
+
return chosen;
|
|
229
|
+
}
|
|
230
|
+
async function foldersIn(dir) {
|
|
231
|
+
return (await readdir(dir, { withFileTypes: true }).catch((error) => {
|
|
232
|
+
if (error.code === "ENOENT") return [];
|
|
233
|
+
throw error;
|
|
234
|
+
})).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* A migrator for one target, connected as its owner and closed afterwards.
|
|
238
|
+
*
|
|
239
|
+
* No `migrationTableSchema`: the owner role's `search_path` is pinned to the
|
|
240
|
+
* construct's schema, so the history lands there — `app.kysely_migration` for
|
|
241
|
+
* the database, `auth_database.kysely_migration` for its tenant — and two
|
|
242
|
+
* constructs in one Postgres never share one.
|
|
243
|
+
*
|
|
244
|
+
* Unordered migrations are allowed. Files are timestamped, so a branch merged
|
|
245
|
+
* after another's migration ran carries an older one; refusing it would make
|
|
246
|
+
* every such merge a manual repair.
|
|
247
|
+
*/
|
|
248
|
+
async function withMigrator(options, target, run) {
|
|
249
|
+
const { Kysely, PostgresDialect, sql, Migrator } = await kyselyFrom(options.sources?.[target.id]?.file ?? (target.of ? options.sources?.[target.of]?.file : void 0) ?? join(options.root, "package.json"));
|
|
250
|
+
const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool(poolConfig(ownerUrl(options, target))) }) });
|
|
251
|
+
try {
|
|
252
|
+
return await run(new Migrator({
|
|
253
|
+
db,
|
|
254
|
+
provider: folderProvider(join(options.root, target.folder), sql),
|
|
255
|
+
allowUnorderedMigrations: true
|
|
256
|
+
}));
|
|
257
|
+
} finally {
|
|
258
|
+
await db.destroy();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The owner's URL, with no fallback to the runtime one.
|
|
263
|
+
*
|
|
264
|
+
* The runtime role can create nothing, so falling back would only move the
|
|
265
|
+
* failure to the first `CREATE TABLE` and make it read like a permissions
|
|
266
|
+
* bug. The one exception is a database declared `roles: false`, where there is
|
|
267
|
+
* no owner — both URLs are the master credential by design.
|
|
268
|
+
*/
|
|
269
|
+
function ownerUrl(options, target) {
|
|
270
|
+
const key = provideKey(target.id, "ownerUrl");
|
|
271
|
+
const owner = options.env[key];
|
|
272
|
+
if (owner) return owner;
|
|
273
|
+
const database = options.manifest[target.of ?? target.id];
|
|
274
|
+
if (database?.kind === "database" && database.roles === false) {
|
|
275
|
+
const url = options.env[provideKey(target.id, "url")];
|
|
276
|
+
if (url) return url;
|
|
277
|
+
}
|
|
278
|
+
throw new NoOwnerCredential(target.id, key);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Pool settings for a URL, with `?search_path=` as the startup option Postgres
|
|
282
|
+
* reads. Only a `roles: false` URL carries it — a role pins its own.
|
|
283
|
+
*/
|
|
284
|
+
function poolConfig(url) {
|
|
285
|
+
const parsed = new URL(url);
|
|
286
|
+
const searchPath = parsed.searchParams.get("search_path");
|
|
287
|
+
if (!searchPath) return { connectionString: url };
|
|
288
|
+
parsed.searchParams.delete("search_path");
|
|
289
|
+
return {
|
|
290
|
+
connectionString: parsed.toString(),
|
|
291
|
+
options: `-c search_path=${searchPath}`
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** Kysely as the project resolves it from `from`. */
|
|
295
|
+
async function kyselyFrom(from) {
|
|
296
|
+
const require = createRequire(from);
|
|
297
|
+
try {
|
|
298
|
+
const [main, migration] = await Promise.all([import(pathToFileURL(require.resolve("kysely")).href), import(pathToFileURL(require.resolve("kysely/migration")).href)]);
|
|
299
|
+
return {
|
|
300
|
+
Kysely: main.Kysely,
|
|
301
|
+
PostgresDialect: main.PostgresDialect,
|
|
302
|
+
sql: main.sql,
|
|
303
|
+
Migrator: migration.Migrator
|
|
304
|
+
};
|
|
305
|
+
} catch {
|
|
306
|
+
throw new KyselyNotFound(from);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/migrate/index.ts
|
|
311
|
+
/**
|
|
312
|
+
* `gkm migrate` and `gkm migration` — applying a construct's migrations, and
|
|
313
|
+
* writing its next one.
|
|
314
|
+
*
|
|
315
|
+
* Both work on a stage this machine reconciles: its containers up, its roles
|
|
316
|
+
* provisioned, its owner URLs resolved. A deployed stage is migrated by the
|
|
317
|
+
* deploy that ships it, which is the one thing that can reach its database.
|
|
318
|
+
*/
|
|
319
|
+
const logger = console;
|
|
320
|
+
/** `gkm migrate --stage` named a stage only a deploy can reach. */
|
|
321
|
+
var MigrateDeployedStage = class extends Error {
|
|
322
|
+
stage;
|
|
323
|
+
local;
|
|
324
|
+
constructor(stage, local) {
|
|
325
|
+
super(`'${stage}' is a deployed stage, and its database is migrated by \`gkm deploy --stage ${stage}\`, which is what can reach it. \`gkm migrate\` runs against '${local}' or '${TEST_STAGE}'.`);
|
|
326
|
+
this.stage = stage;
|
|
327
|
+
this.local = local;
|
|
328
|
+
this.name = "MigrateDeployedStage";
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
/** `gkm migration` for a Kysely database, with no name for the file. */
|
|
332
|
+
var MigrationNeedsName = class extends Error {
|
|
333
|
+
construct;
|
|
334
|
+
constructor(construct) {
|
|
335
|
+
super(`Name the migration: \`gkm migration ${construct} add_workouts\`. The name is what the file — and the history — calls it.`);
|
|
336
|
+
this.construct = construct;
|
|
337
|
+
this.name = "MigrationNeedsName";
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Reconcile a stage so it can be migrated: containers up, databases, schemas,
|
|
342
|
+
* roles and grants in place, owner URLs resolved.
|
|
343
|
+
*
|
|
344
|
+
* Migrating assumes the roles exist, and this is what makes that true rather
|
|
345
|
+
* than something a developer has to have run first.
|
|
346
|
+
*/
|
|
347
|
+
async function readyStage(cwd, stage) {
|
|
348
|
+
const { workspace } = await loadWorkspaceConfig(cwd);
|
|
349
|
+
const local = workspace.stages.local;
|
|
350
|
+
const target = stage ?? local;
|
|
351
|
+
if (target !== local && target !== "test") throw new MigrateDeployedStage(target, local);
|
|
352
|
+
const sources = {};
|
|
353
|
+
const manifest = await discover({
|
|
354
|
+
patterns: constructGlobs(workspace),
|
|
355
|
+
cwd: workspace.root,
|
|
356
|
+
sources
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
workspace,
|
|
360
|
+
manifest,
|
|
361
|
+
sources,
|
|
362
|
+
env: { ...(await reconcileWorkspace(workspace, {
|
|
363
|
+
stage: target,
|
|
364
|
+
manifest,
|
|
365
|
+
start: true
|
|
366
|
+
})).env }
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** `gkm migrate [construct]`. */
|
|
370
|
+
async function migrateCommand(options = {}) {
|
|
371
|
+
const ready = await readyStage(process.cwd(), options.stage);
|
|
372
|
+
const runs = await migrateDatabases({
|
|
373
|
+
root: ready.workspace.root,
|
|
374
|
+
manifest: ready.manifest,
|
|
375
|
+
sources: ready.sources,
|
|
376
|
+
env: ready.env,
|
|
377
|
+
...options.construct ? { only: options.construct } : {}
|
|
378
|
+
});
|
|
379
|
+
if (runs.length === 0) {
|
|
380
|
+
logger.log("🗄️ No database constructs to migrate.");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
for (const { target, applied } of runs) {
|
|
384
|
+
if (applied.length === 0) {
|
|
385
|
+
logger.log(`🗄️ ${target.folder}: up to date`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
logger.log(`🗄️ ${target.folder}: applied ${applied.length}`);
|
|
389
|
+
for (const name of applied) logger.log(` ✓ ${name}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* What `gkm dev` prints: one line per construct whose folder is ahead of its
|
|
394
|
+
* history, and nothing when every folder is applied.
|
|
395
|
+
*
|
|
396
|
+
* Reported, never applied. A migration being written would otherwise run the
|
|
397
|
+
* moment its file is saved, half-finished, against the database the developer
|
|
398
|
+
* is working in.
|
|
399
|
+
*/
|
|
400
|
+
async function reportPendingMigrations(workspace, env) {
|
|
401
|
+
const sources = {};
|
|
402
|
+
const manifest = await discover({
|
|
403
|
+
patterns: constructGlobs(workspace),
|
|
404
|
+
cwd: workspace.root,
|
|
405
|
+
sources
|
|
406
|
+
});
|
|
407
|
+
const pending = await pendingMigrations({
|
|
408
|
+
root: workspace.root,
|
|
409
|
+
manifest,
|
|
410
|
+
sources,
|
|
411
|
+
env
|
|
412
|
+
});
|
|
413
|
+
for (const { target, pending: names } of pending) logger.log(`⚠️ ${target.folder}: ${names.length} pending (${names.join(", ")}) — run gkm migrate`);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* `gkm migration <construct> [name]` — write the construct's next migration.
|
|
417
|
+
*
|
|
418
|
+
* For a database, an empty `up`/`down` to fill in. For an auth server, what
|
|
419
|
+
* its schema is missing, computed against its tenant once every committed
|
|
420
|
+
* migration has been applied — so the file is only the change, and is
|
|
421
|
+
* reviewed and committed like any other.
|
|
422
|
+
*/
|
|
423
|
+
async function migrationCommand(options) {
|
|
424
|
+
const ready = await readyStage(process.cwd());
|
|
425
|
+
const targets = migrationTargets(ready.manifest);
|
|
426
|
+
const id = canonicalId(options.construct);
|
|
427
|
+
const auth = authConstruct(ready.sources, id);
|
|
428
|
+
if (auth) {
|
|
429
|
+
const target = targets.find((t) => t.id === auth.databaseId);
|
|
430
|
+
if (!target) throw new NoSuchMigrationTarget(auth.databaseId, ids(targets));
|
|
431
|
+
return writeAuthMigration(ready, auth, target, options);
|
|
432
|
+
}
|
|
433
|
+
const target = targets.find((t) => t.id === id);
|
|
434
|
+
if (!target) throw new NoSuchMigrationTarget(options.construct, [...ids(targets), ...authIds(ready.sources)]);
|
|
435
|
+
if (!options.name) throw new MigrationNeedsName(options.construct);
|
|
436
|
+
const file = await write(ready.workspace.root, target, `${stamp(options.now)}_${slug(options.name)}.ts`, STUB);
|
|
437
|
+
logger.log(`📝 ${file}`);
|
|
438
|
+
return file;
|
|
439
|
+
}
|
|
440
|
+
function authConstruct(sources, id) {
|
|
441
|
+
const construct = sources[id]?.construct;
|
|
442
|
+
return construct && typeof construct.pendingMigration === "function" && typeof construct.databaseId === "string" ? construct : void 0;
|
|
443
|
+
}
|
|
444
|
+
function authIds(sources) {
|
|
445
|
+
return Object.keys(sources).filter((id) => authConstruct(sources, id));
|
|
446
|
+
}
|
|
447
|
+
async function writeAuthMigration(ready, auth, target, options) {
|
|
448
|
+
await migrateDatabases({
|
|
449
|
+
root: ready.workspace.root,
|
|
450
|
+
manifest: ready.manifest,
|
|
451
|
+
sources: ready.sources,
|
|
452
|
+
env: ready.env,
|
|
453
|
+
only: target.id
|
|
454
|
+
});
|
|
455
|
+
const secrets = await loadSecretsForApp(ready.workspace.root, ready.workspace.stages.local);
|
|
456
|
+
const pending = await auth.pendingMigration({
|
|
457
|
+
envParser: new EnvironmentParser({
|
|
458
|
+
...secrets,
|
|
459
|
+
...ready.env
|
|
460
|
+
}),
|
|
461
|
+
context: serviceContext
|
|
462
|
+
});
|
|
463
|
+
if (!pending) {
|
|
464
|
+
logger.log(`✓ ${target.folder}: ${auth.id} is up to date`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const file = await write(ready.workspace.root, target, `${stamp(options.now)}_${slug(options.name ?? "better_auth")}.sql`, `${pending.trim()}\n`);
|
|
468
|
+
logger.log(`📝 ${file}`);
|
|
469
|
+
return file;
|
|
470
|
+
}
|
|
471
|
+
const STUB = `import type { Kysely } from 'kysely';
|
|
472
|
+
|
|
473
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
474
|
+
// await db.schema.createTable('…').addColumn(…).execute();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
478
|
+
// await db.schema.dropTable('…').execute();
|
|
479
|
+
}
|
|
480
|
+
`;
|
|
481
|
+
async function write(root, target, file, content) {
|
|
482
|
+
const folder = join(root, target.folder);
|
|
483
|
+
await mkdir(folder, { recursive: true });
|
|
484
|
+
await writeFile(join(folder, file), content, { flag: "wx" });
|
|
485
|
+
return `${target.folder}/${file}`;
|
|
486
|
+
}
|
|
487
|
+
function ids(targets) {
|
|
488
|
+
return targets.map((target) => target.id);
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
export { migrateCommand, migrateDatabases, migrationCommand, reportPendingMigrations };
|
|
492
|
+
|
|
493
|
+
//# sourceMappingURL=migrate-CBICVMAi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-CBICVMAi.mjs","names":[],"sources":["../src/migrate/provider.ts","../src/migrate/databases.ts","../src/migrate/index.ts"],"sourcesContent":["/**\n * One construct's migrations, read from its folder.\n *\n * Two kinds of file, told apart by extension rather than by configuration: a\n * script exports `up` (and may export `down`), and a `.sql` file is its own\n * `up`. The second is what makes a folder independent of the client the app\n * uses — Better Auth's schema is SQL whatever the application queries with.\n */\n\nimport { readdir, readFile } from 'node:fs/promises';\nimport { extname, join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { Kysely, sql } from 'kysely';\nimport type { Migration, MigrationProvider } from 'kysely/migration';\n\nconst SCRIPTS = new Set(['.ts', '.mts', '.js', '.mjs']);\n\n/** Two files in one folder that would be one migration. */\nexport class DuplicateMigration extends Error {\n\tconstructor(\n\t\treadonly folder: string,\n\t\treadonly migration: string,\n\t) {\n\t\tsuper(\n\t\t\t`${folder} has two migrations named '${migration}' — a script and a .sql file ` +\n\t\t\t\t`with the same name. A migration is recorded by name, so only one of ` +\n\t\t\t\t`them could ever run; rename or remove the other.`,\n\t\t);\n\t\tthis.name = 'DuplicateMigration';\n\t}\n}\n\n/** A script in a migrations folder that exports no `up`. */\nexport class MigrationHasNoUp extends Error {\n\tconstructor(readonly file: string) {\n\t\tsuper(\n\t\t\t`${file} is in a migrations folder but exports no \\`up\\` function. ` +\n\t\t\t\t`Export \\`async function up(db: Kysely<unknown>)\\`, or move the file ` +\n\t\t\t\t`out of the folder if it is not a migration.`,\n\t\t);\n\t\tthis.name = 'MigrationHasNoUp';\n\t}\n}\n\n/**\n * The migration files in a folder, by name, without reading them.\n *\n * Also what `gkm dev` compares against a construct's history to report what\n * is pending, which is why it is separate from loading them.\n */\nexport async function migrationFiles(\n\tfolder: string,\n): Promise<Map<string, string>> {\n\tconst entries = await readdir(folder, { withFileTypes: true }).catch(\n\t\t(error: NodeJS.ErrnoException) => {\n\t\t\tif (error.code === 'ENOENT') return [];\n\t\t\tthrow error;\n\t\t},\n\t);\n\n\tconst files = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tif (!entry.isFile() || !isMigration(entry.name)) continue;\n\n\t\tconst name = entry.name.slice(0, -extname(entry.name).length);\n\t\tif (files.has(name)) throw new DuplicateMigration(folder, name);\n\n\t\tfiles.set(name, join(folder, entry.name));\n\t}\n\n\treturn files;\n}\n\n/** A Kysely provider over one construct's folder. */\nexport function folderProvider(\n\tfolder: string,\n\traw: typeof sql,\n): MigrationProvider {\n\treturn {\n\t\tasync getMigrations() {\n\t\t\tconst migrations: Record<string, Migration> = {};\n\n\t\t\tfor (const [name, file] of await migrationFiles(folder)) {\n\t\t\t\tmigrations[name] =\n\t\t\t\t\textname(file) === '.sql'\n\t\t\t\t\t\t? await sqlMigration(file, raw)\n\t\t\t\t\t\t: await scriptMigration(file);\n\t\t\t}\n\n\t\t\treturn migrations;\n\t\t},\n\t};\n}\n\nfunction isMigration(file: string): boolean {\n\tif (file.endsWith('.d.ts')) return false;\n\tif (/\\.(spec|test)\\.[cm]?[jt]s$/.test(file)) return false;\n\n\tconst extension = extname(file);\n\treturn extension === '.sql' || SCRIPTS.has(extension);\n}\n\nasync function scriptMigration(file: string): Promise<Migration> {\n\tconst module = (await import(pathToFileURL(file).href)) as Partial<Migration>;\n\tif (typeof module.up !== 'function') throw new MigrationHasNoUp(file);\n\n\treturn {\n\t\tup: module.up,\n\t\t...(typeof module.down === 'function' ? { down: module.down } : {}),\n\t};\n}\n\n/**\n * A `.sql` file, run whole.\n *\n * One query rather than split on `;`: with no parameters, Postgres takes\n * several statements in one message, and splitting by hand breaks on the first\n * `;` inside a string or a function body. There is no `down` — a generated\n * schema is rolled back by a migration that says how, not by guessing.\n */\nasync function sqlMigration(file: string, raw: typeof sql): Promise<Migration> {\n\tconst statements = await readFile(file, 'utf-8');\n\n\treturn {\n\t\tup: async (db: Kysely<unknown>) => {\n\t\t\tawait raw.raw(statements).execute(db);\n\t\t},\n\t};\n}\n","/**\n * Bring each construct's schema up to its migrations folder.\n *\n * Which constructs, in what order, from which folder, is `migrationTargets`'\n * answer and nobody else's. What is decided here is only how one target is\n * run: as its owner, in its own schema, with a history of its own.\n */\n\nimport { readdir } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport {\n\ttype ConstructManifest,\n\tcanonicalId,\n\tMIGRATIONS_ROOT,\n\ttype MigrationTarget,\n\tmigrationTargets,\n\tprovideKey,\n} from '@geekmidas/manifest';\nimport type { Kysely } from 'kysely';\nimport type { Migrator } from 'kysely/migration';\nimport pg from 'pg';\nimport type { ConstructSource } from '../reconcile/discover';\nimport { folderProvider, migrationFiles } from './provider';\n\nexport interface DatabasesOptions {\n\t/** The project root — where `db/` lives. */\n\troot: string;\n\tmanifest: ConstructManifest;\n\t/**\n\t * Where each construct was declared. A construct's migrations run with the\n\t * Kysely its own file resolves, which is the one its migrations import.\n\t */\n\tsources?: Readonly<Record<string, ConstructSource>>;\n\t/** The resolved environment: each construct's owner URL is read from it. */\n\tenv: Readonly<Record<string, string | undefined>>;\n\t/** One construct, by name. All of them when absent. */\n\tonly?: string;\n}\n\nexport interface MigrationRun {\n\ttarget: MigrationTarget;\n\t/** The migrations this run applied, in order. */\n\tapplied: string[];\n}\n\nexport interface PendingMigrations {\n\ttarget: MigrationTarget;\n\tpending: string[];\n}\n\n/** A folder under `db/` that names no construct. */\nexport class UnknownMigrationFolder extends Error {\n\tconstructor(\n\t\treadonly folder: string,\n\t\treadonly known: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${folder} names no database construct, so its migrations would never ` +\n\t\t\t\t`run. ${known.length > 0 ? `The folders that do: ${known.join(', ')}.` : 'This project declares no database.'} ` +\n\t\t\t\t`Rename the folder after the construct it belongs to.`,\n\t\t);\n\t\tthis.name = 'UnknownMigrationFolder';\n\t}\n}\n\n/** A construct was asked for by a name that takes no migrations. */\nexport class NoSuchMigrationTarget extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly known: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${construct}' is not a database construct. ` +\n\t\t\t\t(known.length > 0\n\t\t\t\t\t? `The ones with migrations: ${known.join(', ')}.`\n\t\t\t\t\t: 'This project declares no database.'),\n\t\t);\n\t\tthis.name = 'NoSuchMigrationTarget';\n\t}\n}\n\n/** A construct with roles has no owner URL to migrate with. */\nexport class NoOwnerCredential extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly key: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct} has no ${key} to migrate with. Migrations run as the owner ` +\n\t\t\t\t`role — the runtime one cannot create a table — and nothing ` +\n\t\t\t\t`resolved its URL. Is the stage reconciled?`,\n\t\t);\n\t\tthis.name = 'NoOwnerCredential';\n\t}\n}\n\n/** A migration failed; the ones before it in the run stay applied. */\nexport class MigrationFailed extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly migration: string | undefined,\n\t\toverride readonly cause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}: migration ${migration ? `'${migration}' ` : ''}failed — ` +\n\t\t\t\t`${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t);\n\t\tthis.name = 'MigrationFailed';\n\t}\n}\n\n/** The project's Kysely could not be found from where a database is declared. */\nexport class KyselyNotFound extends Error {\n\tconstructor(readonly from: string) {\n\t\tsuper(\n\t\t\t`Could not resolve 'kysely' from ${from}. Migrations run with the ` +\n\t\t\t\t`project's own Kysely — install it where the database is declared.`,\n\t\t);\n\t\tthis.name = 'KyselyNotFound';\n\t}\n}\n\n/** Apply every pending migration, construct by construct, parents first. */\nexport async function migrateDatabases(\n\toptions: DatabasesOptions,\n): Promise<MigrationRun[]> {\n\tconst runs: MigrationRun[] = [];\n\n\tfor (const target of await targetsFor(options)) {\n\t\t// No folder, or an empty one, is nothing to do — and no reason to\n\t\t// connect, which a construct nobody has written a migration for yet\n\t\t// should not need.\n\t\tconst files = await migrationFiles(join(options.root, target.folder));\n\t\tif (files.size === 0) {\n\t\t\truns.push({ target, applied: [] });\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst applied = await withMigrator(options, target, async (migrator) => {\n\t\t\tconst { error, results = [] } = await migrator.migrateToLatest();\n\t\t\tif (error) {\n\t\t\t\tconst failed = results.find((result) => result.status === 'Error');\n\t\t\t\tthrow new MigrationFailed(target.id, failed?.migrationName, error);\n\t\t\t}\n\n\t\t\treturn results\n\t\t\t\t.filter((result) => result.status === 'Success')\n\t\t\t\t.map((result) => result.migrationName);\n\t\t});\n\n\t\truns.push({ target, applied });\n\t}\n\n\treturn runs;\n}\n\n/**\n * What each construct's folder has that its history does not — without\n * applying any of it. What `gkm dev` reports.\n */\nexport async function pendingMigrations(\n\toptions: DatabasesOptions,\n): Promise<PendingMigrations[]> {\n\tconst found: PendingMigrations[] = [];\n\n\tfor (const target of await targetsFor(options)) {\n\t\tconst files = await migrationFiles(join(options.root, target.folder));\n\t\tif (files.size === 0) continue;\n\n\t\tconst pending = await withMigrator(options, target, async (migrator) =>\n\t\t\t(await migrator.getMigrations())\n\t\t\t\t.filter((migration) => migration.executedAt === undefined)\n\t\t\t\t.map((migration) => migration.name),\n\t\t);\n\n\t\tif (pending.length > 0) found.push({ target, pending });\n\t}\n\n\treturn found;\n}\n\n/**\n * The targets this run covers, with every folder under `db/` accounted for.\n *\n * A folder that names no construct is an error rather than something to skip:\n * a typo in its name is otherwise a migration that silently never runs.\n */\nasync function targetsFor(\n\toptions: DatabasesOptions,\n): Promise<MigrationTarget[]> {\n\tconst targets = migrationTargets(options.manifest);\n\tconst folders = targets.map((target) => target.folder);\n\n\tfor (const folder of await foldersIn(join(options.root, MIGRATIONS_ROOT))) {\n\t\tconst relative = `${MIGRATIONS_ROOT}/${folder}`;\n\t\tif (!folders.includes(relative)) {\n\t\t\tthrow new UnknownMigrationFolder(relative, folders);\n\t\t}\n\t}\n\n\tif (!options.only) return targets;\n\n\tconst id = canonicalId(options.only);\n\tconst chosen = targets.filter((target) => target.id === id);\n\tif (chosen.length === 0) {\n\t\tthrow new NoSuchMigrationTarget(\n\t\t\toptions.only,\n\t\t\ttargets.map((target) => target.id),\n\t\t);\n\t}\n\n\treturn chosen;\n}\n\nasync function foldersIn(dir: string): Promise<string[]> {\n\tconst entries = await readdir(dir, { withFileTypes: true }).catch(\n\t\t(error: NodeJS.ErrnoException) => {\n\t\t\tif (error.code === 'ENOENT') return [];\n\t\t\tthrow error;\n\t\t},\n\t);\n\n\treturn entries\n\t\t.filter((entry) => entry.isDirectory())\n\t\t.map((entry) => entry.name);\n}\n\n/**\n * A migrator for one target, connected as its owner and closed afterwards.\n *\n * No `migrationTableSchema`: the owner role's `search_path` is pinned to the\n * construct's schema, so the history lands there — `app.kysely_migration` for\n * the database, `auth_database.kysely_migration` for its tenant — and two\n * constructs in one Postgres never share one.\n *\n * Unordered migrations are allowed. Files are timestamped, so a branch merged\n * after another's migration ran carries an older one; refusing it would make\n * every such merge a manual repair.\n */\nasync function withMigrator<T>(\n\toptions: DatabasesOptions,\n\ttarget: MigrationTarget,\n\trun: (migrator: Migrator) => Promise<T>,\n): Promise<T> {\n\tconst from =\n\t\toptions.sources?.[target.id]?.file ??\n\t\t(target.of ? options.sources?.[target.of]?.file : undefined) ??\n\t\tjoin(options.root, 'package.json');\n\tconst { Kysely, PostgresDialect, sql, Migrator } = await kyselyFrom(from);\n\n\tconst db: Kysely<unknown> = new Kysely<unknown>({\n\t\tdialect: new PostgresDialect({\n\t\t\tpool: new pg.Pool(poolConfig(ownerUrl(options, target))),\n\t\t}),\n\t});\n\n\ttry {\n\t\treturn await run(\n\t\t\tnew Migrator({\n\t\t\t\tdb,\n\t\t\t\tprovider: folderProvider(join(options.root, target.folder), sql),\n\t\t\t\tallowUnorderedMigrations: true,\n\t\t\t}),\n\t\t);\n\t} finally {\n\t\tawait db.destroy();\n\t}\n}\n\n/**\n * The owner's URL, with no fallback to the runtime one.\n *\n * The runtime role can create nothing, so falling back would only move the\n * failure to the first `CREATE TABLE` and make it read like a permissions\n * bug. The one exception is a database declared `roles: false`, where there is\n * no owner — both URLs are the master credential by design.\n */\nfunction ownerUrl(options: DatabasesOptions, target: MigrationTarget): string {\n\tconst key = provideKey(target.id, 'ownerUrl');\n\tconst owner = options.env[key];\n\tif (owner) return owner;\n\n\tconst database = options.manifest[target.of ?? target.id];\n\tif (database?.kind === 'database' && database.roles === false) {\n\t\tconst url = options.env[provideKey(target.id, 'url')];\n\t\tif (url) return url;\n\t}\n\n\tthrow new NoOwnerCredential(target.id, key);\n}\n\n/**\n * Pool settings for a URL, with `?search_path=` as the startup option Postgres\n * reads. Only a `roles: false` URL carries it — a role pins its own.\n */\nfunction poolConfig(url: string): pg.PoolConfig {\n\tconst parsed = new URL(url);\n\tconst searchPath = parsed.searchParams.get('search_path');\n\tif (!searchPath) return { connectionString: url };\n\n\tparsed.searchParams.delete('search_path');\n\treturn {\n\t\tconnectionString: parsed.toString(),\n\t\toptions: `-c search_path=${searchPath}`,\n\t};\n}\n\n/** Kysely as the project resolves it from `from`. */\nasync function kyselyFrom(from: string) {\n\tconst require = createRequire(from);\n\n\ttry {\n\t\tconst [main, migration] = await Promise.all([\n\t\t\timport(pathToFileURL(require.resolve('kysely')).href),\n\t\t\timport(pathToFileURL(require.resolve('kysely/migration')).href),\n\t\t]);\n\n\t\treturn {\n\t\t\tKysely: main.Kysely as typeof import('kysely').Kysely,\n\t\t\tPostgresDialect:\n\t\t\t\tmain.PostgresDialect as typeof import('kysely').PostgresDialect,\n\t\t\tsql: main.sql as typeof import('kysely').sql,\n\t\t\tMigrator:\n\t\t\t\tmigration.Migrator as typeof import('kysely/migration').Migrator,\n\t\t};\n\t} catch {\n\t\tthrow new KyselyNotFound(from);\n\t}\n}\n","/**\n * `gkm migrate` and `gkm migration` — applying a construct's migrations, and\n * writing its next one.\n *\n * Both work on a stage this machine reconciles: its containers up, its roles\n * provisioned, its owner URLs resolved. A deployed stage is migrated by the\n * deploy that ships it, which is the one thing that can reach its database.\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructManifest,\n\tcanonicalId,\n\ttype MigrationTarget,\n\tmigrationTargets,\n} from '@geekmidas/manifest';\nimport {\n\ttype ServiceRegisterOptions,\n\tserviceContext,\n} from '@geekmidas/services';\nimport { loadWorkspaceConfig } from '../config';\nimport { loadSecretsForApp } from '../credentials';\nimport { type ConstructSource, discover } from '../reconcile/discover';\nimport { constructGlobs, reconcileWorkspace } from '../reconcile/workspace.js';\nimport { TEST_STAGE } from '../workspace/stages';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport {\n\tmigrateDatabases,\n\tNoSuchMigrationTarget,\n\tpendingMigrations,\n} from './databases';\nimport { slug, stamp } from './names';\n\nexport {\n\tKyselyNotFound,\n\tMigrationFailed,\n\ttype MigrationRun,\n\tmigrateDatabases,\n\tNoOwnerCredential,\n\tNoSuchMigrationTarget,\n\ttype PendingMigrations,\n\tpendingMigrations,\n\tUnknownMigrationFolder,\n} from './databases';\nexport { slug, stamp } from './names';\nexport { DuplicateMigration, MigrationHasNoUp } from './provider';\n\nconst logger = console;\n\n/** `gkm migrate --stage` named a stage only a deploy can reach. */\nexport class MigrateDeployedStage extends Error {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly local: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' is a deployed stage, and its database is migrated by ` +\n\t\t\t\t`\\`gkm deploy --stage ${stage}\\`, which is what can reach it. ` +\n\t\t\t\t`\\`gkm migrate\\` runs against '${local}' or '${TEST_STAGE}'.`,\n\t\t);\n\t\tthis.name = 'MigrateDeployedStage';\n\t}\n}\n\n/** `gkm migration` for a Kysely database, with no name for the file. */\nexport class MigrationNeedsName extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`Name the migration: \\`gkm migration ${construct} add_workouts\\`. ` +\n\t\t\t\t`The name is what the file — and the history — calls it.`,\n\t\t);\n\t\tthis.name = 'MigrationNeedsName';\n\t}\n}\n\nexport interface MigrateOptions {\n\t/** The stage to migrate: the project's local one, or `test`. */\n\tstage?: string;\n\t/** One construct, by name. All of them when absent. */\n\tconstruct?: string;\n}\n\n/** A workspace's stage, reconciled and ready to migrate. */\nexport interface ReadyStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tsources: Record<string, ConstructSource>;\n\t/** Every URL the stage resolved — the owner URLs among them. */\n\tenv: Record<string, string>;\n}\n\n/**\n * Reconcile a stage so it can be migrated: containers up, databases, schemas,\n * roles and grants in place, owner URLs resolved.\n *\n * Migrating assumes the roles exist, and this is what makes that true rather\n * than something a developer has to have run first.\n */\nexport async function readyStage(\n\tcwd: string,\n\tstage?: string,\n): Promise<ReadyStage> {\n\tconst { workspace } = await loadWorkspaceConfig(cwd);\n\tconst local = workspace.stages.local;\n\tconst target = stage ?? local;\n\tif (target !== local && target !== TEST_STAGE) {\n\t\tthrow new MigrateDeployedStage(target, local);\n\t}\n\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst reconciled = await reconcileWorkspace(workspace, {\n\t\tstage: target,\n\t\tmanifest,\n\t\tstart: true,\n\t});\n\n\treturn { workspace, manifest, sources, env: { ...reconciled.env } };\n}\n\n/** `gkm migrate [construct]`. */\nexport async function migrateCommand(\n\toptions: MigrateOptions = {},\n): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst runs = await migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (runs.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to migrate.');\n\t\treturn;\n\t}\n\n\tfor (const { target, applied } of runs) {\n\t\tif (applied.length === 0) {\n\t\t\tlogger.log(`🗄️ ${target.folder}: up to date`);\n\t\t\tcontinue;\n\t\t}\n\t\tlogger.log(`🗄️ ${target.folder}: applied ${applied.length}`);\n\t\tfor (const name of applied) logger.log(` ✓ ${name}`);\n\t}\n}\n\n/**\n * What `gkm dev` prints: one line per construct whose folder is ahead of its\n * history, and nothing when every folder is applied.\n *\n * Reported, never applied. A migration being written would otherwise run the\n * moment its file is saved, half-finished, against the database the developer\n * is working in.\n */\nexport async function reportPendingMigrations(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst pending = await pendingMigrations({\n\t\troot: workspace.root,\n\t\tmanifest,\n\t\tsources,\n\t\tenv,\n\t});\n\n\tfor (const { target, pending: names } of pending) {\n\t\tlogger.log(\n\t\t\t`⚠️ ${target.folder}: ${names.length} pending (${names.join(', ')}) — run gkm migrate`,\n\t\t);\n\t}\n}\n\nexport interface MigrationOptions {\n\t/** The construct the migration is for, by name. */\n\tconstruct: string;\n\t/** What the file is called, after its timestamp. */\n\tname?: string;\n\t/** The file's timestamp; the current time when absent. */\n\tnow?: Date;\n}\n\n/**\n * `gkm migration <construct> [name]` — write the construct's next migration.\n *\n * For a database, an empty `up`/`down` to fill in. For an auth server, what\n * its schema is missing, computed against its tenant once every committed\n * migration has been applied — so the file is only the change, and is\n * reviewed and committed like any other.\n */\nexport async function migrationCommand(\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\tconst ready = await readyStage(process.cwd());\n\tconst targets = migrationTargets(ready.manifest);\n\tconst id = canonicalId(options.construct);\n\n\tconst auth = authConstruct(ready.sources, id);\n\tif (auth) {\n\t\tconst target = targets.find((t) => t.id === auth.databaseId);\n\t\tif (!target) {\n\t\t\tthrow new NoSuchMigrationTarget(auth.databaseId, ids(targets));\n\t\t}\n\t\treturn writeAuthMigration(ready, auth, target, options);\n\t}\n\n\tconst target = targets.find((t) => t.id === id);\n\tif (!target) {\n\t\tthrow new NoSuchMigrationTarget(options.construct, [\n\t\t\t...ids(targets),\n\t\t\t...authIds(ready.sources),\n\t\t]);\n\t}\n\tif (!options.name) throw new MigrationNeedsName(options.construct);\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name)}.ts`,\n\t\tSTUB,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\n/** What an auth construct offers `gkm migration`, duck-typed across copies. */\ninterface AuthConstruct {\n\tid: string;\n\tdatabaseId: string;\n\tpendingMigration(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<string | undefined>;\n}\n\nfunction authConstruct(\n\tsources: Readonly<Record<string, ConstructSource>>,\n\tid: string,\n): AuthConstruct | undefined {\n\tconst construct = sources[id]?.construct as\n\t\t| Partial<AuthConstruct>\n\t\t| undefined;\n\treturn construct &&\n\t\ttypeof construct.pendingMigration === 'function' &&\n\t\ttypeof construct.databaseId === 'string'\n\t\t? (construct as AuthConstruct)\n\t\t: undefined;\n}\n\nfunction authIds(sources: Readonly<Record<string, ConstructSource>>): string[] {\n\treturn Object.keys(sources).filter((id) => authConstruct(sources, id));\n}\n\nasync function writeAuthMigration(\n\tready: ReadyStage,\n\tauth: AuthConstruct,\n\ttarget: MigrationTarget,\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\t// Against a tenant already at its committed migrations, so the comparison\n\t// finds only what the files do not yet say.\n\tawait migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tonly: target.id,\n\t});\n\n\t// Its secret and URL come from the stage's secrets; the tenant's owner URL\n\t// from the reconcile above, which wins.\n\tconst secrets = await loadSecretsForApp(\n\t\tready.workspace.root,\n\t\tready.workspace.stages.local,\n\t);\n\tconst pending = await auth.pendingMigration({\n\t\tenvParser: new EnvironmentParser({ ...secrets, ...ready.env }),\n\t\tcontext: serviceContext,\n\t});\n\n\tif (!pending) {\n\t\tlogger.log(`✓ ${target.folder}: ${auth.id} is up to date`);\n\t\treturn undefined;\n\t}\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name ?? 'better_auth')}.sql`,\n\t\t`${pending.trim()}\\n`,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\nconst STUB = `import type { Kysely } from 'kysely';\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n // await db.schema.createTable('…').addColumn(…).execute();\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n // await db.schema.dropTable('…').execute();\n}\n`;\n\nasync function write(\n\troot: string,\n\ttarget: MigrationTarget,\n\tfile: string,\n\tcontent: string,\n): Promise<string> {\n\tconst folder = join(root, target.folder);\n\tawait mkdir(folder, { recursive: true });\n\tawait writeFile(join(folder, file), content, { flag: 'wx' });\n\treturn `${target.folder}/${file}`;\n}\n\nfunction ids(targets: readonly MigrationTarget[]): string[] {\n\treturn targets.map((target) => target.id);\n}\n"],"mappings":"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"}
|