@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.15
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 +93 -0
- package/README.md +4 -1
- package/dist/package.json +100 -0
- package/dist/scripts/install/create_root_config.d.mts +3 -0
- package/dist/scripts/install/create_root_config.mjs +13 -0
- package/dist/scripts/runtime/manage_database.d.mts +6 -0
- package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
- package/dist/src/application/session.d.mts +96 -0
- package/dist/src/application/session.mjs +380 -0
- package/dist/src/baseline/artifact.d.mts +119 -0
- package/dist/src/baseline/artifact.mjs +522 -0
- package/dist/src/baseline/builder.d.mts +19 -0
- package/dist/src/baseline/builder.mjs +142 -0
- package/dist/src/baseline/input_discovery.d.mts +13 -0
- package/dist/src/baseline/input_discovery.mjs +149 -0
- package/dist/src/baseline/policy_review.d.mts +56 -0
- package/dist/src/baseline/policy_review.mjs +73 -0
- package/dist/src/baseline/preparation.d.mts +63 -0
- package/dist/src/baseline/preparation.mjs +295 -0
- package/dist/src/baseline/privacy_engine.d.mts +162 -0
- package/dist/src/baseline/privacy_engine.mjs +1162 -0
- package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
- package/dist/src/baseline/privacy_structured_json.mjs +464 -0
- package/dist/src/baseline/sanitization_policy.d.mts +69 -0
- package/dist/src/baseline/sanitization_policy.mjs +227 -0
- package/dist/src/baseline/schema_snapshot.d.mts +19 -0
- package/dist/src/baseline/schema_snapshot.mjs +79 -0
- package/dist/src/cli/arguments.d.mts +43 -0
- package/dist/src/cli/arguments.mjs +135 -0
- package/dist/src/cli/guided.d.mts +51 -0
- package/dist/src/cli/guided.mjs +713 -0
- package/dist/src/cli/init_command.d.mts +21 -0
- package/dist/src/cli/init_command.mjs +90 -0
- package/dist/src/cli/project_commands.d.mts +157 -0
- package/dist/src/cli/project_commands.mjs +183 -0
- package/dist/src/cli/rehearsal.d.mts +7 -0
- package/dist/src/cli/rehearsal.mjs +706 -0
- package/dist/src/cli/renderers.d.mts +299 -0
- package/dist/src/cli/renderers.mjs +554 -0
- package/dist/src/cli/runtime_commands.d.mts +265 -0
- package/dist/src/cli/runtime_commands.mjs +599 -0
- package/dist/src/cli/source_commands.d.mts +432 -0
- package/dist/src/cli/source_commands.mjs +299 -0
- package/dist/src/cli/terminal.d.mts +59 -0
- package/dist/src/cli/terminal.mjs +230 -0
- package/dist/src/identity/claim.d.mts +144 -0
- package/dist/src/identity/claim.mjs +947 -0
- package/dist/src/identity/provider_configuration.d.mts +37 -0
- package/dist/src/identity/provider_configuration.mjs +132 -0
- package/dist/src/identity/storage.d.mts +21 -0
- package/dist/src/identity/storage.mjs +122 -0
- package/dist/src/index.d.mts +4 -0
- package/dist/src/index.mjs +3 -0
- package/dist/src/project/configuration.d.mts +181 -0
- package/dist/src/project/configuration.mjs +958 -0
- package/dist/src/project/install_scaffold.d.mts +42 -0
- package/dist/src/project/install_scaffold.mjs +263 -0
- package/dist/src/project/setup.d.mts +87 -0
- package/dist/src/project/setup.mjs +414 -0
- package/dist/src/project/support_report.d.mts +88 -0
- package/dist/src/project/support_report.mjs +83 -0
- package/dist/src/runtime/cleanup.d.mts +85 -0
- package/dist/src/runtime/cleanup.mjs +311 -0
- package/dist/src/runtime/migration_history.d.mts +51 -0
- package/dist/src/runtime/migration_history.mjs +170 -0
- package/dist/src/runtime/plan.d.mts +134 -0
- package/dist/src/runtime/plan.mjs +636 -0
- package/dist/src/runtime/policy.d.mts +66 -0
- package/dist/src/runtime/policy.mjs +369 -0
- package/dist/src/runtime/restore.d.mts +22 -0
- package/dist/src/runtime/restore.mjs +276 -0
- package/dist/src/runtime/service_environment.d.mts +11 -0
- package/dist/src/runtime/service_environment.mjs +58 -0
- package/dist/src/runtime/topology.d.mts +51 -0
- package/dist/src/runtime/topology.mjs +128 -0
- package/dist/src/shared/diagnostics.d.mts +97 -0
- package/dist/src/shared/diagnostics.mjs +170 -0
- package/dist/src/shared/human_output.d.mts +2 -0
- package/{src → dist/src}/shared/human_output.mjs +1 -3
- package/dist/src/shared/node_runtime.d.mts +14 -0
- package/dist/src/shared/node_runtime.mjs +25 -0
- package/dist/src/shared/operation_guard.d.mts +22 -0
- package/dist/src/shared/operation_guard.mjs +150 -0
- package/dist/src/shared/process_environment.d.mts +15 -0
- package/dist/src/shared/process_environment.mjs +50 -0
- package/dist/src/source/access.d.mts +165 -0
- package/dist/src/source/access.mjs +602 -0
- package/dist/src/source/asset_transfer.d.mts +27 -0
- package/dist/src/source/asset_transfer.mjs +202 -0
- package/dist/src/source/baseline.d.mts +66 -0
- package/dist/src/source/baseline.mjs +372 -0
- package/dist/src/source/external_reader.d.mts +42 -0
- package/dist/src/source/external_reader.mjs +278 -0
- package/dist/src/source/postgresql_access.d.mts +86 -0
- package/dist/src/source/postgresql_access.mjs +454 -0
- package/dist/src/source/privacy_paths.d.mts +10 -0
- package/dist/src/source/privacy_paths.mjs +27 -0
- package/dist/src/targets/driver.d.mts +2 -0
- package/dist/src/targets/driver.mjs +13 -0
- package/dist/src/targets/postgresql.d.mts +5 -0
- package/dist/src/targets/postgresql.mjs +641 -0
- package/dist/src/targets/supabase.d.mts +8 -0
- package/dist/src/targets/supabase.mjs +659 -0
- package/dist/src/targets/supabase_environment.d.mts +49 -0
- package/dist/src/targets/supabase_environment.mjs +162 -0
- package/dist/src/targets/target.d.mts +33 -0
- package/dist/src/targets/target.mjs +39 -0
- package/docs/README.md +1 -0
- package/docs/architecture.md +17 -9
- package/docs/configuration.md +49 -8
- package/docs/getting-started.md +28 -0
- package/docs/index.md +1 -0
- package/docs/production-source.md +43 -7
- package/docs/releasing.md +5 -3
- package/docs/roadmap.md +3 -2
- package/docs/sanitization.md +415 -14
- package/docs/standalone-workflow.md +12 -6
- package/docs/troubleshooting.md +5 -1
- package/docs/tutorial.md +5 -0
- package/docs/typescript.md +87 -0
- package/package.json +35 -29
- package/scripts/install/create_root_config.mjs +0 -36
- package/src/README.md +0 -17
- package/src/application/session.mjs +0 -435
- package/src/baseline/artifact.mjs +0 -684
- package/src/baseline/builder.mjs +0 -164
- package/src/baseline/input_discovery.mjs +0 -155
- package/src/baseline/policy_review.mjs +0 -92
- package/src/baseline/preparation.mjs +0 -323
- package/src/baseline/privacy_engine.mjs +0 -952
- package/src/baseline/sanitization_policy.mjs +0 -299
- package/src/baseline/schema_snapshot.mjs +0 -113
- package/src/cli/arguments.mjs +0 -126
- package/src/cli/guided.mjs +0 -814
- package/src/cli/init_command.mjs +0 -94
- package/src/cli/rehearsal.mjs +0 -970
- package/src/cli/renderers.mjs +0 -626
- package/src/cli/runtime_commands.mjs +0 -707
- package/src/cli/source_commands.mjs +0 -347
- package/src/cli/terminal.mjs +0 -275
- package/src/identity/claim.mjs +0 -1163
- package/src/identity/provider_configuration.mjs +0 -177
- package/src/identity/storage.mjs +0 -165
- package/src/project/configuration.mjs +0 -1337
- package/src/project/install_scaffold.mjs +0 -291
- package/src/project/setup.mjs +0 -466
- package/src/project/support_report.mjs +0 -105
- package/src/runtime/cleanup.mjs +0 -381
- package/src/runtime/migration_history.mjs +0 -220
- package/src/runtime/plan.mjs +0 -722
- package/src/runtime/policy.mjs +0 -438
- package/src/runtime/restore.mjs +0 -337
- package/src/runtime/service_environment.mjs +0 -82
- package/src/runtime/topology.mjs +0 -177
- package/src/shared/diagnostics.mjs +0 -207
- package/src/shared/node_runtime.mjs +0 -31
- package/src/shared/operation_guard.mjs +0 -162
- package/src/shared/process_environment.mjs +0 -68
- package/src/source/access.mjs +0 -753
- package/src/source/asset_transfer.mjs +0 -207
- package/src/source/baseline.mjs +0 -482
- package/src/source/external_reader.mjs +0 -255
- package/src/source/postgresql_access.mjs +0 -581
- package/src/source/privacy_paths.mjs +0 -38
- package/src/targets/postgresql.mjs +0 -808
- package/src/targets/supabase.mjs +0 -840
- package/src/targets/supabase_environment.mjs +0 -229
- package/src/targets/target.mjs +0 -66
- /package/{src/project → types}/configuration.d.mts +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Build narrowly validated local restore prerequisites and structural checks. */
|
|
2
|
+
type PolicyObject = Record<string, unknown>;
|
|
3
|
+
interface RuntimeRelation {
|
|
4
|
+
readonly schema: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
}
|
|
7
|
+
interface RuntimeExtension {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly schema: string;
|
|
10
|
+
}
|
|
11
|
+
interface RuntimeTrigger {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly table: RuntimeRelation;
|
|
14
|
+
readonly timing: "before" | "after";
|
|
15
|
+
readonly events: readonly ("insert" | "update" | "delete")[];
|
|
16
|
+
readonly function: RuntimeRelation;
|
|
17
|
+
}
|
|
18
|
+
interface RuntimeLocalRow {
|
|
19
|
+
readonly table: RuntimeRelation;
|
|
20
|
+
readonly keyColumns: readonly string[];
|
|
21
|
+
readonly values: PolicyObject;
|
|
22
|
+
readonly identityAssociation: Readonly<{
|
|
23
|
+
identity: string;
|
|
24
|
+
column: string;
|
|
25
|
+
}> | null;
|
|
26
|
+
}
|
|
27
|
+
interface RuntimeExpectation {
|
|
28
|
+
readonly table: RuntimeRelation;
|
|
29
|
+
readonly rowLevelSecurity: boolean;
|
|
30
|
+
readonly columns: readonly Readonly<{
|
|
31
|
+
name: string;
|
|
32
|
+
generated: boolean;
|
|
33
|
+
identity: boolean;
|
|
34
|
+
}>[];
|
|
35
|
+
readonly foreignKeys: readonly Readonly<{
|
|
36
|
+
columns: readonly string[];
|
|
37
|
+
references: Readonly<{
|
|
38
|
+
schema: string;
|
|
39
|
+
table: string;
|
|
40
|
+
columns: readonly string[];
|
|
41
|
+
}>;
|
|
42
|
+
}>[];
|
|
43
|
+
readonly policies: readonly Readonly<{
|
|
44
|
+
name: string;
|
|
45
|
+
command: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
46
|
+
roles: readonly string[];
|
|
47
|
+
}>[];
|
|
48
|
+
}
|
|
49
|
+
export interface RuntimePolicy {
|
|
50
|
+
readonly policyVersion: 1;
|
|
51
|
+
readonly prerequisites: Readonly<{
|
|
52
|
+
schemas: readonly string[];
|
|
53
|
+
extensions: readonly RuntimeExtension[];
|
|
54
|
+
}>;
|
|
55
|
+
readonly triggers: readonly RuntimeTrigger[];
|
|
56
|
+
readonly localRows: readonly RuntimeLocalRow[];
|
|
57
|
+
readonly expectations: readonly RuntimeExpectation[];
|
|
58
|
+
}
|
|
59
|
+
export declare const IDENTITY_CLAIM_RECEIPT_SQL = "create schema if not exists rehearsal_internal;\ncreate table if not exists rehearsal_internal.identity_claims (\n identity_name text primary key,\n placeholder_user_id uuid not null unique,\n local_user_id uuid not null,\n claimed_at timestamptz not null default now()\n);";
|
|
60
|
+
export declare const validateRuntimePolicy: (input: unknown) => RuntimePolicy;
|
|
61
|
+
export declare const buildRuntimePrerequisiteSql: (input: unknown) => string;
|
|
62
|
+
export declare const buildRuntimePostSchemaSql: (input: unknown) => string;
|
|
63
|
+
export declare const buildRuntimePreparationSql: (input: unknown) => string;
|
|
64
|
+
export declare const buildRuntimeFinalizationSql: (input: unknown) => string;
|
|
65
|
+
export declare const buildRuntimeVerificationSql: (input: unknown) => string;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/** Build narrowly validated local restore prerequisites and structural checks. */
|
|
2
|
+
const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
3
|
+
const IDENTITY_NAME = /^[a-z0-9][a-z0-9-]{1,62}$/u;
|
|
4
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
|
|
5
|
+
const EXTENSIONS = new Set([
|
|
6
|
+
"citext",
|
|
7
|
+
"pg_trgm",
|
|
8
|
+
"pgcrypto",
|
|
9
|
+
"unaccent",
|
|
10
|
+
"uuid-ossp",
|
|
11
|
+
"vector",
|
|
12
|
+
]);
|
|
13
|
+
const object = (value, label) => {
|
|
14
|
+
if (value === null ||
|
|
15
|
+
typeof value !== "object" ||
|
|
16
|
+
Array.isArray(value) ||
|
|
17
|
+
Object.getPrototypeOf(value) !== Object.prototype) {
|
|
18
|
+
throw new Error(`${label} must be an object.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
const keys = (value, allowed, label) => {
|
|
23
|
+
for (const key of Object.keys(value))
|
|
24
|
+
if (!allowed.includes(key))
|
|
25
|
+
throw new Error(`${label}.${key} is unknown.`);
|
|
26
|
+
};
|
|
27
|
+
const id = (value, label) => {
|
|
28
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value))
|
|
29
|
+
throw new Error(`${label} is not a safe PostgreSQL identifier.`);
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
const qid = (value) => `"${id(value, "SQL identifier")}"`;
|
|
33
|
+
const qext = (value) => {
|
|
34
|
+
if (typeof value !== "string" || !EXTENSIONS.has(value))
|
|
35
|
+
throw new Error("Unsupported extension identifier.");
|
|
36
|
+
return `"${value}"`;
|
|
37
|
+
};
|
|
38
|
+
const literal = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
39
|
+
const relation = (value, label) => {
|
|
40
|
+
const relationObject = object(value, label);
|
|
41
|
+
keys(relationObject, ["schema", "name"], label);
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
schema: id(relationObject.schema, `${label}.schema`),
|
|
44
|
+
name: id(relationObject.name, `${label}.name`),
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const array = (value, label) => {
|
|
48
|
+
if (!Array.isArray(value))
|
|
49
|
+
throw new Error(`${label} must be an array.`);
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
export const IDENTITY_CLAIM_RECEIPT_SQL = `create schema if not exists rehearsal_internal;
|
|
53
|
+
create table if not exists rehearsal_internal.identity_claims (
|
|
54
|
+
identity_name text primary key,
|
|
55
|
+
placeholder_user_id uuid not null unique,
|
|
56
|
+
local_user_id uuid not null,
|
|
57
|
+
claimed_at timestamptz not null default now()
|
|
58
|
+
);`;
|
|
59
|
+
export const validateRuntimePolicy = (input) => {
|
|
60
|
+
const policy = object(input, "runtimePolicy");
|
|
61
|
+
keys(policy, ["policyVersion", "prerequisites", "triggers", "localRows", "expectations"], "runtimePolicy");
|
|
62
|
+
if (policy.policyVersion !== 1)
|
|
63
|
+
throw new Error("Runtime policy must use policyVersion 1.");
|
|
64
|
+
const prerequisites = object(policy.prerequisites ?? {}, "runtimePolicy.prerequisites");
|
|
65
|
+
keys(prerequisites, ["schemas", "extensions"], "runtimePolicy.prerequisites");
|
|
66
|
+
const schemas = array(prerequisites.schemas ?? [], "runtimePolicy.prerequisites.schemas").map((value, index) => id(value, `runtimePolicy.prerequisites.schemas[${index}]`));
|
|
67
|
+
const extensions = array(prerequisites.extensions ?? [], "runtimePolicy.prerequisites.extensions").map((entry, index) => {
|
|
68
|
+
const label = `runtimePolicy.prerequisites.extensions[${index}]`;
|
|
69
|
+
const extension = object(entry, label);
|
|
70
|
+
keys(extension, ["name", "schema"], label);
|
|
71
|
+
if (typeof extension.name !== "string" || !EXTENSIONS.has(extension.name))
|
|
72
|
+
throw new Error(`${label}.name is not in Rehearsal's reviewed extension allowlist.`);
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
name: extension.name,
|
|
75
|
+
schema: id(extension.schema, `${label}.schema`),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
const triggers = array(policy.triggers ?? [], "runtimePolicy.triggers").map((entry, index) => {
|
|
79
|
+
const label = `runtimePolicy.triggers[${index}]`;
|
|
80
|
+
const trigger = object(entry, label);
|
|
81
|
+
keys(trigger, ["name", "table", "timing", "events", "function"], label);
|
|
82
|
+
if (trigger.timing !== "before" && trigger.timing !== "after")
|
|
83
|
+
throw new Error(`${label}.timing is unsupported.`);
|
|
84
|
+
const events = array(trigger.events, `${label}.events`);
|
|
85
|
+
if (events.length === 0 ||
|
|
86
|
+
events.some((event) => event !== "insert" && event !== "update" && event !== "delete")) {
|
|
87
|
+
throw new Error(`${label}.events are unsupported.`);
|
|
88
|
+
}
|
|
89
|
+
const normalizedEvents = events;
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
name: id(trigger.name, `${label}.name`),
|
|
92
|
+
table: relation(trigger.table, `${label}.table`),
|
|
93
|
+
timing: trigger.timing,
|
|
94
|
+
events: Object.freeze([...new Set(normalizedEvents)]),
|
|
95
|
+
function: relation(trigger.function, `${label}.function`),
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const localRows = array(policy.localRows ?? [], "runtimePolicy.localRows").map((entry, index) => {
|
|
99
|
+
const label = `runtimePolicy.localRows[${index}]`;
|
|
100
|
+
const row = object(entry, label);
|
|
101
|
+
keys(row, ["table", "keyColumns", "values", "identityAssociation"], label);
|
|
102
|
+
const table = relation(row.table, `${label}.table`);
|
|
103
|
+
const values = object(row.values, `${label}.values`);
|
|
104
|
+
const valueColumns = Object.keys(values).map((name) => id(name, `${label}.values key`));
|
|
105
|
+
const keyColumnsInput = array(row.keyColumns, `${label}.keyColumns`);
|
|
106
|
+
const keyColumns = keyColumnsInput.map((name, keyIndex) => id(name, `${label}.keyColumns[${keyIndex}]`));
|
|
107
|
+
if (keyColumns.length === 0 ||
|
|
108
|
+
keyColumns.some((name) => !valueColumns.includes(name))) {
|
|
109
|
+
throw new Error(`${label}.keyColumns must be present in values.`);
|
|
110
|
+
}
|
|
111
|
+
let identityAssociation = null;
|
|
112
|
+
if (row.identityAssociation != null) {
|
|
113
|
+
const associationLabel = `${label}.identityAssociation`;
|
|
114
|
+
const association = object(row.identityAssociation, associationLabel);
|
|
115
|
+
keys(association, ["identity", "column"], associationLabel);
|
|
116
|
+
if (typeof association.identity !== "string" ||
|
|
117
|
+
!IDENTITY_NAME.test(association.identity)) {
|
|
118
|
+
throw new Error(`${associationLabel}.identity is invalid.`);
|
|
119
|
+
}
|
|
120
|
+
const column = id(association.column, `${associationLabel}.column`);
|
|
121
|
+
if (!keyColumns.includes(column)) {
|
|
122
|
+
throw new Error(`${associationLabel}.column must be a key column.`);
|
|
123
|
+
}
|
|
124
|
+
const placeholder = values[column];
|
|
125
|
+
if (typeof placeholder !== "string" || !UUID.test(placeholder)) {
|
|
126
|
+
throw new Error(`${associationLabel}.column must contain the placeholder UUID.`);
|
|
127
|
+
}
|
|
128
|
+
identityAssociation = Object.freeze({
|
|
129
|
+
identity: association.identity,
|
|
130
|
+
column,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
table,
|
|
135
|
+
keyColumns: Object.freeze(keyColumns),
|
|
136
|
+
values,
|
|
137
|
+
identityAssociation,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const expectations = array(policy.expectations ?? [], "runtimePolicy.expectations").map((entry, index) => {
|
|
141
|
+
const label = `runtimePolicy.expectations[${index}]`;
|
|
142
|
+
const expectation = object(entry, label);
|
|
143
|
+
keys(expectation, ["table", "rowLevelSecurity", "columns", "foreignKeys", "policies"], label);
|
|
144
|
+
if (typeof expectation.rowLevelSecurity !== "boolean")
|
|
145
|
+
throw new Error(`${label}.rowLevelSecurity must be true or false.`);
|
|
146
|
+
const columnsInput = array(expectation.columns, `${label}.columns`);
|
|
147
|
+
if (columnsInput.length === 0)
|
|
148
|
+
throw new Error(`${label}.columns must not be empty.`);
|
|
149
|
+
const foreignKeys = array(expectation.foreignKeys ?? [], `${label}.foreignKeys`).map((foreignKey, fkIndex) => {
|
|
150
|
+
const fkLabel = `${label}.foreignKeys[${fkIndex}]`;
|
|
151
|
+
const foreignKeyObject = object(foreignKey, fkLabel);
|
|
152
|
+
keys(foreignKeyObject, ["columns", "references"], fkLabel);
|
|
153
|
+
const localColumns = array(foreignKeyObject.columns, `${fkLabel}.columns`);
|
|
154
|
+
if (localColumns.length === 0) {
|
|
155
|
+
throw new Error(`${fkLabel}.columns must not be empty.`);
|
|
156
|
+
}
|
|
157
|
+
const references = object(foreignKeyObject.references, `${fkLabel}.references`);
|
|
158
|
+
keys(references, ["schema", "table", "columns"], `${fkLabel}.references`);
|
|
159
|
+
const referencedColumns = array(references.columns, `${fkLabel}.references.columns`);
|
|
160
|
+
if (referencedColumns.length !== localColumns.length) {
|
|
161
|
+
throw new Error(`${fkLabel}.references.columns must match the local columns.`);
|
|
162
|
+
}
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
columns: Object.freeze(localColumns.map((column) => id(column, `${fkLabel}.columns[]`))),
|
|
165
|
+
references: Object.freeze({
|
|
166
|
+
schema: id(references.schema, `${fkLabel}.references.schema`),
|
|
167
|
+
table: id(references.table, `${fkLabel}.references.table`),
|
|
168
|
+
columns: Object.freeze(referencedColumns.map((column) => id(column, `${fkLabel}.references.columns[]`))),
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
const policies = array(expectation.policies ?? [], `${label}.policies`).map((rowPolicy, policyIndex) => {
|
|
173
|
+
const policyLabel = `${label}.policies[${policyIndex}]`;
|
|
174
|
+
const rowPolicyObject = object(rowPolicy, policyLabel);
|
|
175
|
+
keys(rowPolicyObject, ["name", "command", "roles"], policyLabel);
|
|
176
|
+
if (rowPolicyObject.command !== "ALL" &&
|
|
177
|
+
rowPolicyObject.command !== "SELECT" &&
|
|
178
|
+
rowPolicyObject.command !== "INSERT" &&
|
|
179
|
+
rowPolicyObject.command !== "UPDATE" &&
|
|
180
|
+
rowPolicyObject.command !== "DELETE") {
|
|
181
|
+
throw new Error(`${policyLabel}.command is unsupported.`);
|
|
182
|
+
}
|
|
183
|
+
const roles = array(rowPolicyObject.roles, `${policyLabel}.roles`);
|
|
184
|
+
if (roles.length === 0) {
|
|
185
|
+
throw new Error(`${policyLabel}.roles must not be empty.`);
|
|
186
|
+
}
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
name: id(rowPolicyObject.name, `${policyLabel}.name`),
|
|
189
|
+
command: rowPolicyObject.command,
|
|
190
|
+
roles: Object.freeze(roles.map((role) => id(role, `${policyLabel}.roles[]`))),
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
table: relation(expectation.table, `${label}.table`),
|
|
195
|
+
rowLevelSecurity: expectation.rowLevelSecurity,
|
|
196
|
+
columns: Object.freeze(columnsInput.map((column, columnIndex) => {
|
|
197
|
+
const columnLabel = `${label}.columns[${columnIndex}]`;
|
|
198
|
+
const columnObject = object(column, columnLabel);
|
|
199
|
+
keys(columnObject, ["name", "generated", "identity"], columnLabel);
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
name: id(columnObject.name, `${columnLabel}.name`),
|
|
202
|
+
generated: columnObject.generated === true,
|
|
203
|
+
identity: columnObject.identity === true,
|
|
204
|
+
});
|
|
205
|
+
})),
|
|
206
|
+
foreignKeys: Object.freeze(foreignKeys),
|
|
207
|
+
policies: Object.freeze(policies),
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
policyVersion: 1,
|
|
212
|
+
prerequisites: Object.freeze({
|
|
213
|
+
schemas: Object.freeze(schemas),
|
|
214
|
+
extensions: Object.freeze(extensions),
|
|
215
|
+
}),
|
|
216
|
+
triggers: Object.freeze(triggers),
|
|
217
|
+
localRows: Object.freeze(localRows),
|
|
218
|
+
expectations: Object.freeze(expectations),
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
export const buildRuntimePrerequisiteSql = (input) => {
|
|
222
|
+
const policy = validateRuntimePolicy(input);
|
|
223
|
+
const statements = ["begin;"];
|
|
224
|
+
for (const schema of policy.prerequisites.schemas) {
|
|
225
|
+
statements.push(`create schema if not exists ${qid(schema)};`);
|
|
226
|
+
}
|
|
227
|
+
for (const extension of policy.prerequisites.extensions) {
|
|
228
|
+
statements.push(`create extension if not exists ${qext(extension.name)} with schema ${qid(extension.schema)};`);
|
|
229
|
+
}
|
|
230
|
+
statements.push("commit;");
|
|
231
|
+
return `${statements.join("\n")}\n`;
|
|
232
|
+
};
|
|
233
|
+
export const buildRuntimePostSchemaSql = (input) => {
|
|
234
|
+
const policy = validateRuntimePolicy(input);
|
|
235
|
+
const statements = ["begin;"];
|
|
236
|
+
for (const trigger of policy.triggers) {
|
|
237
|
+
const target = `${qid(trigger.table.schema)}.${qid(trigger.table.name)}`;
|
|
238
|
+
const fn = `${qid(trigger.function.schema)}.${qid(trigger.function.name)}`;
|
|
239
|
+
statements.push(`do $rehearsal_trigger$
|
|
240
|
+
begin
|
|
241
|
+
if not exists (
|
|
242
|
+
select 1 from pg_trigger
|
|
243
|
+
where tgname = ${literal(trigger.name)}
|
|
244
|
+
and tgrelid = ${literal(`${trigger.table.schema}.${trigger.table.name}`)}::regclass
|
|
245
|
+
and not tgisinternal
|
|
246
|
+
) then
|
|
247
|
+
execute ${literal(`create trigger ${qid(trigger.name)} ${trigger.timing} ${trigger.events.join(" or ")} on ${target} for each row execute function ${fn}()`)};
|
|
248
|
+
end if;
|
|
249
|
+
end
|
|
250
|
+
$rehearsal_trigger$;`);
|
|
251
|
+
}
|
|
252
|
+
statements.push("commit;");
|
|
253
|
+
return `${statements.join("\n")}\n`;
|
|
254
|
+
};
|
|
255
|
+
export const buildRuntimePreparationSql = (input) => `${buildRuntimePrerequisiteSql(input)}${buildRuntimePostSchemaSql(input)}`;
|
|
256
|
+
export const buildRuntimeFinalizationSql = (input) => {
|
|
257
|
+
const policy = validateRuntimePolicy(input);
|
|
258
|
+
const statements = ["begin;"];
|
|
259
|
+
if (policy.localRows.some((row) => row.identityAssociation)) {
|
|
260
|
+
statements.push(IDENTITY_CLAIM_RECEIPT_SQL);
|
|
261
|
+
}
|
|
262
|
+
for (const row of policy.localRows) {
|
|
263
|
+
const target = `${qid(row.table.schema)}.${qid(row.table.name)}`;
|
|
264
|
+
const columns = Object.keys(row.values);
|
|
265
|
+
statements.push(`insert into ${target} (${columns.map((column) => qid(column)).join(", ")})
|
|
266
|
+
select ${columns.map((name) => `restored.${qid(name)}`).join(", ")}
|
|
267
|
+
from jsonb_populate_record(null::${target}, ${literal(JSON.stringify(row.values))}::jsonb) restored
|
|
268
|
+
on conflict (${row.keyColumns.map((column) => qid(column)).join(", ")}) do nothing;`);
|
|
269
|
+
}
|
|
270
|
+
statements.push("commit;");
|
|
271
|
+
return `${statements.join("\n")}\n`;
|
|
272
|
+
};
|
|
273
|
+
export const buildRuntimeVerificationSql = (input) => {
|
|
274
|
+
const policy = validateRuntimePolicy(input);
|
|
275
|
+
const checks = [];
|
|
276
|
+
for (const extension of policy.prerequisites.extensions) {
|
|
277
|
+
checks.push(`if not exists (
|
|
278
|
+
select 1 from pg_extension e join pg_namespace n on n.oid = e.extnamespace
|
|
279
|
+
where e.extname = ${literal(extension.name)} and n.nspname = ${literal(extension.schema)}
|
|
280
|
+
) then raise exception 'Rehearsal extension prerequisite is missing'; end if;`);
|
|
281
|
+
}
|
|
282
|
+
for (const trigger of policy.triggers) {
|
|
283
|
+
const triggerType = 1 +
|
|
284
|
+
(trigger.timing === "before" ? 2 : 0) +
|
|
285
|
+
(trigger.events.includes("insert") ? 4 : 0) +
|
|
286
|
+
(trigger.events.includes("delete") ? 8 : 0) +
|
|
287
|
+
(trigger.events.includes("update") ? 16 : 0);
|
|
288
|
+
checks.push(`if not exists (
|
|
289
|
+
select 1 from pg_trigger
|
|
290
|
+
where tgname = ${literal(trigger.name)}
|
|
291
|
+
and tgrelid = ${literal(`${trigger.table.schema}.${trigger.table.name}`)}::regclass
|
|
292
|
+
and tgfoid = ${literal(`${trigger.function.schema}.${trigger.function.name}()`)}::regprocedure
|
|
293
|
+
and tgtype = ${triggerType}
|
|
294
|
+
and not tgisinternal
|
|
295
|
+
) then raise exception 'Rehearsal managed trigger is missing'; end if;`);
|
|
296
|
+
}
|
|
297
|
+
for (const row of policy.localRows) {
|
|
298
|
+
const target = `${qid(row.table.schema)}.${qid(row.table.name)}`;
|
|
299
|
+
checks.push(`if not exists (
|
|
300
|
+
select 1
|
|
301
|
+
from ${target} current_row,
|
|
302
|
+
jsonb_populate_record(null::${target}, ${literal(JSON.stringify(row.values))}::jsonb) expected
|
|
303
|
+
where ${row.keyColumns
|
|
304
|
+
.map((column) => row.identityAssociation?.column === column
|
|
305
|
+
? `current_row.${qid(column)}::text = coalesce(
|
|
306
|
+
(select claim.local_user_id::text
|
|
307
|
+
from rehearsal_internal.identity_claims claim
|
|
308
|
+
where claim.identity_name = ${literal(row.identityAssociation.identity)}
|
|
309
|
+
and claim.placeholder_user_id::text = expected.${qid(column)}::text),
|
|
310
|
+
expected.${qid(column)}::text
|
|
311
|
+
)`
|
|
312
|
+
: `current_row.${qid(column)} is not distinct from expected.${qid(column)}`)
|
|
313
|
+
.join(" and ")}
|
|
314
|
+
) then raise exception 'Rehearsal local-only row is missing'; end if;`);
|
|
315
|
+
}
|
|
316
|
+
for (const expectation of policy.expectations) {
|
|
317
|
+
const name = `${expectation.table.schema}.${expectation.table.name}`;
|
|
318
|
+
checks.push(`if not exists (
|
|
319
|
+
select 1 from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
320
|
+
where n.nspname = ${literal(expectation.table.schema)}
|
|
321
|
+
and c.relname = ${literal(expectation.table.name)}
|
|
322
|
+
and c.relrowsecurity = ${expectation.rowLevelSecurity ? "true" : "false"}
|
|
323
|
+
) then raise exception 'Rehearsal table or RLS expectation failed'; end if;`);
|
|
324
|
+
for (const column of expectation.columns) {
|
|
325
|
+
checks.push(`if not exists (
|
|
326
|
+
select 1 from information_schema.columns
|
|
327
|
+
where table_schema = ${literal(expectation.table.schema)}
|
|
328
|
+
and table_name = ${literal(expectation.table.name)}
|
|
329
|
+
and column_name = ${literal(column.name)}
|
|
330
|
+
and (is_generated <> 'NEVER') = ${column.generated ? "true" : "false"}
|
|
331
|
+
and (is_identity = 'YES') = ${column.identity ? "true" : "false"}
|
|
332
|
+
) then raise exception ${literal(`Rehearsal column expectation failed for ${name}.${column.name}`)}; end if;`);
|
|
333
|
+
}
|
|
334
|
+
for (const foreignKey of expectation.foreignKeys) {
|
|
335
|
+
const localColumns = `array[${foreignKey.columns.map(literal).join(", ")}]::text[]`;
|
|
336
|
+
const targetColumns = `array[${foreignKey.references.columns.map(literal).join(", ")}]::text[]`;
|
|
337
|
+
checks.push(`if not exists (
|
|
338
|
+
select 1 from pg_constraint c
|
|
339
|
+
where c.contype = 'f'
|
|
340
|
+
and c.conrelid = ${literal(name)}::regclass
|
|
341
|
+
and c.confrelid = ${literal(`${foreignKey.references.schema}.${foreignKey.references.table}`)}::regclass
|
|
342
|
+
and (select array_agg(a.attname order by u.ordinality)::text[]
|
|
343
|
+
from unnest(c.conkey) with ordinality u(attnum, ordinality)
|
|
344
|
+
join pg_attribute a on a.attrelid = c.conrelid and a.attnum = u.attnum) = ${localColumns}
|
|
345
|
+
and (select array_agg(a.attname order by u.ordinality)::text[]
|
|
346
|
+
from unnest(c.confkey) with ordinality u(attnum, ordinality)
|
|
347
|
+
join pg_attribute a on a.attrelid = c.confrelid and a.attnum = u.attnum) = ${targetColumns}
|
|
348
|
+
) then raise exception 'Rehearsal foreign-key expectation failed'; end if;`);
|
|
349
|
+
}
|
|
350
|
+
for (const rowPolicy of expectation.policies) {
|
|
351
|
+
checks.push(`if not exists (
|
|
352
|
+
select 1 from pg_policies
|
|
353
|
+
where schemaname = ${literal(expectation.table.schema)}
|
|
354
|
+
and tablename = ${literal(expectation.table.name)}
|
|
355
|
+
and policyname = ${literal(rowPolicy.name)}
|
|
356
|
+
and cmd = ${literal(rowPolicy.command)}
|
|
357
|
+
and roles @> array[${rowPolicy.roles.map(literal).join(", ")}]::name[]
|
|
358
|
+
) then raise exception 'Rehearsal row-policy expectation failed'; end if;`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
checks.push(`if exists (select 1 from pg_constraint where not convalidated) then
|
|
362
|
+
raise exception 'Rehearsal found an unvalidated structural constraint'; end if;`);
|
|
363
|
+
return `do $rehearsal_verify$
|
|
364
|
+
begin
|
|
365
|
+
${checks.join("\n ")}
|
|
366
|
+
end
|
|
367
|
+
$rehearsal_verify$;
|
|
368
|
+
`;
|
|
369
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Build the bounded PostgreSQL restore program for a verified sanitized
|
|
3
|
+
* Rehearsal baseline. Do not run directly; this module is reusable script infrastructure.
|
|
4
|
+
*/
|
|
5
|
+
import type { BaselineManifest } from "../baseline/artifact.mjs";
|
|
6
|
+
import type { RuntimeSanitizationPolicy } from "../baseline/sanitization_policy.mjs";
|
|
7
|
+
import type { MigrationFileEntry } from "./migration_history.mjs";
|
|
8
|
+
export declare const summarizeRestoreError: (stderr: unknown) => string;
|
|
9
|
+
export declare const encodeBaselineRecordForCopy: (line: string) => string;
|
|
10
|
+
export declare const buildRestoreSqlPrefix: () => string;
|
|
11
|
+
export declare const buildRestoreSqlSuffix: ({ manifest, tableCounts, restoreSupabaseAuth, }: {
|
|
12
|
+
manifest: RuntimeSanitizationPolicy;
|
|
13
|
+
tableCounts: Readonly<Record<string, number>>;
|
|
14
|
+
restoreSupabaseAuth?: boolean;
|
|
15
|
+
}) => string;
|
|
16
|
+
export declare const createCandidateMigrationReceipt: ({ baselineManifest, currentFiles, }: {
|
|
17
|
+
baselineManifest: BaselineManifest;
|
|
18
|
+
currentFiles: readonly MigrationFileEntry[];
|
|
19
|
+
}) => {
|
|
20
|
+
candidates: MigrationFileEntry[];
|
|
21
|
+
candidateSha256: string;
|
|
22
|
+
};
|