graphile-migrate
Version:
Opinionated SQL-powered migration tool for PostgreSQL
246 lines • 11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const logger_1 = require("@graphile/logger");
const pg_connection_string_1 = require("pg-connection-string");
const querystring = require("querystring");
const url_1 = require("url");
const actions_1 = require("./actions");
const logger_2 = require("./logger");
function isActionSpec(o) {
if (!(typeof o === "object" && o && typeof o["_"] === "string")) {
return false;
}
// After here it's definitely an action spec; but we should still validate the
// other properties.
if ("shadow" in o && typeof o["shadow"] !== "boolean") {
throw new Error(`'${o["_"]}' action has 'shadow' property of type '${typeof o["shadow"]}'; expected 'boolean' (or not set)`);
}
return true;
}
exports.isActionSpec = isActionSpec;
function isSqlActionSpec(o) {
if (!isActionSpec(o) || o._ !== "sql") {
return false;
}
if (typeof o["file"] !== "string") {
throw new Error("SQL command requires 'file' property to be set");
}
return true;
}
exports.isSqlActionSpec = isSqlActionSpec;
function isCommandActionSpec(o) {
if (!isActionSpec(o) || o._ !== "command") {
return false;
}
// Validations
if (typeof o["command"] !== "string") {
throw new Error(`Command action has 'command' property of type '${typeof o["command"]}'; expected 'string'`);
}
return true;
}
exports.isCommandActionSpec = isCommandActionSpec;
async function parseSettings(settings, requireShadow = false) {
if (!settings) {
throw new Error("Expected settings object");
}
if (typeof settings !== "object") {
throw new Error("Expected settings object, received " + typeof settings);
}
const errors = [];
const keysToCheck = Object.keys(settings);
const checkedKeys = [];
async function check(key, callback) {
checkedKeys.push(key);
const value = settings[key];
try {
return await callback(value);
}
catch (e) {
errors.push(`Setting '${key}': ${e.message}`);
return void 0;
}
}
const connectionString = await check("connectionString", (rawConnectionString = process.env.DATABASE_URL) => {
if (typeof rawConnectionString !== "string") {
throw new Error("Expected a string, or for DATABASE_URL envvar to be set");
}
return rawConnectionString;
});
const logger = await check("logger", (rawLogger = logger_2.defaultLogger) => {
if (!(rawLogger instanceof logger_1.Logger)) {
throw new Error("Expected 'logger' to be a @graphile/logger Logger instance");
}
return rawLogger;
});
const rootConnectionString = await check("rootConnectionString", (rawRootConnectionString = process.env.ROOT_DATABASE_URL || "template1") => {
if (typeof rawRootConnectionString !== "string") {
throw new Error("Expected a string, or for ROOT_DATABASE_URL envvar to be set");
}
return rawRootConnectionString;
});
const migrationsFolder = await check("migrationsFolder", (rawMigrationsFolder = `${process.cwd()}/migrations`) => {
if (typeof rawMigrationsFolder !== "string") {
throw new Error("Expected a string");
}
return rawMigrationsFolder;
});
const blankMigrationContent = await check("blankMigrationContent", (rawBlankMigrationContent = "-- Enter migration here\n") => {
if (typeof rawBlankMigrationContent !== "string") {
throw new Error("Expected a string");
}
return rawBlankMigrationContent;
});
const { user, database: databaseName } = pg_connection_string_1.parse(connectionString || "");
const databaseOwner = await check("databaseOwner", (rawDatabaseOwner = user || databaseName) => {
if (typeof rawDatabaseOwner !== "string") {
throw new Error("Expected a string or for user or database name to be specified in connectionString");
}
return rawDatabaseOwner;
});
const shadowConnectionString = await check("shadowConnectionString", (rawShadowConnectionString = process.env.SHADOW_DATABASE_URL) => {
if (requireShadow) {
if (typeof rawShadowConnectionString !== "string") {
throw new Error("Expected `shadowConnectionString` to be a string, or for SHADOW_DATABASE_URL to be set");
}
return rawShadowConnectionString;
}
return null;
});
const { database: shadowDatabaseName } = pg_connection_string_1.parse(shadowConnectionString || "");
await check("pgSettings", pgSettings => {
if (pgSettings) {
if (typeof pgSettings !== "object" || pgSettings === null) {
throw new Error("Expected settings.pgSettings to be an object");
}
const badKeys = Object.keys(pgSettings).filter(key => {
const value = pgSettings[key];
return typeof value !== "string" && typeof value !== "number";
});
if (badKeys.length) {
throw new Error(`Invalid pgSettings for keys '${badKeys.join(", ")}' - expected string` /* Number is acceptable, but prefer string. Boolean not acceptable. */);
}
}
});
const placeholders = await check("placeholders", (rawPlaceholders) => {
if (rawPlaceholders) {
if (typeof rawPlaceholders !== "object" || rawPlaceholders === null) {
throw new Error("Expected settings.placeholders to be an object");
}
const badKeys = Object.keys(rawPlaceholders).filter(key => !/^:[A-Z][0-9A-Z_]+$/.exec(key));
if (badKeys.length) {
throw new Error(`Invalid placeholders keys '${badKeys.join(", ")}' - expected to follow format ':ABCD_EFG_HIJ'`);
}
const badValueKeys = Object.keys(rawPlaceholders).filter(key => {
const value = rawPlaceholders[key];
return typeof value !== "string";
});
if (badValueKeys.length) {
throw new Error(`Invalid placeholders values for keys '${badValueKeys.join(", ")}' - expected string`);
}
return Object.entries(rawPlaceholders).reduce((memo, [key, value]) => {
if (value === "!ENV") {
const envvarKey = key.substring(1);
const envvar = process.env[envvarKey];
if (!envvar) {
throw new Error(`Could not find environmental variable '${envvarKey}'`);
}
memo[key] = envvar;
}
return memo;
}, Object.assign({}, rawPlaceholders));
}
return undefined;
});
const validateAction = actions_1.makeValidateActionCallback(logger);
const rootValidateAction = actions_1.makeValidateActionCallback(logger, true);
const beforeReset = await check("beforeReset", rootValidateAction);
const afterReset = await check("afterReset", rootValidateAction);
const beforeAllMigrations = await check("beforeAllMigrations", validateAction);
const afterAllMigrations = await check("afterAllMigrations", validateAction);
const beforeCurrent = await check("beforeCurrent", validateAction);
const afterCurrent = await check("afterCurrent", validateAction);
const manageGraphileMigrateSchema = await check("manageGraphileMigrateSchema", mgms => {
const type = typeof mgms;
if (type !== "undefined" && type !== "boolean") {
throw new Error(`Expected boolean, received '${type === "object" && !mgms ? "null" : type}'`);
}
return mgms !== false;
});
/******/
const uncheckedKeys = keysToCheck
.filter(key => !checkedKeys.includes(key))
.filter(key => !key.startsWith("//"));
if (uncheckedKeys.length) {
errors.push(`The following config settings were not understood: '${uncheckedKeys.join("', '")}'`);
}
if (connectionString) {
if (!databaseOwner) {
errors.push("Could not determine the database owner, please add the 'databaseOwner' setting.");
}
if (!databaseName) {
errors.push("Could not determine the database name, please ensure connectionString includes the database name.");
}
if (connectionString === rootConnectionString ||
(requireShadow && connectionString === shadowConnectionString)) {
errors.push("connectionString cannot be the same value as rootConnectionString or shadowConnectionString.");
}
}
if (requireShadow && !shadowDatabaseName) {
errors.push("Could not determine the shadow database name, please ensure shadowConnectionString includes the database name.");
}
if (errors.length) {
throw new Error(`Errors occurred during settings validation:\n- ${errors.join("\n- ")}`);
}
if (!databaseName) {
// This is just to appease TypeScript, this should be caught above.
throw new Error("Could not determine databaseName");
}
return Object.assign(Object.assign({}, settings), { beforeReset,
beforeAllMigrations,
beforeCurrent,
afterReset,
afterAllMigrations,
afterCurrent,
rootConnectionString,
connectionString,
manageGraphileMigrateSchema,
databaseOwner,
migrationsFolder,
databaseName, shadowConnectionString: shadowConnectionString
? shadowConnectionString
: void 0, shadowDatabaseName: shadowDatabaseName ? shadowDatabaseName : void 0, placeholders,
blankMigrationContent,
logger });
}
exports.parseSettings = parseSettings;
/**
* Overrides the databaseName in rootConnectionString and returns the resulting
* connection string.
*/
function makeRootDatabaseConnectionString(parsedSettings, databaseName) {
const { rootConnectionString } = parsedSettings;
if (!rootConnectionString) {
throw new Error("Cannot execute SQL as root since rootConnectionString / ROOT_DATABASE_URL is not specified");
}
const parsed = url_1.parse(rootConnectionString, true);
const isJustADatabaseName = !parsed.protocol;
if (parsed.protocol === "socket:") {
parsed.query.db = databaseName;
const query = querystring.stringify(parsed.query);
if (parsed.auth) {
return `socket://${parsed.auth}@${parsed.pathname}?${query}`;
}
else {
return `socket:${parsed.pathname}?${query}`;
}
}
else if (isJustADatabaseName) {
return databaseName;
}
else {
parsed.pathname = `/${databaseName}`;
return url_1.format(parsed);
}
}
exports.makeRootDatabaseConnectionString = makeRootDatabaseConnectionString;
//# sourceMappingURL=settings.js.map