@pithy-sh/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/access.d.ts +172 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +147 -0
- package/dist/access/acting.d.ts +49 -0
- package/dist/access/acting.d.ts.map +1 -0
- package/dist/access/acting.js +21 -0
- package/dist/access/policyCheck.d.ts +33 -0
- package/dist/access/policyCheck.d.ts.map +1 -0
- package/dist/access/policyCheck.js +48 -0
- package/dist/access/usage.d.ts +83 -0
- package/dist/access/usage.d.ts.map +1 -0
- package/dist/access/usage.js +104 -0
- package/dist/access/visibility.d.ts +66 -0
- package/dist/access/visibility.d.ts.map +1 -0
- package/dist/access/visibility.js +86 -0
- package/dist/asyncapi/document.d.ts +46 -0
- package/dist/asyncapi/document.d.ts.map +1 -0
- package/dist/asyncapi/document.js +115 -0
- package/dist/audit/auditEvent.d.ts +5 -0
- package/dist/audit/auditEvent.d.ts.map +1 -1
- package/dist/audit/auditEvent.js +2 -0
- package/dist/capability/bindings.d.ts +47 -2
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +117 -7
- package/dist/capability/capability.d.ts +105 -28
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/composition.d.ts.map +1 -1
- package/dist/capability/composition.js +5 -1
- package/dist/capability/manifest.d.ts +2 -1
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/manifest.js +1 -1
- package/dist/capability/settings.d.ts +96 -4
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +85 -4
- package/dist/cli/command.d.ts +364 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +418 -0
- package/dist/cli/commandCatalog.generated.d.ts +2437 -0
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
- package/dist/cli/commandCatalog.generated.js +2564 -0
- package/dist/cli/commandFacts.d.ts +164 -0
- package/dist/cli/commandFacts.d.ts.map +1 -0
- package/dist/cli/commandFacts.js +216 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +12 -2
- package/dist/controlPlane/context.d.ts +5 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -1
- package/dist/controlPlane/data/connection.d.ts +47 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +55 -1
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +4 -3
- package/dist/controlPlane/http/cors.d.ts +4 -4
- package/dist/controlPlane/http/cors.js +4 -4
- package/dist/controlPlane/http/guard.d.ts +11 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +28 -2
- package/dist/controlPlane/http/handlers.d.ts +60 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +31 -2
- package/dist/controlPlane/http/readiness.d.ts +33 -0
- package/dist/controlPlane/http/readiness.d.ts.map +1 -0
- package/dist/controlPlane/http/readiness.js +39 -0
- package/dist/controlPlane/http/responses.d.ts +147 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +61 -4
- package/dist/controlPlane/http/routes.d.ts +2 -1
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +50 -9
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +2 -1
- package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
- package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
- package/dist/controlPlane/migrationState/migrationState.js +142 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
- package/dist/controlPlane/scope/scope.d.ts +13 -2
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +14 -3
- package/dist/createBackend.d.ts +7 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +41 -6
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +5 -3
- package/dist/entitlement/allowance.d.ts +25 -0
- package/dist/entitlement/allowance.d.ts.map +1 -0
- package/dist/entitlement/allowance.js +80 -0
- package/dist/entitlement/entitlement.d.ts +41 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -1
- package/dist/entitlement/meter.d.ts +57 -0
- package/dist/entitlement/meter.d.ts.map +1 -0
- package/dist/entitlement/meter.js +13 -0
- package/dist/entitlement/require.js +1 -1
- package/dist/error/cause.d.ts +16 -116
- package/dist/error/cause.d.ts.map +1 -1
- package/dist/error/cause.js +19 -153
- package/dist/error/http.d.ts +184 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +368 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +126 -4
- package/dist/error/pithyError.d.ts +44 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +56 -1
- package/dist/error/unresolved.d.ts +265 -0
- package/dist/error/unresolved.d.ts.map +1 -0
- package/dist/error/unresolved.js +599 -0
- package/dist/error/wire.d.ts +20 -0
- package/dist/error/wire.d.ts.map +1 -0
- package/dist/error/wire.js +19 -0
- package/dist/http/authContext.d.ts +7 -0
- package/dist/http/authContext.d.ts.map +1 -1
- package/dist/http/authContext.js +8 -1
- package/dist/http/oidcWebhook.d.ts +3 -3
- package/dist/http/oidcWebhook.js +3 -3
- package/dist/http/routeContract.js +1 -1
- package/dist/http/routeSpec.d.ts +66 -0
- package/dist/http/routeSpec.d.ts.map +1 -0
- package/dist/http/routeSpec.js +40 -0
- package/dist/http/sameOrigin.d.ts +1 -1
- package/dist/http/sameOrigin.js +1 -1
- package/dist/http/scopeGates.d.ts +20 -0
- package/dist/http/scopeGates.d.ts.map +1 -0
- package/dist/http/scopeGates.js +36 -0
- package/dist/http/signedWebhook.d.ts +3 -3
- package/dist/http/signedWebhook.js +3 -3
- package/dist/http/socket.d.ts +52 -0
- package/dist/http/socket.d.ts.map +1 -0
- package/dist/http/socket.js +2 -0
- package/dist/http/subprotocol.d.ts +29 -0
- package/dist/http/subprotocol.d.ts.map +1 -0
- package/dist/http/subprotocol.js +37 -0
- package/dist/http/validation.d.ts +40 -8
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +25 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/logger/record.d.ts +107 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.d.ts +21 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.js +32 -3
- package/dist/migrations/capabilitySets.d.ts +23 -0
- package/dist/migrations/capabilitySets.d.ts.map +1 -0
- package/dist/migrations/capabilitySets.js +74 -0
- package/dist/migrations/groups.d.ts +140 -0
- package/dist/migrations/groups.d.ts.map +1 -0
- package/dist/migrations/groups.js +191 -0
- package/dist/migrations/migrateStep.d.ts +13 -0
- package/dist/migrations/migrateStep.d.ts.map +1 -0
- package/dist/migrations/migrateStep.js +20 -0
- package/dist/migrations/owner.d.ts +7 -0
- package/dist/migrations/owner.d.ts.map +1 -1
- package/dist/migrations/owner.js +11 -7
- package/dist/migrations/registry.d.ts +26 -0
- package/dist/migrations/registry.d.ts.map +1 -1
- package/dist/migrations/registry.js +38 -7
- package/dist/migrations/retained.js +2 -2
- package/dist/migrations/runner.d.ts +76 -5
- package/dist/migrations/runner.d.ts.map +1 -1
- package/dist/migrations/runner.js +130 -19
- package/dist/migrations/state.d.ts +60 -0
- package/dist/migrations/state.d.ts.map +1 -0
- package/dist/migrations/state.js +46 -0
- package/dist/naming/domains.d.ts +17 -0
- package/dist/naming/domains.d.ts.map +1 -1
- package/dist/naming/domains.js +62 -4
- package/dist/naming/environment.d.ts.map +1 -1
- package/dist/naming/environment.js +2 -1
- package/dist/naming/feature.d.ts +27 -5
- package/dist/naming/feature.d.ts.map +1 -1
- package/dist/naming/feature.js +36 -6
- package/dist/naming/limits.d.ts +47 -3
- package/dist/naming/limits.d.ts.map +1 -1
- package/dist/naming/limits.js +47 -3
- package/dist/naming/packageName.d.ts +17 -0
- package/dist/naming/packageName.d.ts.map +1 -0
- package/dist/naming/packageName.js +22 -0
- package/dist/naming/provisionScope.d.ts +6 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +10 -4
- package/dist/naming/resourceNames.d.ts +21 -2
- package/dist/naming/resourceNames.d.ts.map +1 -1
- package/dist/naming/resourceNames.js +21 -3
- package/dist/openapi/document.d.ts +106 -0
- package/dist/openapi/document.d.ts.map +1 -0
- package/dist/openapi/document.js +314 -0
- package/dist/openapi/routeGroups.d.ts +8 -0
- package/dist/openapi/routeGroups.d.ts.map +1 -0
- package/dist/openapi/routeGroups.js +23 -0
- package/dist/seed/writeD1.d.ts +7 -1
- package/dist/seed/writeD1.d.ts.map +1 -1
- package/dist/seed/writeD1.js +9 -3
- package/dist/seed/writeKv.d.ts +2 -0
- package/dist/seed/writeKv.d.ts.map +1 -1
- package/dist/seed/writeKv.js +6 -2
- package/dist/text/comments.js +66 -4
- package/dist/text/literals.d.ts +43 -0
- package/dist/text/literals.d.ts.map +1 -0
- package/dist/text/literals.js +293 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.d.ts.map +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worker/serviceLoopback.d.ts +36 -0
- package/dist/worker/serviceLoopback.d.ts.map +1 -0
- package/dist/worker/serviceLoopback.js +74 -0
- package/dist/workflow/dispatchRoute.d.ts.map +1 -1
- package/dist/workflow/dispatchRoute.js +12 -6
- package/dist/workflow/faults.d.ts +12 -1
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +24 -18
- package/dist/workflow/hostEnv.d.ts +1 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -1
- package/dist/workflow/hostEnv.js +4 -2
- package/dist/workflow/loopback.d.ts.map +1 -1
- package/dist/workflow/loopback.js +2 -1
- package/package.json +4 -4
- package/src/access/access.ts +298 -0
- package/src/access/acting.ts +59 -0
- package/src/access/policyCheck.ts +77 -0
- package/src/access/usage.ts +141 -0
- package/src/access/visibility.ts +109 -0
- package/src/asyncapi/document.ts +164 -0
- package/src/audit/auditEvent.ts +4 -0
- package/src/capability/bindings.ts +159 -13
- package/src/capability/capability.ts +106 -29
- package/src/capability/composition.ts +2 -1
- package/src/capability/settings.ts +165 -4
- package/src/cli/command.ts +865 -0
- package/src/cli/commandCatalog.generated.ts +2723 -0
- package/src/cli/commandFacts.ts +220 -0
- package/src/controlPlane/capability.ts +25 -1
- package/src/controlPlane/context.ts +4 -0
- package/src/controlPlane/data/connection.ts +90 -0
- package/src/controlPlane/error/errors.ts +10 -3
- package/src/controlPlane/http/cors.ts +4 -4
- package/src/controlPlane/http/guard.ts +35 -2
- package/src/controlPlane/http/handlers.ts +41 -0
- package/src/controlPlane/http/readiness.ts +50 -0
- package/src/controlPlane/http/responses.ts +140 -2
- package/src/controlPlane/http/routes.ts +82 -10
- package/src/controlPlane/http/verify.ts +2 -0
- package/src/controlPlane/migrationState/migrationState.ts +200 -0
- package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
- package/src/controlPlane/scope/scope.ts +14 -2
- package/src/createBackend.ts +59 -8
- package/src/createEntrypoint.ts +9 -3
- package/src/entitlement/allowance.ts +85 -0
- package/src/entitlement/entitlement.ts +38 -0
- package/src/entitlement/meter.ts +69 -0
- package/src/entitlement/require.ts +1 -1
- package/src/error/cause.ts +18 -181
- package/src/error/payload.ts +270 -4
- package/src/error/pithyError.ts +76 -0
- package/src/error/unresolved.ts +853 -0
- package/src/error/wire.ts +40 -0
- package/src/http/authContext.ts +13 -1
- package/src/http/oidcWebhook.ts +3 -3
- package/src/http/routeContract.ts +1 -1
- package/src/http/routeSpec.ts +81 -0
- package/src/http/sameOrigin.ts +1 -1
- package/src/http/scopeGates.ts +34 -0
- package/src/http/signedWebhook.ts +3 -3
- package/src/http/socket.ts +55 -0
- package/src/http/subprotocol.ts +47 -0
- package/src/http/validation.ts +50 -8
- package/src/index.ts +1 -0
- package/src/migrations/bookkeeping.ts +39 -2
- package/src/migrations/capabilitySets.ts +87 -0
- package/src/migrations/groups.ts +295 -0
- package/src/migrations/migrateStep.ts +19 -0
- package/src/migrations/owner.ts +10 -6
- package/src/migrations/registry.ts +46 -2
- package/src/migrations/retained.ts +2 -2
- package/src/migrations/runner.ts +214 -22
- package/src/migrations/state.ts +94 -0
- package/src/naming/domains.ts +75 -16
- package/src/naming/environment.ts +2 -1
- package/src/naming/feature.ts +46 -7
- package/src/naming/limits.ts +35 -3
- package/src/naming/packageName.ts +20 -0
- package/src/naming/provisionScope.ts +21 -2
- package/src/naming/resourceNames.ts +52 -5
- package/src/openapi/document.ts +412 -0
- package/src/openapi/routeGroups.ts +25 -0
- package/src/seed/writeD1.ts +13 -4
- package/src/seed/writeKv.ts +6 -2
- package/src/text/comments.ts +56 -4
- package/src/text/literals.ts +294 -0
- package/src/version.generated.ts +1 -1
- package/src/worker/serviceLoopback.ts +105 -0
- package/src/workflow/dispatchRoute.ts +13 -6
- package/src/workflow/faults.ts +56 -27
- package/src/workflow/hostEnv.ts +9 -1
- package/src/workflow/loopback.ts +2 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { SQLiteDate } from "../data/codecs.js";
|
|
4
|
+
import { chunkByBoundParameters, chunkRowsByBoundParameters } from "../data/boundParameters.js";
|
|
5
|
+
import { MIGRATION_GROUP_TABLE, appliedMigrationChain, migrationKysely } from "./bookkeeping.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { sql } from "kysely";
|
|
8
|
+
//#region src/migrations/groups.ts
|
|
9
|
+
/**
|
|
10
|
+
* **Which run applied each migration — the group a rollback names (#694).**
|
|
11
|
+
*
|
|
12
|
+
* `pithy migrate --rollback` used to step back one migration in every database the environment binds, and
|
|
13
|
+
* nothing recorded what a single run had applied. So reversing a deploy that moved three migrations was
|
|
14
|
+
* three invocations and the count came from outside the tool — a human's memory, or a CI log read under
|
|
15
|
+
* pressure. Every run belongs to a group now: the caller's own `--group <value>`, or a generated ISO-8601
|
|
16
|
+
* timestamp, recorded here against every migration the run applies, in every database it touches.
|
|
17
|
+
*
|
|
18
|
+
* **It cannot live in the ledger.** `pithy_migrations` is Kysely's own table, renamed off its default
|
|
19
|
+
* (`./bookkeeping`); Kysely creates it and its insert sets `name` and `timestamp` only. So the group sits
|
|
20
|
+
* in a table *beside* it, written by the runner from the `MigrationResult[]` Kysely hands back — the same
|
|
21
|
+
* placement, and the same reasoning, as `pithy_migrations_owner`: it is not a migration, so no capability
|
|
22
|
+
* ships it, `--rollback` cannot step it back, and `pithy seed --redo`'s full reset cannot wipe it. What a
|
|
23
|
+
* reversal removes is the reversed migrations' own rows, which is bookkeeping catching up with the ledger.
|
|
24
|
+
*
|
|
25
|
+
* **The rows say which run; the ledger says which order.** Every applied migration is recorded here as it
|
|
26
|
+
* applies and forgotten as it is reversed, so these rows are the membership. Where a group *sits* is a
|
|
27
|
+
* question about the ledger, though, because the ledger is what Kysely steps down from — so {@link
|
|
28
|
+
* readGroupPosition} reads both (`appliedMigrationChain`) and compares the group against the chain's tail.
|
|
29
|
+
* Reading the group table alone would answer from the order this table happened to be written in, and the
|
|
30
|
+
* two differ in exactly the cases that matter: a migration applied with no group row at all.
|
|
31
|
+
*
|
|
32
|
+
* **What it cannot see, it names.** A migration applied before this table existed carries no row, and so
|
|
33
|
+
* does one a `seed --redo` reset reapplied or one whose group write failed. Such a migration over a group
|
|
34
|
+
* buries it — reversing the group would reverse a migration nobody recorded — and the position says so by
|
|
35
|
+
* name rather than calling the group reversible. Reading is cheap and additive: one group row per applied
|
|
36
|
+
* migration, and the table is created on the first run that applies anything.
|
|
37
|
+
*/
|
|
38
|
+
/** One applied migration and the run that applied it. The three columns are the whole table. */
|
|
39
|
+
const MigrationGroupRecord = z.object({
|
|
40
|
+
groupKey: z.string().describe("The group this migration was applied under — the caller's `--group` value, or the generated ISO-8601 timestamp of the run. Not `group`: that is SQL's own word, and an operator reading this table by hand should not have to quote a column."),
|
|
41
|
+
migration: z.string().describe("The composed migration name, exactly as the ledger records it."),
|
|
42
|
+
appliedAt: SQLiteDate.describe("When this migration was applied — the moment the run recorded it.")
|
|
43
|
+
}).describe("One applied migration's membership of a migration group, recorded beside the ledger.");
|
|
44
|
+
/**
|
|
45
|
+
* The group a run with no `--group` belongs to: an **ISO-8601 timestamp**, `2026-10-03T19:52:47.611Z`.
|
|
46
|
+
*
|
|
47
|
+
* A timestamp rather than a date, because one run is one group. Date-only would silently merge two
|
|
48
|
+
* unrelated runs on a busy day into one group, and reversing it would then undo more than the caller did.
|
|
49
|
+
* The precision costs nothing, it sorts lexicographically into chronological order, and it tells an
|
|
50
|
+
* operator *when* without their having named anything.
|
|
51
|
+
*/
|
|
52
|
+
function generatedMigrationGroup(now = /* @__PURE__ */ new Date()) {
|
|
53
|
+
return now.toISOString();
|
|
54
|
+
}
|
|
55
|
+
/** Whether the group table has been created yet — a plain `sqlite_master` select, which D1 permits. */
|
|
56
|
+
async function groupTableExists(db) {
|
|
57
|
+
const { rows } = await sql`select name from sqlite_master where type = 'table' and name = ${MIGRATION_GROUP_TABLE}`.execute(db);
|
|
58
|
+
return rows.length > 0;
|
|
59
|
+
}
|
|
60
|
+
/** Every row, in the order it was written — which is the order the migrations applied. */
|
|
61
|
+
async function groupRows(db) {
|
|
62
|
+
if (!await groupTableExists(db)) return [];
|
|
63
|
+
return (await db.selectFrom("pithy_migrations_groups").select([
|
|
64
|
+
"groupKey",
|
|
65
|
+
"migration",
|
|
66
|
+
"appliedAt"
|
|
67
|
+
]).orderBy("id").execute()).map((row) => MigrationGroupRecord.parse(row));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* How many parameters one group row binds — derived from the schema, so a fourth column re-derives the
|
|
71
|
+
* chunk size instead of silently taking the insert back over D1's cap.
|
|
72
|
+
*/
|
|
73
|
+
const GROUP_ROW_COLUMNS = Object.keys(MigrationGroupRecord.shape).length;
|
|
74
|
+
/**
|
|
75
|
+
* Record a group against the migrations a run just applied, creating the table on demand. Idempotent in
|
|
76
|
+
* the only sense that matters: a migration applies once, so a name arrives here once — and a second call
|
|
77
|
+
* under the same group **extends** it, which is the retry case a release whose migrate half-failed needs.
|
|
78
|
+
*/
|
|
79
|
+
async function recordMigrationGroup(database, options) {
|
|
80
|
+
if (options.migrations.length === 0) return;
|
|
81
|
+
const db = migrationKysely(database);
|
|
82
|
+
await db.schema.createTable(MIGRATION_GROUP_TABLE).ifNotExists().addColumn("id", "integer", (column) => column.primaryKey().autoIncrement()).addColumn("groupKey", "text", (column) => column.notNull()).addColumn("migration", "text", (column) => column.notNull()).addColumn("appliedAt", "integer", (column) => column.notNull()).execute();
|
|
83
|
+
const appliedAt = options.at ?? /* @__PURE__ */ new Date();
|
|
84
|
+
const rows = options.migrations.map((migration) => MigrationGroupRecord.encode({
|
|
85
|
+
groupKey: options.group,
|
|
86
|
+
migration,
|
|
87
|
+
appliedAt
|
|
88
|
+
}));
|
|
89
|
+
for (const chunk of chunkRowsByBoundParameters(rows, GROUP_ROW_COLUMNS)) await db.insertInto("pithy_migrations_groups").values(chunk).execute();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every group this database records, oldest first by when each last applied something — and so the last
|
|
93
|
+
* entry is the newest group, which is what a rollback with no `--group` names. Empty for a database
|
|
94
|
+
* nothing has grouped, and reading creates nothing.
|
|
95
|
+
*/
|
|
96
|
+
async function readMigrationGroups(database) {
|
|
97
|
+
const rows = await groupRows(migrationKysely(database));
|
|
98
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
99
|
+
for (const [index, row] of rows.entries()) {
|
|
100
|
+
const existing = byGroup.get(row.groupKey);
|
|
101
|
+
if (existing) {
|
|
102
|
+
existing.migrations.push(row.migration);
|
|
103
|
+
existing.appliedAt = row.appliedAt > existing.appliedAt ? row.appliedAt : existing.appliedAt;
|
|
104
|
+
existing.order = index;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
byGroup.set(row.groupKey, {
|
|
108
|
+
group: row.groupKey,
|
|
109
|
+
appliedAt: row.appliedAt,
|
|
110
|
+
migrations: [row.migration],
|
|
111
|
+
order: index
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return [...byGroup.values()].sort((left, right) => left.order - right.order).map(({ group, appliedAt, migrations }) => ({
|
|
115
|
+
group,
|
|
116
|
+
appliedAt,
|
|
117
|
+
migrations
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Where `group` sits in this database's chain — see {@link GroupPosition}.
|
|
122
|
+
*
|
|
123
|
+
* The test is *is this group the tail of the **ledger***, not *is it the newest by timestamp*: a group
|
|
124
|
+
* extended after another run has the later timestamp and still has that run's migration inside it, so
|
|
125
|
+
* reversing it would reverse the other group's work. Hence the two halves of one rule — nothing applied
|
|
126
|
+
* over the group's newest migration, and nothing applied inside it between two of its own.
|
|
127
|
+
*
|
|
128
|
+
* **Against the ledger, because the ledger is what comes down.** `migrateDown()` steps the ledger's tip,
|
|
129
|
+
* so a group whose rows are the tail of the *group table* is still buried when an ungrouped migration sits
|
|
130
|
+
* over it in the ledger — and that state is reachable from the ordinary dev loop, where `seed --redo`'s
|
|
131
|
+
* reset reapplies a pending migration under no group at all. Asked of the group table alone, this returned
|
|
132
|
+
* `top`, the `down` reversed a migration nobody asked about, and the refusal arrived after the damage.
|
|
133
|
+
*/
|
|
134
|
+
async function readGroupPosition(database, group) {
|
|
135
|
+
const db = migrationKysely(database);
|
|
136
|
+
const groupOf = new Map((await groupRows(db)).map((row) => [row.migration, row.groupKey]));
|
|
137
|
+
const chain = await appliedMigrationChain(db);
|
|
138
|
+
const held = (name) => groupOf.get(name) === group;
|
|
139
|
+
const first = chain.findIndex(held);
|
|
140
|
+
if (first === -1) return { state: "absent" };
|
|
141
|
+
const last = chain.length - 1 - [...chain].reverse().findIndex(held);
|
|
142
|
+
const span = chain.slice(first, last + 1);
|
|
143
|
+
const over = chain.slice(last + 1);
|
|
144
|
+
const inside = span.filter((name) => !held(name));
|
|
145
|
+
if (over.length === 0 && inside.length === 0) return {
|
|
146
|
+
state: "top",
|
|
147
|
+
migrations: span.filter(held).reverse()
|
|
148
|
+
};
|
|
149
|
+
const blocking = [...[...over].reverse(), ...[...inside].reverse()];
|
|
150
|
+
const above = [...new Set(blocking.map((name) => groupOf.get(name)).filter(isGroupKey))];
|
|
151
|
+
const ungrouped = blocking.filter((name) => !groupOf.has(name));
|
|
152
|
+
const top = contiguousTipGroup(chain, groupOf);
|
|
153
|
+
return {
|
|
154
|
+
state: "buried",
|
|
155
|
+
above,
|
|
156
|
+
ungrouped,
|
|
157
|
+
...top === void 0 ? {} : { top }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** A `groupOf` lookup that found a group, for narrowing the `above` list. */
|
|
161
|
+
function isGroupKey(key) {
|
|
162
|
+
return key !== void 0;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The group whose own migrations are the contiguous tip of `chain`, if any.
|
|
166
|
+
*
|
|
167
|
+
* What it is for is the refusals: *reverse the group on top first* is only advice if such a group exists,
|
|
168
|
+
* and after a group was extended over another one none does — each is inside the other, so naming either
|
|
169
|
+
* prints a command that refuses in its turn and the operator is sent in a circle (#694). The candidate can
|
|
170
|
+
* only be the group holding the tip migration, so there is one to test, not a search.
|
|
171
|
+
*/
|
|
172
|
+
function contiguousTipGroup(chain, groupOf) {
|
|
173
|
+
const tip = chain.at(-1);
|
|
174
|
+
const candidate = tip === void 0 ? void 0 : groupOf.get(tip);
|
|
175
|
+
if (candidate === void 0) return void 0;
|
|
176
|
+
const held = chain.filter((name) => groupOf.get(name) === candidate).length;
|
|
177
|
+
return chain.slice(chain.length - held).every((name) => groupOf.get(name) === candidate) ? candidate : void 0;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Forget the group rows of migrations that are no longer applied — what a reversal writes once the `down`
|
|
181
|
+
* has run. By migration name alone, because the ledger's name is unique: one applied migration, one row.
|
|
182
|
+
* A database with no group table has nothing to forget.
|
|
183
|
+
*/
|
|
184
|
+
async function forgetMigrationGroups(database, migrations) {
|
|
185
|
+
if (migrations.length === 0) return;
|
|
186
|
+
const db = migrationKysely(database);
|
|
187
|
+
if (!await groupTableExists(db)) return;
|
|
188
|
+
for (const chunk of chunkByBoundParameters(migrations, 0)) await db.deleteFrom("pithy_migrations_groups").where("migration", "in", chunk).execute();
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
export { MigrationGroupRecord, forgetMigrationGroups, generatedMigrationGroup, readGroupPosition, readMigrationGroups, recordMigrationGroup };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { type AmbientEnv } from "../env/ambient";
|
|
4
|
+
/**
|
|
5
|
+
* **The step that migrates the environment a Worker runs in**, as a sentence ends with it: `then ${step}.`
|
|
6
|
+
*
|
|
7
|
+
* `pithy migrate` defaults to `dev`, so a line that names it bare sends a staging Worker's operator to migrate
|
|
8
|
+
* their laptop (#711). A Worker knows its environment when the composition stamped one (`ENVIRONMENT`), and then
|
|
9
|
+
* the command names it; when it does not, the line says which flag to name rather than naming a command that
|
|
10
|
+
* would act on the wrong database.
|
|
11
|
+
*/
|
|
12
|
+
export declare function migrateStep(env: AmbientEnv): string;
|
|
13
|
+
//# sourceMappingURL=migrateStep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrateStep.d.ts","sourceRoot":"","sources":["../../src/migrations/migrateStep.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAA0B,MAAM,gBAAgB,CAAC;AAEzE;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAInD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { commandName, pithyCommand } from "../cli/command.js";
|
|
4
|
+
import { compositionEnvironment } from "../env/ambient.js";
|
|
5
|
+
//#region src/migrations/migrateStep.ts
|
|
6
|
+
/**
|
|
7
|
+
* **The step that migrates the environment a Worker runs in**, as a sentence ends with it: `then ${step}.`
|
|
8
|
+
*
|
|
9
|
+
* `pithy migrate` defaults to `dev`, so a line that names it bare sends a staging Worker's operator to migrate
|
|
10
|
+
* their laptop (#711). A Worker knows its environment when the composition stamped one (`ENVIRONMENT`), and then
|
|
11
|
+
* the command names it; when it does not, the line says which flag to name rather than naming a command that
|
|
12
|
+
* would act on the wrong database.
|
|
13
|
+
*/
|
|
14
|
+
function migrateStep(env) {
|
|
15
|
+
const environment = compositionEnvironment(env);
|
|
16
|
+
if (environment === void 0) return `migrate this environment with ${commandName("migrate")}, naming it with --env`;
|
|
17
|
+
return `run ${pithyCommand("migrate", { flags: { "--env": environment } })}`;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { migrateStep };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import type { D1Database } from "@cloudflare/workers-types";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { ConflictError } from "../error/pithyError";
|
|
5
6
|
/** The recorded owner of a database: who stamped it, and when. */
|
|
6
7
|
export declare const MigrationOwner: z.ZodObject<{
|
|
7
8
|
project: z.ZodString;
|
|
@@ -26,6 +27,12 @@ export interface ClaimOwnershipOptions {
|
|
|
26
27
|
/** The binding this database is reached by (`DB`), so a refusal names it. Optional. */
|
|
27
28
|
binding?: string;
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The refusal for a database another project owns: both project names in the problem line, both remedies in the
|
|
32
|
+
* action line. Exported so a reader that only checks the claim — `pithy migrate --dry-run`, which stamps nothing —
|
|
33
|
+
* refuses in the words the claim refuses with, rather than a copy of them (#697).
|
|
34
|
+
*/
|
|
35
|
+
export declare function foreignOwnerRefusal(owner: string, project: string, binding: string | undefined): ConflictError;
|
|
29
36
|
/**
|
|
30
37
|
* The project recorded as this database's owner, or `undefined` when none is — read-only, creating
|
|
31
38
|
* nothing. `pithy doctor` and any other reporter reads the stamp through this; only
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"owner.d.ts","sourceRoot":"","sources":["../../src/migrations/owner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"owner.d.ts","sourceRoot":"","sources":["../../src/migrations/owner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,aAAa,EAAmB,MAAM,qBAAqB,CAAC;AAyBrE,kEAAkE;AAClE,eAAO,MAAM,cAAc;;;iBAOuF,CAAC;AACnH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAa7D,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,OAAO,EAAE,gBAAgB,CAAC;IAC1B,4EAA4E;IAC5E,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,oGAAoG;IACpG,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAM9G;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAElG;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CA0C3B"}
|
package/dist/migrations/owner.js
CHANGED
|
@@ -42,11 +42,15 @@ async function currentOwner(db) {
|
|
|
42
42
|
const row = await db.selectFrom("pithy_migrations_owner").select(["project", "stampedAt"]).where("id", "=", OWNER_ROW_ID).executeTakeFirst();
|
|
43
43
|
return row ? MigrationOwner.parse(row) : void 0;
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The refusal for a database another project owns: both project names in the problem line, both remedies in the
|
|
47
|
+
* action line. Exported so a reader that only checks the claim — `pithy migrate --dry-run`, which stamps nothing —
|
|
48
|
+
* refuses in the words the claim refuses with, rather than a copy of them (#697).
|
|
49
|
+
*/
|
|
50
|
+
function foreignOwnerRefusal(owner, project, binding) {
|
|
47
51
|
return new ConflictError({
|
|
48
52
|
message: `${binding ?? "This database"} belongs to project "${owner}", not "${project}".`,
|
|
49
|
-
action: `Bind ${project} to its own database, or set the project name back to ${owner}.
|
|
53
|
+
action: `Bind ${project} to its own database, or set the project name back to ${owner}. Then run this again.`,
|
|
50
54
|
detail: `Recorded owner "${owner}" in ${MIGRATION_OWNER_TABLE}.`
|
|
51
55
|
});
|
|
52
56
|
}
|
|
@@ -74,7 +78,7 @@ async function claimMigrationOwnership(database, options) {
|
|
|
74
78
|
const db = migrationKysely(database);
|
|
75
79
|
const existing = await currentOwner(db);
|
|
76
80
|
if (existing) {
|
|
77
|
-
if (existing.project !== project) throw
|
|
81
|
+
if (existing.project !== project) throw foreignOwnerRefusal(existing.project, project, options.binding);
|
|
78
82
|
return {
|
|
79
83
|
project,
|
|
80
84
|
outcome: "confirmed",
|
|
@@ -93,10 +97,10 @@ async function claimMigrationOwnership(database, options) {
|
|
|
93
97
|
const settled = await currentOwner(db);
|
|
94
98
|
if (!settled) throw new ConflictError({
|
|
95
99
|
message: `${options.binding ?? "This database"} could not be stamped as "${project}".`,
|
|
96
|
-
action: "Check the database is writable
|
|
100
|
+
action: "Check the database is writable, then run this again.",
|
|
97
101
|
detail: `No row in ${MIGRATION_OWNER_TABLE} after the claim insert.`
|
|
98
102
|
});
|
|
99
|
-
if (settled.project !== project) throw
|
|
103
|
+
if (settled.project !== project) throw foreignOwnerRefusal(settled.project, project, options.binding);
|
|
100
104
|
return {
|
|
101
105
|
project,
|
|
102
106
|
outcome: "adopted",
|
|
@@ -104,4 +108,4 @@ async function claimMigrationOwnership(database, options) {
|
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
//#endregion
|
|
107
|
-
export { MigrationOwner, claimMigrationOwnership, readMigrationOwner };
|
|
111
|
+
export { MigrationOwner, claimMigrationOwnership, foreignOwnerRefusal, readMigrationOwner };
|
|
@@ -25,6 +25,32 @@ export interface NamespacedMigrations {
|
|
|
25
25
|
*/
|
|
26
26
|
retained?: readonly string[];
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The composed key for one of a set's migrations: `NNNN_<namespace>_<localKey>`. The one place the shape is
|
|
30
|
+
* written, so the registry and anything naming a declared migration's namespace build the same string.
|
|
31
|
+
*/
|
|
32
|
+
export declare function composedMigrationKey(set: Pick<NamespacedMigrations, "order" | "namespace">, localKey: string): string;
|
|
33
|
+
/** A composed key taken back apart: the three parts {@link createMigrationRegistry} joined. */
|
|
34
|
+
export interface ComposedMigrationKey {
|
|
35
|
+
/** The capability's `migrationOrder` — the zero-padded prefix, as a number. */
|
|
36
|
+
order: number;
|
|
37
|
+
/** The capability namespace — its `name`, which the manifest reports beside every capability. */
|
|
38
|
+
namespace: string;
|
|
39
|
+
/** The capability's own stable key, e.g. `0001_init`. */
|
|
40
|
+
localKey: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Take a composed key back apart — the inverse of the composition below.
|
|
44
|
+
*
|
|
45
|
+
* A declared migration's namespace is known from the set it came from. An **undeclared** one — applied in
|
|
46
|
+
* the database, carried by no provider this build composes — has nothing but its key, so this is the only
|
|
47
|
+
* way to say whose it was. **Null for a key no registry could have composed**, a migration written by hand
|
|
48
|
+
* or by another tool: guessing a namespace for it would be inventing one.
|
|
49
|
+
*
|
|
50
|
+
* The patterns are the same constants the composition enforces, so the two cannot disagree about what a
|
|
51
|
+
* key looks like.
|
|
52
|
+
*/
|
|
53
|
+
export declare function decomposeMigrationKey(key: string): ComposedMigrationKey | null;
|
|
28
54
|
/**
|
|
29
55
|
* Merge namespaced migration sets into one ordered `MigrationProvider` **per database** — matching
|
|
30
56
|
* multi-database D1, where each `c.var.db.<database>` is its own binding and schema. The result is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/migrations/registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAOrE,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB,QAAyB,CAAC;AAE1D,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,QAAqB,CAAC;AAKpD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,wGAAwG;IACxG,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/migrations/registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAOrE,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB,QAAyB,CAAC;AAE1D,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,QAAqB,CAAC;AAKpD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,wGAAwG;IACxG,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AA0BD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,OAAO,GAAG,WAAW,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErH;AAED,+FAA+F;AAC/F,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAUD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAM9E;AAuCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,oBAAoB,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAavG"}
|
|
@@ -20,6 +20,40 @@ function assertValidOrder(order, namespace) {
|
|
|
20
20
|
function assertValidLocalKey(localKey, namespace) {
|
|
21
21
|
if (!LOCAL_KEY_PATTERN.test(localKey)) throw new InternalError({ message: `migration key "${localKey}" (namespace "${namespace}") must match ${LOCAL_KEY_PATTERN}` });
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The composed key for one of a set's migrations: `NNNN_<namespace>_<localKey>`. The one place the shape is
|
|
25
|
+
* written, so the registry and anything naming a declared migration's namespace build the same string.
|
|
26
|
+
*/
|
|
27
|
+
function composedMigrationKey(set, localKey) {
|
|
28
|
+
return `${String(set.order).padStart(ORDER_DIGITS, "0")}_${set.namespace}_${localKey}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The composed key shape, anchored at both ends: the order prefix, a namespace (which admits no separator,
|
|
32
|
+
* so the first underscore after it is the boundary), then a local key, which may hold any number.
|
|
33
|
+
*/
|
|
34
|
+
const COMPOSED_KEY_PATTERN = new RegExp(`^(\\d{${ORDER_DIGITS}})_(${NAMESPACE_PATTERN.source.slice(1, -1)})_(${LOCAL_KEY_PATTERN.source.slice(1, -1)})$`);
|
|
35
|
+
/**
|
|
36
|
+
* Take a composed key back apart — the inverse of the composition below.
|
|
37
|
+
*
|
|
38
|
+
* A declared migration's namespace is known from the set it came from. An **undeclared** one — applied in
|
|
39
|
+
* the database, carried by no provider this build composes — has nothing but its key, so this is the only
|
|
40
|
+
* way to say whose it was. **Null for a key no registry could have composed**, a migration written by hand
|
|
41
|
+
* or by another tool: guessing a namespace for it would be inventing one.
|
|
42
|
+
*
|
|
43
|
+
* The patterns are the same constants the composition enforces, so the two cannot disagree about what a
|
|
44
|
+
* key looks like.
|
|
45
|
+
*/
|
|
46
|
+
function decomposeMigrationKey(key) {
|
|
47
|
+
const match = COMPOSED_KEY_PATTERN.exec(key);
|
|
48
|
+
if (!match) return null;
|
|
49
|
+
const [, order, namespace, localKey] = match;
|
|
50
|
+
if (order === void 0 || namespace === void 0 || localKey === void 0) return null;
|
|
51
|
+
return {
|
|
52
|
+
order: Number(order),
|
|
53
|
+
namespace,
|
|
54
|
+
localKey
|
|
55
|
+
};
|
|
56
|
+
}
|
|
23
57
|
/** Merge one database's sets into a provider, enforcing the per-database invariants. */
|
|
24
58
|
function composeDatabase(database, sets) {
|
|
25
59
|
const seenOrders = /* @__PURE__ */ new Set();
|
|
@@ -34,12 +68,9 @@ function composeDatabase(database, sets) {
|
|
|
34
68
|
seenNamespaces.add(set.namespace);
|
|
35
69
|
}
|
|
36
70
|
const composed = {};
|
|
37
|
-
for (const set of [...sets].sort((a, b) => a.order - b.order)) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const migration = set.migrations[localKey];
|
|
41
|
-
if (migration) composed[`${prefix}_${set.namespace}_${localKey}`] = migration;
|
|
42
|
-
}
|
|
71
|
+
for (const set of [...sets].sort((a, b) => a.order - b.order)) for (const localKey of Object.keys(set.migrations).sort()) {
|
|
72
|
+
const migration = set.migrations[localKey];
|
|
73
|
+
if (migration) composed[composedMigrationKey(set, localKey)] = migration;
|
|
43
74
|
}
|
|
44
75
|
for (const set of sets) declareRetained(set.migrations, set.retained ?? []);
|
|
45
76
|
return { getMigrations: async () => composed };
|
|
@@ -71,4 +102,4 @@ function createMigrationRegistry(sets) {
|
|
|
71
102
|
return registry;
|
|
72
103
|
}
|
|
73
104
|
//#endregion
|
|
74
|
-
export { MAX_MIGRATION_ORDER, NAMESPACE_PATTERN, createMigrationRegistry };
|
|
105
|
+
export { MAX_MIGRATION_ORDER, NAMESPACE_PATTERN, composedMigrationKey, createMigrationRegistry, decomposeMigrationKey };
|
|
@@ -9,8 +9,8 @@ import { CamelCasePlugin } from "kysely";
|
|
|
9
9
|
*
|
|
10
10
|
* A migration's `down` is the tested inverse of its `up`, and for schema that is exactly right. For a
|
|
11
11
|
* vault it is a `drop table` on the only copy of a credential Google issued. `0100_secrets_0001_init` is
|
|
12
|
-
* the whole history of the secrets database, so one `pithy migrate --rollback` — which
|
|
13
|
-
* migration in every composed database — emptied it in place, on staging, with nothing said first.
|
|
12
|
+
* the whole history of the secrets database, so one `pithy migrate --rollback` — which then stepped back
|
|
13
|
+
* one migration in every composed database — emptied it in place, on staging, with nothing said first.
|
|
14
14
|
*
|
|
15
15
|
* So a capability **declares** a table retained, beside the migrations that create it
|
|
16
16
|
* (`DatabaseSpec.retained`), and the declaration is recorded on those migrations' own `down` functions
|
|
@@ -30,8 +30,30 @@ export interface RetainedConsent {
|
|
|
30
30
|
*/
|
|
31
31
|
budget?: RetainedBudget;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* What a forward run records about itself (#694) — see `./groups`.
|
|
35
|
+
*
|
|
36
|
+
* Optional, and absent still records: a run with no group named is a group of its own, stamped with the
|
|
37
|
+
* moment it ran. There is always a group, so a rollback never has to fall back to counting migrations.
|
|
38
|
+
*/
|
|
39
|
+
export interface RunGroupOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The group every migration this run applies is recorded under. Absent, a generated ISO-8601 timestamp
|
|
42
|
+
* is used — per run, by construction. A caller spanning several databases passes **one** value to all
|
|
43
|
+
* of them, so one run is one group however many databases it touches.
|
|
44
|
+
*/
|
|
45
|
+
group?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run every pending migration to latest, recording the run's group against each one. An empty provider
|
|
49
|
+
* resolves to `[]` and records nothing.
|
|
50
|
+
*
|
|
51
|
+
* **The group is written from what Kysely hands back, and before the failure is raised.** A run that
|
|
52
|
+
* half-applies is retried with the key it already has, so the migrations that stuck have to be in the
|
|
53
|
+
* group — otherwise the retry's group would sit on top of an ungrouped migration, and reversing the
|
|
54
|
+
* retry would leave half a release applied with nothing recording it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runMigrations(database: D1Database, provider: MigrationProvider, target?: MigrationTarget, options?: RunGroupOptions): Promise<MigrationResult[]>;
|
|
35
57
|
/** One database's ledger beside the declaration it is supposed to match — see {@link readMigrationLedger}. */
|
|
36
58
|
export interface MigrationLedger {
|
|
37
59
|
/** Declared and not yet applied, in the order they would run. */
|
|
@@ -61,11 +83,50 @@ export interface MigrationLedger {
|
|
|
61
83
|
*/
|
|
62
84
|
export declare function readMigrationLedger(database: D1Database, provider: MigrationProvider): Promise<MigrationLedger>;
|
|
63
85
|
/**
|
|
64
|
-
* Step this database's latest applied migration back — one step,
|
|
65
|
-
*
|
|
66
|
-
*
|
|
86
|
+
* Step this database's latest applied migration back — **one** step, in **one** database.
|
|
87
|
+
*
|
|
88
|
+
* It is no longer what `pithy migrate --rollback` runs: a rollback names a group and reverses that
|
|
89
|
+
* group's portion of each database ({@link reverseMigrationGroup}, #694). What this is, is the single
|
|
90
|
+
* step underneath — the primitive every capability's "its `down` is the inverse of its `up`" suite
|
|
91
|
+
* reverses one migration with, and the honest answer for a caller holding one migration rather than a
|
|
92
|
+
* release.
|
|
93
|
+
*
|
|
94
|
+
* It forgets the reversed migration's group row, so the group bookkeeping follows the ledger whichever
|
|
95
|
+
* way a migration came down. Refused while a retained table here holds rows the caller has not counted in
|
|
96
|
+
* `consent` (#588).
|
|
67
97
|
*/
|
|
68
98
|
export declare function rollbackMigration(database: D1Database, provider: MigrationProvider, target?: MigrationTarget, consent?: RetainedConsent): Promise<MigrationResult[]>;
|
|
99
|
+
/** What one database reverses of a group: the group, and its migrations newest first. */
|
|
100
|
+
export interface MigrationGroupStep {
|
|
101
|
+
/** The group being reversed — what the caller passed to `--group`. */
|
|
102
|
+
group: string;
|
|
103
|
+
/**
|
|
104
|
+
* This database's portion of it, in the order it comes down: newest first. The caller reads it from
|
|
105
|
+
* `readGroupPosition`, which is also what proves the group is the top of this database's chain. It says
|
|
106
|
+
* **whether** there is anything to reverse here; what comes down is read from the ledger again below, so
|
|
107
|
+
* a step that named a migration from the group underneath could not reverse it on the caller's word.
|
|
108
|
+
*/
|
|
109
|
+
migrations: readonly string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* **Reverse one group's portion of one database — the operation behind `pithy migrate --rollback
|
|
113
|
+
* --group` (#694).**
|
|
114
|
+
*
|
|
115
|
+
* Kysely steps down from the tip, so this is `migrateDown()` once per migration in the group, in reverse
|
|
116
|
+
* chain order, and **nothing outside it**. Both halves of that are read from the ledger here, not taken on
|
|
117
|
+
* the caller's word: the group has to be the ledger's own tail before the first `down`, and what each step
|
|
118
|
+
* brings down has to be one of the migrations that read said the group holds — anything else is an
|
|
119
|
+
* internal fault and throws rather than carrying on. **And a migration with no `down` stops the group**:
|
|
120
|
+
* Kysely reports one as `NotExecuted` and leaves it applied, so counting it as reversed is how a group
|
|
121
|
+
* reported itself fully rolled back with half of it still in the database. The caller's pre-flight is what
|
|
122
|
+
* makes a group reverse completely or not at all across databases; this is the floor under it.
|
|
123
|
+
*
|
|
124
|
+
* **One migrator for the whole group**, so the retained guard clears once and the caller's budget is spent
|
|
125
|
+
* once across every `down` it covers (#588). Each migration's group row is forgotten as it comes down —
|
|
126
|
+
* before the step's failure is raised — so a group reversal that dies partway can be re-run under the
|
|
127
|
+
* same group and reverses what is left.
|
|
128
|
+
*/
|
|
129
|
+
export declare function reverseMigrationGroup(database: D1Database, provider: MigrationProvider, step: MigrationGroupStep, target?: MigrationTarget, consent?: RetainedConsent): Promise<MigrationResult[]>;
|
|
69
130
|
/**
|
|
70
131
|
* Fully reset one database's schema: every applied migration's `down` runs, in one pass, in reverse
|
|
71
132
|
* chronological order — Kysely's `NO_MIGRATIONS` target, not just the latest — then every migration's
|
|
@@ -74,6 +135,16 @@ export declare function rollbackMigration(database: D1Database, provider: Migrat
|
|
|
74
135
|
* skip-if-exists) simply work afterward — there is no per-row identity problem to solve. An empty
|
|
75
136
|
* ledger rolls back nothing; an empty provider reapplies nothing. Refused, before the first `down`, while a
|
|
76
137
|
* retained table here holds rows the caller has not counted in `consent` (#588).
|
|
138
|
+
*
|
|
139
|
+
* **It leaves the group bookkeeping exactly as it was (#694).** The table is not a migration, so the `down`
|
|
140
|
+
* pass cannot remove it, and rewriting the rows would restamp somebody's release as having happened during
|
|
141
|
+
* a `seed --redo`. Every group row still describes a migration this reset reapplied.
|
|
142
|
+
*
|
|
143
|
+
* **What it does not do is record a group for the rest.** A reset reapplies everything *declared*, which
|
|
144
|
+
* includes migrations that were pending when it started, and those come back applied under no group — this
|
|
145
|
+
* takes no group and invents none. Such a migration sits over the groups below it, so a `--rollback
|
|
146
|
+
* --group` of one of them is refused by name (`./groups`, `readGroupPosition`) rather than reversing a
|
|
147
|
+
* migration nobody recorded.
|
|
77
148
|
*/
|
|
78
149
|
export declare function resetMigrations(database: D1Database, provider: MigrationProvider, target?: MigrationTarget, consent?: RetainedConsent): Promise<MigrationResult[]>;
|
|
79
150
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/migrations/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAA2B,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/migrations/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAA2B,MAAM,kBAAkB,CAAC;AAOzG,OAAO,EAAgC,cAAc,EAAE,MAAM,YAAY,CAAC;AAkE1E;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;IAChB,uGAAuG;IACvG,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAUD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,CAAC,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,EAAE,CAAC,CA6B5B;AAED,8GAA8G;AAC9G,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAOrH;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,CAAC,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,EAAE,CAAC,CAI5B;AAED,yFAAyF;AACzF,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,kBAAkB,EACxB,MAAM,CAAC,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,EAAE,CAAC,CA0D5B;AAUD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,CAAC,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,EAAE,CAAC,CAO5B;AAWD;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,qGAAqG;IACrG,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,yEAAyE;IACzE,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,UAAU,EACpB,IAAI,EAAE,aAAa,EACnB,MAAM,CAAC,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,EAAE,CAAC,CA+C5B"}
|