@everystack/cli 0.4.29 → 0.4.31
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/package.json +9 -1
- package/src/cli/apply-execute.ts +36 -14
- package/src/cli/authz-reconcile.ts +32 -0
- package/src/cli/commands/db-apply.ts +21 -32
- package/src/cli/commands/db-backup.ts +56 -86
- package/src/cli/commands/db-exec.ts +132 -0
- package/src/cli/commands/db-export.ts +27 -16
- package/src/cli/commands/db-fork.ts +20 -6
- package/src/cli/commands/deploy.ts +3 -4
- package/src/cli/commands/task-probe.ts +66 -0
- package/src/cli/exec-digest.ts +79 -0
- package/src/cli/exec-execute.ts +115 -0
- package/src/cli/exec-log.ts +63 -0
- package/src/cli/exec-run.ts +109 -0
- package/src/cli/index.ts +14 -5
- package/src/cli/schema-swap.ts +12 -6
- package/src/cli/task-poll.ts +67 -0
- package/src/exec.ts +20 -0
package/src/cli/schema-swap.ts
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* ADD CONSTRAINT, the whole transaction rolls back, and the swap is refused — the
|
|
20
20
|
* cross-schema integrity gate, for free.
|
|
21
21
|
* 4. RE-APPLY the schema's authz (RLS, policies, grants) from the declared descriptors. The
|
|
22
|
-
* incoming schema was restored `--no-owner --no-privileges`, so it carries no grants
|
|
22
|
+
* incoming schema was restored `--no-owner --no-privileges`, so it carries no grants — but
|
|
23
|
+
* pg_dump does NOT strip RLS policies, so the declared policies arrive already present.
|
|
24
|
+
* The re-apply drops-then-creates each declared policy (idempotent — a bare CREATE POLICY
|
|
25
|
+
* would collide and roll the swap back) so declared stays the single source of truth.
|
|
23
26
|
* GRANT/POLICY are transactional, so they ride the same transaction — there is no
|
|
24
|
-
* committed instant where the new schema serves with absent or stale
|
|
27
|
+
* committed instant where the new schema serves with absent or stale authz.
|
|
25
28
|
*
|
|
26
29
|
* No REFRESH anywhere: the artifact ships computed rows as tables (defineMaterializedTable),
|
|
27
30
|
* so the swap is a pointer flip, never a recompute.
|
|
@@ -30,7 +33,7 @@
|
|
|
30
33
|
import type { ModelDescriptor } from '@everystack/model';
|
|
31
34
|
import { modelForeignKeys, qualifiedTable } from './schema-compile.js';
|
|
32
35
|
import { compileTableContract } from './authz-compile.js';
|
|
33
|
-
import {
|
|
36
|
+
import { emitSwapAuthzSql } from './authz-reconcile.js';
|
|
34
37
|
import { schemaOf } from './schema-fingerprint.js';
|
|
35
38
|
|
|
36
39
|
const SAFE_SCHEMA = /^[a-z_][a-z0-9_$]*$/;
|
|
@@ -122,12 +125,15 @@ export function renderSchemaSwap(models: ModelDescriptor[], opts: SwapOptions):
|
|
|
122
125
|
assertSafeSchema(retiring);
|
|
123
126
|
|
|
124
127
|
const fks = crossSchemaForeignKeys(models, schema);
|
|
125
|
-
//
|
|
126
|
-
//
|
|
128
|
+
// Re-apply the swapped-in schema's authz from the declared descriptors. The incoming schema
|
|
129
|
+
// was restored `--no-privileges` (no grants), but pg_dump does NOT strip RLS policies — the
|
|
130
|
+
// restore leaves the declared policies already present — so this drops-then-creates each
|
|
131
|
+
// declared policy to stay idempotent against them (a bare CREATE POLICY would collide and roll
|
|
132
|
+
// the swap back). Declared stays the single source of truth. See emitSwapAuthzSql.
|
|
127
133
|
const statsContracts = models
|
|
128
134
|
.filter((m) => (m.schema || 'public') === schema)
|
|
129
135
|
.map((m) => compileTableContract(m));
|
|
130
|
-
const authz =
|
|
136
|
+
const authz = emitSwapAuthzSql({ tables: statsContracts, functions: [] });
|
|
131
137
|
|
|
132
138
|
const statements = [
|
|
133
139
|
...fks.map((f) => f.dropSql),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-poll — poll an ephemeral Task run (dispatched via the ops Lambda) until it stops.
|
|
3
|
+
*
|
|
4
|
+
* Shared by task:probe and the pg-binary verbs (db:backup / db:export, later restore/swap): they all
|
|
5
|
+
* dispatch a task, get back a run id + ARN, then poll `task:status` until STOPPED. The poll is
|
|
6
|
+
* BOUNDED — a Fargate task can sit in PROVISIONING/PENDING on capacity or ENI trouble, and a naked
|
|
7
|
+
* loop would hang the CLI. A few consecutive DescribeTasks blips are tolerated (a throttle shouldn't
|
|
8
|
+
* abort a live task); past that, or the deadline, the caller reconciles via the run id (the task_log
|
|
9
|
+
* row + ECS both carry it). This owns the loop; the caller owns the success/failure messaging.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { invokeAction } from './aws.js';
|
|
13
|
+
import { info } from './output.js';
|
|
14
|
+
|
|
15
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
16
|
+
/** Backup/export/restore can run minutes on large databases — far longer than the probe's handshake. */
|
|
17
|
+
export const DEFAULT_DEADLINE_MS = 30 * 60_000;
|
|
18
|
+
const MAX_CONSECUTIVE_ERRORS = 3;
|
|
19
|
+
|
|
20
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
|
|
22
|
+
export interface TaskStatus {
|
|
23
|
+
lastStatus?: string;
|
|
24
|
+
stopped?: boolean;
|
|
25
|
+
exitCode?: number | null;
|
|
26
|
+
stoppedReason?: string | null;
|
|
27
|
+
/** The task's self-reported result row (db:backup/export write id/key/bytes/fingerprint). */
|
|
28
|
+
result?: Record<string, unknown> | null;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type TaskPollResult =
|
|
33
|
+
| { outcome: 'stopped'; status: TaskStatus }
|
|
34
|
+
| { outcome: 'error'; status: TaskStatus }
|
|
35
|
+
| { outcome: 'timeout'; lastStatus: string };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Poll until the task stops, printing each lifecycle transition. Returns `stopped` (read exitCode),
|
|
39
|
+
* `error` (task:status failed repeatedly — the task may still be running), or `timeout`.
|
|
40
|
+
*/
|
|
41
|
+
export async function pollTaskUntilStopped(
|
|
42
|
+
region: string,
|
|
43
|
+
fn: string,
|
|
44
|
+
ids: { runId: string; taskArn: string },
|
|
45
|
+
opts: { deadlineMs?: number } = {},
|
|
46
|
+
): Promise<TaskPollResult> {
|
|
47
|
+
const deadline = Date.now() + (opts.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
48
|
+
let last = '';
|
|
49
|
+
let consecutiveErrors = 0;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
const status = (await invokeAction(region, fn, 'task:status', { runId: ids.runId, taskArn: ids.taskArn })) as TaskStatus;
|
|
52
|
+
if (status?.error) {
|
|
53
|
+
if (++consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) return { outcome: 'error', status };
|
|
54
|
+
info(` (status check blipped: ${status.error} — retrying)`);
|
|
55
|
+
await sleep(POLL_INTERVAL_MS);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
consecutiveErrors = 0;
|
|
59
|
+
if (status.lastStatus && status.lastStatus !== last) {
|
|
60
|
+
info(` ${status.lastStatus}`);
|
|
61
|
+
last = status.lastStatus;
|
|
62
|
+
}
|
|
63
|
+
if (status.stopped) return { outcome: 'stopped', status };
|
|
64
|
+
await sleep(POLL_INTERVAL_MS);
|
|
65
|
+
}
|
|
66
|
+
return { outcome: 'timeout', lastStatus: last || 'unknown' };
|
|
67
|
+
}
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @everystack/cli/exec — the db:exec core, for the ops-Lambda lane.
|
|
3
|
+
*
|
|
4
|
+
* db:exec applies credential-free write SQL as ONE transaction, DML-only by a semantic catalog
|
|
5
|
+
* digest (no role, no superuser, no regex), bracketed by a crash-truthful ledger. This barrel lets
|
|
6
|
+
* the ops `db:exec` action (in @everystack/server's dbPlugin) load the same core the CLI uses and
|
|
7
|
+
* run it on the operator connection db:seed already uses — so `db:exec --stage` needs no raw admin
|
|
8
|
+
* URL on the operator's machine.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { executeExec, assertNoTxnControl, execSha } from './cli/exec-execute.js';
|
|
12
|
+
export type { ExecIntent, ExecOutcome, ExecuteExecOptions, ExecResult } from './cli/exec-execute.js';
|
|
13
|
+
|
|
14
|
+
export { runExecInTx, SchemaChangedError, railStatements, EXEC_RAIL_DEFAULTS } from './cli/exec-run.js';
|
|
15
|
+
export type { TxRunner, ExecStmtResult, ExecRails, RunExecInTxOptions } from './cli/exec-run.js';
|
|
16
|
+
|
|
17
|
+
export { catalogDigestQuery } from './cli/exec-digest.js';
|
|
18
|
+
|
|
19
|
+
export { ENSURE_EXEC_LOG_SQL, renderExecIntentInsert, renderExecOutcomeUpdate } from './cli/exec-log.js';
|
|
20
|
+
export type { ExecIntentRow, ExecOutcomeRow } from './cli/exec-log.js';
|