@rehearsal-db/core 0.1.0-beta.3 → 0.1.0-beta.5
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 +38 -1
- package/README.md +13 -2
- package/SUPPORT.md +5 -0
- package/docs/commands.md +21 -2
- package/docs/getting-started.md +16 -2
- package/docs/releasing.md +1 -1
- package/docs/sanitization.md +9 -0
- package/package.json +6 -3
- package/scripts/lib/rehearsal/baseline_preparation.mjs +138 -23
- package/scripts/lib/rehearsal/diagnostics.mjs +1 -1
- package/scripts/lib/rehearsal/input_discovery.mjs +155 -0
- package/scripts/lib/rehearsal/policy_review.mjs +92 -0
- package/scripts/lib/rehearsal/support_report.mjs +102 -0
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +759 -133
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Complete and safely apply a generated REVIEW REQUIRED policy draft.
|
|
3
|
+
* The caller owns the interactive presentation; this module owns validation and
|
|
4
|
+
* concurrent-change refusal.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { validateRuntimeSanitizationPolicy } from "./sanitization_policy.mjs";
|
|
9
|
+
|
|
10
|
+
export const SAFE_COLUMN_PRESET = Object.freeze({
|
|
11
|
+
action: "REPLACE WITH SYNTHETIC",
|
|
12
|
+
generated: "NEVER",
|
|
13
|
+
identity: "NO",
|
|
14
|
+
foreignKey: null,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const createSafeTablePreset = (columns) =>
|
|
18
|
+
Object.fromEntries(
|
|
19
|
+
columns.map((column) => [
|
|
20
|
+
typeof column === "string" ? column : column.name,
|
|
21
|
+
{ ...SAFE_COLUMN_PRESET },
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const suggestPolicyExceptionColumns = (columns) =>
|
|
26
|
+
columns
|
|
27
|
+
.map((column) => (typeof column === "string" ? column : column.name))
|
|
28
|
+
.filter(
|
|
29
|
+
(column) =>
|
|
30
|
+
column === "id" ||
|
|
31
|
+
column.endsWith("_id") ||
|
|
32
|
+
["created_at", "updated_at", "deleted_at"].includes(column),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const readReviewablePolicyDraft = async (path) => {
|
|
36
|
+
const source = await readFile(path, "utf8");
|
|
37
|
+
const draft = JSON.parse(source);
|
|
38
|
+
if (draft?.draft !== true || !Array.isArray(draft.tables)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Interactive review requires a generated sanitization policy with draft: true.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return { source, draft };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const completePolicyDraft = async ({
|
|
47
|
+
draft,
|
|
48
|
+
reviewColumn,
|
|
49
|
+
reviewTable,
|
|
50
|
+
}) => {
|
|
51
|
+
if (draft?.draft !== true || !Array.isArray(draft.tables)) {
|
|
52
|
+
throw new Error("A generated REVIEW REQUIRED policy draft is required.");
|
|
53
|
+
}
|
|
54
|
+
const tables = [];
|
|
55
|
+
for (const table of draft.tables) {
|
|
56
|
+
const columnNames = (table.columns ?? []).map((column) => column.name);
|
|
57
|
+
const tableDecisions = reviewTable
|
|
58
|
+
? await reviewTable({ table: table.name, columns: columnNames })
|
|
59
|
+
: {};
|
|
60
|
+
const columns = [];
|
|
61
|
+
for (const column of table.columns ?? []) {
|
|
62
|
+
const reviewed =
|
|
63
|
+
tableDecisions?.[column.name] ??
|
|
64
|
+
(await reviewColumn?.({
|
|
65
|
+
table: table.name,
|
|
66
|
+
column: column.name,
|
|
67
|
+
}));
|
|
68
|
+
if (!reviewed) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Policy review did not classify ${table.name}.${column.name}.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
columns.push({ name: column.name, ...reviewed });
|
|
74
|
+
}
|
|
75
|
+
tables.push({ ...table, columns });
|
|
76
|
+
}
|
|
77
|
+
const reviewed = { ...draft, tables };
|
|
78
|
+
delete reviewed.draft;
|
|
79
|
+
return validateRuntimeSanitizationPolicy(reviewed);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const applyReviewedPolicy = async ({ path, originalSource, policy }) => {
|
|
83
|
+
validateRuntimeSanitizationPolicy(policy);
|
|
84
|
+
if ((await readFile(path, "utf8")) !== originalSource) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"The sanitization policy changed during review; no reviewed policy was written.",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
await writeFile(path, `${JSON.stringify(policy, null, 2)}\n`, {
|
|
90
|
+
mode: 0o600,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a copy-ready, data-free support report. The report intentionally omits
|
|
3
|
+
* project names, paths, command output, environment-variable names, row data,
|
|
4
|
+
* migration contents, and baseline identifiers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { arch, platform, release } from "node:os";
|
|
9
|
+
import { REHEARSAL_VERSION, redactDiagnosticValue } from "./diagnostics.mjs";
|
|
10
|
+
import { runRehearsalDoctor } from "./plan.mjs";
|
|
11
|
+
|
|
12
|
+
export const REHEARSAL_SUPPORT_URL =
|
|
13
|
+
"https://github.com/Ddupasquier/rehearsal-db/issues/new?template=bug.yml";
|
|
14
|
+
|
|
15
|
+
const safeEnvironment = () =>
|
|
16
|
+
Object.fromEntries(
|
|
17
|
+
["HOME", "LANG", "LC_ALL", "PATH", "SHELL", "TMPDIR"].flatMap((key) =>
|
|
18
|
+
process.env[key] === undefined ? [] : [[key, process.env[key]]],
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const runVersionCommand = (command, args) =>
|
|
23
|
+
spawnSync(command, args, {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
env: safeEnvironment(),
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const inspectTool = ({ command, args, runCommand }) => {
|
|
31
|
+
const result = runCommand(command, args);
|
|
32
|
+
if (result.error?.code === "ENOENT") return { status: "not_installed" };
|
|
33
|
+
if (result.status !== 0) return { status: "unavailable" };
|
|
34
|
+
const version = String(result.stdout || result.stderr)
|
|
35
|
+
.trim()
|
|
36
|
+
.split(/\r?\n/u)[0]
|
|
37
|
+
.slice(0, 200);
|
|
38
|
+
return {
|
|
39
|
+
status: "available",
|
|
40
|
+
version: redactDiagnosticValue(version || "version not reported"),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const collectRehearsalSupportReport = async (
|
|
45
|
+
options = {},
|
|
46
|
+
{
|
|
47
|
+
runDoctor = runRehearsalDoctor,
|
|
48
|
+
runCommand = runVersionCommand,
|
|
49
|
+
system = {
|
|
50
|
+
platform: platform(),
|
|
51
|
+
release: release(),
|
|
52
|
+
architecture: arch(),
|
|
53
|
+
node: process.versions.node,
|
|
54
|
+
},
|
|
55
|
+
} = {},
|
|
56
|
+
) => {
|
|
57
|
+
const doctor = await runDoctor(options);
|
|
58
|
+
return {
|
|
59
|
+
rehearsalVersion: REHEARSAL_VERSION,
|
|
60
|
+
system,
|
|
61
|
+
tools: {
|
|
62
|
+
npm: inspectTool({ command: "npm", args: ["--version"], runCommand }),
|
|
63
|
+
supabase: inspectTool({
|
|
64
|
+
command: "supabase",
|
|
65
|
+
args: ["--version"],
|
|
66
|
+
runCommand,
|
|
67
|
+
}),
|
|
68
|
+
dockerClient: inspectTool({
|
|
69
|
+
command: "docker",
|
|
70
|
+
args: ["--version"],
|
|
71
|
+
runCommand,
|
|
72
|
+
}),
|
|
73
|
+
dockerServer: inspectTool({
|
|
74
|
+
command: "docker",
|
|
75
|
+
args: ["info", "--format", "{{.ServerVersion}}"],
|
|
76
|
+
runCommand,
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
readiness: {
|
|
80
|
+
state: doctor.state,
|
|
81
|
+
checks: doctor.checks.map(({ id, label, status, remediation }) => ({
|
|
82
|
+
id,
|
|
83
|
+
label,
|
|
84
|
+
status,
|
|
85
|
+
...(status === "fail" && remediation ? { remediation } : {}),
|
|
86
|
+
})),
|
|
87
|
+
quarantinedHostedVariableCount:
|
|
88
|
+
doctor.ambientHostedVariables?.presentButQuarantined.length ?? 0,
|
|
89
|
+
},
|
|
90
|
+
privacy: {
|
|
91
|
+
omits: [
|
|
92
|
+
"row values",
|
|
93
|
+
"credentials",
|
|
94
|
+
"project paths",
|
|
95
|
+
"migration SQL",
|
|
96
|
+
"baseline identifiers",
|
|
97
|
+
],
|
|
98
|
+
reviewBeforeSharing: true,
|
|
99
|
+
},
|
|
100
|
+
supportUrl: REHEARSAL_SUPPORT_URL,
|
|
101
|
+
};
|
|
102
|
+
};
|