@nodii/saga 0.6.1 → 0.7.0
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/dist/migrations/index.d.ts +19 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +154 -5
- package/dist/migrations/index.js.map +1 -1
- package/dist/state-store/postgres.d.ts.map +1 -1
- package/dist/state-store/postgres.js +10 -4
- package/dist/state-store/postgres.js.map +1 -1
- package/package.json +2 -1
- package/src/migrations/001-saga-state.sql +131 -5
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
type SqlClient = {
|
|
2
2
|
unsafe: (sql: string, args?: unknown[]) => Promise<any>;
|
|
3
3
|
};
|
|
4
|
+
/**
|
|
5
|
+
* Return the canonical A1 RLS DDL for `saga_state`. Returns the inlined
|
|
6
|
+
* constant verbatim — no runtime `@nodii/db-rls` call (see `SAGA_STATE_RLS_DDL`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function generateSagaStateRlsDdl(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Return the canonical A1 RLS DDL for `saga_outbox`. Inlined constant verbatim —
|
|
11
|
+
* no runtime `@nodii/db-rls` call (see `SAGA_OUTBOX_RLS_DDL`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateSagaOutboxRlsDdl(): string;
|
|
4
14
|
export declare function getSagaStateMigrationSQL(): string;
|
|
5
15
|
/**
|
|
6
16
|
* Apply the saga_state + saga_outbox migrations idempotently. Safe to call
|
|
@@ -13,7 +23,15 @@ export declare function getSagaStateMigrationSQL(): string;
|
|
|
13
23
|
* whitespace blocks are skipped.
|
|
14
24
|
*/
|
|
15
25
|
export declare function applySagaMigrations(sql: SqlClient): Promise<void>;
|
|
16
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Internal — exported only for unit-test coverage on the splitter.
|
|
28
|
+
*
|
|
29
|
+
* Dollar-quote-aware: a `$$`-delimited body (e.g. the D412 `DO $$ ... $$;`
|
|
30
|
+
* guarded TEXT→UUID migration) may contain inner lines ending in `;` that
|
|
31
|
+
* must NOT terminate the statement. We track an open `$tag$` ... `$tag$`
|
|
32
|
+
* span and only honor a trailing `;` as a statement boundary when no dollar
|
|
33
|
+
* quote is open. (postgres.js sends each split statement via `.unsafe`.)
|
|
34
|
+
*/
|
|
17
35
|
export declare function splitStatements(ddl: string): string[];
|
|
18
36
|
export {};
|
|
19
37
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAOA,KAAK,SAAS,GAAG;IAEf,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CACzD,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAOA,KAAK,SAAS,GAAG;IAEf,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CACzD,CAAC;AA8CF;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AA8CD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAgGD,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAMvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBrD"}
|
package/dist/migrations/index.js
CHANGED
|
@@ -4,6 +4,106 @@
|
|
|
4
4
|
// per-service migration runner. The SQL is shipped alongside this module
|
|
5
5
|
// as `001-saga-state.sql` so a service that uses a different runner
|
|
6
6
|
// (Flyway, golang-migrate, alembic, etc.) can copy it directly.
|
|
7
|
+
/**
|
|
8
|
+
* Canonical A1 RLS DDL for `saga_state` (D412 / 08-rls v7 § 4) — INLINED as a
|
|
9
|
+
* string constant so the runtime + published `@nodii/saga` package carry NO
|
|
10
|
+
* dependency on `@nodii/db-rls` (a runtime/peer dep would pull the whole RLS
|
|
11
|
+
* pooling + migrate-gen surface into saga and trigger a version cascade).
|
|
12
|
+
*
|
|
13
|
+
* saga_state does full CRUD, so it carries all four verbs: 1 owner policy + 4
|
|
14
|
+
* authenticated policies + 4 services policies, plus the per-role GRANTs.
|
|
15
|
+
* Postgres has no `CREATE POLICY IF NOT EXISTS`, so each `CREATE POLICY` is
|
|
16
|
+
* preceded by a matching `DROP POLICY IF EXISTS ... ;` to keep the migration
|
|
17
|
+
* idempotent. ENABLE + GRANT are already idempotent.
|
|
18
|
+
*
|
|
19
|
+
* This block is byte-equal to `@nodii/db-rls`'s
|
|
20
|
+
* `withRls("saga_state", { allow: ["SELECT","INSERT","UPDATE","DELETE"] })` +
|
|
21
|
+
* `generateTableGrants(...)` output (with the DROP-before-CREATE
|
|
22
|
+
* interleaving + grants reordered after ENABLE). The byte-equality is asserted
|
|
23
|
+
* in `tests/rls.integration.test.ts` against the live db-rls generators
|
|
24
|
+
* (db-rls is a DEV/test-only dependency that guards canonical drift — it is
|
|
25
|
+
* NEVER imported by shipped saga source). The vendored copy in
|
|
26
|
+
* `001-saga-state.sql` is byte-equal to this constant (sans the DO-block ALTER
|
|
27
|
+
* which lives above the table DDL there).
|
|
28
|
+
*/
|
|
29
|
+
const SAGA_STATE_RLS_DDL = `ALTER TABLE saga_state ENABLE ROW LEVEL SECURITY;
|
|
30
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_state TO nodii_services;
|
|
31
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_state TO authenticated;
|
|
32
|
+
DROP POLICY IF EXISTS saga_state_owner_policy ON saga_state;
|
|
33
|
+
CREATE POLICY saga_state_owner_policy ON saga_state FOR ALL TO nodii_owner USING (true) WITH CHECK (true);
|
|
34
|
+
DROP POLICY IF EXISTS saga_state_auth_select_policy ON saga_state;
|
|
35
|
+
CREATE POLICY saga_state_auth_select_policy ON saga_state FOR SELECT TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
36
|
+
DROP POLICY IF EXISTS saga_state_auth_insert_policy ON saga_state;
|
|
37
|
+
CREATE POLICY saga_state_auth_insert_policy ON saga_state FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
38
|
+
DROP POLICY IF EXISTS saga_state_auth_update_policy ON saga_state;
|
|
39
|
+
CREATE POLICY saga_state_auth_update_policy ON saga_state FOR UPDATE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))) WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
40
|
+
DROP POLICY IF EXISTS saga_state_auth_delete_policy ON saga_state;
|
|
41
|
+
CREATE POLICY saga_state_auth_delete_policy ON saga_state FOR DELETE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
42
|
+
DROP POLICY IF EXISTS saga_state_svc_select_policy ON saga_state;
|
|
43
|
+
CREATE POLICY saga_state_svc_select_policy ON saga_state FOR SELECT TO nodii_services USING (tenant_id = auth.get_tenant_id());
|
|
44
|
+
DROP POLICY IF EXISTS saga_state_svc_insert_policy ON saga_state;
|
|
45
|
+
CREATE POLICY saga_state_svc_insert_policy ON saga_state FOR INSERT TO nodii_services WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
46
|
+
DROP POLICY IF EXISTS saga_state_svc_update_policy ON saga_state;
|
|
47
|
+
CREATE POLICY saga_state_svc_update_policy ON saga_state FOR UPDATE TO nodii_services USING (tenant_id = auth.get_tenant_id()) WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
48
|
+
DROP POLICY IF EXISTS saga_state_svc_delete_policy ON saga_state;
|
|
49
|
+
CREATE POLICY saga_state_svc_delete_policy ON saga_state FOR DELETE TO nodii_services USING (tenant_id = auth.get_tenant_id());`;
|
|
50
|
+
/**
|
|
51
|
+
* Return the canonical A1 RLS DDL for `saga_state`. Returns the inlined
|
|
52
|
+
* constant verbatim — no runtime `@nodii/db-rls` call (see `SAGA_STATE_RLS_DDL`).
|
|
53
|
+
*/
|
|
54
|
+
export function generateSagaStateRlsDdl() {
|
|
55
|
+
return SAGA_STATE_RLS_DDL;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Canonical A1 RLS DDL for `saga_outbox` (D412 / 08-rls v7 § 1.1 — "every table
|
|
59
|
+
* with a `tenant_id` column gets the 5-policy A1 RLS, no exceptions"). The
|
|
60
|
+
* Pattern-2 outbox is tenant-scoped (every begin/completion row carries the
|
|
61
|
+
* saga's `tenant_id`) and `readOutboxForSaga` filters by `saga_id` ONLY — so RLS
|
|
62
|
+
* is the sole tenant-isolation boundary on this table, exactly as for
|
|
63
|
+
* `saga_state`.
|
|
64
|
+
*
|
|
65
|
+
* Identical structure to `SAGA_STATE_RLS_DDL` (1 owner + 4 authenticated + 4
|
|
66
|
+
* services policies + per-role GRANTs), retargeted to `saga_outbox`; byte-equal
|
|
67
|
+
* to `withRls("saga_outbox", { allow: [...] })` + `generateTableGrants(...)`,
|
|
68
|
+
* asserted in `tests/rls.integration.test.ts`.
|
|
69
|
+
*
|
|
70
|
+
* `saga_outbox.tenant_id` is `UUID` but — unlike `saga_state` — intentionally
|
|
71
|
+
* NULLABLE: the participant-worker writes a NULL tenant for tenant-less sagas
|
|
72
|
+
* (participant-worker.ts) and `createSaga` permits an absent tenant. Under these
|
|
73
|
+
* policies a NULL-tenant row is invisible to every non-owner role
|
|
74
|
+
* (`NULL = auth.get_tenant_id()` and `NULL IN (...)` are both NULL/false), i.e.
|
|
75
|
+
* owner-only — a safe, non-leaking outcome. A1 mandates the 5-policy RLS on
|
|
76
|
+
* every `tenant_id` table; it does NOT mandate NOT NULL (that is a `saga_state`
|
|
77
|
+
* choice), so this is fully A1-conformant.
|
|
78
|
+
*/
|
|
79
|
+
const SAGA_OUTBOX_RLS_DDL = `ALTER TABLE saga_outbox ENABLE ROW LEVEL SECURITY;
|
|
80
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_outbox TO nodii_services;
|
|
81
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_outbox TO authenticated;
|
|
82
|
+
DROP POLICY IF EXISTS saga_outbox_owner_policy ON saga_outbox;
|
|
83
|
+
CREATE POLICY saga_outbox_owner_policy ON saga_outbox FOR ALL TO nodii_owner USING (true) WITH CHECK (true);
|
|
84
|
+
DROP POLICY IF EXISTS saga_outbox_auth_select_policy ON saga_outbox;
|
|
85
|
+
CREATE POLICY saga_outbox_auth_select_policy ON saga_outbox FOR SELECT TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
86
|
+
DROP POLICY IF EXISTS saga_outbox_auth_insert_policy ON saga_outbox;
|
|
87
|
+
CREATE POLICY saga_outbox_auth_insert_policy ON saga_outbox FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
88
|
+
DROP POLICY IF EXISTS saga_outbox_auth_update_policy ON saga_outbox;
|
|
89
|
+
CREATE POLICY saga_outbox_auth_update_policy ON saga_outbox FOR UPDATE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))) WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
90
|
+
DROP POLICY IF EXISTS saga_outbox_auth_delete_policy ON saga_outbox;
|
|
91
|
+
CREATE POLICY saga_outbox_auth_delete_policy ON saga_outbox FOR DELETE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
92
|
+
DROP POLICY IF EXISTS saga_outbox_svc_select_policy ON saga_outbox;
|
|
93
|
+
CREATE POLICY saga_outbox_svc_select_policy ON saga_outbox FOR SELECT TO nodii_services USING (tenant_id = auth.get_tenant_id());
|
|
94
|
+
DROP POLICY IF EXISTS saga_outbox_svc_insert_policy ON saga_outbox;
|
|
95
|
+
CREATE POLICY saga_outbox_svc_insert_policy ON saga_outbox FOR INSERT TO nodii_services WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
96
|
+
DROP POLICY IF EXISTS saga_outbox_svc_update_policy ON saga_outbox;
|
|
97
|
+
CREATE POLICY saga_outbox_svc_update_policy ON saga_outbox FOR UPDATE TO nodii_services USING (tenant_id = auth.get_tenant_id()) WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
98
|
+
DROP POLICY IF EXISTS saga_outbox_svc_delete_policy ON saga_outbox;
|
|
99
|
+
CREATE POLICY saga_outbox_svc_delete_policy ON saga_outbox FOR DELETE TO nodii_services USING (tenant_id = auth.get_tenant_id());`;
|
|
100
|
+
/**
|
|
101
|
+
* Return the canonical A1 RLS DDL for `saga_outbox`. Inlined constant verbatim —
|
|
102
|
+
* no runtime `@nodii/db-rls` call (see `SAGA_OUTBOX_RLS_DDL`).
|
|
103
|
+
*/
|
|
104
|
+
export function generateSagaOutboxRlsDdl() {
|
|
105
|
+
return SAGA_OUTBOX_RLS_DDL;
|
|
106
|
+
}
|
|
7
107
|
/**
|
|
8
108
|
* Canonical saga_state + saga_outbox migration. Kept inline here (NOT
|
|
9
109
|
* read from disk at runtime) so the published artifact + the synthetic-
|
|
@@ -15,7 +115,7 @@
|
|
|
15
115
|
const SAGA_STATE_MIGRATION_SQL = `
|
|
16
116
|
CREATE TABLE IF NOT EXISTS saga_state (
|
|
17
117
|
id TEXT PRIMARY KEY,
|
|
18
|
-
tenant_id
|
|
118
|
+
tenant_id UUID NOT NULL,
|
|
19
119
|
type TEXT NOT NULL,
|
|
20
120
|
status TEXT NOT NULL,
|
|
21
121
|
current_step TEXT,
|
|
@@ -43,6 +143,20 @@ CREATE TABLE IF NOT EXISTS saga_state (
|
|
|
43
143
|
);
|
|
44
144
|
ALTER TABLE saga_state ADD COLUMN IF NOT EXISTS reaper_grace_ms BIGINT;
|
|
45
145
|
ALTER TABLE saga_state ADD COLUMN IF NOT EXISTS undo_stack JSONB NOT NULL DEFAULT '[]'::jsonb;
|
|
146
|
+
DO $$
|
|
147
|
+
BEGIN
|
|
148
|
+
IF EXISTS (
|
|
149
|
+
SELECT 1 FROM information_schema.columns
|
|
150
|
+
WHERE table_schema = current_schema()
|
|
151
|
+
AND table_name = 'saga_state'
|
|
152
|
+
AND column_name = 'tenant_id'
|
|
153
|
+
AND data_type <> 'uuid'
|
|
154
|
+
) THEN
|
|
155
|
+
EXECUTE 'ALTER TABLE saga_state ALTER COLUMN tenant_id TYPE uuid USING tenant_id::uuid';
|
|
156
|
+
END IF;
|
|
157
|
+
END
|
|
158
|
+
$$;
|
|
159
|
+
ALTER TABLE saga_state ALTER COLUMN tenant_id SET NOT NULL;
|
|
46
160
|
CREATE UNIQUE INDEX IF NOT EXISTS saga_state_trigger_trace_id_uniq
|
|
47
161
|
ON saga_state (trigger_trace_id);
|
|
48
162
|
CREATE INDEX IF NOT EXISTS saga_state_dead_letter_idx
|
|
@@ -59,13 +173,29 @@ CREATE TABLE IF NOT EXISTS saga_outbox (
|
|
|
59
173
|
step_name TEXT NOT NULL,
|
|
60
174
|
event_type TEXT NOT NULL,
|
|
61
175
|
payload JSONB NOT NULL,
|
|
62
|
-
tenant_id
|
|
176
|
+
tenant_id UUID,
|
|
63
177
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
64
178
|
delivered_at TIMESTAMPTZ
|
|
65
179
|
);
|
|
66
180
|
CREATE INDEX IF NOT EXISTS saga_outbox_saga_idx ON saga_outbox (saga_id, created_at);
|
|
67
181
|
CREATE INDEX IF NOT EXISTS saga_outbox_undelivered_idx ON saga_outbox (delivered_at)
|
|
68
182
|
WHERE delivered_at IS NULL;
|
|
183
|
+
DO $$
|
|
184
|
+
BEGIN
|
|
185
|
+
IF EXISTS (
|
|
186
|
+
SELECT 1 FROM information_schema.columns
|
|
187
|
+
WHERE table_schema = current_schema()
|
|
188
|
+
AND table_name = 'saga_outbox'
|
|
189
|
+
AND column_name = 'tenant_id'
|
|
190
|
+
AND data_type <> 'uuid'
|
|
191
|
+
) THEN
|
|
192
|
+
EXECUTE 'ALTER TABLE saga_outbox ALTER COLUMN tenant_id TYPE uuid USING tenant_id::uuid';
|
|
193
|
+
EXECUTE 'UPDATE saga_outbox o SET tenant_id = s.tenant_id FROM saga_state s WHERE o.saga_id = s.id AND o.tenant_id IS NULL';
|
|
194
|
+
END IF;
|
|
195
|
+
END
|
|
196
|
+
$$;
|
|
197
|
+
${generateSagaStateRlsDdl()}
|
|
198
|
+
${generateSagaOutboxRlsDdl()}
|
|
69
199
|
`.trim();
|
|
70
200
|
export function getSagaStateMigrationSQL() {
|
|
71
201
|
return SAGA_STATE_MIGRATION_SQL;
|
|
@@ -87,16 +217,35 @@ export async function applySagaMigrations(sql) {
|
|
|
87
217
|
await sql.unsafe(stmt);
|
|
88
218
|
}
|
|
89
219
|
}
|
|
90
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Internal — exported only for unit-test coverage on the splitter.
|
|
222
|
+
*
|
|
223
|
+
* Dollar-quote-aware: a `$$`-delimited body (e.g. the D412 `DO $$ ... $$;`
|
|
224
|
+
* guarded TEXT→UUID migration) may contain inner lines ending in `;` that
|
|
225
|
+
* must NOT terminate the statement. We track an open `$tag$` ... `$tag$`
|
|
226
|
+
* span and only honor a trailing `;` as a statement boundary when no dollar
|
|
227
|
+
* quote is open. (postgres.js sends each split statement via `.unsafe`.)
|
|
228
|
+
*/
|
|
91
229
|
export function splitStatements(ddl) {
|
|
92
230
|
const out = [];
|
|
93
231
|
let buf = "";
|
|
232
|
+
let openTag = null;
|
|
94
233
|
for (const rawLine of ddl.split("\n")) {
|
|
95
234
|
const line = rawLine.trim();
|
|
96
|
-
|
|
235
|
+
// Skip comment-only / blank lines, but only when we're not inside a
|
|
236
|
+
// dollar-quoted body (a `--` inside `$$` is body text, not a comment).
|
|
237
|
+
if (openTag === null && (!line || line.startsWith("--")))
|
|
97
238
|
continue;
|
|
98
239
|
buf = buf ? `${buf}\n${rawLine}` : rawLine;
|
|
99
|
-
|
|
240
|
+
// Toggle dollar-quote state across every `$tag$` token on this line.
|
|
241
|
+
for (const m of rawLine.matchAll(/\$[A-Za-z0-9_]*\$/g)) {
|
|
242
|
+
const tag = m[0];
|
|
243
|
+
if (openTag === null)
|
|
244
|
+
openTag = tag;
|
|
245
|
+
else if (openTag === tag)
|
|
246
|
+
openTag = null;
|
|
247
|
+
}
|
|
248
|
+
if (openTag === null && line.endsWith(";")) {
|
|
100
249
|
const cleaned = buf.trim();
|
|
101
250
|
if (cleaned)
|
|
102
251
|
out.push(cleaned);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,oEAAoE;AACpE,gEAAgE;AAOhE;;;;;;;GAOG;AACH,MAAM,wBAAwB,GAAG
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,oEAAoE;AACpE,gEAAgE;AAOhE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;gIAoBqG,CAAC;AAEjI;;;GAGG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;kIAoBsG,CAAC;AAEnI;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,mBAAmB,CAAC;AAC7B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,wBAAwB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkF/B,uBAAuB,EAAE;EACzB,wBAAwB,EAAE;CAC3B,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,UAAU,wBAAwB;IACtC,OAAO,wBAAwB,CAAC;AAClC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAc;IACtD,MAAM,GAAG,GAAG,wBAAwB,EAAE,CAAC;IACvC,MAAM,UAAU,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACxC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,oEAAoE;QACpE,uEAAuE;QACvE,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAAE,SAAS;QACnE,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QAC3C,qEAAqE;QACrE,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC;YACvD,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACjB,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO,GAAG,GAAG,CAAC;iBAC/B,IAAI,OAAO,KAAK,GAAG;gBAAE,OAAO,GAAG,IAAI,CAAC;QAC3C,CAAC;QACD,IAAI,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;YAC3B,IAAI,OAAO;gBAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,GAAG,GAAG,EAAE,CAAC;QACX,CAAC;IACH,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACrC,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/state-store/postgres.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/state-store/postgres.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EACV,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,cAAc,EAEd,cAAc,EACf,MAAM,UAAU,CAAC;AAIlB,KAAK,SAAS,GAAG;IAGf,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACxD,GAAG,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B,CAAC;AAIF,MAAM,WAAW,0BAA0B;IACzC,6EAA6E;IAC7E,GAAG,EAAE,SAAS,CAAC;IACf,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;KACvB,KAAK,IAAI,CAAC;CACZ;AAED,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAIjB;gBAEC,IAAI,EAAE,0BAA0B;IAMtC,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA8C5C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAoBrD,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,CACb,IAAI,CACF,YAAY,EACV,QAAQ,GACR,cAAc,GACd,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,mBAAmB,GACnB,qBAAqB,GACrB,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,iBAAiB,CACpB,CACF,GACA,OAAO,CAAC,IAAI,CAAC;IAoCV,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC;IA6BV,qBAAqB,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,IAAI,CAAC;IAQV,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7C,IAAI,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAoC7D;;;;uDAImD;IAC7C,eAAe,CACnB,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,YAAY,EAAE,CAAC;IAwB1B,4CAA4C;IACtC,gBAAgB,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYX,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAC9C;QACE,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;KACnB,EAAE,CACJ;CAiBF"}
|
|
@@ -7,13 +7,19 @@
|
|
|
7
7
|
// `src/migrations/001-saga-state.sql`.
|
|
8
8
|
//
|
|
9
9
|
// Wire shape:
|
|
10
|
-
// - tenant_id
|
|
11
|
-
//
|
|
10
|
+
// - tenant_id: persisted as UUID NOT NULL (D412 / 08-rls v7 A1 — the stale
|
|
11
|
+
// audit-only § 5.5 "no RLS, TEXT tenant" posture is SUPERSEDED). The
|
|
12
|
+
// migration (`src/migrations/`) emits the canonical 5-policy A1 RLS DDL
|
|
13
|
+
// for saga_state, INLINED as a string constant that is byte-equal to
|
|
14
|
+
// @nodii/db-rls's `withRls` (db-rls is a dev/test-only dep; the published
|
|
15
|
+
// package has no runtime db-rls dependency).
|
|
16
|
+
// - parent_saga_id, parent_relationship: persisted as TEXT.
|
|
12
17
|
// - step_outputs + children + compensation_log: jsonb columns.
|
|
13
18
|
// - next_resume_at: the reaper queries this; we keep it nullable.
|
|
14
19
|
// - All reads/writes go through the configured `nodii_services` pool
|
|
15
|
-
// (or any postgres.Sql)
|
|
16
|
-
//
|
|
20
|
+
// (or any postgres.Sql). The manual `tenant_id = $N` filter in `list()`
|
|
21
|
+
// is kept as defense-in-depth; RLS (gated on the app.tenant_id GUC the
|
|
22
|
+
// adopter binds per 08-rls § 7) is the new enforcement layer.
|
|
17
23
|
//
|
|
18
24
|
// 16KB cap on step_outputs entries (§ 5.5 three-level resolution): we
|
|
19
25
|
// enforce on `appendStepOutput` — entries exceeding the cap are stored
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/state-store/postgres.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,EAAE;AACF,sEAAsE;AACtE,sEAAsE;AACtE,0EAA0E;AAC1E,8DAA8D;AAC9D,uCAAuC;AACvC,EAAE;AACF,cAAc;AACd,
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/state-store/postgres.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodii/saga",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Saga orchestration library for the Nodii microservice stack — createSaga + step + stepParallel + cross-saga signals + sagaContext interceptor + SagaAdminService factory + @sagaCallable discipline tag + real PostgresSagaStateStore + RedisSignalBus + Pattern 2 async-step + reaper. Polyglot per D159 (TS + Python + Go).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@nodii/db-rls": "0.7.2",
|
|
47
48
|
"@nodii/grpc-auth": "0.9.1",
|
|
48
49
|
"@nodii/telemetry": "0.9.0",
|
|
49
50
|
"@types/bun": "^1.3.13",
|
|
@@ -4,13 +4,21 @@
|
|
|
4
4
|
-- 5.5 lists it under "lifecycle" alongside completed_at). Plus a small
|
|
5
5
|
-- saga_outbox for Pattern 2 begin/completion events.
|
|
6
6
|
--
|
|
7
|
-
-- This file is the
|
|
8
|
-
--
|
|
9
|
-
--
|
|
7
|
+
-- This file is the downstream-facing copy of the saga migration for services
|
|
8
|
+
-- that run their own migration tool (Flyway / sqlx-cli / golang-migrate /
|
|
9
|
+
-- alembic). The runtime source of truth is the inlined SAGA_STATE_MIGRATION_SQL
|
|
10
|
+
-- constant in `src/migrations/index.ts` (applied by `applySagaMigrations`); this
|
|
11
|
+
-- .sql must stay in lockstep with it. A drift-guard test in
|
|
12
|
+
-- `tests/rls.integration.test.ts` asserts the two produce an identical statement
|
|
13
|
+
-- sequence (via `splitStatements`), so an edit to one that misses the other
|
|
14
|
+
-- fails CI.
|
|
10
15
|
|
|
11
16
|
CREATE TABLE IF NOT EXISTS saga_state (
|
|
12
17
|
id TEXT PRIMARY KEY,
|
|
13
|
-
tenant_id
|
|
18
|
+
-- D412 / 08-rls v7 (A1): tenant_id is UUID NOT NULL. Fresh installs get the
|
|
19
|
+
-- UUID type directly; pre-D412 installs (tenant_id TEXT) are migrated in
|
|
20
|
+
-- place by the guarded ALTER block below.
|
|
21
|
+
tenant_id UUID NOT NULL,
|
|
14
22
|
type TEXT NOT NULL,
|
|
15
23
|
status TEXT NOT NULL,
|
|
16
24
|
current_step TEXT,
|
|
@@ -62,6 +70,27 @@ ALTER TABLE saga_state ADD COLUMN IF NOT EXISTS reaper_grace_ms BIGINT;
|
|
|
62
70
|
-- durable undo stack existed (saga admin-authz + undo-durability wave).
|
|
63
71
|
ALTER TABLE saga_state ADD COLUMN IF NOT EXISTS undo_stack JSONB NOT NULL DEFAULT '[]'::jsonb;
|
|
64
72
|
|
|
73
|
+
-- D412 / 08-rls v7 (A1) — idempotent in-place migration of pre-D412 installs
|
|
74
|
+
-- whose tenant_id is still TEXT. Guarded so re-runs (and fresh UUID installs)
|
|
75
|
+
-- are a no-op: only ALTER when the column's current type is not already uuid.
|
|
76
|
+
-- `USING tenant_id::uuid` fails loudly on non-UUID rows — under A1 those are
|
|
77
|
+
-- malformed tenant identifiers that must be fixed, not silently coerced.
|
|
78
|
+
DO $$
|
|
79
|
+
BEGIN
|
|
80
|
+
IF EXISTS (
|
|
81
|
+
SELECT 1 FROM information_schema.columns
|
|
82
|
+
WHERE table_schema = current_schema()
|
|
83
|
+
AND table_name = 'saga_state'
|
|
84
|
+
AND column_name = 'tenant_id'
|
|
85
|
+
AND data_type <> 'uuid'
|
|
86
|
+
) THEN
|
|
87
|
+
EXECUTE 'ALTER TABLE saga_state ALTER COLUMN tenant_id TYPE uuid USING tenant_id::uuid';
|
|
88
|
+
END IF;
|
|
89
|
+
END
|
|
90
|
+
$$;
|
|
91
|
+
-- Enforce NOT NULL (A1: every tenant-scoped row carries a tenant). Idempotent.
|
|
92
|
+
ALTER TABLE saga_state ALTER COLUMN tenant_id SET NOT NULL;
|
|
93
|
+
|
|
65
94
|
-- Spec § 5.5 — four indexes.
|
|
66
95
|
CREATE UNIQUE INDEX IF NOT EXISTS saga_state_trigger_trace_id_uniq
|
|
67
96
|
ON saga_state (trigger_trace_id);
|
|
@@ -84,7 +113,11 @@ CREATE TABLE IF NOT EXISTS saga_outbox (
|
|
|
84
113
|
step_name TEXT NOT NULL,
|
|
85
114
|
event_type TEXT NOT NULL,
|
|
86
115
|
payload JSONB NOT NULL,
|
|
87
|
-
tenant_id
|
|
116
|
+
-- D412 / 08-rls v7 (A1): tenant_id is UUID so the canonical UUID-typed A1
|
|
117
|
+
-- policies below can bind. NULLABLE by design (unlike saga_state): the
|
|
118
|
+
-- participant-worker writes a NULL tenant for tenant-less sagas, and a NULL
|
|
119
|
+
-- tenant row is owner-only (invisible to non-owner roles) under the policies.
|
|
120
|
+
tenant_id UUID,
|
|
88
121
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
89
122
|
delivered_at TIMESTAMPTZ
|
|
90
123
|
);
|
|
@@ -92,3 +125,96 @@ CREATE TABLE IF NOT EXISTS saga_outbox (
|
|
|
92
125
|
CREATE INDEX IF NOT EXISTS saga_outbox_saga_idx ON saga_outbox (saga_id, created_at);
|
|
93
126
|
CREATE INDEX IF NOT EXISTS saga_outbox_undelivered_idx ON saga_outbox (delivered_at)
|
|
94
127
|
WHERE delivered_at IS NULL;
|
|
128
|
+
|
|
129
|
+
-- D412 / 08-rls v7 (A1) — idempotent in-place migration of pre-D412 installs
|
|
130
|
+
-- whose saga_outbox.tenant_id is still TEXT. Guarded so re-runs (and fresh UUID
|
|
131
|
+
-- installs) are a no-op. Both the type change AND the one-time backfill of any
|
|
132
|
+
-- legacy NULL tenant (from the parent saga_state row) live INSIDE the guard, so
|
|
133
|
+
-- the backfill runs only during the actual upgrade — not as an unbounded scan on
|
|
134
|
+
-- every boot. The backfill runs after the type change (both columns uuid) via a
|
|
135
|
+
-- dynamic EXECUTE so it is planned against the just-altered column; orphaned rows
|
|
136
|
+
-- whose saga is gone simply stay NULL (owner-only under RLS).
|
|
137
|
+
DO $$
|
|
138
|
+
BEGIN
|
|
139
|
+
IF EXISTS (
|
|
140
|
+
SELECT 1 FROM information_schema.columns
|
|
141
|
+
WHERE table_schema = current_schema()
|
|
142
|
+
AND table_name = 'saga_outbox'
|
|
143
|
+
AND column_name = 'tenant_id'
|
|
144
|
+
AND data_type <> 'uuid'
|
|
145
|
+
) THEN
|
|
146
|
+
EXECUTE 'ALTER TABLE saga_outbox ALTER COLUMN tenant_id TYPE uuid USING tenant_id::uuid';
|
|
147
|
+
EXECUTE 'UPDATE saga_outbox o SET tenant_id = s.tenant_id FROM saga_state s WHERE o.saga_id = s.id AND o.tenant_id IS NULL';
|
|
148
|
+
END IF;
|
|
149
|
+
END
|
|
150
|
+
$$;
|
|
151
|
+
|
|
152
|
+
-- D412 / 08-rls v7 (A1) — canonical 5-policy* RLS on saga_state. The CREATE
|
|
153
|
+
-- POLICY statements below are byte-equal to
|
|
154
|
+
-- withRls('saga_state', { allow: ['SELECT','INSERT','UPDATE','DELETE'] })
|
|
155
|
+
-- from @nodii/db-rls. generateSagaStateRlsDdl() in migrations/index.ts INLINES
|
|
156
|
+
-- that same block as a string constant (NO runtime db-rls call → the published
|
|
157
|
+
-- @nodii/saga has no runtime db-rls dependency); a byte-equality test asserts
|
|
158
|
+
-- the inlined const stays equal to db-rls's live generator output, so this file
|
|
159
|
+
-- and the inlined const stay in lockstep. *full-CRUD allow → 1 owner
|
|
160
|
+
-- + 4 authenticated + 4 services = 9 policies; "5-policy" is the doctrine's
|
|
161
|
+
-- floor name for the per-role/per-verb family.
|
|
162
|
+
--
|
|
163
|
+
-- CREATE POLICY has no IF NOT EXISTS, so each is preceded by DROP POLICY
|
|
164
|
+
-- IF EXISTS to keep the migration idempotent. ENABLE ROW LEVEL SECURITY and
|
|
165
|
+
-- the GRANTs are already idempotent.
|
|
166
|
+
--
|
|
167
|
+
-- Adopters bind the app.tenant_id GUC (via auth.set_tenant_id / the db-rls
|
|
168
|
+
-- withSystemContext dispatch) on the nodii_services pool; this lib only
|
|
169
|
+
-- emits the DDL (08-rls § 7).
|
|
170
|
+
ALTER TABLE saga_state ENABLE ROW LEVEL SECURITY;
|
|
171
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_state TO nodii_services;
|
|
172
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_state TO authenticated;
|
|
173
|
+
DROP POLICY IF EXISTS saga_state_owner_policy ON saga_state;
|
|
174
|
+
CREATE POLICY saga_state_owner_policy ON saga_state FOR ALL TO nodii_owner USING (true) WITH CHECK (true);
|
|
175
|
+
DROP POLICY IF EXISTS saga_state_auth_select_policy ON saga_state;
|
|
176
|
+
CREATE POLICY saga_state_auth_select_policy ON saga_state FOR SELECT TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
177
|
+
DROP POLICY IF EXISTS saga_state_auth_insert_policy ON saga_state;
|
|
178
|
+
CREATE POLICY saga_state_auth_insert_policy ON saga_state FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
179
|
+
DROP POLICY IF EXISTS saga_state_auth_update_policy ON saga_state;
|
|
180
|
+
CREATE POLICY saga_state_auth_update_policy ON saga_state FOR UPDATE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))) WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
181
|
+
DROP POLICY IF EXISTS saga_state_auth_delete_policy ON saga_state;
|
|
182
|
+
CREATE POLICY saga_state_auth_delete_policy ON saga_state FOR DELETE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
183
|
+
DROP POLICY IF EXISTS saga_state_svc_select_policy ON saga_state;
|
|
184
|
+
CREATE POLICY saga_state_svc_select_policy ON saga_state FOR SELECT TO nodii_services USING (tenant_id = auth.get_tenant_id());
|
|
185
|
+
DROP POLICY IF EXISTS saga_state_svc_insert_policy ON saga_state;
|
|
186
|
+
CREATE POLICY saga_state_svc_insert_policy ON saga_state FOR INSERT TO nodii_services WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
187
|
+
DROP POLICY IF EXISTS saga_state_svc_update_policy ON saga_state;
|
|
188
|
+
CREATE POLICY saga_state_svc_update_policy ON saga_state FOR UPDATE TO nodii_services USING (tenant_id = auth.get_tenant_id()) WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
189
|
+
DROP POLICY IF EXISTS saga_state_svc_delete_policy ON saga_state;
|
|
190
|
+
CREATE POLICY saga_state_svc_delete_policy ON saga_state FOR DELETE TO nodii_services USING (tenant_id = auth.get_tenant_id());
|
|
191
|
+
|
|
192
|
+
-- D412 / 08-rls v7 (A1) — canonical 5-policy* RLS on saga_outbox (every table
|
|
193
|
+
-- with a tenant_id column, no exceptions). saga_outbox is tenant-scoped (each
|
|
194
|
+
-- row carries the saga's tenant_id) and readOutboxForSaga filters by saga_id
|
|
195
|
+
-- ONLY, so RLS is the sole tenant-isolation boundary. Byte-equal to
|
|
196
|
+
-- withRls('saga_outbox', { allow: ['SELECT','INSERT','UPDATE','DELETE'] })
|
|
197
|
+
-- from @nodii/db-rls; generateSagaOutboxRlsDdl() in migrations/index.ts inlines
|
|
198
|
+
-- the same block (no runtime db-rls dep), byte-equality-asserted in
|
|
199
|
+
-- tests/rls.integration.test.ts. *1 owner + 4 authenticated + 4 services = 9.
|
|
200
|
+
ALTER TABLE saga_outbox ENABLE ROW LEVEL SECURITY;
|
|
201
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_outbox TO nodii_services;
|
|
202
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON saga_outbox TO authenticated;
|
|
203
|
+
DROP POLICY IF EXISTS saga_outbox_owner_policy ON saga_outbox;
|
|
204
|
+
CREATE POLICY saga_outbox_owner_policy ON saga_outbox FOR ALL TO nodii_owner USING (true) WITH CHECK (true);
|
|
205
|
+
DROP POLICY IF EXISTS saga_outbox_auth_select_policy ON saga_outbox;
|
|
206
|
+
CREATE POLICY saga_outbox_auth_select_policy ON saga_outbox FOR SELECT TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
207
|
+
DROP POLICY IF EXISTS saga_outbox_auth_insert_policy ON saga_outbox;
|
|
208
|
+
CREATE POLICY saga_outbox_auth_insert_policy ON saga_outbox FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
209
|
+
DROP POLICY IF EXISTS saga_outbox_auth_update_policy ON saga_outbox;
|
|
210
|
+
CREATE POLICY saga_outbox_auth_update_policy ON saga_outbox FOR UPDATE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))) WITH CHECK (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
211
|
+
DROP POLICY IF EXISTS saga_outbox_auth_delete_policy ON saga_outbox;
|
|
212
|
+
CREATE POLICY saga_outbox_auth_delete_policy ON saga_outbox FOR DELETE TO authenticated USING (tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id())));
|
|
213
|
+
DROP POLICY IF EXISTS saga_outbox_svc_select_policy ON saga_outbox;
|
|
214
|
+
CREATE POLICY saga_outbox_svc_select_policy ON saga_outbox FOR SELECT TO nodii_services USING (tenant_id = auth.get_tenant_id());
|
|
215
|
+
DROP POLICY IF EXISTS saga_outbox_svc_insert_policy ON saga_outbox;
|
|
216
|
+
CREATE POLICY saga_outbox_svc_insert_policy ON saga_outbox FOR INSERT TO nodii_services WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
217
|
+
DROP POLICY IF EXISTS saga_outbox_svc_update_policy ON saga_outbox;
|
|
218
|
+
CREATE POLICY saga_outbox_svc_update_policy ON saga_outbox FOR UPDATE TO nodii_services USING (tenant_id = auth.get_tenant_id()) WITH CHECK (tenant_id = auth.get_tenant_id());
|
|
219
|
+
DROP POLICY IF EXISTS saga_outbox_svc_delete_policy ON saga_outbox;
|
|
220
|
+
CREATE POLICY saga_outbox_svc_delete_policy ON saga_outbox FOR DELETE TO nodii_services USING (tenant_id = auth.get_tenant_id());
|