@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.
@@ -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 grants.
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 { emitReconcileSql } from './authz-reconcile.js';
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
- // Full authz for the swapped-in schema, from scratch (live side empty = every declared grant
126
- // is a create) the incoming schema was restored with privileges stripped.
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 = emitReconcileSql({ tables: statsContracts, functions: [] }, { tables: [], functions: [] });
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';