-
-
Notifications
You must be signed in to change notification settings - Fork 59
/
Copy pathactions.ts
195 lines (182 loc) · 5.84 KB
/
actions.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import { Logger } from "@graphile/logger";
import { exec as rawExec } from "child_process";
import { promises as fsp } from "fs";
import { parse } from "pg-connection-string";
import { promisify } from "util";
import { mergeWithoutClobbering } from "./lib";
import { generatePlaceholderReplacement } from "./migration";
import { withClient } from "./pg";
import {
isActionSpec,
isCommandActionSpec,
isSqlActionSpec,
makeRootDatabaseConnectionString,
ParsedSettings,
} from "./settings";
interface ActionSpecBase {
_: string;
shadow?: boolean;
/**
* USE THIS WITH CARE! Currently only supported by the afterReset hook, all
* other hooks will throw an error when set. Runs the file using the
* rootConnectionString role (i.e. a superuser, but with database name from
* connectionString), useful for creating extensions.
*/
root?: boolean;
}
export const DO_NOT_USE_DATABASE_URL = "postgres://PLEASE:USE@GM_DBURL/INSTEAD";
export interface SqlActionSpec extends ActionSpecBase {
_: "sql";
file: string;
}
export interface CommandActionSpec extends ActionSpecBase {
_: "command";
command: string;
}
export type ActionSpec = SqlActionSpec | CommandActionSpec;
const exec = promisify(rawExec);
export async function executeActions(
parsedSettings: ParsedSettings,
shadow = false,
actions: ActionSpec[],
): Promise<void> {
if (!actions) {
return;
}
const connectionString = shadow
? parsedSettings.shadowConnectionString
: parsedSettings.connectionString;
if (!connectionString) {
throw new Error(
"Could not determine connection string for running commands",
);
}
const { database: databaseName, user: databaseUser } = parse(
connectionString,
);
if (!databaseName) {
throw new Error("Could not extract database name from connection string");
}
for (const actionSpec of actions) {
if (actionSpec.shadow !== undefined && actionSpec.shadow !== shadow) {
continue;
}
const hookConnectionString = actionSpec.root
? makeRootDatabaseConnectionString(parsedSettings, databaseName)
: connectionString;
if (actionSpec._ === "sql") {
const body = await fsp.readFile(
`${parsedSettings.migrationsFolder}/${actionSpec.file}`,
"utf8",
);
await withClient(
hookConnectionString,
parsedSettings,
async (pgClient, context) => {
const query = generatePlaceholderReplacement(
parsedSettings,
context,
)(body);
await pgClient.query({
text: query,
});
},
);
} else if (actionSpec._ === "command") {
// Run the command
const { stdout, stderr } = await exec(actionSpec.command, {
env: mergeWithoutClobbering(
{
...process.env,
DATABASE_URL: DO_NOT_USE_DATABASE_URL, // DO NOT USE THIS! It can be misleading.
},
{
GM_DBNAME: databaseName,
// When `root: true`, GM_DBUSER may be perceived as ambiguous, so we must not set it.
...(actionSpec.root
? null
: {
GM_DBUSER: databaseUser,
}),
GM_DBURL: hookConnectionString,
...(shadow
? {
GM_SHADOW: "1",
}
: null),
},
"please ensure this environmental variable is not set because graphile-migrate sets it dynamically for children.",
),
encoding: "utf8",
// 50MB of log data should be enough for any reasonable migration... right?
maxBuffer: 50 * 1024 * 1024,
});
if (stdout) {
parsedSettings.logger.info(stdout);
}
if (stderr) {
parsedSettings.logger.error(stderr);
}
}
}
}
export function makeValidateActionCallback(logger: Logger, allowRoot = false) {
return async (inputValue: unknown): Promise<ActionSpec[]> => {
const specs: ActionSpec[] = [];
if (inputValue) {
const rawSpecArray = Array.isArray(inputValue)
? inputValue
: [inputValue];
for (const trueRawSpec of rawSpecArray) {
// This fudge is for backwards compatibility with v0.0.3
const isV003OrBelowCommand =
typeof trueRawSpec === "object" &&
trueRawSpec &&
!trueRawSpec["_"] &&
typeof trueRawSpec["command"] === "string";
if (isV003OrBelowCommand) {
logger.warn(
"DEPRECATED: graphile-migrate now requires command action specs to have an `_: 'command'` property; we'll back-fill this for now, but please update your configuration",
);
}
const rawSpec = isV003OrBelowCommand
? { _: "command", ...trueRawSpec }
: trueRawSpec;
if (rawSpec && typeof rawSpec === "string") {
const sqlSpec: SqlActionSpec = rawSpec.startsWith("!")
? {
_: "sql",
file: rawSpec.substring(1),
root: true,
}
: {
_: "sql",
file: rawSpec,
};
specs.push(sqlSpec);
} else if (isActionSpec(rawSpec)) {
if (isSqlActionSpec(rawSpec) || isCommandActionSpec(rawSpec)) {
specs.push(rawSpec);
} else {
throw new Error(
`Action spec of type '${rawSpec["_"]}' not supported; perhaps you need to upgrade?`,
);
}
} else {
throw new Error(
`Expected action spec to contain an array of strings or action specs; received '${typeof rawSpec}'`,
);
}
}
}
// Final validations
for (const spec of specs) {
if (!allowRoot && spec._ === "sql" && spec.root) {
throw new Error(
"This hooks isn't permitted to require root privileges.",
);
}
}
return specs;
};
}