@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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { InternalError } from "../error/pithyError.js";
|
|
3
|
+
import { InternalError, ValidationError } from "../error/pithyError.js";
|
|
4
|
+
import { pithyCommand } from "../cli/command.js";
|
|
4
5
|
import { causeMessage } from "../error/cause.js";
|
|
5
|
-
import { MIGRATION_LOCK_TABLE, MIGRATION_TABLE, migrationKysely } from "./bookkeeping.js";
|
|
6
|
+
import { MIGRATION_LOCK_TABLE, MIGRATION_TABLE, appliedMigrationChain, migrationKysely } from "./bookkeeping.js";
|
|
6
7
|
import { batchedProvider } from "./batch.js";
|
|
8
|
+
import { forgetMigrationGroups, generatedMigrationGroup, readGroupPosition, recordMigrationGroup } from "./groups.js";
|
|
7
9
|
import { RetainedBudget, guardRetained, isDownRefusal } from "./retained.js";
|
|
8
10
|
import { sql } from "kysely";
|
|
9
11
|
import { Migrator, NO_MIGRATIONS } from "kysely/migration";
|
|
@@ -31,6 +33,12 @@ import { Migrator, NO_MIGRATIONS } from "kysely/migration";
|
|
|
31
33
|
* are argued.** The short version: a migration is now all-or-nothing where it used to be able to
|
|
32
34
|
* half-apply, and nothing across a migration boundary changed, because the ledger names migrations
|
|
33
35
|
* and a partial chain has to stay representable in it.
|
|
36
|
+
*
|
|
37
|
+
* **Every run belongs to a group, and a reversal names one — see `./groups` (#694).** A forward run
|
|
38
|
+
* records the group it applied each migration under, in a table beside the ledger;
|
|
39
|
+
* {@link reverseMigrationGroup} reverses one group's portion of one database, in reverse chain order and
|
|
40
|
+
* nothing outside it. {@link rollbackMigration} is still the single step underneath, for a caller holding
|
|
41
|
+
* one migration rather than a release — it is not what `pithy migrate --rollback` runs any more.
|
|
34
42
|
*/
|
|
35
43
|
/**
|
|
36
44
|
* **`allowUnorderedMigrations` is on, and it is not a loosening.** A composed key leads with its
|
|
@@ -63,9 +71,30 @@ function guardOptions(target, consent) {
|
|
|
63
71
|
budget: consent?.budget ?? new RetainedBudget(void 0)
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
|
-
/**
|
|
67
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Run every pending migration to latest, recording the run's group against each one. An empty provider
|
|
76
|
+
* resolves to `[]` and records nothing.
|
|
77
|
+
*
|
|
78
|
+
* **The group is written from what Kysely hands back, and before the failure is raised.** A run that
|
|
79
|
+
* half-applies is retried with the key it already has, so the migrations that stuck have to be in the
|
|
80
|
+
* group — otherwise the retry's group would sit on top of an ungrouped migration, and reversing the
|
|
81
|
+
* retry would leave half a release applied with nothing recording it.
|
|
82
|
+
*/
|
|
83
|
+
async function runMigrations(database, provider, target, options) {
|
|
68
84
|
const { error, results } = await migrator(database, provider, target, void 0).migrateToLatest();
|
|
85
|
+
const applied = (results ?? []).filter((result) => result.direction === "Up" && result.status === "Success").map((result) => result.migrationName);
|
|
86
|
+
try {
|
|
87
|
+
await recordMigrationGroup(database, {
|
|
88
|
+
group: options?.group ?? generatedMigrationGroup(),
|
|
89
|
+
migrations: applied
|
|
90
|
+
});
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
if (error === void 0) throw new InternalError({
|
|
93
|
+
message: `Applied ${applied.length === 1 ? "a migration" : `${applied.length} migrations`}${on(target)}, then couldn't record the group.`,
|
|
94
|
+
detail: `${where(target)} ${reasonOf(cause)}`,
|
|
95
|
+
action: "Check the database is writable. A second run records nothing for them, so they cannot be reversed by group."
|
|
96
|
+
}, { cause });
|
|
97
|
+
}
|
|
69
98
|
return settle("run", error, results, target);
|
|
70
99
|
}
|
|
71
100
|
/**
|
|
@@ -94,15 +123,79 @@ async function readMigrationLedger(database, provider) {
|
|
|
94
123
|
};
|
|
95
124
|
}
|
|
96
125
|
/**
|
|
97
|
-
* Step this database's latest applied migration back — one step,
|
|
98
|
-
*
|
|
99
|
-
*
|
|
126
|
+
* Step this database's latest applied migration back — **one** step, in **one** database.
|
|
127
|
+
*
|
|
128
|
+
* It is no longer what `pithy migrate --rollback` runs: a rollback names a group and reverses that
|
|
129
|
+
* group's portion of each database ({@link reverseMigrationGroup}, #694). What this is, is the single
|
|
130
|
+
* step underneath — the primitive every capability's "its `down` is the inverse of its `up`" suite
|
|
131
|
+
* reverses one migration with, and the honest answer for a caller holding one migration rather than a
|
|
132
|
+
* release.
|
|
133
|
+
*
|
|
134
|
+
* It forgets the reversed migration's group row, so the group bookkeeping follows the ledger whichever
|
|
135
|
+
* way a migration came down. Refused while a retained table here holds rows the caller has not counted in
|
|
136
|
+
* `consent` (#588).
|
|
100
137
|
*/
|
|
101
138
|
async function rollbackMigration(database, provider, target, consent) {
|
|
102
139
|
const { error, results } = await migrator(database, provider, target, consent).migrateDown();
|
|
140
|
+
await forgetReversed(database, results);
|
|
103
141
|
return settle("rollback", error, results, target);
|
|
104
142
|
}
|
|
105
143
|
/**
|
|
144
|
+
* **Reverse one group's portion of one database — the operation behind `pithy migrate --rollback
|
|
145
|
+
* --group` (#694).**
|
|
146
|
+
*
|
|
147
|
+
* Kysely steps down from the tip, so this is `migrateDown()` once per migration in the group, in reverse
|
|
148
|
+
* chain order, and **nothing outside it**. Both halves of that are read from the ledger here, not taken on
|
|
149
|
+
* the caller's word: the group has to be the ledger's own tail before the first `down`, and what each step
|
|
150
|
+
* brings down has to be one of the migrations that read said the group holds — anything else is an
|
|
151
|
+
* internal fault and throws rather than carrying on. **And a migration with no `down` stops the group**:
|
|
152
|
+
* Kysely reports one as `NotExecuted` and leaves it applied, so counting it as reversed is how a group
|
|
153
|
+
* reported itself fully rolled back with half of it still in the database. The caller's pre-flight is what
|
|
154
|
+
* makes a group reverse completely or not at all across databases; this is the floor under it.
|
|
155
|
+
*
|
|
156
|
+
* **One migrator for the whole group**, so the retained guard clears once and the caller's budget is spent
|
|
157
|
+
* once across every `down` it covers (#588). Each migration's group row is forgotten as it comes down —
|
|
158
|
+
* before the step's failure is raised — so a group reversal that dies partway can be re-run under the
|
|
159
|
+
* same group and reverses what is left.
|
|
160
|
+
*/
|
|
161
|
+
async function reverseMigrationGroup(database, provider, step, target, consent) {
|
|
162
|
+
if (step.migrations.length === 0) return [];
|
|
163
|
+
const position = await readGroupPosition(database, step.group);
|
|
164
|
+
if (position.state !== "top") throw new InternalError({
|
|
165
|
+
message: `Group "${step.group}" is not the top of ${target?.binding ?? "this database"}'s chain.`,
|
|
166
|
+
detail: `${where(target)} ${position.state === "absent" ? "It records nothing under that group." : `Applied over it: ${overIt(position.above, position.ungrouped)}.`}`,
|
|
167
|
+
action: `Run ${pithyCommand("doctor")} to see where each database stands.`
|
|
168
|
+
});
|
|
169
|
+
const remaining = new Set(position.migrations);
|
|
170
|
+
const runner = migrator(database, provider, target, consent);
|
|
171
|
+
const reversed = [];
|
|
172
|
+
while (remaining.size > 0) {
|
|
173
|
+
const { error, results } = await runner.migrateDown();
|
|
174
|
+
await forgetReversed(database, results);
|
|
175
|
+
const settled = settle("rollback", error, results, target);
|
|
176
|
+
if (settled.length === 0) break;
|
|
177
|
+
for (const result of settled) {
|
|
178
|
+
if (result.status !== "Success") throw new ValidationError({
|
|
179
|
+
message: `Migration "${result.migrationName}" has no down, so group "${step.group}" cannot be reversed${on(target)}.`,
|
|
180
|
+
action: `Give ${result.migrationName} a down, then reverse the group again.`,
|
|
181
|
+
detail: [where(target), reversed.length === 0 ? "Nothing was reversed. A group reverses completely or not at all." : `Reversed before the refusal: ${reversed.map((entry) => `"${entry.migrationName}"`).join(", ")}. The rest of the group is still applied.`].filter(Boolean).join(" ")
|
|
182
|
+
});
|
|
183
|
+
if (!remaining.delete(result.migrationName)) throw new InternalError({
|
|
184
|
+
message: `Reversing group "${step.group}"${on(target)} would have reversed "${result.migrationName}", which is not in it.`,
|
|
185
|
+
detail: `${where(target)} The group was not the top of this database's chain. Kysely steps down from the tip, so only the newest group can be reversed.`,
|
|
186
|
+
action: `Run ${pithyCommand("doctor")} to see where each database stands.`
|
|
187
|
+
});
|
|
188
|
+
reversed.push(result);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return reversed;
|
|
192
|
+
}
|
|
193
|
+
/** Forget the group rows of whatever actually came down, whether or not the step then failed. */
|
|
194
|
+
async function forgetReversed(database, results) {
|
|
195
|
+
const names = (results ?? []).filter((result) => result.direction === "Down" && result.status === "Success").map((result) => result.migrationName);
|
|
196
|
+
await forgetMigrationGroups(database, names);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
106
199
|
* Fully reset one database's schema: every applied migration's `down` runs, in one pass, in reverse
|
|
107
200
|
* chronological order — Kysely's `NO_MIGRATIONS` target, not just the latest — then every migration's
|
|
108
201
|
* `up` reapplies from empty. The seam behind `pithy seed --redo`'s destructive rebuild: because the
|
|
@@ -110,6 +203,16 @@ async function rollbackMigration(database, provider, target, consent) {
|
|
|
110
203
|
* skip-if-exists) simply work afterward — there is no per-row identity problem to solve. An empty
|
|
111
204
|
* ledger rolls back nothing; an empty provider reapplies nothing. Refused, before the first `down`, while a
|
|
112
205
|
* retained table here holds rows the caller has not counted in `consent` (#588).
|
|
206
|
+
*
|
|
207
|
+
* **It leaves the group bookkeeping exactly as it was (#694).** The table is not a migration, so the `down`
|
|
208
|
+
* pass cannot remove it, and rewriting the rows would restamp somebody's release as having happened during
|
|
209
|
+
* a `seed --redo`. Every group row still describes a migration this reset reapplied.
|
|
210
|
+
*
|
|
211
|
+
* **What it does not do is record a group for the rest.** A reset reapplies everything *declared*, which
|
|
212
|
+
* includes migrations that were pending when it started, and those come back applied under no group — this
|
|
213
|
+
* takes no group and invents none. Such a migration sits over the groups below it, so a `--rollback
|
|
214
|
+
* --group` of one of them is refused by name (`./groups`, `readGroupPosition`) rather than reversing a
|
|
215
|
+
* migration nobody recorded.
|
|
113
216
|
*/
|
|
114
217
|
async function resetMigrations(database, provider, target, consent) {
|
|
115
218
|
const runner = migrator(database, provider, target, consent);
|
|
@@ -120,14 +223,12 @@ async function resetMigrations(database, provider, target, consent) {
|
|
|
120
223
|
return [...downResults, ...upResults];
|
|
121
224
|
}
|
|
122
225
|
/**
|
|
123
|
-
* The applied migration names recorded in the ledger — empty when the ledger table doesn't exist yet.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
226
|
+
* The applied migration names recorded in the ledger — empty when the ledger table doesn't exist yet. The
|
|
227
|
+
* set half of `appliedMigrationChain`, which is the one read of the ledger: order matters to a group
|
|
228
|
+
* rollback and not at all to a membership test, and two reads of one table would drift.
|
|
126
229
|
*/
|
|
127
230
|
async function appliedMigrationNames(db) {
|
|
128
|
-
|
|
129
|
-
const { rows } = await sql`select name from ${sql.table(MIGRATION_TABLE)}`.execute(db);
|
|
130
|
-
return new Set(rows.map((row) => row.name));
|
|
231
|
+
return new Set(await appliedMigrationChain(db));
|
|
131
232
|
}
|
|
132
233
|
/**
|
|
133
234
|
* Surgically drop **one capability's** migrations: run each of `drop.reverse`'s `down` functions in
|
|
@@ -160,6 +261,7 @@ async function dropMigrations(database, drop, target, consent) {
|
|
|
160
261
|
try {
|
|
161
262
|
await down(db);
|
|
162
263
|
await sql`delete from ${sql.table(MIGRATION_TABLE)} where name = ${name}`.execute(db);
|
|
264
|
+
await forgetMigrationGroups(database, [name]);
|
|
163
265
|
results.push({
|
|
164
266
|
migrationName: name,
|
|
165
267
|
direction: "Down",
|
|
@@ -171,7 +273,7 @@ async function dropMigrations(database, drop, target, consent) {
|
|
|
171
273
|
throw new InternalError({
|
|
172
274
|
message: `Couldn't drop "${name}"${on(target)}. ${reasonOf(error)}`,
|
|
173
275
|
detail: `${where(target)}${dropped.length ? ` Dropped before the failure: ${dropped.join(", ")}.` : ""}`,
|
|
174
|
-
action: "Fix the migration's down, or drop its table by hand.
|
|
276
|
+
action: "Fix the migration's down, or drop its table by hand. Then run this again."
|
|
175
277
|
}, { cause: error });
|
|
176
278
|
}
|
|
177
279
|
}
|
|
@@ -182,24 +284,33 @@ const VOICE = {
|
|
|
182
284
|
run: {
|
|
183
285
|
failed: (key) => `Couldn't apply "${key}"`,
|
|
184
286
|
fallback: "The migration run failed",
|
|
185
|
-
action: "Fix the migration
|
|
287
|
+
action: "Fix the migration, then run this again."
|
|
186
288
|
},
|
|
187
289
|
rollback: {
|
|
188
290
|
failed: (key) => `Couldn't roll back "${key}"`,
|
|
189
291
|
fallback: "The rollback failed",
|
|
190
|
-
action:
|
|
292
|
+
action: `Fix the migration's down. Run ${pithyCommand("doctor")} to see where each database stands.`
|
|
191
293
|
},
|
|
192
294
|
resetDown: {
|
|
193
295
|
failed: (key) => `Couldn't roll back "${key}" during reset`,
|
|
194
296
|
fallback: "The schema reset failed while rolling back",
|
|
195
|
-
action: "Fix the migration
|
|
297
|
+
action: "Fix the migration, then run this again."
|
|
196
298
|
},
|
|
197
299
|
resetUp: {
|
|
198
300
|
failed: (key) => `Couldn't reapply "${key}" during reset`,
|
|
199
301
|
fallback: "The schema reset failed while reapplying",
|
|
200
|
-
action: "Fix the migration
|
|
302
|
+
action: "Fix the migration, then run this again."
|
|
201
303
|
}
|
|
202
304
|
};
|
|
305
|
+
/**
|
|
306
|
+
* What sits over a buried group, groups and ungrouped migrations alike, newest first.
|
|
307
|
+
*
|
|
308
|
+
* An ungrouped migration is named with its state, because the two are different problems: another group is
|
|
309
|
+
* something to reverse first, while a migration no group claims cannot be reversed by group at all.
|
|
310
|
+
*/
|
|
311
|
+
function overIt(above, ungrouped) {
|
|
312
|
+
return [...above, ...ungrouped.map((name) => `${name} (ungrouped)`)].join(", ");
|
|
313
|
+
}
|
|
203
314
|
/** ` on DB`, or nothing at all — every problem line here ends with this and then a period. */
|
|
204
315
|
function on(target) {
|
|
205
316
|
return target ? ` on ${target.binding}` : "";
|
|
@@ -243,4 +354,4 @@ function settle(verb, error, results, target) {
|
|
|
243
354
|
return results ?? [];
|
|
244
355
|
}
|
|
245
356
|
//#endregion
|
|
246
|
-
export { dropMigrations, readMigrationLedger, resetMigrations, rollbackMigration, runMigrations };
|
|
357
|
+
export { dropMigrations, readMigrationLedger, resetMigrations, reverseMigrationGroup, rollbackMigration, runMigrations };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { D1Database } from "@cloudflare/workers-types";
|
|
4
|
+
import type { MigrationProvider } from "kysely/migration";
|
|
5
|
+
/**
|
|
6
|
+
* **Where one database stands, read in one place (#729).**
|
|
7
|
+
*
|
|
8
|
+
* The control-plane seam answers `GET {basePath}/migrations` from this: what is applied, what this build
|
|
9
|
+
* declares and the database lacks, what the database holds and this build no longer declares, and which
|
|
10
|
+
* run put it there. Each half already had a reader — {@link readMigrationLedger} for both directions,
|
|
11
|
+
* {@link readMigrationGroups} and {@link readGroupPosition} for the group (#694) — and this composes them
|
|
12
|
+
* rather than reading either table a second way.
|
|
13
|
+
*
|
|
14
|
+
* Read-only. It applies nothing, creates nothing, and reverses nothing: a screen somebody opened to *ask*
|
|
15
|
+
* must not be the thing that changes the schema.
|
|
16
|
+
*/
|
|
17
|
+
/** Whether the last group can still be taken back by name — {@link readGroupPosition}, minus `absent`. */
|
|
18
|
+
export type MigrationGroupStanding = {
|
|
19
|
+
/** The group's migrations are the contiguous tip of this ledger. The CLI's rollback has refusals of its own. */
|
|
20
|
+
state: "top";
|
|
21
|
+
} | {
|
|
22
|
+
/** Something sits over the group or inside it, so reversing it by name is refused. */
|
|
23
|
+
state: "buried";
|
|
24
|
+
/** The groups in the way, newest first. */
|
|
25
|
+
above: string[];
|
|
26
|
+
/** The migrations in the way that no group claims, newest first. */
|
|
27
|
+
ungrouped: string[];
|
|
28
|
+
};
|
|
29
|
+
/** The newest group with anything still applied here, and whether it is still reversible. */
|
|
30
|
+
export interface MigrationLastGroup {
|
|
31
|
+
/** The group's value — what `--rollback --group` takes. */
|
|
32
|
+
group: string;
|
|
33
|
+
/** When it last applied something here. */
|
|
34
|
+
appliedAt: Date;
|
|
35
|
+
/** What it applied here and is still applied, in application order. */
|
|
36
|
+
migrations: string[];
|
|
37
|
+
/** Whether it is the tip of the chain, or what is in its way. */
|
|
38
|
+
position: MigrationGroupStanding;
|
|
39
|
+
}
|
|
40
|
+
/** One database's migration state. */
|
|
41
|
+
export interface MigrationState {
|
|
42
|
+
/** Every applied migration, in the order the ledger applied them. */
|
|
43
|
+
applied: string[];
|
|
44
|
+
/** Declared and not yet applied, in the order they would run. */
|
|
45
|
+
pending: string[];
|
|
46
|
+
/** Applied and not declared by this build, sorted. Kysely refuses the whole chain on any one of these. */
|
|
47
|
+
undeclared: string[];
|
|
48
|
+
/** The newest group with anything applied here, or null when nothing applied here was ever grouped. */
|
|
49
|
+
lastGroup: MigrationLastGroup | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read one database's migration state against the provider this build composes for it.
|
|
53
|
+
*
|
|
54
|
+
* **The last group is the newest one with something still applied.** Group rows are forgotten as their
|
|
55
|
+
* migrations come down, so this is ordinarily the newest group recorded. A group whose rows outlived their
|
|
56
|
+
* ledger rows reads as `absent` and is passed over: naming it as where the environment stands would name a
|
|
57
|
+
* release that is no longer in the database.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readMigrationState(database: D1Database, provider: MigrationProvider): Promise<MigrationState>;
|
|
60
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/migrations/state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAK1D;;;;;;;;;;;GAWG;AAEH,0GAA0G;AAC1G,MAAM,MAAM,sBAAsB,GAC9B;IACE,gHAAgH;IAChH,KAAK,EAAE,KAAK,CAAC;CACd,GACD;IACE,sFAAsF;IACtF,KAAK,EAAE,QAAQ,CAAC;IAChB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,oEAAoE;IACpE,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEN,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,EAAE,IAAI,CAAC;IAChB,uEAAuE;IACvE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,iEAAiE;IACjE,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0GAA0G;IAC1G,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uGAAuG;IACvG,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAInH"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { appliedMigrationChain, migrationKysely } from "./bookkeeping.js";
|
|
4
|
+
import { readGroupPosition, readMigrationGroups } from "./groups.js";
|
|
5
|
+
import { readMigrationLedger } from "./runner.js";
|
|
6
|
+
//#region src/migrations/state.ts
|
|
7
|
+
/**
|
|
8
|
+
* Read one database's migration state against the provider this build composes for it.
|
|
9
|
+
*
|
|
10
|
+
* **The last group is the newest one with something still applied.** Group rows are forgotten as their
|
|
11
|
+
* migrations come down, so this is ordinarily the newest group recorded. A group whose rows outlived their
|
|
12
|
+
* ledger rows reads as `absent` and is passed over: naming it as where the environment stands would name a
|
|
13
|
+
* release that is no longer in the database.
|
|
14
|
+
*/
|
|
15
|
+
async function readMigrationState(database, provider) {
|
|
16
|
+
const { pending, undeclared } = await readMigrationLedger(database, provider);
|
|
17
|
+
const applied = await appliedMigrationChain(migrationKysely(database));
|
|
18
|
+
return {
|
|
19
|
+
applied,
|
|
20
|
+
pending,
|
|
21
|
+
undeclared,
|
|
22
|
+
lastGroup: await lastGroupOf(database, applied)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Walk the groups newest first and stop at the first one this database still holds any of. */
|
|
26
|
+
async function lastGroupOf(database, applied) {
|
|
27
|
+
const held = new Set(applied);
|
|
28
|
+
const groups = await readMigrationGroups(database);
|
|
29
|
+
for (const group of [...groups].reverse()) {
|
|
30
|
+
const position = await readGroupPosition(database, group.group);
|
|
31
|
+
if (position.state === "absent") continue;
|
|
32
|
+
return {
|
|
33
|
+
group: group.group,
|
|
34
|
+
appliedAt: group.appliedAt,
|
|
35
|
+
migrations: group.migrations.filter((name) => held.has(name)),
|
|
36
|
+
position: position.state === "top" ? { state: "top" } : {
|
|
37
|
+
state: "buried",
|
|
38
|
+
above: position.above,
|
|
39
|
+
ungrouped: position.ungrouped
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { readMigrationState };
|
package/dist/naming/domains.d.ts
CHANGED
|
@@ -24,6 +24,23 @@ import { type AmbientEnv } from "../env/ambient";
|
|
|
24
24
|
* them rather than a side effect of a parser version.
|
|
25
25
|
*/
|
|
26
26
|
export declare function isPublicHostname(hostname: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Why a string is not a hostname a Worker can answer on — the reason, not a boolean.
|
|
29
|
+
*
|
|
30
|
+
* **It exists so a refusal can say which rule it broke.** {@link isPublicHostname} is the answer when the
|
|
31
|
+
* caller only needs yes or no; `WorkerDomain` needs the reason, because the three are not interchangeable
|
|
32
|
+
* to the adopter reading the message. A name over the limit is a typo. A name with a scheme on it is a
|
|
33
|
+
* misunderstanding. An internationalized name is neither — it is a thing this kit cannot do, for a reason
|
|
34
|
+
* that is not this kit's, and a message about bare hostnames would send that adopter looking for a typo
|
|
35
|
+
* they do not have (#665).
|
|
36
|
+
*
|
|
37
|
+
* Ordered by how specific the answer is. The length check is first because a name that long is over the
|
|
38
|
+
* limit whatever else is wrong with it, and `punycode` last because it is only reachable for a string that
|
|
39
|
+
* is otherwise a well-formed hostname.
|
|
40
|
+
*/
|
|
41
|
+
export declare function publicHostnameProblem(hostname: string): HostnameProblem | null;
|
|
42
|
+
/** The three ways a string fails {@link publicHostnameProblem}, each with its own sentence to say. */
|
|
43
|
+
export type HostnameProblem = "too-long" | "shape" | "punycode";
|
|
27
44
|
/** The environments a domain may be declared for — every managed one, never `dev`. */
|
|
28
45
|
export declare const DOMAIN_ENVIRONMENTS: ("prod" | "staging")[];
|
|
29
46
|
/** One environment's public address for one Worker. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/naming/domains.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,UAAU,EAAc,MAAM,gBAAgB,CAAC;AAgD7D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,wBAA8D,CAAC;AAE/F,uDAAuD;AACvD,eAAO,MAAM,YAAY;;;
|
|
1
|
+
{"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/naming/domains.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,UAAU,EAAc,MAAM,gBAAgB,CAAC;AAgD7D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAK9E;AAED,sGAAsG;AACtG,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;AAyChE,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,wBAA8D,CAAC;AAE/F,uDAAuD;AACvD,eAAO,MAAM,YAAY;;;iBAwBrB,CAAC;AACL,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;;;;;;iBAOvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAKlH;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C,yFAAyF;AACzF,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAetG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,GAC3B,cAAc,CAShB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,GAAG,GAAE,UAAyB,GAC7B,MAAM,CAER"}
|
package/dist/naming/domains.js
CHANGED
|
@@ -68,14 +68,72 @@ const PUNYCODE_LABEL = /(^|\.)xn--/;
|
|
|
68
68
|
* them rather than a side effect of a parser version.
|
|
69
69
|
*/
|
|
70
70
|
function isPublicHostname(hostname) {
|
|
71
|
-
return
|
|
71
|
+
return publicHostnameProblem(hostname) === null;
|
|
72
|
+
}
|
|
73
|
+
/** The DNS limit on a whole name, and the only length any caller measures. */
|
|
74
|
+
const MAX_HOSTNAME_LENGTH = 253;
|
|
75
|
+
/**
|
|
76
|
+
* Why a string is not a hostname a Worker can answer on — the reason, not a boolean.
|
|
77
|
+
*
|
|
78
|
+
* **It exists so a refusal can say which rule it broke.** {@link isPublicHostname} is the answer when the
|
|
79
|
+
* caller only needs yes or no; `WorkerDomain` needs the reason, because the three are not interchangeable
|
|
80
|
+
* to the adopter reading the message. A name over the limit is a typo. A name with a scheme on it is a
|
|
81
|
+
* misunderstanding. An internationalized name is neither — it is a thing this kit cannot do, for a reason
|
|
82
|
+
* that is not this kit's, and a message about bare hostnames would send that adopter looking for a typo
|
|
83
|
+
* they do not have (#665).
|
|
84
|
+
*
|
|
85
|
+
* Ordered by how specific the answer is. The length check is first because a name that long is over the
|
|
86
|
+
* limit whatever else is wrong with it, and `punycode` last because it is only reachable for a string that
|
|
87
|
+
* is otherwise a well-formed hostname.
|
|
88
|
+
*/
|
|
89
|
+
function publicHostnameProblem(hostname) {
|
|
90
|
+
if (hostname.length > MAX_HOSTNAME_LENGTH) return "too-long";
|
|
91
|
+
if (!HOSTNAME_PATTERN.test(hostname)) return "shape";
|
|
92
|
+
if (PUNYCODE_LABEL.test(hostname)) return "punycode";
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What an adopter is told, per reason, for a hostname this kit refuses.
|
|
97
|
+
*
|
|
98
|
+
* `shape` is the caller's to say, because what a bare hostname is differs between a `pattern` and a
|
|
99
|
+
* `zone` and each already had a sentence worth keeping. The other two are the same wherever they are hit.
|
|
100
|
+
*
|
|
101
|
+
* **The internationalized-domain sentence names Cloudflare**, and that is deliberate: the limit is not
|
|
102
|
+
* ours. `pithy-sh/pithy#691` tested it against a real zone — adding `tëst.pithy-sh.com` as a Worker
|
|
103
|
+
* Custom Domain returns *Internationalized Domain Names not supported* — so the kit refuses what the
|
|
104
|
+
* platform will not route rather than inventing a rule of its own. `docs/ACCEPTED-LIMITS.md` carries the
|
|
105
|
+
* evidence and the condition that would lift it.
|
|
106
|
+
*/
|
|
107
|
+
const HOSTNAME_PROBLEM_MESSAGES = {
|
|
108
|
+
"too-long": `A hostname is at most ${MAX_HOSTNAME_LENGTH} characters — the DNS limit on a whole name.`,
|
|
109
|
+
punycode: "Internationalized domains are not supported: Cloudflare Workers will not route one, so a Worker could never answer on it. Use an ASCII hostname."
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* One `pattern` or `zone` field, held to the one hostname rule.
|
|
113
|
+
*
|
|
114
|
+
* **Through {@link publicHostnameProblem}, never `HOSTNAME_PATTERN` directly.** A configured domain was
|
|
115
|
+
* the third caller of that rule and the only one that did not go through the function — it handed the
|
|
116
|
+
* pattern to `.regex()`, so it took a punycode A-label and a 267-character name that `pithy seed --host`
|
|
117
|
+
* refused for the same string. Two answers to one question, forty lines apart (#665). A rule added to the
|
|
118
|
+
* function now reaches a configured domain without anyone remembering to.
|
|
119
|
+
*/
|
|
120
|
+
function hostnameField(shapeMessage) {
|
|
121
|
+
return z.string().check((ctx) => {
|
|
122
|
+
const problem = publicHostnameProblem(ctx.value);
|
|
123
|
+
if (problem === null) return;
|
|
124
|
+
ctx.issues.push({
|
|
125
|
+
code: "custom",
|
|
126
|
+
input: ctx.value,
|
|
127
|
+
message: problem === "shape" ? shapeMessage : HOSTNAME_PROBLEM_MESSAGES[problem]
|
|
128
|
+
});
|
|
129
|
+
});
|
|
72
130
|
}
|
|
73
131
|
/** The environments a domain may be declared for — every managed one, never `dev`. */
|
|
74
132
|
const DOMAIN_ENVIRONMENTS = ENVIRONMENTS.filter((environment) => environment !== "dev");
|
|
75
133
|
/** One environment's public address for one Worker. */
|
|
76
134
|
const WorkerDomain = z.object({
|
|
77
|
-
pattern:
|
|
78
|
-
zone:
|
|
135
|
+
pattern: hostnameField("A domain is a bare hostname — no scheme, no path, no port (e.g. `api.example.com`, not `https://api.example.com/`).").describe("The hostname this Worker answers on in this environment, e.g. `api.example.com`. Written into `wrangler.jsonc` as a `routes` entry with `custom_domain: true`, and into `vars.BASE_URL` as `https://<pattern>`. A bare hostname rather than a URL, because that is what wrangler's route matcher takes — a scheme here would be silently wrong."),
|
|
136
|
+
zone: hostnameField("A zone is the registrable domain on your Cloudflare account, e.g. `example.com`.").describe("The Cloudflare zone `pattern` sits under — the registrable domain as it appears on your account, e.g. `example.com` for `api.example.com`. Cloudflare needs it to attach a custom domain, and it is not always derivable from the hostname: a zone can be a subdomain, and a public-suffix guess would be wrong for exactly the adopters who are hardest to debug.")
|
|
79
137
|
}).describe("Where one Worker answers in one environment: the hostname, and the Cloudflare zone it sits under.").check((ctx) => {
|
|
80
138
|
const { pattern, zone } = ctx.value;
|
|
81
139
|
if (pattern !== zone && !pattern.endsWith(`.${zone}`)) ctx.issues.push({
|
|
@@ -234,4 +292,4 @@ function originFor(environment, domains, env = ambientEnv()) {
|
|
|
234
292
|
return resolveOrigin(environment, domains, env[BASE_URL_VAR]).origin;
|
|
235
293
|
}
|
|
236
294
|
//#endregion
|
|
237
|
-
export { DOMAIN_ENVIRONMENTS, LOCAL_ORIGIN, WorkerDomain, WorkerDomains, baseUrlFor, domainFor, featureOrigin, isPublicHostname, originFor, resolveOrigin };
|
|
295
|
+
export { DOMAIN_ENVIRONMENTS, LOCAL_ORIGIN, WorkerDomain, WorkerDomains, baseUrlFor, domainFor, featureOrigin, isPublicHostname, originFor, publicHostnameProblem, resolveOrigin };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/naming/environment.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/naming/environment.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,YAAY,YAAI,KAAK,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAEhE,wGAAwG;AACxG,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAKD,kHAAkH;AAClH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAgBD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,QAAqE,CAAC;AAEvG;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIxD;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmCzD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,YAAI,SAAS,EAAE,MAAM,CAAU,CAAC;AAEjE;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,yBA2D9B,CAAC;AAEJ,sGAAsG;AACtG,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { PithyError, ValidationError } from "../error/pithyError.js";
|
|
4
|
+
import { commandName } from "../cli/command.js";
|
|
4
5
|
import { NAME_SEGMENT } from "./segment.js";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
//#region src/naming/environment.ts
|
|
@@ -129,7 +130,7 @@ function assertValidEnvironment(name) {
|
|
|
129
130
|
if (startsWithFeatureMarker(name)) throw new ValidationError({
|
|
130
131
|
message: `"${name}" starts the way a feature's name does.`,
|
|
131
132
|
action: "Start the environment with something other than f and a number.",
|
|
132
|
-
detail: `A feature takes the environment's slot with f<issue>, so <project>-${name}-<worker> can be a feature's Worker name exactly, and
|
|
133
|
+
detail: `A feature takes the environment's slot with f<issue>, so <project>-${name}-<worker> can be a feature's Worker name exactly, and \`${commandName("feature destroy")}\` deletes that Worker.`
|
|
133
134
|
});
|
|
134
135
|
if (name.length > MAX_ENVIRONMENT_NAME && NAME_SEGMENT.test(name)) throw new ValidationError({
|
|
135
136
|
message: `"${name}" is ${name.length} characters. An environment stops at ${MAX_ENVIRONMENT_NAME}.`,
|
package/dist/naming/feature.d.ts
CHANGED
|
@@ -27,10 +27,19 @@
|
|
|
27
27
|
* It lives in `core`, beside the rest of the naming rule, so the CLI and the facade compose one
|
|
28
28
|
* implementation rather than two that drift.
|
|
29
29
|
*/
|
|
30
|
-
/** A Cloudflare resource kind
|
|
31
|
-
export type FeatureResourceKind = "d1" | "kv" | "r2";
|
|
30
|
+
/** A Cloudflare resource kind Pithy names and creates — in a feature, and in every declared environment. */
|
|
31
|
+
export type FeatureResourceKind = "d1" | "kv" | "r2" | "queue";
|
|
32
32
|
/** Every feature resource kind, for tests and for callers that enumerate. */
|
|
33
33
|
export declare const FEATURE_RESOURCE_KINDS: readonly FeatureResourceKind[];
|
|
34
|
+
/**
|
|
35
|
+
* **The segment each kind ends a feature resource's name with — never more than `MAX_FEATURE_KIND` (#662).**
|
|
36
|
+
*
|
|
37
|
+
* `d1`, `kv` and `r2` spell themselves. A queue is `q`, and that is load-bearing: `MAX_FEATURE_KIND` is a term in
|
|
38
|
+
* `FEATURE_DERIVED_PROJECT_NAME`, so spelling it `queue` would take the project budget from 26 to 23 — and a
|
|
39
|
+
* project name is stable forever once anything is provisioned, so a smaller budget orphans every resource of a
|
|
40
|
+
* project already named against the old one. `feature.test.ts` fails on a segment longer than the budget.
|
|
41
|
+
*/
|
|
42
|
+
export declare const FEATURE_KIND_SEGMENT: Readonly<Record<FeatureResourceKind, string>>;
|
|
34
43
|
/** The identity of a feature — everything a feature name is derived from. */
|
|
35
44
|
export interface FeatureIdentity {
|
|
36
45
|
/** The project name, from the root `pithy.config.ts` (e.g. `acme`). */
|
|
@@ -126,10 +135,11 @@ export declare function maxFeatureSlug(parts: FeatureHead, shapes: readonly Feat
|
|
|
126
135
|
*/
|
|
127
136
|
export declare function assertFeatureSlugFits(identity: FeatureIdentity, shapes: readonly FeatureNameShape[]): void;
|
|
128
137
|
/**
|
|
129
|
-
* The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind
|
|
138
|
+
* The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind>`, the
|
|
139
|
+
* kind spelled by {@link FEATURE_KIND_SEGMENT}.
|
|
130
140
|
*
|
|
131
|
-
* Held to **R2's 63**, the strictest of the
|
|
132
|
-
* all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
|
|
141
|
+
* Held to **R2's 63**, the strictest of the kinds a feature provisions and a queue's number too, so one shape is
|
|
142
|
+
* legal for all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
|
|
133
143
|
* feature that provisions a bucket and a database wants them recognizably the same name.
|
|
134
144
|
*/
|
|
135
145
|
export declare function featureResourceName(identity: FeatureIdentity, binding: string, kind: FeatureResourceKind): string;
|
|
@@ -168,6 +178,18 @@ export declare function featureSecretEntryName(identity: FeatureIdentity, secret
|
|
|
168
178
|
* and picks.
|
|
169
179
|
*/
|
|
170
180
|
export declare function featureWorkerName(identity: FeatureIdentity, app: string): string;
|
|
181
|
+
/** The `<thing>` a feature's Artifacts namespace ends with, after {@link FEATURE_TAIL_SEPARATOR} (#673). */
|
|
182
|
+
export declare const FEATURE_ARTIFACTS_THING = "artifacts";
|
|
183
|
+
/**
|
|
184
|
+
* A feature's **Artifacts namespace** — `<project>-f<issue>-<slug>--artifacts` (#673).
|
|
185
|
+
*
|
|
186
|
+
* A declared environment's namespace is `<project>-<env>` and has no `<thing>`. A feature's needs one, because the
|
|
187
|
+
* separator and what follows it are what make a name a feature name: {@link isFeatureOwnedName} reads this one back
|
|
188
|
+
* as this feature's, so `pithy feature destroy` reaps the repos of exactly this branch and of no sibling. Held to the
|
|
189
|
+
* namespace's 63, the same number the rest of a feature's names are derived against, and `artifacts` is shorter than
|
|
190
|
+
* the shortest binding tail, so it moves no slug budget.
|
|
191
|
+
*/
|
|
192
|
+
export declare function featureArtifactsNamespace(identity: FeatureIdentity): string;
|
|
171
193
|
/**
|
|
172
194
|
* The deployed name of one of a feature's **Workflows** — `<project>-f<issue>-<slug>--<capability>-<job>` (#643).
|
|
173
195
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../../src/naming/feature.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,
|
|
1
|
+
{"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../../src/naming/feature.ts"],"names":[],"mappings":"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"}
|