@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 +1 -1
- package/src/cli/authz-reconcile.ts +32 -0
- package/src/cli/schema-swap.ts +12 -6
package/package.json
CHANGED
|
@@ -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;`);
|
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),
|