@rehearsal-db/core 0.1.0-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.
Files changed (35) hide show
  1. package/BENCHMARKS.md +61 -0
  2. package/CHANGELOG.md +59 -0
  3. package/COMPATIBILITY.md +22 -0
  4. package/LICENSE +21 -0
  5. package/README.md +375 -0
  6. package/SECURITY.md +19 -0
  7. package/SUPPORT.md +15 -0
  8. package/docs/adapters.md +23 -0
  9. package/docs/baselines.md +33 -0
  10. package/docs/commands.md +35 -0
  11. package/docs/configuration.md +98 -0
  12. package/docs/getting-started.md +247 -0
  13. package/docs/glossary.md +33 -0
  14. package/docs/production-source.md +33 -0
  15. package/docs/releasing.md +79 -0
  16. package/docs/sanitization.md +69 -0
  17. package/docs/security-model.md +40 -0
  18. package/docs/troubleshooting.md +50 -0
  19. package/docs/tutorial.md +96 -0
  20. package/package.json +77 -0
  21. package/scripts/lib/environment/local_supabase.mjs +197 -0
  22. package/scripts/lib/rehearsal/baseline_artifact.mjs +536 -0
  23. package/scripts/lib/rehearsal/baseline_builder.mjs +155 -0
  24. package/scripts/lib/rehearsal/configuration.d.mts +85 -0
  25. package/scripts/lib/rehearsal/configuration.mjs +559 -0
  26. package/scripts/lib/rehearsal/diagnostics.mjs +193 -0
  27. package/scripts/lib/rehearsal/migration_history.mjs +220 -0
  28. package/scripts/lib/rehearsal/plan.mjs +587 -0
  29. package/scripts/lib/rehearsal/process_environment.mjs +64 -0
  30. package/scripts/lib/rehearsal/runtime_restore.mjs +310 -0
  31. package/scripts/lib/rehearsal/sanitization_policy.mjs +169 -0
  32. package/scripts/lib/rehearsal/schema_snapshot.mjs +113 -0
  33. package/scripts/lib/rehearsal/service_environment.mjs +82 -0
  34. package/scripts/operations/database/manage_rehearsal_database.mjs +784 -0
  35. package/scripts/operations/rehearsal/rehearsal_cli.mjs +565 -0
@@ -0,0 +1,50 @@
1
+ # Troubleshooting
2
+
3
+ ## Doctor says Docker is unavailable
4
+
5
+ Start Docker Desktop or Colima, confirm `docker info`, then rerun `rehearsal doctor`.
6
+ Restarting the computer is rarely necessary.
7
+
8
+ ## A port is already in use
9
+
10
+ Choose three unique non-privileged ports in config and mirror them in the dedicated
11
+ Supabase config. Do not stop an unrelated database to make the default ports fit.
12
+
13
+ ## Baseline checksum mismatch
14
+
15
+ Do not edit the checksum or manifest. The artifact is no longer the reviewed input.
16
+ Rebuild the generation through the project-owned baseline process.
17
+
18
+ ## Historical migration changed
19
+
20
+ Restore the exact represented bytes or intentionally build and review a new baseline.
21
+ Renaming edited history into the candidate suffix does not repair lineage.
22
+
23
+ ## Candidate confirmation mismatch
24
+
25
+ Run `rehearsal candidates` again. Review every filename, then confirm the new digest only
26
+ if the exact set is intended.
27
+
28
+ ## Migration failed
29
+
30
+ The runtime is untrusted and should be removed automatically. Correct the candidate SQL,
31
+ rerun doctor and candidates, then execute with the new digest.
32
+
33
+ ## Application proof failed
34
+
35
+ Run the project proof directly against the generated local environment. Fix the app,
36
+ adapter, fixture expectation, or migration; do not weaken the proof to obtain a pass.
37
+
38
+ ## Local authentication fails
39
+
40
+ Confirm the provider is enabled in the dedicated local Supabase config, the allowlisted
41
+ service environment file exists with owner-only permissions, and the provider callback
42
+ is the local Auth callback. Do not paste provider secrets into config or terminal output.
43
+
44
+ ## Runtime changes disappeared
45
+
46
+ `reset` deliberately restores the immutable baseline. `stop` should preserve Docker
47
+ state, but runtime removal or a failed migration discards untrusted state.
48
+
49
+ Use `--debug` only after ordinary output is insufficient. Diagnostics are redacted, but
50
+ you should still review output before sharing it publicly.
@@ -0,0 +1,96 @@
1
+ # Tutorial: rehearse a migration in a new project
2
+
3
+ This walkthrough uses a fictional `widgets` application. It demonstrates the complete
4
+ consumer workflow without importing private or hosted data.
5
+
6
+ ## Create the application migration history
7
+
8
+ The project begins with one historical migration:
9
+
10
+ ```sql
11
+ create table public.widgets (
12
+ id bigint generated always as identity primary key,
13
+ name text not null
14
+ );
15
+ ```
16
+
17
+ Place it at `supabase/migrations/20260101000000_create_widgets.sql`. Add a second,
18
+ candidate migration:
19
+
20
+ ```sql
21
+ alter table public.widgets add column description text;
22
+ ```
23
+
24
+ Place that at `supabase/migrations/20260101000100_add_widget_description.sql`.
25
+
26
+ ## Configure the isolated runtime
27
+
28
+ Run `npx rehearsal init --write`, then edit the result. Give the runtime unique local
29
+ ports and a unique project ID. Its `rehearsalConfig` must point at a dedicated, unlinked
30
+ Supabase config. Never reuse a hosted project reference or production environment file.
31
+
32
+ Keep the generated `.rehearsal` directory ignored. It contains local artifacts and
33
+ runtime state, not source code.
34
+
35
+ ## Build a synthetic baseline
36
+
37
+ The baseline contains the historical migration bundle, a one-row sanitized data stream,
38
+ and a manifest binding their checksums. Baseline construction is deliberately separate
39
+ from runtime execution: the application owns extraction and policy; the engine accepts
40
+ only a completed, verified artifact.
41
+
42
+ Write the safe rows to `rehearsal/synthetic-data.ndjson` and the exact represented
43
+ statements to `rehearsal/migration-ledger.json`, then run:
44
+
45
+ ```bash
46
+ npx rehearsal baseline create \
47
+ --records=rehearsal/synthetic-data.ndjson \
48
+ --ledger=rehearsal/migration-ledger.json
49
+ ```
50
+
51
+ For an executable sample, inspect `tests/fixtures/rehearsal-project` in the repository.
52
+ The maintained fixture proof invokes this public command through the packed npm tarball
53
+ and never contacts a hosted service.
54
+
55
+ ## Prove planning is non-mutating
56
+
57
+ ```bash
58
+ npx rehearsal doctor
59
+ npx rehearsal explain
60
+ npx rehearsal run --dry-run
61
+ npx rehearsal candidates --json
62
+ ```
63
+
64
+ `explain` and `run --dry-run` return the same execution plan. At this point no local
65
+ database has been restored.
66
+
67
+ ## Run and inspect
68
+
69
+ Copy the exact digest from `candidates`:
70
+
71
+ ```bash
72
+ npx rehearsal run --confirm-candidates=<sha256>
73
+ npx rehearsal inspect migrations
74
+ npx rehearsal status
75
+ ```
76
+
77
+ The historical migration should be `represented_by_baseline`; the description migration
78
+ should be `applied_to_current_runtime`. Test normal creates, updates, and deletes through
79
+ your local application. They affect only this disposable database.
80
+
81
+ ## Prove failure behavior
82
+
83
+ Add a timestamped migration containing invalid SQL, rerun `candidates`, and use its new
84
+ digest. The command must fail with `migration_candidate_failure`, remove the untrusted
85
+ runtime, and never produce a successful receipt.
86
+
87
+ Then restore the valid migration set and rerun:
88
+
89
+ ```bash
90
+ npx rehearsal reset
91
+ npx rehearsal verify
92
+ npx rehearsal stop
93
+ ```
94
+
95
+ That cycle—plan, confirm exact bytes, run, exercise the app, reset—is the normal Rehearsal
96
+ workflow.
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@rehearsal-db/core",
3
+ "version": "0.1.0-beta.1",
4
+ "private": false,
5
+ "description": "Safely rehearse Supabase migrations against sanitized, production-shaped PostgreSQL data.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Ddupasquier/rehearsal-db.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/Ddupasquier/rehearsal-db/issues"
12
+ },
13
+ "homepage": "https://github.com/Ddupasquier/rehearsal-db#readme",
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "tag": "beta",
17
+ "provenance": true
18
+ },
19
+ "type": "module",
20
+ "engines": {
21
+ "node": ">=24 <25"
22
+ },
23
+ "bin": {
24
+ "rehearsal": "scripts/operations/rehearsal/rehearsal_cli.mjs"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./scripts/lib/rehearsal/configuration.d.mts",
29
+ "import": "./scripts/lib/rehearsal/configuration.mjs"
30
+ },
31
+ "./baseline": "./scripts/lib/rehearsal/baseline_artifact.mjs",
32
+ "./baseline-builder": "./scripts/lib/rehearsal/baseline_builder.mjs",
33
+ "./diagnostics": "./scripts/lib/rehearsal/diagnostics.mjs",
34
+ "./migrations": "./scripts/lib/rehearsal/migration_history.mjs",
35
+ "./process-environment": "./scripts/lib/rehearsal/process_environment.mjs",
36
+ "./schema": "./scripts/lib/rehearsal/schema_snapshot.mjs",
37
+ "./service-environment": "./scripts/lib/rehearsal/service_environment.mjs"
38
+ },
39
+ "files": [
40
+ "scripts/lib/environment/local_supabase.mjs",
41
+ "scripts/lib/rehearsal",
42
+ "scripts/operations/database/manage_rehearsal_database.mjs",
43
+ "scripts/operations/rehearsal/rehearsal_cli.mjs",
44
+ "docs/*.md",
45
+ "README.md",
46
+ "BENCHMARKS.md",
47
+ "CHANGELOG.md",
48
+ "SECURITY.md",
49
+ "COMPATIBILITY.md",
50
+ "SUPPORT.md",
51
+ "LICENSE"
52
+ ],
53
+ "scripts": {
54
+ "test": "vitest run",
55
+ "test:unit": "vitest run tests/scripts",
56
+ "test:docs": "vitest run tests/contracts/documentationCommands.test.mjs",
57
+ "test:fixture": "node scripts/operations/rehearsal/prove_independent_fixture.mjs",
58
+ "check": "npm run check:syntax && npm run test && npm run format:check && npm run package:audit",
59
+ "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
60
+ "format": "prettier --write .",
61
+ "format:check": "prettier --check .",
62
+ "package:audit": "node scripts/operations/rehearsal/audit_package.mjs"
63
+ },
64
+ "keywords": [
65
+ "postgresql",
66
+ "supabase",
67
+ "migration",
68
+ "testing",
69
+ "database"
70
+ ],
71
+ "license": "MIT",
72
+ "dependencies": {},
73
+ "devDependencies": {
74
+ "prettier": "^3.9.6",
75
+ "vitest": "^4.1.10"
76
+ }
77
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Purpose: Start and inspect local-only Supabase workdirs without inheriting hosted
3
+ * credentials. Do not run directly; this module is reusable script infrastructure.
4
+ */
5
+
6
+ import { spawnSync } from "node:child_process";
7
+ import { createCleanProcessEnvironment } from "../rehearsal/process_environment.mjs";
8
+
9
+ export const parseSupabaseStatusEnvironment = (output) => {
10
+ const values = {};
11
+ for (const line of output.split("\n")) {
12
+ const match = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
13
+ if (!match) continue;
14
+ let value = match[2].trim();
15
+ if (value.startsWith('"') && value.endsWith('"')) value = JSON.parse(value);
16
+ values[match[1]] = value;
17
+ }
18
+ return values;
19
+ };
20
+
21
+ export const runLocalCommand = (
22
+ command,
23
+ args,
24
+ {
25
+ capture = false,
26
+ cwd = process.cwd(),
27
+ input,
28
+ environment = {},
29
+ maxBuffer = 16 * 1024 * 1024,
30
+ } = {},
31
+ ) => {
32
+ const shouldPipe = capture || input !== undefined;
33
+ const result = spawnSync(command, args, {
34
+ cwd,
35
+ encoding: "utf8",
36
+ env: createCleanProcessEnvironment({ overrides: environment }),
37
+ input,
38
+ maxBuffer,
39
+ stdio: shouldPipe ? ["pipe", "pipe", "pipe"] : "inherit",
40
+ });
41
+ if (result.error) throw result.error;
42
+ if (result.status !== 0) {
43
+ const detail = [result.stdout, result.stderr]
44
+ .filter(Boolean)
45
+ .join("\n")
46
+ .trim();
47
+ throw new Error(
48
+ `${command} ${args.join(" ")} failed${detail ? `:\n${detail}` : "."}`,
49
+ );
50
+ }
51
+ return result.stdout ?? "";
52
+ };
53
+
54
+ export const localCommandSucceeds = (
55
+ command,
56
+ args,
57
+ { cwd = process.cwd(), environment = {} } = {},
58
+ ) =>
59
+ spawnSync(command, args, {
60
+ cwd,
61
+ env: createCleanProcessEnvironment({ overrides: environment }),
62
+ stdio: "ignore",
63
+ }).status === 0;
64
+
65
+ export const ensureLocalContainerRuntime = ({ cwd = process.cwd() } = {}) => {
66
+ if (localCommandSucceeds("docker", ["info"], { cwd })) return;
67
+ if (localCommandSucceeds("colima", ["version"], { cwd })) {
68
+ runLocalCommand(
69
+ "colima",
70
+ ["start", "--cpu", "4", "--memory", "4", "--disk", "40"],
71
+ { cwd },
72
+ );
73
+ }
74
+ if (!localCommandSucceeds("docker", ["info"], { cwd })) {
75
+ throw new Error(
76
+ "A local Docker-compatible runtime is required. Install Docker or Colima, then rerun the command.",
77
+ );
78
+ }
79
+ };
80
+
81
+ const withWorkdir = (args, workdir) =>
82
+ workdir ? [...args, "--workdir", workdir] : args;
83
+
84
+ export const readLocalSupabaseEnvironment = ({
85
+ cwd,
86
+ workdir,
87
+ environment = {},
88
+ } = {}) => {
89
+ const output = runLocalCommand(
90
+ "supabase",
91
+ withWorkdir(["status", "-o", "env"], workdir),
92
+ { capture: true, cwd, environment },
93
+ );
94
+ const values = parseSupabaseStatusEnvironment(output);
95
+ const localEnvironment = {
96
+ apiUrl: values.API_URL,
97
+ publishableKey: values.PUBLISHABLE_KEY ?? values.ANON_KEY,
98
+ serviceRoleKey: values.SERVICE_ROLE_KEY ?? values.SECRET_KEY,
99
+ studioUrl: values.STUDIO_URL,
100
+ };
101
+ for (const [label, value] of [
102
+ ["API URL", localEnvironment.apiUrl],
103
+ ["publishable key", localEnvironment.publishableKey],
104
+ ["service-role key", localEnvironment.serviceRoleKey],
105
+ ]) {
106
+ if (!value) throw new Error(`Local Supabase status omitted its ${label}.`);
107
+ }
108
+ const hostname = new URL(localEnvironment.apiUrl).hostname;
109
+ if (!["127.0.0.1", "::1", "localhost"].includes(hostname)) {
110
+ throw new Error("Refusing to use a non-loopback Supabase status target.");
111
+ }
112
+ return localEnvironment;
113
+ };
114
+
115
+ export const startLocalSupabase = ({
116
+ cwd,
117
+ workdir,
118
+ exclude = [],
119
+ environment = {},
120
+ applyMigrations = true,
121
+ } = {}) => {
122
+ ensureLocalContainerRuntime({ cwd });
123
+ const startArguments = withWorkdir(
124
+ ["start", ...(exclude.length ? ["--exclude", exclude.join(",")] : [])],
125
+ workdir,
126
+ );
127
+ runLocalCommand("supabase", startArguments, {
128
+ capture: true,
129
+ cwd,
130
+ environment,
131
+ });
132
+ if (applyMigrations) {
133
+ runLocalCommand(
134
+ "supabase",
135
+ withWorkdir(["migration", "up", "--local"], workdir),
136
+ { capture: true, cwd, environment },
137
+ );
138
+ }
139
+ return readLocalSupabaseEnvironment({ cwd, workdir, environment });
140
+ };
141
+
142
+ export const stopLocalSupabase = ({ cwd, workdir, environment = {} } = {}) => {
143
+ if (!localCommandSucceeds("docker", ["info"], { cwd })) return;
144
+ runLocalCommand("supabase", withWorkdir(["stop"], workdir), {
145
+ cwd,
146
+ environment,
147
+ });
148
+ };
149
+
150
+ export const removeLocalSupabaseProjectResources = ({
151
+ cwd = process.cwd(),
152
+ projectId,
153
+ } = {}) => {
154
+ if (!/^[a-z0-9][a-z0-9-]{1,62}$/u.test(projectId ?? "")) {
155
+ throw new Error(
156
+ "Refusing to remove local Supabase resources without an exact safe project id.",
157
+ );
158
+ }
159
+ const label = `com.supabase.cli.project=${projectId}`;
160
+ const list = (resource, format) =>
161
+ runLocalCommand(
162
+ "docker",
163
+ [resource, "ls", "--filter", `label=${label}`, "--format", format],
164
+ { capture: true, cwd },
165
+ )
166
+ .split("\n")
167
+ .map((value) => value.trim())
168
+ .filter(Boolean);
169
+ const containers = runLocalCommand(
170
+ "docker",
171
+ ["ps", "--all", "--filter", `label=${label}`, "--format", "{{.ID}}"],
172
+ { capture: true, cwd },
173
+ )
174
+ .split("\n")
175
+ .map((value) => value.trim())
176
+ .filter(Boolean);
177
+ if (containers.length) {
178
+ runLocalCommand("docker", ["rm", "--force", ...containers], {
179
+ capture: true,
180
+ cwd,
181
+ });
182
+ }
183
+ const volumes = list("volume", "{{.Name}}");
184
+ if (volumes.length) {
185
+ runLocalCommand("docker", ["volume", "rm", "--force", ...volumes], {
186
+ capture: true,
187
+ cwd,
188
+ });
189
+ }
190
+ const networks = list("network", "{{.ID}}");
191
+ if (networks.length) {
192
+ runLocalCommand("docker", ["network", "rm", ...networks], {
193
+ capture: true,
194
+ cwd,
195
+ });
196
+ }
197
+ };