@everystack/cli 0.4.16 → 0.4.18
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.
|
|
3
|
+
"version": "0.4.18",
|
|
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>",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"structured-headers": "1.0.1",
|
|
83
83
|
"tsx": "4.21.0",
|
|
84
84
|
"typescript": "5.9.3",
|
|
85
|
-
"@everystack/model": "0.4.
|
|
85
|
+
"@everystack/model": "0.4.4"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"@everystack/server": ">=0.1.0",
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
renderSchemaLogInsert,
|
|
43
43
|
derivedSearchPath,
|
|
44
44
|
renderSetSearchPath,
|
|
45
|
+
renderEnsureObjectSchemas,
|
|
45
46
|
ENSURE_RECONCILER_SQL,
|
|
46
47
|
} from '../derived-apply.js';
|
|
47
48
|
import { resolveDbSource, createUrlRunner, connectingVia, type DbSource } from '../db-source.js';
|
|
@@ -151,6 +152,12 @@ export async function executeReconcile(
|
|
|
151
152
|
// as it was. See derivedSearchPath.
|
|
152
153
|
const setSearchPath = renderSetSearchPath(derivedSearchPath(parsed.objects), atomic);
|
|
153
154
|
|
|
155
|
+
// A schema populated only by derived objects has no model in it, so the state layer never
|
|
156
|
+
// CREATE SCHEMAs it — the derived apply ensures its own object schemas (+ their USAGE
|
|
157
|
+
// grants) before anything is created in them. Empty for all-public apps. See
|
|
158
|
+
// renderEnsureObjectSchemas.
|
|
159
|
+
const ensureSchemas = renderEnsureObjectSchemas(parsed.objects);
|
|
160
|
+
|
|
154
161
|
if (atomic) await runner('BEGIN');
|
|
155
162
|
try {
|
|
156
163
|
// The session guard: postgres-js `max: 1` is a pool of one, not a session lease — a
|
|
@@ -166,7 +173,7 @@ export async function executeReconcile(
|
|
|
166
173
|
if (atomic) {
|
|
167
174
|
// The SET rides the same simple-query batch (and the same transaction) as the DDL,
|
|
168
175
|
// so bare body refs resolve to their real schema; SET LOCAL resets it at COMMIT.
|
|
169
|
-
await runner([setSearchPath, ...rendered.statements].filter(Boolean).join(';\n'));
|
|
176
|
+
await runner([...ensureSchemas, setSearchPath, ...rendered.statements].filter(Boolean).join(';\n'));
|
|
170
177
|
} else {
|
|
171
178
|
// Self-committing statements (CONCURRENTLY, VACUUM) refuse ANY transaction block —
|
|
172
179
|
// including the implicit one a joined multi-statement simple query runs in, so the
|
|
@@ -174,6 +181,7 @@ export async function executeReconcile(
|
|
|
174
181
|
// earlier statements committed: the documented cost of the unwrapped path; the next
|
|
175
182
|
// plan re-converges from what actually landed. The session SET precedes them so every
|
|
176
183
|
// create resolves its bare refs.
|
|
184
|
+
for (const statement of ensureSchemas) await runner(statement);
|
|
177
185
|
if (setSearchPath) await runner(setSearchPath);
|
|
178
186
|
for (const statement of rendered.statements) await runner(statement);
|
|
179
187
|
}
|
|
@@ -211,7 +219,7 @@ export async function executeReconcile(
|
|
|
211
219
|
bookkeeping.push(renderSchemaLogInsert({
|
|
212
220
|
kind: 'compute reconcile',
|
|
213
221
|
sql: rendered.statements.length > 0
|
|
214
|
-
? [setSearchPath, ...rendered.statements].filter(Boolean).join(';\n')
|
|
222
|
+
? [...ensureSchemas, setSearchPath, ...rendered.statements].filter(Boolean).join(';\n')
|
|
215
223
|
: '-- provenance-only (baseline/rebaseline/prune)',
|
|
216
224
|
outcome: 'applied',
|
|
217
225
|
actor: options.actor, gitRef: options.gitRef, planRef: options.planRef,
|
package/src/cli/derived-apply.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { normalizeSql, parseQualified, type SourceObject } from './derived-source.js';
|
|
17
17
|
import type { ReconcilePlan } from './derived-plan.js';
|
|
18
|
+
import { parseGrantAttachments } from './derived-grants.js';
|
|
18
19
|
import { quoteIdent, quoteQualified } from './pg-ident.js';
|
|
19
20
|
|
|
20
21
|
/** SQL string literal with '' doubling (standard_conforming_strings). */
|
|
@@ -136,6 +137,43 @@ export function renderSetSearchPath(schemas: string[], local: boolean): string |
|
|
|
136
137
|
return `SET ${local ? 'LOCAL ' : ''}search_path = ${schemas.map(quoteIdent).join(', ')}`;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* `CREATE SCHEMA IF NOT EXISTS` for each non-public schema the declared objects LIVE
|
|
142
|
+
* in — migration-compile's rule 0a mirrored into the derived layer. Rule 0a only
|
|
143
|
+
* creates schemas a MODEL lives in, so a schema populated purely by derived objects
|
|
144
|
+
* (the derivation half of a stats/stat_views split) was never created and a
|
|
145
|
+
* from-scratch apply died on its first CREATE. Object schemas only, never
|
|
146
|
+
* declaredDeps schemas: a dependency's schema belongs to the models that own it, and
|
|
147
|
+
* a missing one must still fail at CREATE instead of being silently minted empty.
|
|
148
|
+
*
|
|
149
|
+
* `GRANT USAGE` rides along per schema (0a-bis mirrored), derived from the objects'
|
|
150
|
+
* own grant attachments: a role granted SELECT on a matview in a non-public schema
|
|
151
|
+
* cannot reach it without USAGE, so the object grant is dead without this. A PUBLIC
|
|
152
|
+
* that appears only as explicitly-revoked gets nothing. Empty for all-public apps —
|
|
153
|
+
* their apply stays byte-identical. Idempotent when the state layer already created
|
|
154
|
+
* the schema (a model schema also carrying derived objects).
|
|
155
|
+
*/
|
|
156
|
+
export function renderEnsureObjectSchemas(objects: SourceObject[]): string[] {
|
|
157
|
+
const statements: string[] = [];
|
|
158
|
+
const schemas = [...new Set(objects.map((o) => o.schema))].filter((s) => s !== 'public').sort();
|
|
159
|
+
for (const schema of schemas) {
|
|
160
|
+
statements.push(`CREATE SCHEMA IF NOT EXISTS ${quoteIdent(schema)}`);
|
|
161
|
+
const roles = new Set<string>();
|
|
162
|
+
for (const o of objects) {
|
|
163
|
+
if (o.schema !== schema) continue;
|
|
164
|
+
const { grants } = parseGrantAttachments(o.attachments);
|
|
165
|
+
for (const [role, privileges] of Object.entries(grants)) {
|
|
166
|
+
if (privileges.length > 0) roles.add(role);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (roles.size > 0) {
|
|
170
|
+
const targets = [...roles].sort().map((r) => (r.toUpperCase() === 'PUBLIC' ? 'PUBLIC' : r)).join(', ');
|
|
171
|
+
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdent(schema)} TO ${targets}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return statements;
|
|
175
|
+
}
|
|
176
|
+
|
|
139
177
|
export function renderReconcileSql(plan: ReconcilePlan, source: SourceObject[]): RenderedReconcile {
|
|
140
178
|
const srcById = new Map(source.map((o) => [o.identity, o]));
|
|
141
179
|
const statements: string[] = [];
|
package/src/cli/model-render.ts
CHANGED
package/src/cli/schema-source.ts
CHANGED
|
@@ -96,6 +96,8 @@ function baseColumnSource(columnName: string, spec: FieldSpec): { call: string;
|
|
|
96
96
|
: { call: `varchar(${n})`, builder: 'varchar' };
|
|
97
97
|
case 'jsonb':
|
|
98
98
|
return { call: `jsonb(${n})`, builder: 'jsonb' };
|
|
99
|
+
case 'json':
|
|
100
|
+
return { call: `json(${n})`, builder: 'json' };
|
|
99
101
|
case 'enum': {
|
|
100
102
|
if (!spec.enumName) throw new Error('enum field is missing enumName');
|
|
101
103
|
return { call: `${enumConstName(spec.enumName)}(${n})`, builder: enumConstName(spec.enumName) };
|
package/src/cli/swap-execute.ts
CHANGED
|
@@ -107,7 +107,7 @@ export async function executeSwap(runner: QueryRunner, opts: ExecuteSwapOptions)
|
|
|
107
107
|
try { await runner(`DROP SCHEMA IF EXISTS "${plan.incoming}" CASCADE`); } catch { /* best effort */ }
|
|
108
108
|
return {
|
|
109
109
|
status: 'refused-integrity',
|
|
110
|
-
reason: `the swap transaction failed
|
|
110
|
+
reason: `the swap transaction failed and rolled back whole: ${String(err?.message ?? err)}. Candidate causes: a cross-schema FK re-validating against the new data (an app row references a ${opts.schema} key the artifact dropped), or an authz statement colliding with authz the artifact already carries — the message above names the actual failing statement.`,
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|