@everystack/cli 0.3.22 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/package.json +9 -4
- package/src/cli/apply-authority.ts +150 -0
- package/src/cli/backfill.ts +231 -0
- package/src/cli/branch-db.ts +269 -0
- package/src/cli/commands/db-apply.ts +117 -2
- package/src/cli/commands/db-approvers.ts +71 -0
- package/src/cli/commands/db-backfill.ts +127 -0
- package/src/cli/commands/db-branch.ts +220 -0
- package/src/cli/commands/db-check.ts +16 -58
- package/src/cli/commands/db-plan.ts +21 -0
- package/src/cli/commands/db-sync.ts +26 -0
- package/src/cli/db-build.ts +161 -0
- package/src/cli/derived-source.ts +3 -2
- package/src/cli/edge-plan.ts +35 -8
- package/src/cli/index.ts +20 -1
- package/src/cli/model-api.ts +7 -0
- package/src/cli/schema-source.ts +5 -0
- package/src/cli/state-apply.ts +30 -0
package/src/cli/edge-plan.ts
CHANGED
|
@@ -29,12 +29,22 @@ import type { ModelDescriptor } from '@everystack/model';
|
|
|
29
29
|
import type { SchemaSnapshot } from './schema-introspect.js';
|
|
30
30
|
import type { AuthzContract } from './authz-contract.js';
|
|
31
31
|
import { generateMigrationSql, unmodeledTables } from './migration-generate.js';
|
|
32
|
-
import { classifyGeneratedStatements } from './state-apply.js';
|
|
32
|
+
import { classifyGeneratedStatements, classifyDestructive } from './state-apply.js';
|
|
33
33
|
import { fingerprintLive, fingerprintState, fingerprintModels, stableStringify } from './schema-fingerprint.js';
|
|
34
34
|
import { compileDeclaredState } from './declared-diff.js';
|
|
35
35
|
import { compileTableRenames } from './schema-compile.js';
|
|
36
36
|
|
|
37
|
-
export const PLAN_VERSION =
|
|
37
|
+
export const PLAN_VERSION = 2;
|
|
38
|
+
|
|
39
|
+
/** Classification counts (brick 9, decision 11): destructive = drops + narrowings. */
|
|
40
|
+
export interface PlanClassification {
|
|
41
|
+
/** Executable statements that lose no data. */
|
|
42
|
+
additive: number;
|
|
43
|
+
/** `DROP TABLE/COLUMN/TYPE` — the data is gone. */
|
|
44
|
+
drops: number;
|
|
45
|
+
/** Lossy/risky `ALTER COLUMN … TYPE` — data loss wearing an ALTER. */
|
|
46
|
+
narrowings: number;
|
|
47
|
+
}
|
|
38
48
|
|
|
39
49
|
export interface EdgePlan {
|
|
40
50
|
v: number;
|
|
@@ -47,8 +57,9 @@ export interface EdgePlan {
|
|
|
47
57
|
/** The edge, in db:generate's statement grammar (no held drops — mint refuses them). */
|
|
48
58
|
statements: string[];
|
|
49
59
|
executable: number;
|
|
50
|
-
/** Executable statements that
|
|
60
|
+
/** Executable statements that LOSE DATA (drops + narrowing type changes) — red, gated at apply. */
|
|
51
61
|
destructive: number;
|
|
62
|
+
classification: PlanClassification;
|
|
52
63
|
notices: number;
|
|
53
64
|
/** Live tables no model declares — untouched by the edge, riding into `to` as-is. */
|
|
54
65
|
unmodeled: string[];
|
|
@@ -135,6 +146,13 @@ export function mintEdgePlan(
|
|
|
135
146
|
);
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
// Data-destructive only: dropping a policy/grant is recoverable metadata
|
|
150
|
+
// (the authz phase is data-safe by construction); dropping a table,
|
|
151
|
+
// column, or type is not — and neither is a narrowing type change, which
|
|
152
|
+
// is data loss wearing an ALTER.
|
|
153
|
+
const breakdown = classifyDestructive(classified.executable);
|
|
154
|
+
const destructive = breakdown.drops.length + breakdown.narrowings.length;
|
|
155
|
+
|
|
138
156
|
return {
|
|
139
157
|
v: PLAN_VERSION,
|
|
140
158
|
fromFingerprint: fingerprintLive(snapshot, contract).hash,
|
|
@@ -142,10 +160,12 @@ export function mintEdgePlan(
|
|
|
142
160
|
declaredFingerprint: fingerprintModels(models, { schema: opts.schema }).hash,
|
|
143
161
|
statements,
|
|
144
162
|
executable: classified.executable.length,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
destructive,
|
|
164
|
+
classification: {
|
|
165
|
+
additive: classified.executable.length - destructive,
|
|
166
|
+
drops: breakdown.drops.length,
|
|
167
|
+
narrowings: breakdown.narrowings.length,
|
|
168
|
+
},
|
|
149
169
|
notices: classified.notices.length,
|
|
150
170
|
unmodeled: unmodeledTables(models, snapshot),
|
|
151
171
|
gitRef: opts.gitRef ?? null,
|
|
@@ -183,7 +203,14 @@ export function buildPlanSummary(plan: EdgePlan): string[] {
|
|
|
183
203
|
}
|
|
184
204
|
lines.push(`${plan.executable} executable statement(s), ${plan.notices} notice(s)`);
|
|
185
205
|
if (plan.destructive > 0) {
|
|
186
|
-
|
|
206
|
+
const { drops, narrowings } = classifyDestructive(classifyGeneratedStatements(plan.statements).executable);
|
|
207
|
+
lines.push(
|
|
208
|
+
`! ${plan.destructive} DESTRUCTIVE statement(s) — ${drops.length} drop(s), ${narrowings.length} narrowing type change(s). Data loss carried explicitly; the apply is gated (--confirm + snapshot, approver set when declared):`,
|
|
209
|
+
);
|
|
210
|
+
for (const statement of [...drops, ...narrowings]) {
|
|
211
|
+
const ddl = statement.split('\n').find((l) => l.trim() !== '' && !l.trim().startsWith('--')) ?? statement;
|
|
212
|
+
lines.push(`! ${ddl.trim()}`);
|
|
213
|
+
}
|
|
187
214
|
}
|
|
188
215
|
if (plan.unmodeled.length > 0) {
|
|
189
216
|
lines.push(`· ${plan.unmodeled.length} unmodeled table(s) ride through untouched: ${plan.unmodeled.join(', ')}`);
|
package/src/cli/index.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { dbDiffCommand } from './commands/db-diff.js';
|
|
|
18
18
|
import { dbPlanCommand } from './commands/db-plan.js';
|
|
19
19
|
import { dbApplyCommand } from './commands/db-apply.js';
|
|
20
20
|
import { dbCheckCommand } from './commands/db-check.js';
|
|
21
|
+
import { dbApproversCommand } from './commands/db-approvers.js';
|
|
22
|
+
import { dbBackfillCommand } from './commands/db-backfill.js';
|
|
23
|
+
import { dbTemplateRefreshCommand, dbBranchCommand } from './commands/db-branch.js';
|
|
21
24
|
import { dbSnapshotCommand, dbSnapshotsCommand } from './commands/db-snapshot.js';
|
|
22
25
|
import { dbBackupProbeCommand, dbBackupCommand, dbBackupsCommand, dbRestoreCommand, dbBackupDownloadCommand } from './commands/db-backup.js';
|
|
23
26
|
import { consoleCommand } from './commands/console.js';
|
|
@@ -200,6 +203,18 @@ async function main() {
|
|
|
200
203
|
case 'db:check':
|
|
201
204
|
await dbCheckCommand(flags);
|
|
202
205
|
break;
|
|
206
|
+
case 'db:approvers':
|
|
207
|
+
await dbApproversCommand(flags);
|
|
208
|
+
break;
|
|
209
|
+
case 'db:backfill':
|
|
210
|
+
await dbBackfillCommand(flags);
|
|
211
|
+
break;
|
|
212
|
+
case 'db:template:refresh':
|
|
213
|
+
await dbTemplateRefreshCommand(flags);
|
|
214
|
+
break;
|
|
215
|
+
case 'db:branch':
|
|
216
|
+
await dbBranchCommand(flags);
|
|
217
|
+
break;
|
|
203
218
|
case 'db:authz:pull':
|
|
204
219
|
await dbAuthzPullCommand(flags);
|
|
205
220
|
break;
|
|
@@ -327,8 +342,12 @@ Usage:
|
|
|
327
342
|
everystack db:sync [--database-url <url>] [--models <barrel>] [--sql-dir db/sql] [--schema-out <file.ts>] [--allow-drops] [--overwrite-drift] [--baseline] [--json] Make the database match your checkout — one verb, both layers: apply the state diff (tables+authz, one transaction, verified by re-diff), reconcile the derived layer against db/sql, report the resulting fingerprint vs the models' declared one. Dev databases only (direct connection required); DROPs held back unless --allow-drops; derived drift refuses unless --overwrite-drift; exit 1 when not converged
|
|
328
343
|
everystack db:diff --from-models <barrel> [--to-models db/models/index.ts] [--allow-drops] [--check] [--json] The state edge between two declared states, NO database: the SQL db:generate would produce, computed purely — CI plan previews (--check exits 1 on a non-empty edge) and computed rollbacks (swap the flags)
|
|
329
344
|
everystack db:plan [--stage <name> | --database-url <url>] [--models <barrel>] [--allow-drops] [--out db.plan.json | --out -] Mint a verified edge against a target: asks the TARGET its fingerprint, diffs the models, writes ONE reviewable plan (edge + both endpoint fingerprints). Held drops refuse the mint (--allow-drops carries destruction explicitly). Read-only — works via the ops Lambda; plans are ephemeral, never committed
|
|
330
|
-
everystack db:apply --plan <file.plan.json> [--database-url <url>] [--models <barrel>] [--force-descent <snapshot-ref> --confirm] Run a reviewed plan: verify the target is EXACTLY where the plan started (live fingerprint == plan.from, else refuse — the concurrency lock), verify the checkout DESCENDS from the commit declaring the target's state (the fast-forward rule, else refuse — "rebase first"
|
|
345
|
+
everystack db:apply --plan <file.plan.json> [--database-url <url>] [--stage <name>] [--models <barrel>] [--confirm] [--snapshot-ref <ref>] [--force-descent <snapshot-ref> --confirm] Run a reviewed plan: verify the target is EXACTLY where the plan started (live fingerprint == plan.from, else refuse — the concurrency lock), verify the checkout DESCENDS from the commit declaring the target's state (the fast-forward rule, else refuse — "rebase first"), and for DESTRUCTIVE plans require --confirm always + a snapshot (automatic via db:backup with --stage, else --snapshot-ref) + the stage's approver set when declared (STS identity-verified). Every refusal is recorded in schema_log. Apply as one transaction (plan_ref stamped), verify it landed exactly on plan.to; idempotent when already there; direct connection required
|
|
331
346
|
everystack db:check [--models <barrel>] [--sql-dir db/sql] [--schema-out <file.ts>] [--database-url <url>] [--json] The CI gate, per PR: the merged declared state must COMPOSE (models load, no duplicate tables, db/sql parses) and generated artifacts must MATCH regeneration byte-for-byte; with a scratch PostgreSQL it builds the state from scratch on an ephemeral database (created + dropped) and requires fingerprint MATCH. Exit 1 on any failure; never touches a real target
|
|
347
|
+
everystack db:approvers --stage <name> [--set "cto,arn:..."] [--remove] Declare who can DESTROY: the stage's destructive-approver set (SSM parameter, admin-writable). Destructive db:apply runs are then identity-verified (STS) against it; --set '' disables destructive applies; --remove returns the stage to ceremony-only
|
|
348
|
+
everystack db:backfill [--database-url <url>] [--dir db/backfills] [--apply] [--mark-applied <file.sql>] [--json] One-shot data jobs in their own lane: plan shows applied (by CONTENT identity — renames/comment edits are no-ops) / pending (in order, unbounded-pass advisories) / blocked (a name that already ran in a different form — one-shot jobs are immutable). --apply runs each pending job as its own transaction, recorded in everystack.backfill_log (a failure rolls back alone, is recorded, stops the run); --mark-applied records without running. Never runs as a schema side effect; direct connection required
|
|
349
|
+
everystack db:template:refresh [--database-url <url>] [--models <barrel>] [--sql-dir db/sql] [--seed "<cmd>"] [--no-seed] (Re)build the dev template <base>_tpl FROM THE DECLARED STATE (both layers, fingerprint MATCH bar) + seed-as-code (the app's db:seed script, run with DATABASE_URL pointed at the template; --seed overrides). All or nothing: a failed build/seed leaves NO template. Never a data copy
|
|
350
|
+
everystack db:branch [--list | --drop --confirm | --prune --confirm] [--database-url <url>] Mint (or find) the current git branch's database from the template (CREATE DATABASE … TEMPLATE — schema, authz, derived layer, and seed rows inherited), print its DATABASE_URL, then db:sync evolves it with the checkout. --list maps every branch DB to its branch; --prune drops the ones whose branch is gone (never guesses: unknown mappings are kept)
|
|
332
351
|
everystack db:authz:pull [--stage <name>] [--dir authz] Introspect live authz (rls/grants/policies/secdef) → reviewable contract files
|
|
333
352
|
everystack db:authz:diff [--stage <name>] [--dir authz] Validate the live DB against the committed contract (non-zero exit on drift)
|
|
334
353
|
everystack db:authz:test [--stage <name>] [--dir authz] Red-team enforcement: SET ROLE + attempt per role/table/command (non-zero exit on a hole)
|
package/src/cli/model-api.ts
CHANGED
|
@@ -34,3 +34,10 @@ export * from './authz-owner-probe.js';
|
|
|
34
34
|
|
|
35
35
|
// --- the SECDEF function catalog the contract introspection needs -----------
|
|
36
36
|
export { FUNCTIONS_SQL, catalogFunctionToDescriptor } from './security-catalog.js';
|
|
37
|
+
|
|
38
|
+
// --- databases FROM the declared state: ephemeral test DBs + the builder ----
|
|
39
|
+
// `createEphemeralDatabase(adminUrl, models, { sources })` is the packaged
|
|
40
|
+
// create-sync-drop shape for consumer test suites: a fresh database at the
|
|
41
|
+
// declared state (state + authz + derived), verified to fingerprint MATCH,
|
|
42
|
+
// dropped when you're done — never long-lived, never poisoned.
|
|
43
|
+
export * from './db-build.js';
|
package/src/cli/schema-source.ts
CHANGED
|
@@ -304,6 +304,11 @@ export function compileDrizzleSource(allModels: ModelDescriptor[], _opts: Compil
|
|
|
304
304
|
const { call, builder: pgBuilder } = baseColumnSource(toSnakeCase(key), spec);
|
|
305
305
|
pgCoreBuilders.add(pgBuilder);
|
|
306
306
|
const mods = columnModifiers(spec, isComposite, modelsByDescriptor);
|
|
307
|
+
if (spec.isDeprecated) {
|
|
308
|
+
// The contract phase (decision 13): the column stays in the database and
|
|
309
|
+
// stays readable; the strikethrough warns new code away at author time.
|
|
310
|
+
colLines.push(` /** @deprecated contract phase — readable, not writable; the physical drop is a later, gated act. */`);
|
|
311
|
+
}
|
|
307
312
|
colLines.push(` ${key}: ${call}${mods},`);
|
|
308
313
|
}
|
|
309
314
|
|
package/src/cli/state-apply.ts
CHANGED
|
@@ -56,6 +56,36 @@ export function classifyGeneratedStatements(statements: string[]): ClassifiedSta
|
|
|
56
56
|
return { executable, heldDrops, notices };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The destructive subset of an executable statement stream — destructive
|
|
61
|
+
* means DATA LOSS (brick 9's classification, design decision 11):
|
|
62
|
+
*
|
|
63
|
+
* drops — `DROP TABLE/COLUMN/TYPE`: the data is gone.
|
|
64
|
+
* narrowings — lossy/risky `ALTER COLUMN … SET DATA TYPE`: data loss
|
|
65
|
+
* wearing an ALTER. Detection rides the emitter's structural
|
|
66
|
+
* contract, not prose: a SAFE type change is a bare
|
|
67
|
+
* `SET DATA TYPE`; anything lossy or risky carries a `USING`
|
|
68
|
+
* cast (schema-diff's `classifyTypeChange`, conservative by
|
|
69
|
+
* default — the contract is pinned by test).
|
|
70
|
+
*
|
|
71
|
+
* Recoverable metadata is never destructive: policies, grants, constraints,
|
|
72
|
+
* defaults, nullability all re-declare from the models without touching a row.
|
|
73
|
+
*/
|
|
74
|
+
export interface DestructiveBreakdown {
|
|
75
|
+
drops: string[];
|
|
76
|
+
narrowings: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function classifyDestructive(executable: string[]): DestructiveBreakdown {
|
|
80
|
+
const drops: string[] = [];
|
|
81
|
+
const narrowings: string[] = [];
|
|
82
|
+
for (const statement of executable) {
|
|
83
|
+
if (/\bDROP\s+(TABLE|COLUMN|TYPE)\b/.test(statement)) drops.push(statement);
|
|
84
|
+
else if (/\bSET DATA TYPE\b/.test(statement) && /\bUSING\b/.test(statement)) narrowings.push(statement);
|
|
85
|
+
}
|
|
86
|
+
return { drops, narrowings };
|
|
87
|
+
}
|
|
88
|
+
|
|
59
89
|
/** The current git HEAD, for schema_log's intent pointer. Null outside a repo. */
|
|
60
90
|
export function currentGitRef(): string | null {
|
|
61
91
|
try {
|