@everystack/server 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/CHANGELOG.md +35 -0
- package/package.json +3 -3
- package/src/db.ts +43 -2
- package/src/model-wire.ts +94 -0
- package/src/owner-adoption.ts +75 -4
- package/src/owner-enactment.ts +355 -20
- package/src/plugin.ts +280 -60
- package/src/provision.ts +265 -16
- package/src/role-chain.ts +114 -0
- package/src/sql-lex.ts +217 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ All notable changes to `@everystack/server` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.31] - 2026-09-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- a real SELECT-only role for db:query's read connection
|
|
12
|
+
- version-guard the Model-derived wire payloads (provision, converge-ownership, reconcile)
|
|
13
|
+
- automate the version-drift check — a manual note is not a gate
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- one shipped descent step for both lanes, bound end to end by the real CLI; trees materialize inside the project
|
|
18
|
+
- the compile-failure detail keeps the cause; the monorepo test skips loudly; the stash filter sits on the refuse-closed side
|
|
19
|
+
- owner-adoption fast-follows — identity guard, self-grant revoke, ENACT transaction
|
|
20
|
+
- roll the session back after a failed grant transaction
|
|
21
|
+
- the reader's TABLES and SEQUENCES defaults converge independently
|
|
22
|
+
- an E-string prefix only counts when it stands alone
|
|
23
|
+
- throw a private sentinel instead of calling tx.rollback() — closes a console-sandbox swallow/neutralize hole
|
|
24
|
+
- rds_superuser membership is not superuser — probe grant capability honestly
|
|
25
|
+
- the shape-pin also covers enum value domains, not just field names
|
|
26
|
+
- symmetric v-prefix in the mismatch message, restore db:provision's --direct detail
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- use drizzle's tx.rollback() instead of a hand-rolled sentinel Symbol
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
- note the accepted narrowing db:query's read-only transaction introduces
|
|
35
|
+
|
|
36
|
+
## [0.4.30] - 2026-09-04
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- ADOPT resolves its master from ADMIN_DATABASE_URL on a BYO/external RDS
|
|
41
|
+
|
|
7
42
|
## [0.4.29] - 2026-09-04
|
|
8
43
|
|
|
9
44
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.31",
|
|
4
4
|
"description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -187,8 +187,8 @@
|
|
|
187
187
|
"sst": "4.13.1",
|
|
188
188
|
"ts-jest": "29.4.9",
|
|
189
189
|
"typescript": "5.9.3",
|
|
190
|
-
"@everystack/cli": "0.4.
|
|
191
|
-
"@everystack/auth": "0.4.
|
|
190
|
+
"@everystack/cli": "0.4.85",
|
|
191
|
+
"@everystack/auth": "0.4.5"
|
|
192
192
|
},
|
|
193
193
|
"scripts": {
|
|
194
194
|
"test": "jest",
|
package/src/db.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference path="./sst-env.d.ts" />
|
|
2
1
|
/**
|
|
3
2
|
* @everystack/server/db — Database and secret helpers.
|
|
4
3
|
*
|
|
@@ -150,6 +149,26 @@ export function getOperatorDatabaseUrl(): { url: string; source: 'admin' | 'mast
|
|
|
150
149
|
return null;
|
|
151
150
|
}
|
|
152
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The connection ADOPT (ownership reassignment) runs on: the linked SST Postgres master when one is
|
|
154
|
+
* present, else the `ADMIN_DATABASE_URL` operator secret.
|
|
155
|
+
*
|
|
156
|
+
* A brownfield / external-RDS app — the exact `db:pull --abilities live` audience — has NO linked
|
|
157
|
+
* `sst.aws.Postgres` component. It wires the database as secrets, and its `ADMIN_DATABASE_URL` is a
|
|
158
|
+
* privileged operator (commonly BYPASSRLS and a member of `postgres`, i.e. master-capable) that the
|
|
159
|
+
* ops Lambda already carries. Resolving ADOPT's master ONLY through the component dead-ended those
|
|
160
|
+
* apps on the missing component. This falls back to the admin secret so ADOPT has a connection to
|
|
161
|
+
* try. Whether that connection can ACTUALLY reassign is NOT decided here — ADOPT's live
|
|
162
|
+
* `master_can_act` probe decides it: a least-privilege admin resolves but the probe then refuses
|
|
163
|
+
* precisely (naming the grant to run), while a master-capable admin proceeds.
|
|
164
|
+
*
|
|
165
|
+
* `master` (linked component) is preferred because its presence guarantees master capability; the
|
|
166
|
+
* admin fallback is probe-gated. Never the least-privilege `DATABASE_URL`.
|
|
167
|
+
*/
|
|
168
|
+
export function getAdoptMasterUrl(): string | null {
|
|
169
|
+
return getMasterDatabaseUrl() ?? getAdminDatabaseUrl();
|
|
170
|
+
}
|
|
171
|
+
|
|
153
172
|
export function getJwtSecret(): Uint8Array {
|
|
154
173
|
// PascalCase Secret (SST_RESOURCE_JwtSecret → { value })
|
|
155
174
|
const pascal = sstLink('JwtSecret')?.value as string | undefined;
|
|
@@ -322,9 +341,29 @@ let adminFallbackWarned = false;
|
|
|
322
341
|
* fallback — which is why the guard that refuses a privileged app credential must not treat
|
|
323
342
|
* local dev as a violation. See
|
|
324
343
|
* plan:the-operator-credential-is-the-ops-channel-only-doctrine-doc.
|
|
344
|
+
*
|
|
345
|
+
* ONE PHYSICAL CONNECTION, ALWAYS. This connection is pinned to `max: 1` and a caller's
|
|
346
|
+
* `maxConnections` is deliberately ignored on this path — two guarantees rest on there being
|
|
347
|
+
* exactly one backend behind it:
|
|
348
|
+
*
|
|
349
|
+
* 1. `db:query`'s `pg_advisory_unlock_all()` cleanup (plugin.ts) must run on the SAME backend
|
|
350
|
+
* that took the lock. A session advisory lock survives ROLLBACK, so with a pool the
|
|
351
|
+
* release can land on a different connection and the lock is held until the container dies.
|
|
352
|
+
* 2. Session state a statement leaves behind — `SET search_path`, a temp table, a prepared
|
|
353
|
+
* statement — must not surface in an unrelated later call. With one connection "the last
|
|
354
|
+
* call rolled back" is the whole story; with a pool it is a per-connection lottery.
|
|
355
|
+
*
|
|
356
|
+
* Pooling buys the ops channel nothing either way: its actions (migrate, seed, console,
|
|
357
|
+
* db:query) run one at a time.
|
|
325
358
|
*/
|
|
326
359
|
export function createAdminDb<T extends Record<string, unknown>>(
|
|
327
360
|
schema: T,
|
|
361
|
+
/**
|
|
362
|
+
* Accepted for signature parity with `createDb`. `maxConnections` is IGNORED for the operator
|
|
363
|
+
* connection — see the "ONE PHYSICAL CONNECTION" note above. It still reaches `createDb` on
|
|
364
|
+
* the no-operator-credential fallback below, which is the API connection, a different
|
|
365
|
+
* connection kind with its own rules.
|
|
366
|
+
*/
|
|
328
367
|
options?: { maxConnections?: number }
|
|
329
368
|
): {
|
|
330
369
|
db: ReturnType<typeof drizzle>;
|
|
@@ -352,7 +391,9 @@ export function createAdminDb<T extends Record<string, unknown>>(
|
|
|
352
391
|
return createDb(schema, options);
|
|
353
392
|
}
|
|
354
393
|
|
|
355
|
-
|
|
394
|
+
// Pinned to 1, not `options?.maxConnections` — the advisory-lock cleanup and the
|
|
395
|
+
// no-session-leak guarantee both require a single physical connection. See the docblock.
|
|
396
|
+
adminSqlClient = postgres(adminUrl, connectionOptions(adminUrl, 1));
|
|
356
397
|
adminDbInstance = drizzle(adminSqlClient, { schema });
|
|
357
398
|
return { db: adminDbInstance, schema };
|
|
358
399
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Model-derived WIRE-CONTRACT gate — the server half.
|
|
3
|
+
*
|
|
4
|
+
* `db:provision`, `db:converge-ownership` and `db:reconcile` take payloads the CLI computes FROM
|
|
5
|
+
* the Models barrel: owner assignments (`declaredOwners` / `declaredObjects`) and derived-object
|
|
6
|
+
* descriptors (`declared`). The operator's `@everystack/cli` and the one bundled into this Lambda
|
|
7
|
+
* are separate installs on independent release trains, so those shapes can drift between the two
|
|
8
|
+
* — a field renamed, retyped, or given new required meaning rides through silently and lands as
|
|
9
|
+
* wrong ownership or a wrong derived object on a real database.
|
|
10
|
+
*
|
|
11
|
+
* So each of those payloads carries `modelContract`, and this refuses anything but EXACT equality
|
|
12
|
+
* with the MODEL_WIRE_CONTRACT the bundled CLI states. Same shape of check `db:apply` has run
|
|
13
|
+
* against `APPLY_CONTRACT_VERSION` since M1, and deliberately a SEPARATE number: apply versions a
|
|
14
|
+
* persisted plan artifact, this versions an ephemeral wire payload.
|
|
15
|
+
*
|
|
16
|
+
* Two rules this file exists to make unmissable:
|
|
17
|
+
*
|
|
18
|
+
* MISSING IS A REFUSAL, never a default. A caller that does not send `modelContract` is on a
|
|
19
|
+
* version that predates the field, which IS the skew being guarded. Defaulting it to "probably
|
|
20
|
+
* fine" would reintroduce exactly the silent path the guard removes.
|
|
21
|
+
*
|
|
22
|
+
* THE REFUSAL IS PRE-DISPATCH. Callers run this before resolving a connection, before any
|
|
23
|
+
* master client is opened, and before any DDL — so a skewed payload never reaches the database
|
|
24
|
+
* at all. `__tests__/model-wire-actions.test.ts` proves that with a mocked operator connection
|
|
25
|
+
* that must never be called.
|
|
26
|
+
*
|
|
27
|
+
* The CLI's own half of the handshake — reading `_health` and refusing to dispatch to a stage
|
|
28
|
+
* that advertises a different number, or none at all — lives in
|
|
29
|
+
* `packages/cli/src/cli/model-wire-lane.ts`. Neither half closes the hole alone: this one cannot
|
|
30
|
+
* see a stage deployed BEFORE the guard existed (it has no code to refuse with), and that one is
|
|
31
|
+
* a client claim, which a server must never trust.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Null when the payload speaks this server's Model-derived wire contract; otherwise the refusal
|
|
36
|
+
* to return to the caller verbatim.
|
|
37
|
+
*
|
|
38
|
+
* `verb` is the action name, so the operator reads which edge refused.
|
|
39
|
+
*/
|
|
40
|
+
export function modelWireRefusal(
|
|
41
|
+
verb: string,
|
|
42
|
+
declared: unknown,
|
|
43
|
+
enforced: number,
|
|
44
|
+
): string | null {
|
|
45
|
+
if (typeof declared === 'number' && declared === enforced) return null;
|
|
46
|
+
|
|
47
|
+
// Three genuinely different situations, so three leads rather than one template with holes in
|
|
48
|
+
// it. Two formatting rules earn their keep here, both learned the hard way:
|
|
49
|
+
//
|
|
50
|
+
// ABSENT gets its own sentence. Splicing nothing into "declares v…" rendered the literal
|
|
51
|
+
// "declares v(none)", which reads as a broken template exactly where an operator has to trust
|
|
52
|
+
// what the message says.
|
|
53
|
+
//
|
|
54
|
+
// Every VERSION NUMBER renders as `v<n>`, on both sides. The first fix for the above put the
|
|
55
|
+
// prefix on the enforced side only, giving "declares model-wire contract 5 … enforces v1" —
|
|
56
|
+
// two numbers in one sentence, formatted two ways, inviting the reader to wonder whether they
|
|
57
|
+
// mean the same kind of thing. They do.
|
|
58
|
+
//
|
|
59
|
+
// A non-numeric claim gets neither: `v"1"` would be worse than saying plainly that it is
|
|
60
|
+
// malformed, and a client sending one has a bug distinct from being out of date.
|
|
61
|
+
const lead =
|
|
62
|
+
declared === undefined || declared === null
|
|
63
|
+
? `this ${verb} payload declares NO model-wire contract — the everystack CLI that sent it `
|
|
64
|
+
+ `predates the handshake — and this stage's ops Lambda enforces v${enforced}.`
|
|
65
|
+
: typeof declared === 'number'
|
|
66
|
+
? `this ${verb} payload declares model-wire contract v${declared}, and this stage's ops `
|
|
67
|
+
+ `Lambda enforces v${enforced}.`
|
|
68
|
+
: `this ${verb} payload declares a malformed model-wire contract `
|
|
69
|
+
+ `(${JSON.stringify(declared)}); this stage's ops Lambda enforces v${enforced}.`;
|
|
70
|
+
|
|
71
|
+
// An older CLI is the overwhelmingly likely cause: a CURRENT CLI catches the skew at the
|
|
72
|
+
// `_health` handshake and never dispatches, so a payload that gets here at all came from one
|
|
73
|
+
// that does not run that preflight. Hence the remedy leads with upgrading the CLI — the same
|
|
74
|
+
// reasoning db:apply's refusal spells out.
|
|
75
|
+
return (
|
|
76
|
+
`${lead} The CLI that computed this Model-derived payload and the one deployed with this `
|
|
77
|
+
+ `stage do not agree on its shape. Upgrade the CLI to match (or, if the stage was rolled `
|
|
78
|
+
+ `back, redeploy it); nothing was applied.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The refusal for a server that cannot even STATE its contract, because `@everystack/cli/wire`
|
|
84
|
+
* would not load. Fail closed: a stage that cannot name the shape it understands must never
|
|
85
|
+
* accept a Model-derived payload on the strength of the caller's word for it.
|
|
86
|
+
*/
|
|
87
|
+
export function modelWireUnavailableRefusal(verb: string, cause: unknown): string {
|
|
88
|
+
return (
|
|
89
|
+
`${verb} cannot verify the Model-derived wire contract — this stage's ops Lambda failed to `
|
|
90
|
+
+ `load @everystack/cli/wire (${String(cause)}), so it cannot state which payload shape it `
|
|
91
|
+
+ `understands. Refusing rather than guessing. Redeploy the stage with a matching `
|
|
92
|
+
+ `@everystack/cli; nothing was applied.`
|
|
93
|
+
);
|
|
94
|
+
}
|
package/src/owner-adoption.ts
CHANGED
|
@@ -188,6 +188,59 @@ export interface AdopteeProbe {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
const OWNER_IDENT = /^[a-z_][a-z0-9_]*$/;
|
|
191
|
+
|
|
192
|
+
/** One unquoted identifier component — a schema, an object name, a type word. */
|
|
193
|
+
const IDENT_PART = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
194
|
+
/** A double-quoted component. The catalog spells exactly two kinds this way: `"char"` and a
|
|
195
|
+
* mixed-case custom type. Nothing that could carry SQL is allowed inside. */
|
|
196
|
+
const QUOTED_PART = /^"[A-Za-z0-9_ ]+"$/;
|
|
197
|
+
const isPart = (s: string): boolean => IDENT_PART.test(s) || QUOTED_PART.test(s);
|
|
198
|
+
|
|
199
|
+
/** `schema.name` — exactly two components, each a bare or quoted identifier. */
|
|
200
|
+
function isQualifiedName(s: string): boolean {
|
|
201
|
+
const parts = s.split('.');
|
|
202
|
+
return parts.length === 2 && parts.every(isPart);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* One argument type in the catalog's `format_type` spelling: a single word (`integer`), several
|
|
207
|
+
* (`character varying`, `timestamp with time zone`), a schema-qualified custom type
|
|
208
|
+
* (`api.my_enum`), a quoted one (`"char"`), each with any number of trailing `[]` dimensions.
|
|
209
|
+
*/
|
|
210
|
+
function isArgType(s: string): boolean {
|
|
211
|
+
let t = s;
|
|
212
|
+
while (t.endsWith('[]')) t = t.slice(0, -2);
|
|
213
|
+
if (!t) return false;
|
|
214
|
+
const words = t.split(' ');
|
|
215
|
+
if (words.some((w) => w === '')) return false; // leading, trailing or doubled space
|
|
216
|
+
if (words.length > 1) return words.every((w) => IDENT_PART.test(w));
|
|
217
|
+
return t.includes('.') ? isQualifiedName(t) : isPart(t);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Is `identity` shaped like an identity everystack spells — `schema.name` for a view/matview,
|
|
222
|
+
* `schema.name(argtype, …)` for a function?
|
|
223
|
+
*
|
|
224
|
+
* DEFENCE IN DEPTH (BF5). `identity` is interpolated RAW into `ALTER … OWNER TO` below. Today it
|
|
225
|
+
* cannot be anything else: the list is barrel-sourced (the Models export, not user input) and
|
|
226
|
+
* resolution-gated (`to_regprocedure`/`to_regclass` return NULL for a bogus identity, and only a
|
|
227
|
+
* probe that EXISTS reaches the ALTER). But a raw interpolation deserves its own gate, and
|
|
228
|
+
* OWNER_IDENT cannot be that gate — a bare role name has no dots, parens, commas or spaces, and an
|
|
229
|
+
* identity legitimately has all four. So the interpolation gets a validator shaped for what it
|
|
230
|
+
* actually carries, and everything else is refused by name.
|
|
231
|
+
*/
|
|
232
|
+
export function isValidObjectIdentity(identity: string): boolean {
|
|
233
|
+
const open = identity.indexOf('(');
|
|
234
|
+
if (open === -1) return isQualifiedName(identity);
|
|
235
|
+
if (!identity.endsWith(')')) return false;
|
|
236
|
+
if (!isQualifiedName(identity.slice(0, open))) return false;
|
|
237
|
+
const args = identity.slice(open + 1, -1);
|
|
238
|
+
if (args.includes('(') || args.includes(')')) return false;
|
|
239
|
+
if (args === '') return true; // `api.report()` — a function of no arguments
|
|
240
|
+
// The catalog joins argument types with `, `; tolerate the separating space, nothing else.
|
|
241
|
+
return args.split(',').every((a) => isArgType(a.startsWith(' ') ? a.slice(1) : a));
|
|
242
|
+
}
|
|
243
|
+
|
|
191
244
|
const KEYWORD: Record<Adoptee['kind'], string> = {
|
|
192
245
|
function: 'FUNCTION', view: 'VIEW', 'materialized view': 'MATERIALIZED VIEW',
|
|
193
246
|
};
|
|
@@ -245,6 +298,13 @@ export interface AdoptionPlan {
|
|
|
245
298
|
reassigned: Adoptee[];
|
|
246
299
|
/** Precise pre-flight refusals — nothing is applied if this is non-empty (all or nothing). */
|
|
247
300
|
refusals: string[];
|
|
301
|
+
/**
|
|
302
|
+
* The declared owners the master self-granted `SET` on, BY NAME. That membership exists only to
|
|
303
|
+
* make the ALTERs legal, so the caller revokes each one once the transaction has committed —
|
|
304
|
+
* otherwise it accumulates on the master across ADOPT runs. Named, not spelled as SQL, because
|
|
305
|
+
* only the caller knows when the commit happened.
|
|
306
|
+
*/
|
|
307
|
+
selfGrantedOwners: string[];
|
|
248
308
|
}
|
|
249
309
|
|
|
250
310
|
/**
|
|
@@ -257,13 +317,17 @@ export interface AdoptionPlan {
|
|
|
257
317
|
export function planAdoptionReassignment(adoptees: Adoptee[], probes: AdopteeProbe[]): AdoptionPlan {
|
|
258
318
|
const byId = new Map(probes.map((p) => [p.identity, p]));
|
|
259
319
|
const alters: string[] = [];
|
|
260
|
-
const
|
|
261
|
-
const plan: AdoptionPlan = { statements: [], reassigned: [], refusals: [] };
|
|
320
|
+
const selfGranted = new Set<string>(); // one per declared owner the master must self-grant SET on
|
|
321
|
+
const plan: AdoptionPlan = { statements: [], reassigned: [], refusals: [], selfGrantedOwners: [] };
|
|
262
322
|
|
|
263
323
|
for (const a of adoptees) {
|
|
264
324
|
const p = byId.get(a.identity);
|
|
265
325
|
if (!p || !p.exists) continue; // not in the DB — reconcile creates it owned correctly, nothing to adopt
|
|
266
326
|
if (p.liveOwner === a.declaredOwner) continue; // already correct
|
|
327
|
+
if (!isValidObjectIdentity(a.identity)) {
|
|
328
|
+
plan.refusals.push(`${JSON.stringify(a.identity)}: not a valid qualified object identity (expected schema.name, or schema.name(argtype, ...) for a function) — refusing to interpolate it into ALTER ${KEYWORD[a.kind]} ... OWNER TO.`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
267
331
|
if (p.isExtensionMember) {
|
|
268
332
|
plan.refusals.push(`${a.identity}: owned by an extension — never reassigned (its OID binds to the extension).`);
|
|
269
333
|
continue;
|
|
@@ -292,7 +356,7 @@ export function planAdoptionReassignment(adoptees: Adoptee[], probes: AdopteePro
|
|
|
292
356
|
// it self-grants SET inside the transaction; otherwise it is a precise refusal naming THIS
|
|
293
357
|
// conjunct (not the current-owner one), with the fix.
|
|
294
358
|
if (p.masterAdminOfDeclared) {
|
|
295
|
-
|
|
359
|
+
selfGranted.add(a.declaredOwner);
|
|
296
360
|
} else {
|
|
297
361
|
plan.refusals.push(`${a.identity}: the master cannot SET ROLE to the declared owner '${a.declaredOwner}' and lacks ADMIN OPTION on it to grant itself. As a role with admin on '${a.declaredOwner}': GRANT ${a.declaredOwner} TO <master> WITH SET TRUE; then re-run.`);
|
|
298
362
|
continue;
|
|
@@ -302,6 +366,13 @@ export function planAdoptionReassignment(adoptees: Adoptee[], probes: AdopteePro
|
|
|
302
366
|
plan.reassigned.push({ ...a, currentOwner: p.liveOwner });
|
|
303
367
|
}
|
|
304
368
|
// Self-grants first (they make the SET conjunct true), then the reassignments — one transaction.
|
|
305
|
-
|
|
369
|
+
// A role membership granted inside a transaction is visible to the commands that follow it there,
|
|
370
|
+
// which is exactly why the ALTERs can use it; the matching REVOKE therefore belongs strictly
|
|
371
|
+
// AFTER the commit, and is the caller's job (see selfGrantedOwners).
|
|
372
|
+
plan.selfGrantedOwners = [...selfGranted];
|
|
373
|
+
plan.statements = [
|
|
374
|
+
...plan.selfGrantedOwners.map((o) => `GRANT ${o} TO CURRENT_USER WITH SET TRUE, INHERIT FALSE`),
|
|
375
|
+
...alters,
|
|
376
|
+
];
|
|
306
377
|
return plan;
|
|
307
378
|
}
|