@rehearsal-db/core 0.1.0-beta.5 → 0.1.0-beta.7
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 +56 -1
- package/COMPATIBILITY.md +5 -0
- package/README.md +112 -377
- package/SECURITY.md +2 -1
- package/docs/adapters.md +5 -0
- package/docs/baselines.md +81 -22
- package/docs/commands.md +105 -75
- package/docs/configuration.md +109 -8
- package/docs/getting-started.md +88 -227
- package/docs/glossary.md +7 -8
- package/docs/releasing.md +10 -1
- package/docs/roadmap.md +39 -0
- package/docs/security-model.md +5 -1
- package/docs/troubleshooting.md +69 -1
- package/docs/tutorial.md +70 -59
- package/package.json +4 -2
- package/scripts/lib/environment/local_supabase.mjs +8 -8
- package/scripts/lib/rehearsal/baseline_artifact.mjs +32 -3
- package/scripts/lib/rehearsal/cleanup.mjs +381 -0
- package/scripts/lib/rehearsal/configuration.d.mts +24 -3
- package/scripts/lib/rehearsal/configuration.mjs +337 -59
- package/scripts/lib/rehearsal/diagnostics.mjs +4 -2
- package/scripts/lib/rehearsal/plan.mjs +89 -14
- package/scripts/lib/rehearsal/runtime_restore.mjs +6 -2
- package/scripts/lib/rehearsal/setup.mjs +60 -28
- package/scripts/lib/runtime/postgresql_runtime.mjs +772 -0
- package/scripts/lib/runtime/runtime_target.mjs +41 -0
- package/scripts/lib/runtime/supabase_runtime.mjs +792 -0
- package/scripts/operations/database/manage_rehearsal_database.mjs +6 -785
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +330 -31
|
@@ -105,6 +105,14 @@ const resultCheck = async ({ id, label, run, remediation }) => {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
const assertRuntimeConfigMatches = async ({ config, paths }) => {
|
|
108
|
+
if (config.runtime.target === "postgresql") {
|
|
109
|
+
if (paths.rehearsalConfig !== null || paths.supabaseWorkdir !== null) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"The PostgreSQL target unexpectedly resolved Supabase paths.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return `isolated PostgreSQL image ${config.postgresql.image} and a loopback database port are configured`;
|
|
115
|
+
}
|
|
108
116
|
const source = await readFile(paths.rehearsalConfig, "utf8");
|
|
109
117
|
const expected = [
|
|
110
118
|
["project_id", config.runtime.projectId],
|
|
@@ -217,7 +225,8 @@ export const buildRehearsalPlan = async (options = {}) => {
|
|
|
217
225
|
project: config.project.name,
|
|
218
226
|
},
|
|
219
227
|
environment: {
|
|
220
|
-
|
|
228
|
+
target: config.runtime.target,
|
|
229
|
+
kind: `isolated_local_${config.runtime.target}`,
|
|
221
230
|
applicationUrl: config.runtime.applicationUrl,
|
|
222
231
|
projectId: config.runtime.projectId,
|
|
223
232
|
ports: config.runtime.ports,
|
|
@@ -226,9 +235,9 @@ export const buildRehearsalPlan = async (options = {}) => {
|
|
|
226
235
|
authenticationProviders: config.safety.authenticationProviders,
|
|
227
236
|
barriers: [
|
|
228
237
|
"versioned configuration accepts loopback hosts only",
|
|
229
|
-
|
|
238
|
+
`runtime uses a dedicated local ${config.runtime.target === "supabase" ? "Supabase workdir" : "PostgreSQL container and volume"} and project id`,
|
|
230
239
|
"child processes receive an allowlisted environment",
|
|
231
|
-
"
|
|
240
|
+
"configuration accepts only loopback runtime URLs and quarantines common hosted credentials",
|
|
232
241
|
...(config.safety.authenticationProviders.length
|
|
233
242
|
? [
|
|
234
243
|
`external identity is limited to: ${config.safety.authenticationProviders.join(", ")}`,
|
|
@@ -382,14 +391,37 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
382
391
|
remediation: "Use npm for the initial Rehearsal contract.",
|
|
383
392
|
},
|
|
384
393
|
{
|
|
385
|
-
id:
|
|
386
|
-
|
|
394
|
+
id:
|
|
395
|
+
config.runtime.target === "supabase"
|
|
396
|
+
? "supabase-cli"
|
|
397
|
+
: "postgresql-image",
|
|
398
|
+
label:
|
|
399
|
+
config.runtime.target === "supabase"
|
|
400
|
+
? "Supabase CLI"
|
|
401
|
+
: "PostgreSQL image",
|
|
387
402
|
run: async () => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
403
|
+
if (config.runtime.target === "supabase") {
|
|
404
|
+
const result = commandAvailable("supabase");
|
|
405
|
+
if (!result.available)
|
|
406
|
+
throw new Error("Supabase CLI is unavailable.");
|
|
407
|
+
return result.version;
|
|
408
|
+
}
|
|
409
|
+
const result = commandAvailable("docker", [
|
|
410
|
+
"image",
|
|
411
|
+
"inspect",
|
|
412
|
+
config.postgresql.image,
|
|
413
|
+
]);
|
|
414
|
+
if (!result.available) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`PostgreSQL image ${config.postgresql.image} is unavailable locally.`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return config.postgresql.image;
|
|
391
420
|
},
|
|
392
|
-
remediation:
|
|
421
|
+
remediation:
|
|
422
|
+
config.runtime.target === "supabase"
|
|
423
|
+
? "Install the Supabase CLI in the project and retry."
|
|
424
|
+
: `Pull and review ${config.postgresql.image}, then retry.`,
|
|
393
425
|
},
|
|
394
426
|
{
|
|
395
427
|
id: "docker",
|
|
@@ -412,13 +444,17 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
412
444
|
run: async () => {
|
|
413
445
|
await Promise.all([
|
|
414
446
|
verifyPath(paths.migrationDirectory, "directory"),
|
|
415
|
-
|
|
447
|
+
...(paths.rehearsalConfig
|
|
448
|
+
? [verifyPath(paths.rehearsalConfig, "file")]
|
|
449
|
+
: []),
|
|
416
450
|
verifyPath(paths.sanitizationPolicy, "file"),
|
|
417
451
|
...(paths.runtimeAdapter
|
|
418
452
|
? [verifyPath(paths.runtimeAdapter, "file")]
|
|
419
453
|
: []),
|
|
420
454
|
]);
|
|
421
|
-
return
|
|
455
|
+
return config.runtime.target === "supabase"
|
|
456
|
+
? "migrations, local runtime config, and sanitization policy exist"
|
|
457
|
+
: "migrations and sanitization policy exist";
|
|
422
458
|
},
|
|
423
459
|
remediation:
|
|
424
460
|
"Correct the missing project path in the Rehearsal configuration.",
|
|
@@ -446,6 +482,9 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
446
482
|
id: "service-environment",
|
|
447
483
|
label: "Local service credentials",
|
|
448
484
|
run: async () => {
|
|
485
|
+
if (config.runtime.target === "postgresql") {
|
|
486
|
+
return "no local service credentials required";
|
|
487
|
+
}
|
|
449
488
|
const environment = await readRehearsalServiceEnvironment({
|
|
450
489
|
path: paths.serviceEnvironment,
|
|
451
490
|
keys: config.supabase.serviceEnvironmentVariables,
|
|
@@ -472,6 +511,42 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
472
511
|
);
|
|
473
512
|
const occupied = availability.filter((entry) => !entry.available);
|
|
474
513
|
if (occupied.length) {
|
|
514
|
+
if (config.runtime.target === "postgresql") {
|
|
515
|
+
const status = spawnSync(
|
|
516
|
+
"docker",
|
|
517
|
+
[
|
|
518
|
+
"ps",
|
|
519
|
+
"--filter",
|
|
520
|
+
`label=com.rehearsal-db.project=${config.runtime.projectId}`,
|
|
521
|
+
"--format",
|
|
522
|
+
"{{.Ports}}",
|
|
523
|
+
],
|
|
524
|
+
{
|
|
525
|
+
cwd: projectRoot,
|
|
526
|
+
encoding: "utf8",
|
|
527
|
+
env: Object.fromEntries(
|
|
528
|
+
["HOME", "LANG", "LC_ALL", "PATH", "SHELL", "TMPDIR"].flatMap(
|
|
529
|
+
(key) =>
|
|
530
|
+
process.env[key] === undefined
|
|
531
|
+
? []
|
|
532
|
+
: [[key, process.env[key]]],
|
|
533
|
+
),
|
|
534
|
+
),
|
|
535
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
if (
|
|
539
|
+
status.status !== 0 ||
|
|
540
|
+
!String(status.stdout).includes(
|
|
541
|
+
`127.0.0.1:${config.runtime.ports.database}->5432/tcp`,
|
|
542
|
+
)
|
|
543
|
+
) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Configured database port is occupied by an unverified process: database=${config.runtime.ports.database}.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return "occupied only by the configured local Rehearsal runtime";
|
|
549
|
+
}
|
|
475
550
|
const serviceEnvironment = await readRehearsalServiceEnvironment({
|
|
476
551
|
path: paths.serviceEnvironment,
|
|
477
552
|
keys: config.supabase.serviceEnvironmentVariables,
|
|
@@ -576,7 +651,7 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
576
651
|
},
|
|
577
652
|
{
|
|
578
653
|
id: "safety-policy",
|
|
579
|
-
label: "Hosted
|
|
654
|
+
label: "Hosted target configuration",
|
|
580
655
|
run: async () => {
|
|
581
656
|
if (
|
|
582
657
|
config.safety.hostedAccess !== "disabled" ||
|
|
@@ -585,8 +660,8 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
585
660
|
throw new Error("The safety policy is not fail closed.");
|
|
586
661
|
}
|
|
587
662
|
return config.safety.authenticationProviders.length
|
|
588
|
-
? `hosted
|
|
589
|
-
: "hosted
|
|
663
|
+
? `hosted database targets disabled; hosted credentials quarantined; local identity providers: ${config.safety.authenticationProviders.join(", ")}`
|
|
664
|
+
: "hosted database targets disabled; common hosted credentials quarantined";
|
|
590
665
|
},
|
|
591
666
|
remediation: "Restore the version-1 fail-closed safety policy.",
|
|
592
667
|
},
|
|
@@ -236,7 +236,11 @@ const buildForeignKeyChecks = (manifest) =>
|
|
|
236
236
|
})
|
|
237
237
|
.join("\n\t");
|
|
238
238
|
|
|
239
|
-
export const buildRestoreSqlSuffix = ({
|
|
239
|
+
export const buildRestoreSqlSuffix = ({
|
|
240
|
+
manifest,
|
|
241
|
+
tableCounts,
|
|
242
|
+
restoreSupabaseAuth = true,
|
|
243
|
+
}) => {
|
|
240
244
|
if (!Array.isArray(manifest?.tables) || !tableCounts) {
|
|
241
245
|
throw new Error("A reviewed manifest and exact row counts are required.");
|
|
242
246
|
}
|
|
@@ -252,7 +256,7 @@ truncate table ${manifest.tables
|
|
|
252
256
|
.map((table) => `public.${quoteIdentifier(table.name)}`)
|
|
253
257
|
.join(", ")}
|
|
254
258
|
restart identity cascade;
|
|
255
|
-
${buildAuthRestoreSql(manifest)}
|
|
259
|
+
${restoreSupabaseAuth ? buildAuthRestoreSql(manifest) : ""}
|
|
256
260
|
${tables.map(buildTableRestoreSql).join("\n")}
|
|
257
261
|
${tables.map(buildIdentitySequenceSql).filter(Boolean).join("\n")}
|
|
258
262
|
set local session_replication_role = origin;
|
|
@@ -9,7 +9,9 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
9
9
|
import {
|
|
10
10
|
inspectDetectedProject,
|
|
11
11
|
renderDetectedConfig,
|
|
12
|
+
renderDetectedPostgresqlConfig,
|
|
12
13
|
} from "./configuration.mjs";
|
|
14
|
+
import { resolveRuntimeTarget } from "../runtime/runtime_target.mjs";
|
|
13
15
|
|
|
14
16
|
const CONFIG_PATH = "rehearsal.config.mjs";
|
|
15
17
|
const LOCAL_SUPABASE_CONFIG_PATH =
|
|
@@ -209,20 +211,26 @@ const assertCreateTargetAvailable = async (file) => {
|
|
|
209
211
|
export const planRehearsalSetup = async ({
|
|
210
212
|
projectRoot = process.cwd(),
|
|
211
213
|
isPortAvailable,
|
|
214
|
+
target = "supabase",
|
|
212
215
|
} = {}) => {
|
|
213
216
|
const root = resolve(projectRoot);
|
|
217
|
+
const runtimeTarget = resolveRuntimeTarget(target).id;
|
|
214
218
|
const detected = await inspectDetectedProject({ projectRoot: root });
|
|
215
219
|
const ports = await findAvailableRehearsalPorts({
|
|
216
220
|
...(isPortAvailable ? { isAvailable: isPortAvailable } : {}),
|
|
217
221
|
});
|
|
218
222
|
const applicationUrl = "http://localhost:5175";
|
|
219
223
|
const projectId = `${detected.projectName}-rehearsal`;
|
|
220
|
-
const sourceSupabaseConfig =
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
const sourceSupabaseConfig =
|
|
225
|
+
runtimeTarget === "supabase"
|
|
226
|
+
? await readOptionalFile(join(root, "supabase/config.toml"))
|
|
227
|
+
: null;
|
|
223
228
|
const originalGitignore = await readOptionalFile(join(root, GITIGNORE_PATH));
|
|
224
229
|
const nextGitignore = appendGitignoreEntries(originalGitignore);
|
|
225
|
-
for (const path of [
|
|
230
|
+
for (const path of [
|
|
231
|
+
CONFIG_PATH,
|
|
232
|
+
...(runtimeTarget === "supabase" ? [LOCAL_SUPABASE_CONFIG_PATH] : []),
|
|
233
|
+
]) {
|
|
226
234
|
if (await pathExists(join(root, path))) {
|
|
227
235
|
throw new Error(
|
|
228
236
|
`Rehearsal setup will not overwrite the existing file at ${path}.`,
|
|
@@ -235,23 +243,34 @@ export const planRehearsalSetup = async ({
|
|
|
235
243
|
absolutePath: join(root, CONFIG_PATH),
|
|
236
244
|
action: "create",
|
|
237
245
|
mode: 0o600,
|
|
238
|
-
content:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
content: renderSafeLocalSupabaseConfig({
|
|
249
|
-
projectId,
|
|
250
|
-
applicationUrl,
|
|
251
|
-
ports,
|
|
252
|
-
databaseMajorVersion: databaseMajorVersionFrom(sourceSupabaseConfig),
|
|
253
|
-
}),
|
|
246
|
+
content:
|
|
247
|
+
runtimeTarget === "supabase"
|
|
248
|
+
? renderDetectedConfig(detected, {
|
|
249
|
+
applicationUrl,
|
|
250
|
+
ports,
|
|
251
|
+
})
|
|
252
|
+
: renderDetectedPostgresqlConfig(detected, {
|
|
253
|
+
applicationUrl,
|
|
254
|
+
databasePort: ports.database,
|
|
255
|
+
}),
|
|
254
256
|
},
|
|
257
|
+
...(runtimeTarget === "supabase"
|
|
258
|
+
? [
|
|
259
|
+
{
|
|
260
|
+
path: LOCAL_SUPABASE_CONFIG_PATH,
|
|
261
|
+
absolutePath: join(root, LOCAL_SUPABASE_CONFIG_PATH),
|
|
262
|
+
action: "create",
|
|
263
|
+
mode: 0o600,
|
|
264
|
+
content: renderSafeLocalSupabaseConfig({
|
|
265
|
+
projectId,
|
|
266
|
+
applicationUrl,
|
|
267
|
+
ports,
|
|
268
|
+
databaseMajorVersion:
|
|
269
|
+
databaseMajorVersionFrom(sourceSupabaseConfig),
|
|
270
|
+
}),
|
|
271
|
+
},
|
|
272
|
+
]
|
|
273
|
+
: []),
|
|
255
274
|
{
|
|
256
275
|
path: GITIGNORE_PATH,
|
|
257
276
|
absolutePath: join(root, GITIGNORE_PATH),
|
|
@@ -270,15 +289,24 @@ export const planRehearsalSetup = async ({
|
|
|
270
289
|
projectRoot: root,
|
|
271
290
|
project: detected.projectName,
|
|
272
291
|
projectId,
|
|
292
|
+
target: runtimeTarget,
|
|
273
293
|
applicationUrl,
|
|
274
294
|
detected,
|
|
275
|
-
ports,
|
|
295
|
+
ports: runtimeTarget === "supabase" ? ports : { database: ports.database },
|
|
276
296
|
isPortAvailable: isPortAvailable ?? isRehearsalPortAvailable,
|
|
277
297
|
files,
|
|
278
298
|
safety: [
|
|
279
299
|
"hosted access remains disabled",
|
|
280
|
-
|
|
281
|
-
|
|
300
|
+
runtimeTarget === "supabase"
|
|
301
|
+
? "the Supabase runtime binds to dedicated local ports"
|
|
302
|
+
: "PostgreSQL binds only to a dedicated loopback port",
|
|
303
|
+
...(runtimeTarget === "supabase"
|
|
304
|
+
? [
|
|
305
|
+
"external identity providers, realtime, edge functions, and analytics start disabled",
|
|
306
|
+
]
|
|
307
|
+
: [
|
|
308
|
+
"the runtime uses an already-installed official PostgreSQL image and never pulls implicitly",
|
|
309
|
+
]),
|
|
282
310
|
"configuration files are never overwritten and .gitignore updates are concurrency-checked",
|
|
283
311
|
],
|
|
284
312
|
};
|
|
@@ -331,13 +359,17 @@ export const summarizeRehearsalSetup = (plan, { mode }) => ({
|
|
|
331
359
|
mode,
|
|
332
360
|
project: plan.project,
|
|
333
361
|
projectId: plan.projectId,
|
|
362
|
+
target: plan.target,
|
|
334
363
|
applicationUrl: plan.applicationUrl,
|
|
335
364
|
detected: plan.detected,
|
|
336
|
-
ports:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
365
|
+
ports:
|
|
366
|
+
plan.target === "supabase"
|
|
367
|
+
? {
|
|
368
|
+
api: plan.ports.api,
|
|
369
|
+
database: plan.ports.database,
|
|
370
|
+
studio: plan.ports.studio,
|
|
371
|
+
}
|
|
372
|
+
: { database: plan.ports.database },
|
|
341
373
|
files: plan.files.map((file) => ({ path: file.path, action: file.action })),
|
|
342
374
|
safety: plan.safety,
|
|
343
375
|
nextAction:
|