@rehearsal-db/core 1.0.2 → 1.2.0
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 -4
- package/COMPATIBILITY.md +41 -4
- package/README.md +8 -4
- package/dist/package.json +3 -1
- package/dist/src/cli/arguments.mjs +1 -0
- package/dist/src/cli/basic_command_handlers.mjs +4 -0
- package/dist/src/cli/editor_command_handlers.mjs +24 -0
- package/dist/src/project/configuration_contract.d.mts +1 -0
- package/dist/src/project/configuration_normalization.mjs +2 -1
- package/dist/src/project/configuration_rendering.mjs +3 -2
- package/dist/src/project/configuration_resolution.mjs +0 -2
- package/dist/src/project/project_detection.d.mts +2 -0
- package/dist/src/project/project_detection.mjs +1 -0
- package/dist/src/project/runtime_project_identity.d.mts +5 -0
- package/dist/src/project/runtime_project_identity.mjs +41 -0
- package/dist/src/project/setup.mjs +3 -1
- package/dist/src/runtime/migration_history.d.mts +5 -1
- package/dist/src/runtime/migration_history.mjs +24 -6
- package/dist/src/runtime/migration_impact.d.mts +125 -0
- package/dist/src/runtime/migration_impact.mjs +278 -0
- package/dist/src/runtime/migration_impact_catalog.d.mts +18 -0
- package/dist/src/runtime/migration_impact_catalog.mjs +116 -0
- package/dist/src/runtime/migration_impact_plan.d.mts +47 -0
- package/dist/src/runtime/migration_impact_plan.mjs +297 -0
- package/dist/src/runtime/migration_impact_queries.d.mts +22 -0
- package/dist/src/runtime/migration_impact_queries.mjs +270 -0
- package/dist/src/runtime/migration_insights.d.mts +137 -0
- package/dist/src/runtime/migration_insights.mjs +143 -0
- package/dist/src/runtime/migration_insights_input.d.mts +13 -0
- package/dist/src/runtime/migration_insights_input.mjs +153 -0
- package/dist/src/runtime/migration_sql_lexer.d.mts +14 -0
- package/dist/src/runtime/migration_sql_lexer.mjs +204 -0
- package/dist/src/runtime/migration_sql_scan.d.mts +16 -0
- package/dist/src/runtime/migration_sql_scan.mjs +369 -0
- package/dist/src/runtime/plan.d.mts +2 -1
- package/dist/src/runtime/plan.mjs +4 -4
- package/dist/src/runtime/policy.d.mts +2 -0
- package/dist/src/runtime/policy.mjs +13 -1
- package/dist/src/runtime/privileges.d.mts +25 -0
- package/dist/src/runtime/privileges.mjs +198 -0
- package/dist/src/runtime/topology.d.mts +1 -1
- package/dist/src/runtime/topology.mjs +2 -2
- package/dist/src/shared/cancellation.mjs +1 -0
- package/dist/src/shared/held_path_ancestry.d.mts +7 -0
- package/dist/src/shared/held_path_ancestry.mjs +110 -0
- package/dist/src/shared/operation_guard.mjs +1 -0
- package/dist/src/targets/migration_impact_runtime.d.mts +22 -0
- package/dist/src/targets/migration_impact_runtime.mjs +367 -0
- package/dist/src/targets/supabase.mjs +1 -0
- package/docs/README.md +20 -7
- package/docs/adapters.md +10 -0
- package/docs/architecture.md +75 -1
- package/docs/commands.md +169 -34
- package/docs/configuration.md +42 -9
- package/docs/getting-started.md +4 -4
- package/docs/index.md +1 -0
- package/docs/releasing.md +15 -11
- package/docs/roadmap.md +49 -9
- package/docs/runtime-lifecycle.md +15 -4
- package/docs/runtime-policies.md +145 -0
- package/docs/stability.md +8 -4
- package/docs/updating.md +73 -0
- package/docs/vscode.md +30 -19
- package/package.json +3 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** Validate opt-in effective privileges and build catalog-only verification checks. */
|
|
2
|
+
const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
3
|
+
const TYPE_IDENTIFIER = /^[a-z_][a-z0-9_]{0,62}$/u;
|
|
4
|
+
const PRIVILEGES = {
|
|
5
|
+
schema: ["CREATE", "USAGE"],
|
|
6
|
+
table: [
|
|
7
|
+
"SELECT",
|
|
8
|
+
"INSERT",
|
|
9
|
+
"UPDATE",
|
|
10
|
+
"DELETE",
|
|
11
|
+
"TRUNCATE",
|
|
12
|
+
"REFERENCES",
|
|
13
|
+
"TRIGGER",
|
|
14
|
+
"MAINTAIN",
|
|
15
|
+
],
|
|
16
|
+
sequence: ["USAGE", "SELECT", "UPDATE"],
|
|
17
|
+
function: ["EXECUTE"],
|
|
18
|
+
};
|
|
19
|
+
const COLUMN_PRIVILEGES = new Set(["SELECT", "INSERT", "UPDATE", "REFERENCES"]);
|
|
20
|
+
const object = (value, label) => {
|
|
21
|
+
if (value === null ||
|
|
22
|
+
typeof value !== "object" ||
|
|
23
|
+
Array.isArray(value) ||
|
|
24
|
+
Object.getPrototypeOf(value) !== Object.prototype) {
|
|
25
|
+
throw new Error(`${label} must be an object.`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
const keys = (value, allowed, label) => {
|
|
30
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
31
|
+
throw new Error(`${label} contains unsupported fields.`);
|
|
32
|
+
};
|
|
33
|
+
const identifier = (value, label) => {
|
|
34
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value))
|
|
35
|
+
throw new Error(`${label} is not a safe PostgreSQL identifier.`);
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
const permissions = (value, allowed, label) => {
|
|
39
|
+
if (!Array.isArray(value) ||
|
|
40
|
+
value.length > allowed.length ||
|
|
41
|
+
value.includes(undefined) ||
|
|
42
|
+
value.some((entry) => typeof entry !== "string" || !allowed.includes(entry)) ||
|
|
43
|
+
new Set(value).size !== value.length) {
|
|
44
|
+
throw new Error(`${label} must contain distinct supported privileges.`);
|
|
45
|
+
}
|
|
46
|
+
return Object.freeze([...value]);
|
|
47
|
+
};
|
|
48
|
+
export const validateRuntimePrivileges = (input) => {
|
|
49
|
+
if (!Array.isArray(input) ||
|
|
50
|
+
input.length > 1_024 ||
|
|
51
|
+
input.includes(undefined))
|
|
52
|
+
throw new Error("runtimePolicy.privileges must be an array of at most 1024 declarations.");
|
|
53
|
+
const observed = new Map();
|
|
54
|
+
return Object.freeze(input.map((value, index) => {
|
|
55
|
+
const label = `runtimePolicy.privileges[${index}]`;
|
|
56
|
+
const entry = object(value, label);
|
|
57
|
+
if (typeof entry.kind !== "string" ||
|
|
58
|
+
!Object.hasOwn(PRIVILEGES, entry.kind)) {
|
|
59
|
+
throw new Error(`${label}.kind is unsupported.`);
|
|
60
|
+
}
|
|
61
|
+
const kind = entry.kind;
|
|
62
|
+
keys(entry, [
|
|
63
|
+
"kind",
|
|
64
|
+
"schema",
|
|
65
|
+
"role",
|
|
66
|
+
"required",
|
|
67
|
+
"forbidden",
|
|
68
|
+
...(kind === "schema" ? [] : ["name"]),
|
|
69
|
+
...(kind === "function" ? ["argumentTypes"] : []),
|
|
70
|
+
], label);
|
|
71
|
+
const schema = identifier(entry.schema, `${label}.schema`);
|
|
72
|
+
const role = identifier(entry.role, `${label}.role`);
|
|
73
|
+
const allowed = PRIVILEGES[kind];
|
|
74
|
+
const required = permissions(Object.hasOwn(entry, "required") ? entry.required : [], allowed, `${label}.required`);
|
|
75
|
+
const forbidden = permissions(Object.hasOwn(entry, "forbidden") ? entry.forbidden : [], allowed, `${label}.forbidden`);
|
|
76
|
+
if (required.length + forbidden.length === 0)
|
|
77
|
+
throw new Error(`${label} must declare a required or forbidden privilege.`);
|
|
78
|
+
const base = { schema, role, required, forbidden };
|
|
79
|
+
let normalized;
|
|
80
|
+
if (kind === "schema")
|
|
81
|
+
normalized = Object.freeze({ ...base, kind });
|
|
82
|
+
else {
|
|
83
|
+
const name = identifier(entry.name, `${label}.name`);
|
|
84
|
+
if (kind !== "function")
|
|
85
|
+
normalized = Object.freeze({ ...base, kind, name });
|
|
86
|
+
else {
|
|
87
|
+
if (!Array.isArray(entry.argumentTypes) ||
|
|
88
|
+
entry.argumentTypes.length > 32 ||
|
|
89
|
+
entry.argumentTypes.includes(undefined))
|
|
90
|
+
throw new Error(`${label}.argumentTypes must be an array of at most 32 types.`);
|
|
91
|
+
const argumentTypes = entry.argumentTypes.map((value, argumentIndex) => {
|
|
92
|
+
const typeLabel = `${label}.argumentTypes[${argumentIndex}]`;
|
|
93
|
+
const type = object(value, typeLabel);
|
|
94
|
+
keys(type, ["schema", "name"], typeLabel);
|
|
95
|
+
if (type.schema !== "pg_catalog" ||
|
|
96
|
+
typeof type.name !== "string" ||
|
|
97
|
+
!TYPE_IDENTIFIER.test(type.name)) {
|
|
98
|
+
throw new Error(`${typeLabel} must name a safe pg_catalog type.`);
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
schema: "pg_catalog",
|
|
102
|
+
name: type.name,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
normalized = Object.freeze({
|
|
106
|
+
...base,
|
|
107
|
+
kind,
|
|
108
|
+
name,
|
|
109
|
+
argumentTypes: Object.freeze(argumentTypes),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const identity = JSON.stringify([
|
|
114
|
+
kind,
|
|
115
|
+
schema,
|
|
116
|
+
normalized.kind === "schema" ? null : normalized.name,
|
|
117
|
+
role,
|
|
118
|
+
normalized.kind === "function" ? normalized.argumentTypes : null,
|
|
119
|
+
]);
|
|
120
|
+
const earlier = observed.get(identity) ?? {
|
|
121
|
+
required: new Set(),
|
|
122
|
+
forbidden: new Set(),
|
|
123
|
+
};
|
|
124
|
+
if (required.some((privilege) => forbidden.includes(privilege) || earlier.forbidden.has(privilege)) ||
|
|
125
|
+
forbidden.some((privilege) => earlier.required.has(privilege))) {
|
|
126
|
+
throw new Error(`${label} has overlapping required and forbidden privileges.`);
|
|
127
|
+
}
|
|
128
|
+
required.forEach((privilege) => earlier.required.add(privilege));
|
|
129
|
+
forbidden.forEach((privilege) => earlier.forbidden.add(privilege));
|
|
130
|
+
observed.set(identity, earlier);
|
|
131
|
+
return normalized;
|
|
132
|
+
}));
|
|
133
|
+
};
|
|
134
|
+
const literal = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
135
|
+
const typeLookup = (type) => `select t.oid from pg_catalog.pg_type t
|
|
136
|
+
join pg_catalog.pg_namespace n on n.oid = t.typnamespace
|
|
137
|
+
where n.nspname = 'pg_catalog' and t.typname = ${literal(type.name)}`;
|
|
138
|
+
const objectLookup = (entry) => {
|
|
139
|
+
if (entry.kind === "schema")
|
|
140
|
+
return `select n.oid into rehearsal_object from pg_catalog.pg_namespace n
|
|
141
|
+
where n.nspname = ${literal(entry.schema)};`;
|
|
142
|
+
if (entry.kind === "function")
|
|
143
|
+
return `select p.oid into rehearsal_object from pg_catalog.pg_proc p
|
|
144
|
+
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
|
|
145
|
+
where n.nspname = ${literal(entry.schema)} and p.proname = ${literal(entry.name)}
|
|
146
|
+
and p.prokind = 'f' and p.pronargs = ${entry.argumentTypes.length}
|
|
147
|
+
${entry.argumentTypes.map((type, index) => `and p.proargtypes[${index}] = (${typeLookup(type)})`).join("\n ")};`;
|
|
148
|
+
return `select c.oid into rehearsal_object from pg_catalog.pg_class c
|
|
149
|
+
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
|
|
150
|
+
where n.nspname = ${literal(entry.schema)} and c.relname = ${literal(entry.name)}
|
|
151
|
+
and c.relkind ${entry.kind === "sequence" ? "= 'S'" : "in ('r', 'p', 'v', 'm', 'f')"};`;
|
|
152
|
+
};
|
|
153
|
+
const permissionCheck = (entry, privilege, forbidden) => {
|
|
154
|
+
const functionName = `pg_catalog.has_${entry.kind}_privilege`;
|
|
155
|
+
const tableCheck = `${functionName}(rehearsal_role, rehearsal_object, ${literal(privilege)})`;
|
|
156
|
+
const expression = entry.kind === "table" && forbidden && COLUMN_PRIVILEGES.has(privilege)
|
|
157
|
+
? `(${tableCheck} or pg_catalog.has_any_column_privilege(rehearsal_role, rehearsal_object, ${literal(privilege)}))`
|
|
158
|
+
: tableCheck;
|
|
159
|
+
const evaluation = privilege === "MAINTAIN"
|
|
160
|
+
? `if pg_catalog.current_setting('server_version_num')::integer < 170000 then
|
|
161
|
+
rehearsal_allowed := false;
|
|
162
|
+
else
|
|
163
|
+
rehearsal_allowed := ${expression};
|
|
164
|
+
end if;`
|
|
165
|
+
: `rehearsal_allowed := ${expression};`;
|
|
166
|
+
return `${evaluation}
|
|
167
|
+
if rehearsal_allowed is null then
|
|
168
|
+
raise exception 'Rehearsal privilege object is unavailable';
|
|
169
|
+
end if;
|
|
170
|
+
if rehearsal_allowed is ${forbidden ? "true" : "false"} then
|
|
171
|
+
raise exception 'Rehearsal ${forbidden ? "forbidden privilege is present" : "required privilege is missing"}';
|
|
172
|
+
end if;`;
|
|
173
|
+
};
|
|
174
|
+
/** Blocks are embedded inside the policy verifier; no ACL or object writes are emitted. */
|
|
175
|
+
export const buildRuntimePrivilegeVerificationChecks = (entries) => entries.map((entry) => `declare
|
|
176
|
+
rehearsal_role pg_catalog.oid;
|
|
177
|
+
rehearsal_object pg_catalog.oid;
|
|
178
|
+
rehearsal_allowed boolean;
|
|
179
|
+
begin
|
|
180
|
+
select r.oid into rehearsal_role from pg_catalog.pg_roles r
|
|
181
|
+
where r.rolname = ${literal(entry.role)};
|
|
182
|
+
if rehearsal_role is null then
|
|
183
|
+
raise exception 'Rehearsal privilege role is missing';
|
|
184
|
+
end if;
|
|
185
|
+
${entry.kind === "function"
|
|
186
|
+
? entry.argumentTypes
|
|
187
|
+
.map((type) => `if not exists (${typeLookup(type)}) then
|
|
188
|
+
raise exception 'Rehearsal privilege argument type is missing';
|
|
189
|
+
end if;`)
|
|
190
|
+
.join("\n ")
|
|
191
|
+
: ""}
|
|
192
|
+
${objectLookup(entry)}
|
|
193
|
+
if rehearsal_object is null then
|
|
194
|
+
raise exception 'Rehearsal privilege object is missing';
|
|
195
|
+
end if;
|
|
196
|
+
${entry.required.map((privilege) => permissionCheck(entry, privilege, false)).join("\n ")}
|
|
197
|
+
${entry.forbidden.map((privilege) => permissionCheck(entry, privilege, true)).join("\n ")}
|
|
198
|
+
end;`);
|
|
@@ -36,7 +36,7 @@ declare const candidateSummary: (inspection: MigrationInspection) => Readonly<{
|
|
|
36
36
|
status: string;
|
|
37
37
|
}[];
|
|
38
38
|
}>;
|
|
39
|
-
export declare const inspectRuntimeTopologyCandidates: (options?: RehearsalConfigPathOptions) => Promise<Readonly<{
|
|
39
|
+
export declare const inspectRuntimeTopologyCandidates: (options?: RehearsalConfigPathOptions, inspectMigrations?: typeof inspectRehearsalMigrations) => Promise<Readonly<{
|
|
40
40
|
topology: Readonly<{
|
|
41
41
|
projectRoot: string;
|
|
42
42
|
primary: RuntimeTopologyTarget;
|
|
@@ -103,13 +103,13 @@ const candidateSummary = (inspection) => {
|
|
|
103
103
|
candidates: Object.freeze(candidates),
|
|
104
104
|
});
|
|
105
105
|
};
|
|
106
|
-
export const inspectRuntimeTopologyCandidates = async (options = {}) => {
|
|
106
|
+
export const inspectRuntimeTopologyCandidates = async (options = {}, inspectMigrations = inspectRehearsalMigrations) => {
|
|
107
107
|
const topology = await loadRuntimeTopology(options);
|
|
108
108
|
const targets = [];
|
|
109
109
|
for (const target of topology.targets) {
|
|
110
110
|
targets.push(Object.freeze({
|
|
111
111
|
...target,
|
|
112
|
-
summary: candidateSummary(await
|
|
112
|
+
summary: candidateSummary(await inspectMigrations({
|
|
113
113
|
projectRoot: topology.projectRoot,
|
|
114
114
|
configPath: target.configPath,
|
|
115
115
|
})),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const HELD_PATH_ANCESTRY_MAXIMUM_DIRECTORIES = 128;
|
|
2
|
+
export interface HeldPathAncestry {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
assertUnchanged(): Promise<void>;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare const holdPathAncestry: (directory: string) => Promise<HeldPathAncestry>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timestamp-based concurrent-change guard for a canonical directory ancestry.
|
|
3
|
+
* This is not atomic openat resolution or a sandbox: it cannot prevent every
|
|
4
|
+
* transient access or privileged timestamp manipulation. Callers retain their
|
|
5
|
+
* own project-containment, regular-file and exact-input identity checks, use
|
|
6
|
+
* only `path` for reads, assert before/after those reads, and close in finally.
|
|
7
|
+
*/
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
10
|
+
import { isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
11
|
+
export const HELD_PATH_ANCESTRY_MAXIMUM_DIRECTORIES = 128;
|
|
12
|
+
const unavailable = () => new Error("Path ancestry is unavailable, unsafe or changed.");
|
|
13
|
+
const identity = (details) => {
|
|
14
|
+
if (!details.isDirectory() ||
|
|
15
|
+
details.isSymbolicLink() ||
|
|
16
|
+
typeof details.dev !== "bigint" ||
|
|
17
|
+
typeof details.ino !== "bigint" ||
|
|
18
|
+
typeof details.ctimeNs !== "bigint" ||
|
|
19
|
+
typeof details.mtimeNs !== "bigint")
|
|
20
|
+
throw unavailable();
|
|
21
|
+
return {
|
|
22
|
+
dev: details.dev,
|
|
23
|
+
ino: details.ino,
|
|
24
|
+
ctimeNs: details.ctimeNs,
|
|
25
|
+
mtimeNs: details.mtimeNs,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const same = (left, right) => left.dev === right.dev &&
|
|
29
|
+
left.ino === right.ino &&
|
|
30
|
+
left.ctimeNs === right.ctimeNs &&
|
|
31
|
+
left.mtimeNs === right.mtimeNs;
|
|
32
|
+
export const holdPathAncestry = async (directory) => {
|
|
33
|
+
const handles = [];
|
|
34
|
+
const entries = [];
|
|
35
|
+
let closed = false;
|
|
36
|
+
let closing;
|
|
37
|
+
const close = () => {
|
|
38
|
+
if (closing)
|
|
39
|
+
return closing;
|
|
40
|
+
closed = true;
|
|
41
|
+
closing = (async () => {
|
|
42
|
+
let failed = false;
|
|
43
|
+
for (const handle of handles.splice(0).reverse()) {
|
|
44
|
+
try {
|
|
45
|
+
await handle.close();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
failed = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (failed)
|
|
52
|
+
throw unavailable();
|
|
53
|
+
})();
|
|
54
|
+
return closing;
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
if (!isAbsolute(directory))
|
|
58
|
+
throw unavailable();
|
|
59
|
+
const path = resolve(directory);
|
|
60
|
+
const root = parse(path).root;
|
|
61
|
+
const segments = relative(root, path).split(sep).filter(Boolean);
|
|
62
|
+
if (segments.length + 1 > HELD_PATH_ANCESTRY_MAXIMUM_DIRECTORIES)
|
|
63
|
+
throw unavailable();
|
|
64
|
+
// Reject aliases and all symlink ancestors, including ancestors above the
|
|
65
|
+
// caller's project root. Root aliases must be canonicalized by the caller.
|
|
66
|
+
if ((await realpath(path)) !== path)
|
|
67
|
+
throw unavailable();
|
|
68
|
+
const paths = [root];
|
|
69
|
+
for (const segment of segments)
|
|
70
|
+
paths.push(join(paths.at(-1), segment));
|
|
71
|
+
for (const current of paths) {
|
|
72
|
+
const before = identity(await lstat(current, { bigint: true }));
|
|
73
|
+
const handle = await open(current, constants.O_RDONLY |
|
|
74
|
+
constants.O_DIRECTORY |
|
|
75
|
+
constants.O_NOFOLLOW |
|
|
76
|
+
constants.O_NONBLOCK);
|
|
77
|
+
// Retain immediately: a later stat failure must still close this handle.
|
|
78
|
+
handles.push(handle);
|
|
79
|
+
const opened = identity(await handle.stat({ bigint: true }));
|
|
80
|
+
const after = identity(await lstat(current, { bigint: true }));
|
|
81
|
+
if (!same(before, opened) || !same(before, after))
|
|
82
|
+
throw unavailable();
|
|
83
|
+
entries.push({ path: current, handle, identity: before });
|
|
84
|
+
}
|
|
85
|
+
const assertUnchanged = async () => {
|
|
86
|
+
try {
|
|
87
|
+
if (closed || (await realpath(path)) !== path)
|
|
88
|
+
throw unavailable();
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const held = identity(await entry.handle.stat({ bigint: true }));
|
|
91
|
+
const lexical = identity(await lstat(entry.path, { bigint: true }));
|
|
92
|
+
if (!same(entry.identity, held) || !same(entry.identity, lexical))
|
|
93
|
+
throw unavailable();
|
|
94
|
+
}
|
|
95
|
+
if ((await realpath(path)) !== path)
|
|
96
|
+
throw unavailable();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
await close().catch(() => { });
|
|
100
|
+
throw unavailable();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
await assertUnchanged();
|
|
104
|
+
return Object.freeze({ path, assertUnchanged, close });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
await close().catch(() => { });
|
|
108
|
+
throw unavailable();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BaselineManifest } from "../baseline/artifact.mjs";
|
|
2
|
+
import type { RuntimeTopologyTarget } from "../runtime/topology.mjs";
|
|
3
|
+
import { runOwnedProcess } from "../shared/owned_process.mjs";
|
|
4
|
+
export type MigrationImpactRuntimeFailureReason = "runtime_unavailable" | "ownership_mismatch" | "stale_runtime" | "query_failed" | "lock_timeout" | "statement_timeout" | "transaction_timeout" | "cancelled";
|
|
5
|
+
export declare class MigrationImpactRuntimeError extends Error {
|
|
6
|
+
readonly reason: MigrationImpactRuntimeFailureReason;
|
|
7
|
+
constructor(reason: MigrationImpactRuntimeFailureReason);
|
|
8
|
+
}
|
|
9
|
+
export interface MigrationImpactRuntime {
|
|
10
|
+
readonly identitySha256: string;
|
|
11
|
+
runSql(sql: string | (() => string)): Promise<string>;
|
|
12
|
+
assertCurrent(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface OpenMigrationImpactRuntimeOptions {
|
|
15
|
+
readonly projectRoot: string;
|
|
16
|
+
readonly target: RuntimeTopologyTarget;
|
|
17
|
+
readonly baseline: BaselineManifest;
|
|
18
|
+
readonly signal?: AbortSignal;
|
|
19
|
+
readonly runProcessImplementation?: typeof runOwnedProcess;
|
|
20
|
+
}
|
|
21
|
+
/** No lifecycle, adapter, source-reader, URL or credential-file access is permitted here. */
|
|
22
|
+
export declare function openMigrationImpactRuntime(options: OpenMigrationImpactRuntimeOptions): Promise<MigrationImpactRuntime>;
|