@softure-ai/db 0.1.5 → 0.1.6
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/CHANGELOG.md +16 -0
- package/README.md +215 -21
- package/dist/cli/run.d.ts +8 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +71 -21
- package/dist/cli/run.js.map +1 -1
- package/dist/client.d.ts +39 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +64 -8
- package/dist/client.js.map +1 -1
- package/dist/configured.d.ts +31 -0
- package/dist/configured.d.ts.map +1 -0
- package/dist/configured.js +74 -0
- package/dist/configured.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/migrations/adopt.d.ts +8 -1
- package/dist/migrations/adopt.d.ts.map +1 -1
- package/dist/migrations/adopt.js +53 -87
- package/dist/migrations/adopt.js.map +1 -1
- package/dist/migrations/apply.d.ts +35 -0
- package/dist/migrations/apply.d.ts.map +1 -0
- package/dist/migrations/apply.js +115 -0
- package/dist/migrations/apply.js.map +1 -0
- package/dist/migrations/migrator.d.ts +55 -27
- package/dist/migrations/migrator.d.ts.map +1 -1
- package/dist/migrations/migrator.js +187 -103
- package/dist/migrations/migrator.js.map +1 -1
- package/dist/migrations/problems.d.ts +9 -0
- package/dist/migrations/problems.d.ts.map +1 -1
- package/dist/migrations/problems.js +9 -3
- package/dist/migrations/problems.js.map +1 -1
- package/dist/migrations/reference.d.ts +23 -0
- package/dist/migrations/reference.d.ts.map +1 -0
- package/dist/migrations/reference.js +64 -0
- package/dist/migrations/reference.js.map +1 -0
- package/dist/migrations/session.d.ts +2 -1
- package/dist/migrations/session.d.ts.map +1 -1
- package/dist/migrations/session.js +19 -2
- package/dist/migrations/session.js.map +1 -1
- package/dist/migrations/stubs.d.ts +23 -0
- package/dist/migrations/stubs.d.ts.map +1 -0
- package/dist/migrations/stubs.js +99 -0
- package/dist/migrations/stubs.js.map +1 -0
- package/dist/testing.d.ts +11 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +28 -9
- package/dist/testing.js.map +1 -1
- package/package.json +22 -5
- package/src/cli/run.ts +84 -23
- package/src/client.ts +88 -16
- package/src/configured.ts +109 -0
- package/src/index.ts +9 -0
- package/src/migrations/adopt.ts +65 -100
- package/src/migrations/apply.ts +135 -0
- package/src/migrations/migrator.ts +246 -128
- package/src/migrations/problems.ts +13 -5
- package/src/migrations/reference.ts +78 -0
- package/src/migrations/session.ts +28 -2
- package/src/migrations/stubs.ts +139 -0
- package/src/testing.ts +38 -10
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Newest first. Each version lists what changed for an app that uses `@softure-ai/db`. When an app has run a version in
|
|
4
|
+
production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
|
|
5
|
+
"Definition of done"). Versions before the first one below are described in their GitHub Releases (`db@x.y.z`).
|
|
6
|
+
|
|
7
|
+
## 0.1.6
|
|
8
|
+
|
|
9
|
+
- One database handle per process: `getConfiguredDatabase` opens the config's `database.handle` or URL once, and modules, health route and commands share it.
|
|
10
|
+
- App migrations run around module migrations (`before` and `after` hooks).
|
|
11
|
+
- Adoption: app baselines, `--adopt <module>@<version> --through <n>`, dependencies adopted first, and modules whose SQL references app tables (stub tables).
|
|
12
|
+
- **Breaking:** `pg` and `@electric-sql/pglite` are optional peer dependencies, no longer dependencies. An app installs
|
|
13
|
+
the driver its `DATABASE_URL` uses and lists `"@softure-ai/db"` and that driver in `serverExternalPackages`
|
|
14
|
+
([README §2](README.md#2-installation)); a URL whose driver is missing fails with a message naming the package.
|
|
15
|
+
- A migrate bundle keeps drizzle's driver adapters external (`--external:drizzle-orm/pglite`,
|
|
16
|
+
`--external:drizzle-orm/node-postgres`).
|
package/README.md
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# @softure-ai/db
|
|
2
2
|
|
|
3
|
-
**Status:** wave 0 · implemented in FD-4 (`db-migrator`), 0.1.5, prepared for its first release (the owner's tag, `scripts/release/README.md`).
|
|
4
|
-
|
|
5
3
|
The database layer every SOFTURE module stands on. Standard:
|
|
6
|
-
[docs/02-module-standard.md](../../docs/02-module-standard.md) §4.
|
|
7
|
-
`src/db/client.ts`, `src/db/test-db.ts`, `scripts/migrate.ts`, `docker/Dockerfile` (`migrate.cjs`).
|
|
4
|
+
[docs/02-module-standard.md](../../docs/02-module-standard.md) §4.
|
|
8
5
|
|
|
9
6
|
## 1. What it provides
|
|
10
7
|
|
|
@@ -18,9 +15,49 @@ Postgres schema (with a ledger, checksums, a lock, a dry run and adoption of exi
|
|
|
18
15
|
npm install @softure-ai/db drizzle-orm
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
Node ≥ 22, ESM only
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
Node ≥ 22, **ESM only**: module packages locate their migrations through `import.meta.url`, which a
|
|
19
|
+
CJS bundle (esbuild `--format=cjs`) leaves empty, so `resolveMigrationsDir` throws naming the bundle
|
|
20
|
+
format; the app script below also uses top-level await. Bundle migrate scripts with `--format=esm`.
|
|
21
|
+
`drizzle-orm` (`^0.45.2`) is a peer dependency, so the app, the modules and
|
|
22
|
+
this package share one drizzle.
|
|
23
|
+
|
|
24
|
+
The drivers are optional peers: install the one the app's URL uses, and only that one goes into the
|
|
25
|
+
app image. Each loads only when a URL needs it.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install pg # postgres:// and postgresql://
|
|
29
|
+
npm install @electric-sql/pglite # pglite:// (dev without a server)
|
|
30
|
+
npm install -D @electric-sql/pglite # a Postgres app's createTestDatabase, --adopt and baseline
|
|
31
|
+
npm install -D @types/pg # only with skipLibCheck: false
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Needs | `pg` (`^8.11.0`) | `@electric-sql/pglite` (`^0.5.0`) |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `postgres://`, `postgresql://` | yes | no |
|
|
37
|
+
| `pglite://` | no | yes |
|
|
38
|
+
| `createTestDatabase` (`@softure-ai/db/testing`) | no | yes (dev) |
|
|
39
|
+
| `softure migrate --adopt`, an app `baseline` (compare against a scratch PGlite) | URL's driver | yes |
|
|
40
|
+
|
|
41
|
+
A URL whose driver is missing fails at the first connection: `createDatabase: postgres:// URLs need
|
|
42
|
+
the "pg" package, which is not installed; …`, with Node's error as `cause`.
|
|
43
|
+
|
|
44
|
+
**Next.js.** List this package and the driver in `serverExternalPackages`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// next.config.ts
|
|
48
|
+
const nextConfig: NextConfig = {
|
|
49
|
+
serverExternalPackages: ["@softure-ai/db", "pg"], // or "@electric-sql/pglite" for pglite://
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Bundled, the package's `import()` of each driver and drizzle adapter is resolved at build time, so
|
|
54
|
+
`next build` fails with `Can't resolve '@electric-sql/pglite'` in an app without PGlite (drizzle's
|
|
55
|
+
adapter imports it statically). External, it loads from `node_modules` at run time, and standalone
|
|
56
|
+
output tracing follows the same imports, so `.next/standalone` carries the installed driver and
|
|
57
|
+
drizzle and nothing else (the example app installs `pg` only:
|
|
58
|
+
[`next.config.ts`](../../examples/next-app/next.config.ts)). The config and the shared handles live on `globalThis`, so this copy and the modules bundled next
|
|
59
|
+
to it share them. Hiding the imports from the bundler instead (`turbopackIgnore`) builds, but
|
|
60
|
+
tracing skips the driver and the image fails at the first query.
|
|
24
61
|
|
|
25
62
|
## 3. Configuration
|
|
26
63
|
|
|
@@ -42,11 +79,67 @@ await handle.close();
|
|
|
42
79
|
```
|
|
43
80
|
|
|
44
81
|
- `DatabaseHandle` is `{ kind: "postgres", db, pool, close } | { kind: "pglite", db, client, close }`.
|
|
45
|
-
- `Database
|
|
46
|
-
transaction, so one helper serves both (`db.transaction(async (tx) => helper(tx))`).
|
|
82
|
+
- `Database<TSchema>` is the drizzle database of either driver; `Queryable<TSchema>` is a `Database`
|
|
83
|
+
or an open transaction, so one helper serves both (`db.transaction(async (tx) => helper(tx))`).
|
|
84
|
+
Without a type argument both accept any schema, so an app database made with
|
|
85
|
+
`drizzle({ client, schema })`, and its transactions, pass to module functions as they are.
|
|
47
86
|
- An unsupported scheme throws, naming the scheme only; the URL (and its password) is never
|
|
48
87
|
printed.
|
|
49
88
|
|
|
89
|
+
### One handle per process
|
|
90
|
+
|
|
91
|
+
Modules open their database through the config, never on their own: the app's handle when
|
|
92
|
+
`database.handle` is set, otherwise one process-wide handle per URL (`getSharedDatabase(url)`). An
|
|
93
|
+
app that also has its own client must make sure both are the same handle. A second handle is a second
|
|
94
|
+
pool on Postgres; on `pglite://` it is a second PGlite instance on the same directory, which corrupts
|
|
95
|
+
it without an error (the next open fails with `could not locate a valid checkpoint record`).
|
|
96
|
+
|
|
97
|
+
**Either** hand the app's client to the config. The function is called on first use, never while the
|
|
98
|
+
config is defined (a build does not connect), and must return the app's one handle:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// db/client.ts: the app's own client, memoized: `next dev` re-evaluates modules on every change
|
|
102
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
103
|
+
import { createPgliteHandle, createPostgresHandle, type DatabaseHandle } from "@softure-ai/db";
|
|
104
|
+
import pg from "pg";
|
|
105
|
+
|
|
106
|
+
const cache = globalThis as typeof globalThis & { appDatabase?: Promise<DatabaseHandle> };
|
|
107
|
+
|
|
108
|
+
export function getAppDatabase(url: string): Promise<DatabaseHandle> {
|
|
109
|
+
cache.appDatabase ??= url.startsWith("pglite://")
|
|
110
|
+
? createPgliteHandle(new PGlite(url.slice("pglite://".length)))
|
|
111
|
+
: createPostgresHandle(new pg.Pool({ connectionString: url }).on("error", () => undefined));
|
|
112
|
+
return cache.appDatabase;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// softure.config.ts
|
|
116
|
+
const url = process.env.DATABASE_URL ?? "";
|
|
117
|
+
defineSoftureConfig({ database: { url, handle: () => getAppDatabase(url) }, /* … */ });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Every module adapter, the ops health route, `softure migrate` (and the app's migration hooks it
|
|
121
|
+
runs), `softure-blog`, `softure-mail` and ops scripts then use that handle; a command closes it
|
|
122
|
+
when it is done, which writes a `pglite://` directory back.
|
|
123
|
+
|
|
124
|
+
**Or** build the app's typed drizzle instance over the shared handle, so there is nothing to hand
|
|
125
|
+
over:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { getSharedDatabase } from "@softure-ai/db";
|
|
129
|
+
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
|
|
130
|
+
import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
|
|
131
|
+
import * as schema from "./schema";
|
|
132
|
+
|
|
133
|
+
export async function getDb() {
|
|
134
|
+
const handle = await getSharedDatabase(config.database.url);
|
|
135
|
+
return handle.kind === "postgres" ? drizzlePg({ client: handle.pool, schema }) : drizzlePglite({ client: handle.client, schema });
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`getConfiguredDatabase(config.database)` is what modules call; app code that needs the database
|
|
140
|
+
the modules use can call it too. A migration run on a PGlite handle the app shares keeps the app's
|
|
141
|
+
session settings (`TimeZone`, `search_path`); only what the run changed is put back.
|
|
142
|
+
|
|
50
143
|
## 4. Mounting
|
|
51
144
|
|
|
52
145
|
Nothing to mount. Migrations run as a deploy step:
|
|
@@ -69,31 +162,117 @@ process.exitCode = await runMigrateCli({ config, argv: process.argv.slice(2) });
|
|
|
69
162
|
# build stage: copy each module's SQL next to the bundle, then bundle the runner
|
|
70
163
|
RUN npx tsx scripts/migrate.ts --export-migrations ./softure-migrations
|
|
71
164
|
RUN npx esbuild scripts/migrate.ts --bundle --platform=node --format=esm --target=node22 \
|
|
72
|
-
--external:pg --external:@electric-sql/pglite
|
|
73
|
-
|
|
165
|
+
--external:pg --external:@electric-sql/pglite \
|
|
166
|
+
--external:drizzle-orm/pglite --external:drizzle-orm/node-postgres --outfile=migrate.mjs
|
|
167
|
+
# run stage (needs the URL's driver, and @electric-sql/pglite for --adopt or an app baseline)
|
|
74
168
|
COPY --from=builder /app/migrate.mjs ./migrate.mjs
|
|
75
169
|
COPY --from=builder /app/softure-migrations ./softure-migrations
|
|
76
170
|
CMD ["node", "migrate.mjs", "--migrations-dir", "./softure-migrations"]
|
|
77
171
|
```
|
|
78
172
|
|
|
173
|
+
A config that imports `server-only` throws under plain Node: run the export as
|
|
174
|
+
`npx tsx --conditions=react-server scripts/migrate.ts --export-migrations …` and bundle with
|
|
175
|
+
`--alias:server-only=./scripts/empty.mjs` (an empty file). Path aliases (`@/…`) need nothing extra: tsx and
|
|
176
|
+
esbuild both read `paths` from `tsconfig.json`. The [ops container recipe](../../modules/ops/README.md#container-recipe)
|
|
177
|
+
uses the same commands.
|
|
178
|
+
|
|
179
|
+
The drivers and drizzle's two driver adapters stay external: bundled, the adapter of a driver the app
|
|
180
|
+
does not install imports it at the top of `migrate.mjs`, which then fails at start. At run time they
|
|
181
|
+
come from the standalone output, which carries the installed driver and its adapter.
|
|
182
|
+
|
|
79
183
|
A bundle cannot find package folders (`import.meta.url` points at the bundle), hence
|
|
80
|
-
`--export-migrations` at build time and `--migrations-dir` at run time. The
|
|
184
|
+
`--export-migrations` at build time and `--migrations-dir` at run time. The build stage needs no
|
|
185
|
+
`DATABASE_URL`: the config accepts an empty `database.url` (`DATABASE_URL ?? ""`) and only a command that
|
|
186
|
+
connects refuses it. The ledger's own migration
|
|
81
187
|
ships as code.
|
|
82
188
|
|
|
189
|
+
**The app's own migrations** run in the same step, through hooks. `before` runs ahead of the module
|
|
190
|
+
files (app tables a module references, normally the app's whole history), `after` behind them (app
|
|
191
|
+
migrations that reference a table a module creates). With drizzle, define them once and pass the same
|
|
192
|
+
object to the migrate script and to `createTestDatabase`:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// db/app-migrations.ts
|
|
196
|
+
import type { AppMigrations, DatabaseHandle } from "@softure-ai/db";
|
|
197
|
+
|
|
198
|
+
async function runDrizzle(handle: DatabaseHandle, migrationsFolder: string): Promise<void> {
|
|
199
|
+
if (handle.kind === "postgres") {
|
|
200
|
+
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
201
|
+
await migrate(handle.db, { migrationsFolder });
|
|
202
|
+
} else {
|
|
203
|
+
const { migrate } = await import("drizzle-orm/pglite/migrator");
|
|
204
|
+
await migrate(handle.db, { migrationsFolder });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const appMigrations: AppMigrations = {
|
|
209
|
+
before: (handle) => runDrizzle(handle, "./drizzle"),
|
|
210
|
+
// only when an app migration needs a module table: a second drizzle folder
|
|
211
|
+
// after: (handle) => runDrizzle(handle, "./drizzle-after"),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// scripts/migrate.ts
|
|
215
|
+
process.exitCode = await runMigrateCli({ config, argv: process.argv.slice(2), app: appMigrations });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- Order of a run: the ledger, `before`, the module files in dependency order, `after`. Everything
|
|
219
|
+
runs under the migration lock, after every module file passed its checks; nothing runs when a check
|
|
220
|
+
fails. The hooks run on every run, so they must skip what they already applied (drizzle does).
|
|
221
|
+
- A hook that throws is reported as `db.app_migration_failed` with the error and its causes (drizzle
|
|
222
|
+
puts the Postgres message in `cause`). A failing `before` stops the run before any module file; a
|
|
223
|
+
failing `after` leaves the module files applied.
|
|
224
|
+
- `--plan` runs no hook (it says so); `--adopt` without `--plan` runs `before` first, so the app
|
|
225
|
+
migration that moves a table into the module's schema and the adoption are one command.
|
|
226
|
+
|
|
227
|
+
**A baseline: fresh databases after an adoption.** The app keeps its old migrations, so every fresh
|
|
228
|
+
database (the unit-test template, an integration stack, a reset dev database) recreates the tables it
|
|
229
|
+
moved into a module's schema before the module files run. `baseline` names, per module, the last module
|
|
230
|
+
file the app's own history creates; `migrate` then adopts or migrates by itself, on every database:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
export const appMigrations: AppMigrations = {
|
|
234
|
+
before: (handle) => runDrizzle(handle, "./drizzle"),
|
|
235
|
+
// the app's history creates auth's 0001 (users, sessions); 0002.. are new to it
|
|
236
|
+
baseline: { auth: 1 },
|
|
237
|
+
};
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- For a listed module the ledger has never seen, after `before` and after the modules it depends on:
|
|
241
|
+
an empty schema is migrated normally; otherwise the schema is compared with what files 1..n create
|
|
242
|
+
(as `--adopt` does), files 1..n are recorded as `adopted` and the later files are applied. A
|
|
243
|
+
difference stops the run (`db.schema_mismatch`, every difference printed); nothing of that module is
|
|
244
|
+
recorded and `after` does not run.
|
|
245
|
+
- Once the module is in the ledger the entry has no effect, so the same command serves every deploy.
|
|
246
|
+
Keep the number when the module ships new files: it describes the app's history, which no longer
|
|
247
|
+
changes.
|
|
248
|
+
- Only a prefix 1..n can be adopted (the ledger refuses a file numbered below an applied one): when the
|
|
249
|
+
app's tables already carry a later file's change, align them in the app's own migration or raise n.
|
|
250
|
+
- A module that is not enabled, has no schema, or an n outside 1..files refuses the run before anything
|
|
251
|
+
runs. `--plan` marks the files a baseline may adopt (`adopted if its schema already holds objects`).
|
|
252
|
+
- The comparison builds the reference on a scratch PGlite, only when a module is actually adopted,
|
|
253
|
+
with stubs of the app tables `before` created (section 5, adoption).
|
|
254
|
+
- Under node-postgres a hook queries through the pool while the migrator holds one connection, so the
|
|
255
|
+
pool needs at least two (`createDatabase`'s default is 10).
|
|
256
|
+
- The `softure` bin reads only the config; an app with its own migrations uses the script above.
|
|
257
|
+
- In a bundle, the drizzle folder must be copied next to the bundle, as the module files are.
|
|
258
|
+
|
|
83
259
|
**Options of `softure migrate`:**
|
|
84
260
|
|
|
85
261
|
| Option | Effect |
|
|
86
262
|
| --- | --- |
|
|
87
263
|
| (none) | apply every pending migration |
|
|
88
264
|
| `--plan` | print what would be applied (or adopted); change nothing |
|
|
89
|
-
| `--adopt <module>@<version>` | record an existing schema as migrated after comparing it (section 5) |
|
|
265
|
+
| `--adopt <module>@<version>` | record an existing schema as migrated after comparing it, after applying the pending migrations of its dependencies (section 5) |
|
|
266
|
+
| `--through <n>` | with `--adopt`: adopt files 1..n only; the next migrate applies the rest |
|
|
90
267
|
| `--migrations-dir <dir>` | read module files from `<dir>/<module id>/` |
|
|
91
268
|
| `--export-migrations <dir>` | copy module files to `<dir>/<module id>/` and exit; needs no database; replaces older `.sql` copies and refuses a module folder holding anything else |
|
|
92
269
|
|
|
93
270
|
Exit codes: 0 done, 1 a problem or failure (each printed on stderr), 2 a usage error.
|
|
94
271
|
|
|
95
|
-
The same operations as functions: `migrate(handle, { modules, migrationsDir?, onApplied? })
|
|
96
|
-
`
|
|
272
|
+
The same operations as functions: `migrate(handle, { modules, migrationsDir?, app?, onApplied?, onAdopted?, onAppMigrated? })`
|
|
273
|
+
(`runAppMigrations(handle, app, phase)` runs one hook alone under the lock),
|
|
274
|
+
`planMigrations(handle, { modules, app? })` (`pending`, and `baseline`: the pending files a baseline may adopt),
|
|
275
|
+
`adoptModule(handle, { modules, module, version, through?, dryRun? })`,
|
|
97
276
|
`exportMigrations(modules, dir)`. Each returns `{ ok: true, value } | { ok: false, error, problems }`;
|
|
98
277
|
`describeProblem(problem)` gives the English line.
|
|
99
278
|
|
|
@@ -118,16 +297,16 @@ module_version, method ('applied' | 'adopted'), applied_at`, primary key `(modul
|
|
|
118
297
|
`SET LOCAL search_path TO <schema>, public` first. Name other modules' tables with their schema
|
|
119
298
|
(`REFERENCES notes.notes (id)`), and create extensions `WITH SCHEMA public`.
|
|
120
299
|
|
|
121
|
-
**Each run:** modules in dependency order (`sortModulesByDependencies`), files in number order.
|
|
300
|
+
**Each run:** the ledger, the app's `before` hook, modules in dependency order (`sortModulesByDependencies`), files in number order.
|
|
122
301
|
Before anything runs, every file of every module is checked: an applied file that was edited or
|
|
123
302
|
renamed (`db.migration_changed`), deleted (`db.migration_missing`), or a new file numbered below an
|
|
124
303
|
applied one (`db.migration_out_of_order`) refuses the whole run. The run holds a session-level
|
|
125
304
|
`pg_advisory_lock`, so a second runner waits, then finds nothing to do. A failing file is rolled
|
|
126
305
|
back completely and stops the run; the files before it stay applied. The migration connection is
|
|
127
|
-
closed after the run, never returned to the pool.
|
|
306
|
+
closed after the run, never returned to the pool. The app's `after` hook runs last (section 4).
|
|
128
307
|
|
|
129
308
|
**Refused modules:** the id `softure`; the schemas `softure`, `public`, `information_schema`,
|
|
130
|
-
`pg_*` and names over 63 bytes; migrations without a `dbSchema`.
|
|
309
|
+
`drizzle` (drizzle's ledger in every app), `pg_*` and names over 63 bytes; migrations without a `dbSchema`.
|
|
131
310
|
|
|
132
311
|
**Adoption** ([docs/05](../../docs/05-adoption-playbook.md) step 3): after the app's own migration
|
|
133
312
|
moved its tables into the module's schema, `--adopt <module>@<version>` (with `--plan` first)
|
|
@@ -136,14 +315,26 @@ one through `pg_catalog`: relations (and whether they are unlogged), columns (ty
|
|
|
136
315
|
not null, default, identity, generated), sequences with their parameters, constraints and indexes
|
|
137
316
|
(names and definitions), triggers, functions, and enum, domain, range and composite types. Only an
|
|
138
317
|
exact match records the files as `adopted`; every difference is printed (`missing in database: …`,
|
|
139
|
-
`unexpected in database: …`).
|
|
140
|
-
|
|
318
|
+
`unexpected in database: …`). `--through <n>` compares with and adopts files 1..n only; the next
|
|
319
|
+
migrate applies the rest. The version must equal the enabled module's and the module must be new to
|
|
320
|
+
the ledger. The pending migrations of the modules it depends on are applied first (listed as
|
|
321
|
+
`apply` lines; a dry run applies nothing); when one fails because its tables exist too, adopt that
|
|
322
|
+
module first. Module SQL may reference the app's own tables (`REFERENCES public.app_users (id)`):
|
|
323
|
+
the scratch database first gets stubs of every table outside the ledger, the enabled modules'
|
|
324
|
+
schemas and `drizzle`, copied from the live database (columns with their types, primary keys,
|
|
325
|
+
unique constraints, plain unique indexes and the enum types they use; no defaults, NOT NULL, checks
|
|
326
|
+
or foreign keys). The tables must therefore exist when the comparison runs: `--adopt` and a baseline
|
|
327
|
+
run `before` first, a `--plan` dry run reads only what is already there. A stub that cannot be
|
|
328
|
+
created (a domain or extension type) is skipped, and named in the failure when the module needs it.
|
|
329
|
+
For fresh databases of an app that adopted a module, declare a baseline (section 4). Undo:
|
|
141
330
|
`DELETE FROM softure.migrations WHERE module = '<id>' AND method = 'adopted'`.
|
|
142
331
|
|
|
143
332
|
**Tests:** `createTestDatabase(modules)` from `@softure-ai/db/testing` returns `{ db, client, close }`:
|
|
144
333
|
an in-memory PGlite with the ledger and the listed modules migrated (list the module and the
|
|
145
334
|
modules it depends on). Each module set is migrated once per test worker and copied per call; a
|
|
146
|
-
broken migration throws naming the file.
|
|
335
|
+
broken migration throws naming the file. An app with its own migrations passes the same hooks,
|
|
336
|
+
`createTestDatabase(modules, { app: appMigrations })`, so module tables can reference app tables; the
|
|
337
|
+
template is cached per hook object, so import the shared object rather than writing hooks inline.
|
|
147
338
|
|
|
148
339
|
```ts
|
|
149
340
|
import { createTestDatabase } from "@softure-ai/db/testing";
|
|
@@ -187,4 +378,7 @@ No personal data. The ledger holds module ids, file names and checksums.
|
|
|
187
378
|
transaction-mode PgBouncer.
|
|
188
379
|
- Adoption does not compare grants, comments, column order, row-level security policies or view
|
|
189
380
|
bodies, and needs PGlite installed where it runs (also in an image that otherwise uses `pg`).
|
|
381
|
+
- The app table stubs of the reference carry no domains, extension types, views or functions: a
|
|
382
|
+
module whose SQL needs one of those from the app cannot be adopted (`db.adopt_reference_failed`
|
|
383
|
+
names the skipped stub objects).
|
|
190
384
|
- No cached connection for Next.js dev reloads yet; the Next adapter (identity ID-1, FD-7) adds it.
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import { type AppMigrations } from "../migrations/migrator.js";
|
|
2
3
|
export interface CliOutput {
|
|
3
4
|
readonly log: (line: string) => void;
|
|
4
5
|
readonly error: (line: string) => void;
|
|
@@ -10,11 +11,17 @@ export interface RunMigrateCliOptions {
|
|
|
10
11
|
/** Relative paths resolve against it. Default: `process.cwd()`. */
|
|
11
12
|
readonly cwd?: string;
|
|
12
13
|
readonly output?: CliOutput;
|
|
14
|
+
/**
|
|
15
|
+
* The app's own migrations: migrate runs `before`, the module files (adopting `baseline` where
|
|
16
|
+
* it applies), then `after`; `--adopt` (without `--plan`) runs `before` first; `--plan` runs no
|
|
17
|
+
* hook and marks the files a baseline may adopt.
|
|
18
|
+
*/
|
|
19
|
+
readonly app?: AppMigrations;
|
|
13
20
|
}
|
|
14
21
|
export declare const EXIT_OK = 0;
|
|
15
22
|
export declare const EXIT_FAILED = 1;
|
|
16
23
|
export declare const EXIT_USAGE = 2;
|
|
17
|
-
export declare const MIGRATE_USAGE = "Usage: softure migrate [options]\n\nApplies the SQL migrations of every enabled module, each in its own schema.\n\nOptions:\n --plan show what would be applied (or adopted) and change nothing\n --adopt <module>@<version> record an existing schema as migrated, after comparing it\n --migrations-dir <dir> read module files from <dir>/<module id>/ (bundled runners)\n --export-migrations <dir> copy module files to <dir>/<module id>/ and exit (build stage)\n --help show this help";
|
|
24
|
+
export declare const MIGRATE_USAGE = "Usage: softure migrate [options]\n\nApplies the SQL migrations of every enabled module, each in its own schema.\n\nOptions:\n --plan show what would be applied (or adopted) and change nothing\n --adopt <module>@<version> record an existing schema as migrated, after comparing it;\n applies the pending migrations of its dependencies first\n --through <n> with --adopt: adopt files 1..n only, leave the rest to migrate\n --migrations-dir <dir> read module files from <dir>/<module id>/ (bundled runners)\n --export-migrations <dir> copy module files to <dir>/<module id>/ and exit (build stage)\n --help show this help";
|
|
18
25
|
/** Runs the command and returns the process exit code: 0 done, 1 failed, 2 usage error. */
|
|
19
26
|
export declare function runMigrateCli(options: RunMigrateCliOptions): Promise<number>;
|
|
20
27
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/cli/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAKtD,OAAO,EAA6C,KAAK,aAAa,EAAsB,MAAM,2BAA2B,CAAC;AAG9H,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,GAAG,SAAS,CAAC,CAAC;IAC7D,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,eAAO,MAAM,aAAa,+tBAWqB,CAAC;AAehD,2FAA2F;AAC3F,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4ClF"}
|
package/dist/cli/run.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
// import { runMigrateCli } from "@softure-ai/db/cli";
|
|
6
6
|
// import config from "../softure.config";
|
|
7
7
|
// process.exitCode = await runMigrateCli({ config, argv: process.argv.slice(2) });
|
|
8
|
+
//
|
|
9
|
+
// An app with its own migrations passes them as `app: { before, after }` (see MigrateOptions.app).
|
|
8
10
|
import { resolve } from "node:path";
|
|
9
11
|
import { parseArgs } from "node:util";
|
|
10
12
|
import { pathToFileURL } from "node:url";
|
|
11
|
-
import {
|
|
13
|
+
import { openCommandDatabase } from "../configured.js";
|
|
12
14
|
import { adoptModule } from "../migrations/adopt.js";
|
|
13
15
|
import { exportMigrations } from "../migrations/export.js";
|
|
14
|
-
import { migrate, planMigrations } from "../migrations/migrator.js";
|
|
16
|
+
import { migrate, planMigrations, runAppMigrations } from "../migrations/migrator.js";
|
|
15
17
|
import { describeProblem } from "../migrations/problems.js";
|
|
16
18
|
export const EXIT_OK = 0;
|
|
17
19
|
export const EXIT_FAILED = 1;
|
|
@@ -22,7 +24,9 @@ Applies the SQL migrations of every enabled module, each in its own schema.
|
|
|
22
24
|
|
|
23
25
|
Options:
|
|
24
26
|
--plan show what would be applied (or adopted) and change nothing
|
|
25
|
-
--adopt <module>@<version> record an existing schema as migrated, after comparing it
|
|
27
|
+
--adopt <module>@<version> record an existing schema as migrated, after comparing it;
|
|
28
|
+
applies the pending migrations of its dependencies first
|
|
29
|
+
--through <n> with --adopt: adopt files 1..n only, leave the rest to migrate
|
|
26
30
|
--migrations-dir <dir> read module files from <dir>/<module id>/ (bundled runners)
|
|
27
31
|
--export-migrations <dir> copy module files to <dir>/<module id>/ and exit (build stage)
|
|
28
32
|
--help show this help`;
|
|
@@ -52,22 +56,24 @@ export async function runMigrateCli(options) {
|
|
|
52
56
|
result.value.forEach((step) => output.log(`exported ${formatStep(step)}`));
|
|
53
57
|
return EXIT_OK;
|
|
54
58
|
}
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
59
|
+
const database = options.config.database;
|
|
60
|
+
if (database === null) {
|
|
57
61
|
output.error("softure migrate: the config has no database; set database.url in softure.config");
|
|
58
62
|
return EXIT_FAILED;
|
|
59
63
|
}
|
|
60
|
-
let
|
|
64
|
+
let opened;
|
|
61
65
|
try {
|
|
62
|
-
handle
|
|
66
|
+
// The app's configured handle when it set one, so the app's hooks and the run share one instance.
|
|
67
|
+
opened = await openCommandDatabase(database);
|
|
63
68
|
}
|
|
64
69
|
catch (error) {
|
|
65
70
|
output.error(`softure migrate: ${describeError(error)}`);
|
|
66
71
|
return EXIT_FAILED;
|
|
67
72
|
}
|
|
73
|
+
const { handle } = opened;
|
|
68
74
|
try {
|
|
69
75
|
const migrationsDir = command.migrationsDir === undefined ? undefined : pathToFileURL(`${resolve(cwd, command.migrationsDir)}/`);
|
|
70
|
-
return await runDatabaseCommand(handle, { command, modules: options.config.modules, migrationsDir, output });
|
|
76
|
+
return await runDatabaseCommand(handle, { command, modules: options.config.modules, migrationsDir, output, app: options.app ?? {} });
|
|
71
77
|
}
|
|
72
78
|
catch (error) {
|
|
73
79
|
// Connection and driver errors: the message only, never a stack or the database URL.
|
|
@@ -75,34 +81,61 @@ export async function runMigrateCli(options) {
|
|
|
75
81
|
return EXIT_FAILED;
|
|
76
82
|
}
|
|
77
83
|
finally {
|
|
78
|
-
await
|
|
84
|
+
await opened.close();
|
|
79
85
|
}
|
|
80
86
|
}
|
|
81
87
|
async function runDatabaseCommand(handle, input) {
|
|
82
|
-
const { command, modules, output } = input;
|
|
88
|
+
const { command, modules, output, app } = input;
|
|
83
89
|
const dirOption = input.migrationsDir === undefined ? {} : { migrationsDir: input.migrationsDir };
|
|
90
|
+
const hasAppMigrations = app.before !== undefined || app.after !== undefined;
|
|
91
|
+
if (command.plan && hasAppMigrations) {
|
|
92
|
+
output.log("app migrations: not planned; the app runs them itself on migrate");
|
|
93
|
+
}
|
|
84
94
|
if (command.kind === "adopt") {
|
|
85
|
-
|
|
95
|
+
if (!command.plan) {
|
|
96
|
+
const before = await runAppMigrations(handle, app, "before");
|
|
97
|
+
if (!before.ok)
|
|
98
|
+
return reportFailure(output, before);
|
|
99
|
+
if (before.value.ran)
|
|
100
|
+
output.log("applied app migrations (before)");
|
|
101
|
+
}
|
|
102
|
+
const result = await adoptModule(handle, {
|
|
103
|
+
modules,
|
|
104
|
+
module: command.module,
|
|
105
|
+
version: command.version,
|
|
106
|
+
dryRun: command.plan,
|
|
107
|
+
...(command.through === undefined ? {} : { through: command.through }),
|
|
108
|
+
...dirOption,
|
|
109
|
+
});
|
|
86
110
|
if (!result.ok)
|
|
87
111
|
return reportFailure(output, result);
|
|
88
112
|
const verb = command.plan ? "would" : "did";
|
|
89
|
-
result.value.ledger.forEach((step) => output.log(`${verb} apply ${formatStep(step)}`));
|
|
113
|
+
[...result.value.ledger, ...result.value.dependencies].forEach((step) => output.log(`${verb} apply ${formatStep(step)}`));
|
|
90
114
|
result.value.adopted.forEach((step) => output.log(`${verb} adopt ${formatStep(step)}`));
|
|
91
|
-
|
|
115
|
+
const scope = command.through === undefined ? "" : ` through ${formatVersion(command.through)}`;
|
|
116
|
+
output.log(`${command.module}@${command.version}: the schema matches its migrations${scope}${command.plan ? " (plan only, nothing written)" : "; adopted"}`);
|
|
92
117
|
return EXIT_OK;
|
|
93
118
|
}
|
|
94
119
|
if (command.plan) {
|
|
95
|
-
const result = await planMigrations(handle, { modules, ...dirOption });
|
|
120
|
+
const result = await planMigrations(handle, { modules, ...dirOption, app });
|
|
96
121
|
if (!result.ok)
|
|
97
122
|
return reportFailure(output, result);
|
|
98
|
-
result.value.
|
|
123
|
+
const baseline = new Set(result.value.baseline);
|
|
124
|
+
result.value.pending.forEach((step) => output.log(`pending ${formatStep(step)}${baseline.has(step) ? " (adopted if its schema already holds objects)" : ""}`));
|
|
99
125
|
output.log(result.value.pending.length === 0 ? "nothing to apply" : `${result.value.pending.length} migration(s) to apply`);
|
|
100
126
|
return EXIT_OK;
|
|
101
127
|
}
|
|
102
|
-
const result = await migrate(handle, {
|
|
128
|
+
const result = await migrate(handle, {
|
|
129
|
+
modules,
|
|
130
|
+
...dirOption,
|
|
131
|
+
app,
|
|
132
|
+
onApplied: (step) => output.log(`applied ${formatStep(step)}`),
|
|
133
|
+
onAdopted: (step) => output.log(`adopted ${formatStep(step)}`),
|
|
134
|
+
onAppMigrated: (phase) => output.log(`applied app migrations (${phase})`),
|
|
135
|
+
});
|
|
103
136
|
if (!result.ok)
|
|
104
137
|
return reportFailure(output, result);
|
|
105
|
-
output.log(result.value.applied.length
|
|
138
|
+
output.log(formatSummary(result.value.applied.length, result.value.adopted.length));
|
|
106
139
|
return EXIT_OK;
|
|
107
140
|
}
|
|
108
141
|
function parseCommand(argv) {
|
|
@@ -113,6 +146,7 @@ function parseCommand(argv) {
|
|
|
113
146
|
options: {
|
|
114
147
|
plan: { type: "boolean" },
|
|
115
148
|
adopt: { type: "string" },
|
|
149
|
+
through: { type: "string" },
|
|
116
150
|
"migrations-dir": { type: "string" },
|
|
117
151
|
"export-migrations": { type: "string" },
|
|
118
152
|
help: { type: "boolean" },
|
|
@@ -128,26 +162,42 @@ function parseCommand(argv) {
|
|
|
128
162
|
return { kind: "help" };
|
|
129
163
|
const exportDir = values["export-migrations"];
|
|
130
164
|
if (exportDir !== undefined) {
|
|
131
|
-
const others = ["plan", "adopt", "migrations-dir"].filter((name) => values[name] !== undefined);
|
|
165
|
+
const others = ["plan", "adopt", "through", "migrations-dir"].filter((name) => values[name] !== undefined);
|
|
132
166
|
return others.length > 0 ? `--export-migrations cannot be combined with --${others.join(", --")}` : { kind: "export", targetDir: exportDir };
|
|
133
167
|
}
|
|
134
168
|
const plan = values.plan === true;
|
|
135
169
|
const migrationsDir = values["migrations-dir"];
|
|
136
170
|
if (values.adopt === undefined) {
|
|
137
|
-
return { kind: "migrate", plan, migrationsDir };
|
|
171
|
+
return values.through === undefined ? { kind: "migrate", plan, migrationsDir } : "--through needs --adopt <module>@<version>";
|
|
138
172
|
}
|
|
139
173
|
const match = ADOPT_TARGET.exec(values.adopt);
|
|
140
174
|
if (match?.[1] === undefined || match[2] === undefined) {
|
|
141
175
|
return `--adopt expects <module>@<x.y.z>, e.g. auth@0.1.0, got "${values.adopt}"`;
|
|
142
176
|
}
|
|
143
|
-
|
|
177
|
+
const through = values.through === undefined ? undefined : parseThrough(values.through);
|
|
178
|
+
if (through === null) {
|
|
179
|
+
return `--through expects a positive whole number, got "${values.through ?? ""}"`;
|
|
180
|
+
}
|
|
181
|
+
return { kind: "adopt", plan, module: match[1], version: match[2], through, migrationsDir };
|
|
182
|
+
}
|
|
183
|
+
/** A positive whole number written in digits, or null. */
|
|
184
|
+
function parseThrough(text) {
|
|
185
|
+
return /^[1-9]\d*$/.test(text) ? Number(text) : null;
|
|
144
186
|
}
|
|
145
187
|
function reportFailure(output, failure) {
|
|
146
188
|
failure.problems.forEach((problem) => output.error(describeProblem(problem)));
|
|
147
189
|
return EXIT_FAILED;
|
|
148
190
|
}
|
|
149
191
|
function formatStep(step) {
|
|
150
|
-
return `${step.module} ${
|
|
192
|
+
return `${step.module} ${formatVersion(step.version)}_${step.name}.sql`;
|
|
193
|
+
}
|
|
194
|
+
function formatVersion(version) {
|
|
195
|
+
return String(version).padStart(4, "0");
|
|
196
|
+
}
|
|
197
|
+
function formatSummary(applied, adopted) {
|
|
198
|
+
if (applied === 0 && adopted === 0)
|
|
199
|
+
return "nothing to apply";
|
|
200
|
+
return adopted === 0 ? `${applied} migration(s) applied` : `${applied} migration(s) applied, ${adopted} adopted`;
|
|
151
201
|
}
|
|
152
202
|
function describeError(error) {
|
|
153
203
|
return error instanceof Error ? error.message : String(error);
|
package/dist/cli/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,iGAAiG;AACjG,EAAE;AACF,0BAA0B;AAC1B,wDAAwD;AACxD,4CAA4C;AAC5C,qFAAqF;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
|