@everystack/cli 0.4.7 → 0.4.9
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.9",
|
|
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.2"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"@everystack/server": ">=0.1.0",
|
|
@@ -35,6 +35,7 @@ import { resolveDbSource, createUrlRunner, connectingVia, type DbSource } from '
|
|
|
35
35
|
import { resolveConfig, opsFunction } from '../config.js';
|
|
36
36
|
import { invokeAction, lambdaQueryRunner } from '../aws.js';
|
|
37
37
|
import { executeApplyPlan, type ApplyPlanResult } from '../apply-execute.js';
|
|
38
|
+
import { pgDumpPreflightError } from './db-backup.js';
|
|
38
39
|
import { step, success, fail, info, warn } from '../output.js';
|
|
39
40
|
|
|
40
41
|
// The apply core lives in apply-execute.ts (shared with the ops Lambda's
|
|
@@ -116,6 +117,19 @@ async function applyPlanViaStage(
|
|
|
116
117
|
|
|
117
118
|
info(`plan ${planHash(plan).slice(0, 12)}: ${plan.fromFingerprint.slice(0, 12)} → ${plan.toFingerprint.slice(0, 12)} (${plan.executable} statement(s)${plan.destructive ? `, ${plan.destructive} destructive` : ''})`);
|
|
118
119
|
|
|
120
|
+
// A destructive staged apply snapshots via db:backup INSIDE the Lambda, before the DDL. Preflight
|
|
121
|
+
// that capability HERE so a missing/incompatible pg_dump layer fails fast with the install remedy —
|
|
122
|
+
// not deep in the ceremony after the authority check, the way it once surfaced mid-apply.
|
|
123
|
+
if (plan.destructive > 0) {
|
|
124
|
+
step('Preflighting the pg_dump layer (the destructive apply snapshots before the DDL)...');
|
|
125
|
+
const probe: any = await invokeAction(region, fn, 'db:backup:probe', {}).catch((err: any) => ({ error: err?.message ?? String(err) }));
|
|
126
|
+
const remedy = pgDumpPreflightError(probe);
|
|
127
|
+
if (remedy) {
|
|
128
|
+
fail(`Cannot take the pre-apply snapshot — the destructive plan was NOT applied.\n${remedy}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
try {
|
|
120
134
|
// Descent — the fast-forward rule needs the git checkout, so the CLI
|
|
121
135
|
// decides it (the Lambda has no checkout). Read the stage's state read-only
|
|
@@ -48,7 +48,7 @@ export async function dbBackupProbeCommand(flags: Record<string, string>): Promi
|
|
|
48
48
|
|
|
49
49
|
if (result?.error) {
|
|
50
50
|
fail(result.error);
|
|
51
|
-
info('
|
|
51
|
+
info('Install @everystack/pg-tools and redeploy — the layer attaches automatically. For a bring-your-own layer, pass pgDumpLayer(..., { layerArn }) in sst.config.ts.');
|
|
52
52
|
process.exit(1);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -71,7 +71,7 @@ export async function dbBackupProbeCommand(flags: Record<string, string>): Promi
|
|
|
71
71
|
export function pgDumpPreflightError(probe: any): string | null {
|
|
72
72
|
if (!probe || probe.error) {
|
|
73
73
|
const detail = probe?.error ?? 'the pg_dump layer probe returned no result';
|
|
74
|
-
return `${detail}\
|
|
74
|
+
return `${detail}\nInstall @everystack/pg-tools and redeploy — the layer attaches automatically. For a bring-your-own layer, pass pgDumpLayer(..., { layerArn }) in sst.config.ts.`;
|
|
75
75
|
}
|
|
76
76
|
if (probe.compatible === false) {
|
|
77
77
|
return `pg_dump ${probe.pgDump ?? '(unknown)'} (major ${probe.pgDumpMajor}) is older than the server (major ${probe.serverMajor}) — it will refuse to dump (no bypass exists). Rebuild the layer with pg_dump >= ${probe.serverMajor}.`;
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
triggerDropSql,
|
|
41
41
|
renderProvenanceDelete,
|
|
42
42
|
renderSchemaLogInsert,
|
|
43
|
+
derivedSearchPath,
|
|
44
|
+
renderSetSearchPath,
|
|
43
45
|
ENSURE_RECONCILER_SQL,
|
|
44
46
|
} from '../derived-apply.js';
|
|
45
47
|
import { resolveDbSource, createUrlRunner, connectingVia, type DbSource } from '../db-source.js';
|
|
@@ -143,6 +145,12 @@ export async function executeReconcile(
|
|
|
143
145
|
// (CONCURRENTLY index builds, VACUUM); those self-commit, so we fall back to the unwrapped path.
|
|
144
146
|
const atomic = isTransactionalBatch(rendered.statements);
|
|
145
147
|
|
|
148
|
+
// Bare table refs in derived bodies (a db/pull deparse from the all-public era) resolve
|
|
149
|
+
// against the search_path at CREATE time — put every declared schema on it, or the build
|
|
150
|
+
// fails on the first non-public table. Null for an all-public app: the apply stays exactly
|
|
151
|
+
// as it was. See derivedSearchPath.
|
|
152
|
+
const setSearchPath = renderSetSearchPath(derivedSearchPath(parsed.objects), atomic);
|
|
153
|
+
|
|
146
154
|
if (atomic) await runner('BEGIN');
|
|
147
155
|
try {
|
|
148
156
|
// The session guard: postgres-js `max: 1` is a pool of one, not a session lease — a
|
|
@@ -156,17 +164,28 @@ export async function executeReconcile(
|
|
|
156
164
|
|
|
157
165
|
if (rendered.statements.length > 0) {
|
|
158
166
|
if (atomic) {
|
|
159
|
-
|
|
167
|
+
// The SET rides the same simple-query batch (and the same transaction) as the DDL,
|
|
168
|
+
// 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'));
|
|
160
170
|
} else {
|
|
161
171
|
// Self-committing statements (CONCURRENTLY, VACUUM) refuse ANY transaction block —
|
|
162
172
|
// including the implicit one a joined multi-statement simple query runs in, so the
|
|
163
173
|
// batch MUST go one statement per protocol message. A failure mid-way leaves the
|
|
164
174
|
// earlier statements committed: the documented cost of the unwrapped path; the next
|
|
165
|
-
// plan re-converges from what actually landed.
|
|
175
|
+
// plan re-converges from what actually landed. The session SET precedes them so every
|
|
176
|
+
// create resolves its bare refs.
|
|
177
|
+
if (setSearchPath) await runner(setSearchPath);
|
|
166
178
|
for (const statement of rendered.statements) await runner(statement);
|
|
167
179
|
}
|
|
168
180
|
}
|
|
169
181
|
|
|
182
|
+
// Reset the search_path before introspecting. pg_get_viewdef renders a ref BARE while its
|
|
183
|
+
// schema is in scope and QUALIFIED once it is off — so the defHash we record must be read
|
|
184
|
+
// under the SAME (default) path every future introspection uses, or the qualified live
|
|
185
|
+
// deparse reads as drift against a bare recorded hash on the very next run. Canonical
|
|
186
|
+
// capture, always: SET LOCAL resets within the transaction; the session path otherwise.
|
|
187
|
+
if (setSearchPath) await runner(atomic ? 'SET LOCAL search_path TO DEFAULT' : 'RESET search_path');
|
|
188
|
+
|
|
170
189
|
// Re-read the catalog so provenance records the def hashes of what NOW exists, not what we hoped
|
|
171
190
|
// would exist. Inside the transaction this reads the batch's own not-yet-committed writes.
|
|
172
191
|
const after = await introspectDerived(runner);
|
|
@@ -196,7 +215,9 @@ export async function executeReconcile(
|
|
|
196
215
|
for (const identity of rendered.remove) bookkeeping.push(renderProvenanceDelete(identity));
|
|
197
216
|
bookkeeping.push(renderSchemaLogInsert({
|
|
198
217
|
kind: 'compute reconcile',
|
|
199
|
-
sql: rendered.statements.
|
|
218
|
+
sql: rendered.statements.length > 0
|
|
219
|
+
? [setSearchPath, ...rendered.statements].filter(Boolean).join(';\n')
|
|
220
|
+
: '-- provenance-only (baseline/rebaseline/prune)',
|
|
200
221
|
outcome: 'applied',
|
|
201
222
|
actor: options.actor, gitRef: options.gitRef, planRef: options.planRef,
|
|
202
223
|
durationMs: now() - started,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
|
-
import { step, success, fail, info } from '../output.js';
|
|
14
|
+
import { step, success, fail, info, warn } from '../output.js';
|
|
15
15
|
import { invalidateCachedConfig } from '../discover.js';
|
|
16
16
|
import { resolveConfig } from '../config.js';
|
|
17
17
|
import { probeDeployedFunctions } from '../deploy-probe.js';
|
|
@@ -23,11 +23,11 @@ export function buildDeployArgs(flags: Record<string, string>): string[] {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* The environment the sst child receives: the FULL ambient environment plus color.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* The environment the sst child receives: the FULL ambient environment plus color. The wrapper
|
|
27
|
+
* forwards everything so a consumer's own sst.config.ts env reads behave the same run directly or
|
|
28
|
+
* through `everystack deploy`. (This contract was pinned when the pg_dump layer depended on
|
|
29
|
+
* PGDUMP_LAYER_ARN reaching sst — now deprecated: the layer ships via @everystack/pg-tools +
|
|
30
|
+
* pgDumpLayer() and reads no ambient env at deploy.)
|
|
31
31
|
*/
|
|
32
32
|
export function deployEnv(base: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
33
33
|
return { ...base, FORCE_COLOR: '1' };
|
|
@@ -38,11 +38,11 @@ export async function deployCommand(flags: Record<string, string>): Promise<void
|
|
|
38
38
|
const args = buildDeployArgs(flags);
|
|
39
39
|
|
|
40
40
|
step(`Deploying stage "${stage}"...`);
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
41
|
+
// PGDUMP_LAYER_ARN is deprecated: the layer now ships via @everystack/pg-tools and pgDumpLayer()
|
|
42
|
+
// needs no env var. Still forwarded to sst for one more minor (pgDumpLayer honors it as an alias),
|
|
43
|
+
// then removed — surface it loudly so the operator migrates off it.
|
|
44
44
|
if (process.env.PGDUMP_LAYER_ARN) {
|
|
45
|
-
|
|
45
|
+
warn('PGDUMP_LAYER_ARN is DEPRECATED and will be removed next minor. The pg_dump layer now ships via @everystack/pg-tools + pgDumpLayer() with no env var. Drop it once your sst.config.ts uses pgDumpLayer().');
|
|
46
46
|
}
|
|
47
47
|
await new Promise<void>((resolve) => {
|
|
48
48
|
// `npx` resolves the consumer's local sst (the same way the export step runs expo).
|
package/src/cli/derived-apply.ts
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* the reconciler writes it and never reads it.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { normalizeSql, type SourceObject } from './derived-source.js';
|
|
16
|
+
import { normalizeSql, parseQualified, type SourceObject } from './derived-source.js';
|
|
17
17
|
import type { ReconcilePlan } from './derived-plan.js';
|
|
18
|
-
import { quoteQualified } from './pg-ident.js';
|
|
18
|
+
import { quoteIdent, quoteQualified } from './pg-ident.js';
|
|
19
19
|
|
|
20
20
|
/** SQL string literal with '' doubling (standard_conforming_strings). */
|
|
21
21
|
export function escapeLiteral(value: string): string {
|
|
@@ -102,6 +102,40 @@ function objectSql(obj: SourceObject): string[] {
|
|
|
102
102
|
return statements;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The schemas a from-scratch derived apply must put on the search_path before it
|
|
107
|
+
* creates anything. Derived bodies carry BARE table refs — db:pull deparsed them
|
|
108
|
+
* when every table lived in `public`, so a view over `stats.list_casts` still reads
|
|
109
|
+
* `FROM list_casts`. Postgres resolves and STORES those refs against the search_path
|
|
110
|
+
* AT CREATE TIME, so a build whose path is public-only cannot resolve them once the
|
|
111
|
+
* tables move to non-public schemas (the multi-schema split). The needed set is the
|
|
112
|
+
* union of every schema the declared objects and their declared dependencies live in
|
|
113
|
+
* — a deterministic function of the source, so the build stays hermetic. `public`
|
|
114
|
+
* always trails (tables/extensions still there; a db:pull-era bare ref was globally
|
|
115
|
+
* unique in public, so order never has to disambiguate).
|
|
116
|
+
*/
|
|
117
|
+
export function derivedSearchPath(objects: SourceObject[]): string[] {
|
|
118
|
+
const schemas = new Set<string>();
|
|
119
|
+
for (const o of objects) {
|
|
120
|
+
schemas.add(o.schema);
|
|
121
|
+
for (const dep of o.declaredDeps ?? []) schemas.add(parseQualified(dep).schema);
|
|
122
|
+
}
|
|
123
|
+
const nonPublic = [...schemas].filter((s) => s !== 'public').sort();
|
|
124
|
+
return [...nonPublic, 'public'];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The `SET search_path` that prefixes a derived apply — or `null` when every object
|
|
129
|
+
* and dependency lives in `public` (the default), so an all-public app's apply stays
|
|
130
|
+
* byte-identical and the override appears only when a schema split actually needs it.
|
|
131
|
+
* `LOCAL` scopes it to the apply transaction (the atomic batch); the non-atomic
|
|
132
|
+
* CONCURRENTLY path has no transaction, so it sets the session for the apply's life.
|
|
133
|
+
*/
|
|
134
|
+
export function renderSetSearchPath(schemas: string[], local: boolean): string | null {
|
|
135
|
+
if (schemas.length === 1 && schemas[0] === 'public') return null;
|
|
136
|
+
return `SET ${local ? 'LOCAL ' : ''}search_path = ${schemas.map(quoteIdent).join(', ')}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
export function renderReconcileSql(plan: ReconcilePlan, source: SourceObject[]): RenderedReconcile {
|
|
106
140
|
const srcById = new Map(source.map((o) => [o.identity, o]));
|
|
107
141
|
const statements: string[] = [];
|