@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
package/src/runtime/policy.mjs
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
/** Build narrowly validated local restore prerequisites and structural checks. */
|
|
2
|
-
|
|
3
|
-
const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
4
|
-
const IDENTITY_NAME = /^[a-z0-9][a-z0-9-]{1,62}$/u;
|
|
5
|
-
const UUID =
|
|
6
|
-
/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
|
|
7
|
-
const EXTENSIONS = new Set([
|
|
8
|
-
"citext",
|
|
9
|
-
"pg_trgm",
|
|
10
|
-
"pgcrypto",
|
|
11
|
-
"unaccent",
|
|
12
|
-
"uuid-ossp",
|
|
13
|
-
"vector",
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
const object = (value, label) => {
|
|
17
|
-
if (
|
|
18
|
-
value === null ||
|
|
19
|
-
typeof value !== "object" ||
|
|
20
|
-
Array.isArray(value) ||
|
|
21
|
-
Object.getPrototypeOf(value) !== Object.prototype
|
|
22
|
-
) {
|
|
23
|
-
throw new Error(`${label} must be an object.`);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
};
|
|
27
|
-
const keys = (value, allowed, label) => {
|
|
28
|
-
for (const key of Object.keys(value))
|
|
29
|
-
if (!allowed.includes(key)) throw new Error(`${label}.${key} is unknown.`);
|
|
30
|
-
};
|
|
31
|
-
const id = (value, label) => {
|
|
32
|
-
if (!IDENTIFIER.test(value ?? ""))
|
|
33
|
-
throw new Error(`${label} is not a safe PostgreSQL identifier.`);
|
|
34
|
-
return value;
|
|
35
|
-
};
|
|
36
|
-
const qid = (value) => `"${id(value, "SQL identifier")}"`;
|
|
37
|
-
const qext = (value) => {
|
|
38
|
-
if (!EXTENSIONS.has(value))
|
|
39
|
-
throw new Error("Unsupported extension identifier.");
|
|
40
|
-
return `"${value}"`;
|
|
41
|
-
};
|
|
42
|
-
const literal = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
43
|
-
const relation = (value, label) => {
|
|
44
|
-
object(value, label);
|
|
45
|
-
keys(value, ["schema", "name"], label);
|
|
46
|
-
return Object.freeze({
|
|
47
|
-
schema: id(value.schema, `${label}.schema`),
|
|
48
|
-
name: id(value.name, `${label}.name`),
|
|
49
|
-
});
|
|
50
|
-
};
|
|
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
|
-
|
|
60
|
-
export const validateRuntimePolicy = (input) => {
|
|
61
|
-
const policy = object(input, "runtimePolicy");
|
|
62
|
-
keys(
|
|
63
|
-
policy,
|
|
64
|
-
["policyVersion", "prerequisites", "triggers", "localRows", "expectations"],
|
|
65
|
-
"runtimePolicy",
|
|
66
|
-
);
|
|
67
|
-
if (policy.policyVersion !== 1)
|
|
68
|
-
throw new Error("Runtime policy must use policyVersion 1.");
|
|
69
|
-
const prerequisites = object(
|
|
70
|
-
policy.prerequisites ?? {},
|
|
71
|
-
"runtimePolicy.prerequisites",
|
|
72
|
-
);
|
|
73
|
-
keys(prerequisites, ["schemas", "extensions"], "runtimePolicy.prerequisites");
|
|
74
|
-
const schemas = (prerequisites.schemas ?? []).map((value, index) =>
|
|
75
|
-
id(value, `runtimePolicy.prerequisites.schemas[${index}]`),
|
|
76
|
-
);
|
|
77
|
-
const extensions = (prerequisites.extensions ?? []).map((entry, index) => {
|
|
78
|
-
const label = `runtimePolicy.prerequisites.extensions[${index}]`;
|
|
79
|
-
object(entry, label);
|
|
80
|
-
keys(entry, ["name", "schema"], label);
|
|
81
|
-
if (!EXTENSIONS.has(entry.name))
|
|
82
|
-
throw new Error(
|
|
83
|
-
`${label}.name is not in Rehearsal's reviewed extension allowlist.`,
|
|
84
|
-
);
|
|
85
|
-
return Object.freeze({
|
|
86
|
-
name: entry.name,
|
|
87
|
-
schema: id(entry.schema, `${label}.schema`),
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
const triggers = (policy.triggers ?? []).map((entry, index) => {
|
|
91
|
-
const label = `runtimePolicy.triggers[${index}]`;
|
|
92
|
-
object(entry, label);
|
|
93
|
-
keys(entry, ["name", "table", "timing", "events", "function"], label);
|
|
94
|
-
if (!["before", "after"].includes(entry.timing))
|
|
95
|
-
throw new Error(`${label}.timing is unsupported.`);
|
|
96
|
-
if (
|
|
97
|
-
!Array.isArray(entry.events) ||
|
|
98
|
-
entry.events.length === 0 ||
|
|
99
|
-
entry.events.some(
|
|
100
|
-
(event) => !["insert", "update", "delete"].includes(event),
|
|
101
|
-
)
|
|
102
|
-
) {
|
|
103
|
-
throw new Error(`${label}.events are unsupported.`);
|
|
104
|
-
}
|
|
105
|
-
return Object.freeze({
|
|
106
|
-
name: id(entry.name, `${label}.name`),
|
|
107
|
-
table: relation(entry.table, `${label}.table`),
|
|
108
|
-
timing: entry.timing,
|
|
109
|
-
events: Object.freeze([...new Set(entry.events)]),
|
|
110
|
-
function: relation(entry.function, `${label}.function`),
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
const localRows = (policy.localRows ?? []).map((entry, index) => {
|
|
114
|
-
const label = `runtimePolicy.localRows[${index}]`;
|
|
115
|
-
object(entry, label);
|
|
116
|
-
keys(
|
|
117
|
-
entry,
|
|
118
|
-
["table", "keyColumns", "values", "identityAssociation"],
|
|
119
|
-
label,
|
|
120
|
-
);
|
|
121
|
-
const table = relation(entry.table, `${label}.table`);
|
|
122
|
-
object(entry.values, `${label}.values`);
|
|
123
|
-
const valueColumns = Object.keys(entry.values).map((name) =>
|
|
124
|
-
id(name, `${label}.values key`),
|
|
125
|
-
);
|
|
126
|
-
if (
|
|
127
|
-
!Array.isArray(entry.keyColumns) ||
|
|
128
|
-
entry.keyColumns.length === 0 ||
|
|
129
|
-
entry.keyColumns.some((name) => !valueColumns.includes(name))
|
|
130
|
-
) {
|
|
131
|
-
throw new Error(`${label}.keyColumns must be present in values.`);
|
|
132
|
-
}
|
|
133
|
-
let identityAssociation = null;
|
|
134
|
-
if (entry.identityAssociation != null) {
|
|
135
|
-
const associationLabel = `${label}.identityAssociation`;
|
|
136
|
-
object(entry.identityAssociation, associationLabel);
|
|
137
|
-
keys(entry.identityAssociation, ["identity", "column"], associationLabel);
|
|
138
|
-
if (!IDENTITY_NAME.test(entry.identityAssociation.identity ?? "")) {
|
|
139
|
-
throw new Error(`${associationLabel}.identity is invalid.`);
|
|
140
|
-
}
|
|
141
|
-
const column = id(
|
|
142
|
-
entry.identityAssociation.column,
|
|
143
|
-
`${associationLabel}.column`,
|
|
144
|
-
);
|
|
145
|
-
if (!entry.keyColumns.includes(column)) {
|
|
146
|
-
throw new Error(`${associationLabel}.column must be a key column.`);
|
|
147
|
-
}
|
|
148
|
-
if (!UUID.test(entry.values[column] ?? "")) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`${associationLabel}.column must contain the placeholder UUID.`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
identityAssociation = Object.freeze({
|
|
154
|
-
identity: entry.identityAssociation.identity,
|
|
155
|
-
column,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
return Object.freeze({
|
|
159
|
-
table,
|
|
160
|
-
keyColumns: Object.freeze(entry.keyColumns),
|
|
161
|
-
values: entry.values,
|
|
162
|
-
identityAssociation,
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
const expectations = (policy.expectations ?? []).map((entry, index) => {
|
|
166
|
-
const label = `runtimePolicy.expectations[${index}]`;
|
|
167
|
-
object(entry, label);
|
|
168
|
-
keys(
|
|
169
|
-
entry,
|
|
170
|
-
["table", "rowLevelSecurity", "columns", "foreignKeys", "policies"],
|
|
171
|
-
label,
|
|
172
|
-
);
|
|
173
|
-
if (typeof entry.rowLevelSecurity !== "boolean")
|
|
174
|
-
throw new Error(`${label}.rowLevelSecurity must be true or false.`);
|
|
175
|
-
if (!Array.isArray(entry.columns) || entry.columns.length === 0)
|
|
176
|
-
throw new Error(`${label}.columns must not be empty.`);
|
|
177
|
-
const foreignKeys = (entry.foreignKeys ?? []).map((foreignKey, fkIndex) => {
|
|
178
|
-
const fkLabel = `${label}.foreignKeys[${fkIndex}]`;
|
|
179
|
-
object(foreignKey, fkLabel);
|
|
180
|
-
keys(foreignKey, ["columns", "references"], fkLabel);
|
|
181
|
-
if (
|
|
182
|
-
!Array.isArray(foreignKey.columns) ||
|
|
183
|
-
foreignKey.columns.length === 0
|
|
184
|
-
) {
|
|
185
|
-
throw new Error(`${fkLabel}.columns must not be empty.`);
|
|
186
|
-
}
|
|
187
|
-
object(foreignKey.references, `${fkLabel}.references`);
|
|
188
|
-
keys(
|
|
189
|
-
foreignKey.references,
|
|
190
|
-
["schema", "table", "columns"],
|
|
191
|
-
`${fkLabel}.references`,
|
|
192
|
-
);
|
|
193
|
-
if (
|
|
194
|
-
!Array.isArray(foreignKey.references.columns) ||
|
|
195
|
-
foreignKey.references.columns.length !== foreignKey.columns.length
|
|
196
|
-
) {
|
|
197
|
-
throw new Error(
|
|
198
|
-
`${fkLabel}.references.columns must match the local columns.`,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
return Object.freeze({
|
|
202
|
-
columns: Object.freeze(
|
|
203
|
-
foreignKey.columns.map((column) =>
|
|
204
|
-
id(column, `${fkLabel}.columns[]`),
|
|
205
|
-
),
|
|
206
|
-
),
|
|
207
|
-
references: Object.freeze({
|
|
208
|
-
schema: id(
|
|
209
|
-
foreignKey.references.schema,
|
|
210
|
-
`${fkLabel}.references.schema`,
|
|
211
|
-
),
|
|
212
|
-
table: id(foreignKey.references.table, `${fkLabel}.references.table`),
|
|
213
|
-
columns: Object.freeze(
|
|
214
|
-
foreignKey.references.columns.map((column) =>
|
|
215
|
-
id(column, `${fkLabel}.references.columns[]`),
|
|
216
|
-
),
|
|
217
|
-
),
|
|
218
|
-
}),
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
const policies = (entry.policies ?? []).map((rowPolicy, policyIndex) => {
|
|
222
|
-
const policyLabel = `${label}.policies[${policyIndex}]`;
|
|
223
|
-
object(rowPolicy, policyLabel);
|
|
224
|
-
keys(rowPolicy, ["name", "command", "roles"], policyLabel);
|
|
225
|
-
if (
|
|
226
|
-
!["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"].includes(
|
|
227
|
-
rowPolicy.command,
|
|
228
|
-
)
|
|
229
|
-
) {
|
|
230
|
-
throw new Error(`${policyLabel}.command is unsupported.`);
|
|
231
|
-
}
|
|
232
|
-
if (!Array.isArray(rowPolicy.roles) || rowPolicy.roles.length === 0) {
|
|
233
|
-
throw new Error(`${policyLabel}.roles must not be empty.`);
|
|
234
|
-
}
|
|
235
|
-
return Object.freeze({
|
|
236
|
-
name: id(rowPolicy.name, `${policyLabel}.name`),
|
|
237
|
-
command: rowPolicy.command,
|
|
238
|
-
roles: Object.freeze(
|
|
239
|
-
rowPolicy.roles.map((role) => id(role, `${policyLabel}.roles[]`)),
|
|
240
|
-
),
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
return Object.freeze({
|
|
244
|
-
table: relation(entry.table, `${label}.table`),
|
|
245
|
-
rowLevelSecurity: entry.rowLevelSecurity,
|
|
246
|
-
columns: Object.freeze(
|
|
247
|
-
entry.columns.map((column, columnIndex) => {
|
|
248
|
-
const columnLabel = `${label}.columns[${columnIndex}]`;
|
|
249
|
-
object(column, columnLabel);
|
|
250
|
-
keys(column, ["name", "generated", "identity"], columnLabel);
|
|
251
|
-
return Object.freeze({
|
|
252
|
-
name: id(column.name, `${columnLabel}.name`),
|
|
253
|
-
generated: column.generated === true,
|
|
254
|
-
identity: column.identity === true,
|
|
255
|
-
});
|
|
256
|
-
}),
|
|
257
|
-
),
|
|
258
|
-
foreignKeys: Object.freeze(foreignKeys),
|
|
259
|
-
policies: Object.freeze(policies),
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
return Object.freeze({
|
|
263
|
-
policyVersion: 1,
|
|
264
|
-
prerequisites: Object.freeze({
|
|
265
|
-
schemas: Object.freeze(schemas),
|
|
266
|
-
extensions: Object.freeze(extensions),
|
|
267
|
-
}),
|
|
268
|
-
triggers: Object.freeze(triggers),
|
|
269
|
-
localRows: Object.freeze(localRows),
|
|
270
|
-
expectations: Object.freeze(expectations),
|
|
271
|
-
});
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
export const buildRuntimePrerequisiteSql = (input) => {
|
|
275
|
-
const policy = validateRuntimePolicy(input);
|
|
276
|
-
const statements = ["begin;"];
|
|
277
|
-
for (const schema of policy.prerequisites.schemas) {
|
|
278
|
-
statements.push(`create schema if not exists ${qid(schema)};`);
|
|
279
|
-
}
|
|
280
|
-
for (const extension of policy.prerequisites.extensions) {
|
|
281
|
-
statements.push(
|
|
282
|
-
`create extension if not exists ${qext(extension.name)} with schema ${qid(extension.schema)};`,
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
statements.push("commit;");
|
|
286
|
-
return `${statements.join("\n")}\n`;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
export const buildRuntimePostSchemaSql = (input) => {
|
|
290
|
-
const policy = validateRuntimePolicy(input);
|
|
291
|
-
const statements = ["begin;"];
|
|
292
|
-
for (const trigger of policy.triggers) {
|
|
293
|
-
const target = `${qid(trigger.table.schema)}.${qid(trigger.table.name)}`;
|
|
294
|
-
const fn = `${qid(trigger.function.schema)}.${qid(trigger.function.name)}`;
|
|
295
|
-
statements.push(`do $rehearsal_trigger$
|
|
296
|
-
begin
|
|
297
|
-
if not exists (
|
|
298
|
-
select 1 from pg_trigger
|
|
299
|
-
where tgname = ${literal(trigger.name)}
|
|
300
|
-
and tgrelid = ${literal(`${trigger.table.schema}.${trigger.table.name}`)}::regclass
|
|
301
|
-
and not tgisinternal
|
|
302
|
-
) then
|
|
303
|
-
execute ${literal(
|
|
304
|
-
`create trigger ${qid(trigger.name)} ${trigger.timing} ${trigger.events.join(" or ")} on ${target} for each row execute function ${fn}()`,
|
|
305
|
-
)};
|
|
306
|
-
end if;
|
|
307
|
-
end
|
|
308
|
-
$rehearsal_trigger$;`);
|
|
309
|
-
}
|
|
310
|
-
statements.push("commit;");
|
|
311
|
-
return `${statements.join("\n")}\n`;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
export const buildRuntimePreparationSql = (input) =>
|
|
315
|
-
`${buildRuntimePrerequisiteSql(input)}${buildRuntimePostSchemaSql(input)}`;
|
|
316
|
-
|
|
317
|
-
export const buildRuntimeFinalizationSql = (input) => {
|
|
318
|
-
const policy = validateRuntimePolicy(input);
|
|
319
|
-
const statements = ["begin;"];
|
|
320
|
-
if (policy.localRows.some((row) => row.identityAssociation)) {
|
|
321
|
-
statements.push(IDENTITY_CLAIM_RECEIPT_SQL);
|
|
322
|
-
}
|
|
323
|
-
for (const row of policy.localRows) {
|
|
324
|
-
const target = `${qid(row.table.schema)}.${qid(row.table.name)}`;
|
|
325
|
-
const columns = Object.keys(row.values);
|
|
326
|
-
statements.push(
|
|
327
|
-
`insert into ${target} (${columns.map(qid).join(", ")})
|
|
328
|
-
select ${columns.map((name) => `restored.${qid(name)}`).join(", ")}
|
|
329
|
-
from jsonb_populate_record(null::${target}, ${literal(JSON.stringify(row.values))}::jsonb) restored
|
|
330
|
-
on conflict (${row.keyColumns.map(qid).join(", ")}) do nothing;`,
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
statements.push("commit;");
|
|
334
|
-
return `${statements.join("\n")}\n`;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
export const buildRuntimeVerificationSql = (input) => {
|
|
338
|
-
const policy = validateRuntimePolicy(input);
|
|
339
|
-
const checks = [];
|
|
340
|
-
for (const extension of policy.prerequisites.extensions) {
|
|
341
|
-
checks.push(`if not exists (
|
|
342
|
-
select 1 from pg_extension e join pg_namespace n on n.oid = e.extnamespace
|
|
343
|
-
where e.extname = ${literal(extension.name)} and n.nspname = ${literal(extension.schema)}
|
|
344
|
-
) then raise exception 'Rehearsal extension prerequisite is missing'; end if;`);
|
|
345
|
-
}
|
|
346
|
-
for (const trigger of policy.triggers) {
|
|
347
|
-
const triggerType =
|
|
348
|
-
1 +
|
|
349
|
-
(trigger.timing === "before" ? 2 : 0) +
|
|
350
|
-
(trigger.events.includes("insert") ? 4 : 0) +
|
|
351
|
-
(trigger.events.includes("delete") ? 8 : 0) +
|
|
352
|
-
(trigger.events.includes("update") ? 16 : 0);
|
|
353
|
-
checks.push(`if not exists (
|
|
354
|
-
select 1 from pg_trigger
|
|
355
|
-
where tgname = ${literal(trigger.name)}
|
|
356
|
-
and tgrelid = ${literal(`${trigger.table.schema}.${trigger.table.name}`)}::regclass
|
|
357
|
-
and tgfoid = ${literal(`${trigger.function.schema}.${trigger.function.name}()`)}::regprocedure
|
|
358
|
-
and tgtype = ${triggerType}
|
|
359
|
-
and not tgisinternal
|
|
360
|
-
) then raise exception 'Rehearsal managed trigger is missing'; end if;`);
|
|
361
|
-
}
|
|
362
|
-
for (const row of policy.localRows) {
|
|
363
|
-
const target = `${qid(row.table.schema)}.${qid(row.table.name)}`;
|
|
364
|
-
checks.push(`if not exists (
|
|
365
|
-
select 1
|
|
366
|
-
from ${target} current_row,
|
|
367
|
-
jsonb_populate_record(null::${target}, ${literal(JSON.stringify(row.values))}::jsonb) expected
|
|
368
|
-
where ${row.keyColumns
|
|
369
|
-
.map((column) =>
|
|
370
|
-
row.identityAssociation?.column === column
|
|
371
|
-
? `current_row.${qid(column)}::text = coalesce(
|
|
372
|
-
(select claim.local_user_id::text
|
|
373
|
-
from rehearsal_internal.identity_claims claim
|
|
374
|
-
where claim.identity_name = ${literal(row.identityAssociation.identity)}
|
|
375
|
-
and claim.placeholder_user_id::text = expected.${qid(column)}::text),
|
|
376
|
-
expected.${qid(column)}::text
|
|
377
|
-
)`
|
|
378
|
-
: `current_row.${qid(column)} is not distinct from expected.${qid(column)}`,
|
|
379
|
-
)
|
|
380
|
-
.join(" and ")}
|
|
381
|
-
) then raise exception 'Rehearsal local-only row is missing'; end if;`);
|
|
382
|
-
}
|
|
383
|
-
for (const expectation of policy.expectations) {
|
|
384
|
-
const name = `${expectation.table.schema}.${expectation.table.name}`;
|
|
385
|
-
checks.push(`if not exists (
|
|
386
|
-
select 1 from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
387
|
-
where n.nspname = ${literal(expectation.table.schema)}
|
|
388
|
-
and c.relname = ${literal(expectation.table.name)}
|
|
389
|
-
and c.relrowsecurity = ${expectation.rowLevelSecurity ? "true" : "false"}
|
|
390
|
-
) then raise exception 'Rehearsal table or RLS expectation failed'; end if;`);
|
|
391
|
-
for (const column of expectation.columns) {
|
|
392
|
-
checks.push(`if not exists (
|
|
393
|
-
select 1 from information_schema.columns
|
|
394
|
-
where table_schema = ${literal(expectation.table.schema)}
|
|
395
|
-
and table_name = ${literal(expectation.table.name)}
|
|
396
|
-
and column_name = ${literal(column.name)}
|
|
397
|
-
and (is_generated <> 'NEVER') = ${column.generated ? "true" : "false"}
|
|
398
|
-
and (is_identity = 'YES') = ${column.identity ? "true" : "false"}
|
|
399
|
-
) then raise exception ${literal(`Rehearsal column expectation failed for ${name}.${column.name}`)}; end if;`);
|
|
400
|
-
}
|
|
401
|
-
for (const foreignKey of expectation.foreignKeys) {
|
|
402
|
-
const localColumns = `array[${foreignKey.columns.map(literal).join(", ")}]::text[]`;
|
|
403
|
-
const targetColumns = `array[${foreignKey.references.columns.map(literal).join(", ")}]::text[]`;
|
|
404
|
-
checks.push(`if not exists (
|
|
405
|
-
select 1 from pg_constraint c
|
|
406
|
-
where c.contype = 'f'
|
|
407
|
-
and c.conrelid = ${literal(name)}::regclass
|
|
408
|
-
and c.confrelid = ${literal(
|
|
409
|
-
`${foreignKey.references.schema}.${foreignKey.references.table}`,
|
|
410
|
-
)}::regclass
|
|
411
|
-
and (select array_agg(a.attname order by u.ordinality)::text[]
|
|
412
|
-
from unnest(c.conkey) with ordinality u(attnum, ordinality)
|
|
413
|
-
join pg_attribute a on a.attrelid = c.conrelid and a.attnum = u.attnum) = ${localColumns}
|
|
414
|
-
and (select array_agg(a.attname order by u.ordinality)::text[]
|
|
415
|
-
from unnest(c.confkey) with ordinality u(attnum, ordinality)
|
|
416
|
-
join pg_attribute a on a.attrelid = c.confrelid and a.attnum = u.attnum) = ${targetColumns}
|
|
417
|
-
) then raise exception 'Rehearsal foreign-key expectation failed'; end if;`);
|
|
418
|
-
}
|
|
419
|
-
for (const rowPolicy of expectation.policies) {
|
|
420
|
-
checks.push(`if not exists (
|
|
421
|
-
select 1 from pg_policies
|
|
422
|
-
where schemaname = ${literal(expectation.table.schema)}
|
|
423
|
-
and tablename = ${literal(expectation.table.name)}
|
|
424
|
-
and policyname = ${literal(rowPolicy.name)}
|
|
425
|
-
and cmd = ${literal(rowPolicy.command)}
|
|
426
|
-
and roles @> array[${rowPolicy.roles.map(literal).join(", ")}]::name[]
|
|
427
|
-
) then raise exception 'Rehearsal row-policy expectation failed'; end if;`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
checks.push(`if exists (select 1 from pg_constraint where not convalidated) then
|
|
431
|
-
raise exception 'Rehearsal found an unvalidated structural constraint'; end if;`);
|
|
432
|
-
return `do $rehearsal_verify$
|
|
433
|
-
begin
|
|
434
|
-
${checks.join("\n ")}
|
|
435
|
-
end
|
|
436
|
-
$rehearsal_verify$;
|
|
437
|
-
`;
|
|
438
|
-
};
|