@selvajs/cli 4.6.20 → 4.6.21-beta.1
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/commands/doctor.js +79 -1
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Validate deployment without starting it: files, secrets, layout, providers,
|
|
2
2
|
// boot persistence. Read-only; yellow warnings don't fail, red failures exit 1.
|
|
3
3
|
|
|
4
|
-
import { existsSync, readFileSync, accessSync, constants, statSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, accessSync, constants, statSync } from 'node:fs';
|
|
5
5
|
import { join, resolve, dirname, delimiter } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
@@ -57,6 +57,7 @@ export async function runDoctor() {
|
|
|
57
57
|
|
|
58
58
|
if (used.has('supabase')) {
|
|
59
59
|
checks.push(checkSupabase(env));
|
|
60
|
+
checks.push(checkSupabaseMigrations(dir, env));
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
if (providers.auth === 'header') {
|
|
@@ -170,6 +171,83 @@ async function checkSupabase(env) {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
// App↔DB schema handshake (audit O3). Expected head = newest migration shipped
|
|
175
|
+
// by the installed @selvajs/supabase-provider; actual = what the database
|
|
176
|
+
// reports via the selva.migration_head() RPC. Red on skew — the app degrades
|
|
177
|
+
// /api/health to 503 at boot in the same state, so catching it here saves a
|
|
178
|
+
// confusing deploy.
|
|
179
|
+
async function checkSupabaseMigrations(dir, env) {
|
|
180
|
+
if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
181
|
+
return yellow('Migration-head check skipped — Supabase env incomplete');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const migrationsDir = join(
|
|
185
|
+
dir,
|
|
186
|
+
'node_modules',
|
|
187
|
+
'@selvajs',
|
|
188
|
+
'supabase-provider',
|
|
189
|
+
'supabase',
|
|
190
|
+
'migrations'
|
|
191
|
+
);
|
|
192
|
+
let expected = null;
|
|
193
|
+
try {
|
|
194
|
+
expected =
|
|
195
|
+
readdirSync(migrationsDir)
|
|
196
|
+
.filter((f) => /^\d{14}_.+\.sql$/.test(f))
|
|
197
|
+
.map((f) => f.slice(0, 14))
|
|
198
|
+
.sort()
|
|
199
|
+
.pop() ?? null;
|
|
200
|
+
} catch {
|
|
201
|
+
// Provider package not installed in this deployment.
|
|
202
|
+
}
|
|
203
|
+
if (!expected) {
|
|
204
|
+
return yellow(
|
|
205
|
+
'@selvajs/supabase-provider migrations not found — skipping migration-head check'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const timer = setTimeout(() => controller.abort(), 4000);
|
|
212
|
+
const res = await fetch(env.SUPABASE_URL + '/rest/v1/rpc/migration_head', {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: {
|
|
215
|
+
apikey: env.SUPABASE_SERVICE_ROLE_KEY,
|
|
216
|
+
Authorization: `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
|
|
217
|
+
'Content-Type': 'application/json',
|
|
218
|
+
// The function lives in the selva schema the app is pinned to.
|
|
219
|
+
'Content-Profile': 'selva'
|
|
220
|
+
},
|
|
221
|
+
body: '{}',
|
|
222
|
+
signal: controller.signal
|
|
223
|
+
});
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
if (res.status === 404) {
|
|
226
|
+
return red(
|
|
227
|
+
`Database is missing selva.migration_head() — migrations are pending (expected head ${expected}). Sync + run: npx supabase db push`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
return yellow(`migration_head RPC responded ${res.status} — cannot verify schema head`);
|
|
232
|
+
}
|
|
233
|
+
const actual = String((await res.json()) ?? '');
|
|
234
|
+
if (!actual) {
|
|
235
|
+
return red(
|
|
236
|
+
`Database reports no applied migrations (expected head ${expected}). Sync + run: npx supabase db push`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (actual < expected) {
|
|
240
|
+
return red(
|
|
241
|
+
`Database migration head ${actual} is behind the installed provider (${expected}). Sync + run: npx supabase db push`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return green(`Database migration head ${actual} matches the installed provider`);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
// Soft-fail on network errors, same as checkSupabase.
|
|
247
|
+
return yellow(`Migration-head check skipped (${err.message ?? err})`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
173
251
|
function checkPackage(dir, name) {
|
|
174
252
|
const path = join(dir, 'node_modules', ...name.split('/'), 'package.json');
|
|
175
253
|
if (!existsSync(path)) return red(`${name} not installed (run npm install)`);
|