@everystack/cli 0.4.29 → 0.4.30

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.4.29",
3
+ "version": "0.4.30",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -90,6 +90,38 @@ export function emitReconcileSql(declared: AuthzContract, live: AuthzContract):
90
90
  return sql;
91
91
  }
92
92
 
93
+ /**
94
+ * Emit authz for a freshly-landed swap schema, idempotent against authz the DATA
95
+ * ARTIFACT already carries. This is db:swap's re-apply step, and it differs from
96
+ * `emitReconcileSql(declared, {tables:[]})` in one load-bearing way: it drops each
97
+ * declared policy before creating it.
98
+ *
99
+ * Why: db:export dumps with `pg_dump --no-privileges`, which strips GRANTs but NOT
100
+ * RLS policies (there is no pg_dump flag for that, and the `-Fc` archive can't be
101
+ * text-filtered). So the restored `<schema>_incoming` arrives with the declared
102
+ * policies ALREADY present. A bare `CREATE POLICY` then fails "policy ... already
103
+ * exists" and rolls the whole atomic swap back — the collision GridironDB hit on
104
+ * every stats table with RLS. Drop-then-create makes the re-apply overwrite
105
+ * whatever the artifact carried, keeping the declared Model the single source of
106
+ * truth. RLS enable/force and GRANT are idempotent already (ENABLE RLS on an
107
+ * already-enabled table is a no-op; the dump carried no grants).
108
+ */
109
+ export function emitSwapAuthzSql(declared: AuthzContract): string[] {
110
+ const sql: string[] = [];
111
+ for (const d of declared.tables) {
112
+ const table = quoteQualified(d.table);
113
+ if (d.rls.enabled) sql.push(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;`);
114
+ if (d.rls.forced) sql.push(`ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;`);
115
+ for (const p of d.policies) {
116
+ sql.push(policyDropSql(table, p.name)); // idempotent against the policy the dump restored
117
+ sql.push(policyCreateSql(table, p));
118
+ }
119
+ reconcileGrants(table, d, undefined, sql);
120
+ reconcileColumnGrants(table, d, undefined, sql);
121
+ }
122
+ return sql;
123
+ }
124
+
93
125
  function reconcileRls(table: string, d: TableContract, l: TableContract | undefined, out: string[]): void {
94
126
  if (d.rls.enabled && !l?.rls.enabled) out.push(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;`);
95
127
  if (d.rls.forced && !l?.rls.forced) out.push(`ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;`);
@@ -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),