diff --git a/src/actions.ts b/src/actions.ts index 509d1df..34048c8 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -2,7 +2,7 @@ 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 { inspect, promisify } from "util"; import { mergeWithoutClobbering } from "./lib"; import { generatePlaceholderReplacement } from "./migration"; @@ -127,13 +127,14 @@ export async function executeActions( if (stderr) { parsedSettings.logger.error(stderr); } - } catch (e: any) { - const { stdout, stderr } = e; - if (stdout) { - parsedSettings.logger.info(stdout); - } - if (stderr) { - parsedSettings.logger.error(stderr); + } catch (e) { + if (typeof e === "object" && e !== null) { + if ("stdout" in e && typeof e.stdout === "string" && e.stdout) { + parsedSettings.logger.info(e.stdout); + } + if ("stderr" in e && typeof e.stderr === "string" && e.stderr) { + parsedSettings.logger.error(e.stderr); + } } throw e; } @@ -146,14 +147,15 @@ export function makeValidateActionCallback(logger: Logger, allowRoot = false) { const specs: ActionSpec[] = []; if (inputValue) { const rawSpecArray = Array.isArray(inputValue) - ? inputValue + ? (inputValue as unknown[]) : [inputValue]; for (const trueRawSpec of rawSpecArray) { // This fudge is for backwards compatibility with v0.0.3 const isV003OrBelowCommand = typeof trueRawSpec === "object" && - trueRawSpec && - !trueRawSpec["_"] && + trueRawSpec !== null && + !("_" in trueRawSpec && trueRawSpec._) && + "command" in trueRawSpec && typeof trueRawSpec["command"] === "string"; if (isV003OrBelowCommand) { logger.warn( @@ -181,7 +183,7 @@ export function makeValidateActionCallback(logger: Logger, allowRoot = false) { specs.push(rawSpec); } else { throw new Error( - `Action spec of type '${rawSpec["_"]}' not supported; perhaps you need to upgrade?`, + `Action spec '${inspect(rawSpec)}' not supported; perhaps you need to upgrade?`, ); } } else { diff --git a/src/cli.ts b/src/cli.ts index d0b6a80..347767d 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,6 +10,7 @@ import { runCommand } from "./commands/run"; import { statusCommand } from "./commands/status"; import { uncommitCommand } from "./commands/uncommit"; import { watchCommand } from "./commands/watch"; +import { isLoggedError } from "./lib"; import { version } from "./version"; function wrapHandler( @@ -20,8 +21,8 @@ function wrapHandler( const newHandler: yargs.CommandModule["handler"] = async (argv) => { try { return await Promise.resolve(handler(argv)); - } catch (e: any) { - if (!e["_gmlogged"]) { + } catch (e) { + if (!isLoggedError(e)) { // eslint-disable-next-line no-console console.error(e); } @@ -35,7 +36,7 @@ function wrapHandler( }; } -yargs +const f = yargs .parserConfiguration({ "boolean-negation": true, "camel-case-expansion": false, @@ -99,3 +100,10 @@ You are running graphile-migrate v${version}. ╚═══════════════════════════════════╝ `, ).argv; + +if ("then" in f && typeof f.then === "function") { + f.then(null, (e: Error) => { + // eslint-disable-next-line no-console + console.error(e); + }); +} diff --git a/src/commands/_common.ts b/src/commands/_common.ts index a25b1f4..ed9a5af 100644 --- a/src/commands/_common.ts +++ b/src/commands/_common.ts @@ -2,6 +2,7 @@ import { constants, promises as fsp } from "fs"; import * as JSON5 from "json5"; import { resolve } from "path"; import { parse } from "pg-connection-string"; +import { pathToFileURL } from "url"; import { Settings } from "../settings"; @@ -33,13 +34,17 @@ export async function getSettingsFromJSON(path: string): Promise { let data; try { data = await fsp.readFile(path, "utf8"); - } catch (e: any) { - throw new Error(`Failed to read '${path}': ${e.message}`); + } catch (e) { + throw new Error( + `Failed to read '${path}': ${e instanceof Error ? e.message : String(e)}`, + ); } try { return JSON5.parse(data); - } catch (e: any) { - throw new Error(`Failed to parse '${path}': ${e.message}`); + } catch (e) { + throw new Error( + `Failed to parse '${path}': ${e instanceof Error ? e.message : String(e)}`, + ); } } @@ -64,20 +69,21 @@ interface Options { */ export async function getSettings(options: Options = {}): Promise { const { configFile } = options; - const tryRequire = (path: string): Settings => { + const tryRequire = async (path: string): Promise => { // If the file is e.g. `foo.js` then Node `require('foo.js')` would look in // `node_modules`; we don't want this - instead force it to be a relative // path. - const relativePath = resolve(process.cwd(), path); + const relativePath = pathToFileURL(resolve(process.cwd(), path)).href; try { - return require(relativePath); - } catch (e: any) { + return (await import(relativePath)) as Settings; + } catch (e) { throw new Error( - `Failed to import '${relativePath}'; error:\n ${e.stack.replace( - /\n/g, - "\n ", - )}`, + `Failed to import '${relativePath}'; error:\n ${ + e instanceof Error && e.stack + ? e.stack.replace(/\n/g, "\n ") + : String(e) + }`, ); } }; @@ -120,7 +126,7 @@ export function readStdin(): Promise { process.stdin.on("readable", () => { let chunk; // Use a loop to make sure we read all available data. - while ((chunk = process.stdin.read()) !== null) { + while ((chunk = process.stdin.read() as string | null) !== null) { data += chunk; } }); diff --git a/src/commands/commit.ts b/src/commands/commit.ts index a4e08c0..1911624 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -111,7 +111,9 @@ export async function _commit( currentLocation, parsedSettings.blankMigrationContent.trim() + "\n", ); - } catch (e: any) { + } catch (err) { + const e = err instanceof Error ? err : new Error(String(err)); + logDbError(parsedSettings, e); parsedSettings.logger.error("ABORTING..."); diff --git a/src/commands/reset.ts b/src/commands/reset.ts index c50caad..e70d69d 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -46,9 +46,9 @@ export async function _reset( databaseName, )} OWNER ${escapeIdentifier(databaseOwner)};`, ); - } catch (e: any) { + } catch (e) { throw new Error( - `Failed to create database '${databaseName}' with owner '${databaseOwner}': ${e.message}`, + `Failed to create database '${databaseName}' with owner '${databaseOwner}': ${e instanceof Error ? e.message : String(e)}`, ); } await pgClient.query( diff --git a/src/commands/run.ts b/src/commands/run.ts index f15683d..f4a065f 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,4 +1,5 @@ import { promises as fsp } from "fs"; +import { QueryResultRow } from "pg"; import { CommandModule } from "yargs"; import { DO_NOT_USE_DATABASE_URL } from "../actions"; @@ -19,7 +20,7 @@ interface RunArgv extends CommonArgv { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function run( +export async function run( settings: Settings, content: string, filename: string, diff --git a/src/commands/watch.ts b/src/commands/watch.ts index 1b2401d..6f0c502 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -14,6 +14,8 @@ import { readCurrentMigration, writeCurrentMigration, } from "../current"; +import { DbCurrent } from "../interfaces"; +import { isLoggedError } from "../lib"; import { CommonArgv, getSettings } from "./_common"; interface WatchArgv extends CommonArgv { @@ -57,7 +59,7 @@ export function _makeCurrentMigrationRunner( // 2: Get last current.sql from graphile_migrate.current const { rows: [previousCurrent], - } = await lockingPgClient.query( + } = await lockingPgClient.query( ` select * from graphile_migrate.current @@ -161,7 +163,8 @@ export function _makeCurrentMigrationRunner( : "" })`, ); - } catch (e: any) { + } catch (err) { + const e = err instanceof Error ? err : new Error(String(err)); logDbError(parsedSettings, e); throw e; } @@ -198,10 +201,10 @@ export async function _watch( running = true; run() - .catch((error) => { - if (!error["_gmlogged"]) { + .catch((error: unknown) => { + if (!isLoggedError(error)) { parsedSettings.logger.error( - `Error occurred whilst processing migration: ${error.message}`, + `Error occurred whilst processing migration: ${error instanceof Error ? error.message : String(error)}`, { error }, ); } diff --git a/src/current.ts b/src/current.ts index 24ec6c1..e0c324f 100644 --- a/src/current.ts +++ b/src/current.ts @@ -2,6 +2,7 @@ import * as assert from "assert"; import { promises as fsp, Stats } from "fs"; import { isNoTransactionDefined } from "./header"; +import { errorCode } from "./lib"; import { parseMigrationText, serializeHeader } from "./migration"; import { ParsedSettings } from "./settings"; @@ -10,8 +11,8 @@ export const VALID_FILE_REGEX = /^([0-9]+)(-[-_a-zA-Z0-9]*)?\.sql$/; async function statOrNull(path: string): Promise { try { return await fsp.stat(path); - } catch (e: any) { - if (e.code === "ENOENT") { + } catch (e) { + if (errorCode(e) === "ENOENT") { return null; } throw e; @@ -21,8 +22,8 @@ async function statOrNull(path: string): Promise { async function readFileOrNull(path: string): Promise { try { return await fsp.readFile(path, "utf8"); - } catch (e: any) { - if (e.code === "ENOENT") { + } catch (e) { + if (errorCode(e) === "ENOENT") { return null; } throw e; @@ -31,8 +32,10 @@ async function readFileOrNull(path: string): Promise { async function readFileOrError(path: string): Promise { try { return await fsp.readFile(path, "utf8"); - } catch (e: any) { - throw new Error(`Failed to read file at '${path}': ${e.message}`); + } catch (e) { + throw new Error( + `Failed to read file at '${path}': ${e instanceof Error ? e.message : String(e)}`, + ); } } diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 562e64a..d6a5b18 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -1,10 +1,11 @@ import * as chalk from "chalk"; +import { QueryResultRow } from "pg"; import indent from "./indent"; import { Client } from "./pg"; import { ParsedSettings } from "./settings"; -interface InstrumentationError extends Error { +export interface InstrumentationError extends Error { severity?: string; code?: string; detail?: string; @@ -14,19 +15,21 @@ interface InstrumentationError extends Error { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function runQueryWithErrorInstrumentation( - pgClient: Client, - body: string, - filename: string, -): Promise { +export async function runQueryWithErrorInstrumentation< + T extends QueryResultRow = QueryResultRow, +>(pgClient: Client, body: string, filename: string): Promise { try { - const { rows } = await pgClient.query({ + const { rows } = await pgClient.query({ text: body, }); return rows; - } catch (e: any) { - if (e.position) { - const p = parseInt(e.position, 10); + } catch (e) { + if ( + e instanceof Error && + "position" in e && + (typeof e.position === "string" || typeof e.position === "number") + ) { + const p = parseInt(String(e.position), 10); let line = 1; let column = 0; let idx = 0; @@ -61,9 +64,11 @@ export async function runQueryWithErrorInstrumentation( chalk.reset(indent(indent(snippet, codeIndent), indentString)), indentString + chalk.red("-".repeat(positionWithinLine - 1 + codeIndent) + "^"), - indentString + chalk.red.bold(e.code) + chalk.red(": " + e.message), + indentString + + chalk.red.bold((e as InstrumentationError).code) + + chalk.red(": " + e.message), ]; - e["_gmMessageOverride"] = lines.join("\n"); + (e as InstrumentationError)["_gmMessageOverride"] = lines.join("\n"); } throw e; } diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..bda5aa0 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,6 @@ +/** Represents the graphile_migrate.current type in the DB */ +export interface DbCurrent { + filename: string; + content: string; + date: Date; +} diff --git a/src/lib.ts b/src/lib.ts index 530c14e..a06dd5d 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -16,3 +16,21 @@ export function mergeWithoutClobbering( return result; } + +export function isLoggedError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "_gmlogged" in error && + error._gmlogged === true + ); +} + +export function errorCode(e: unknown): string | null { + return typeof e === "object" && + e !== null && + "code" in e && + typeof e.code === "string" + ? e.code + : null; +} diff --git a/src/memoize.ts b/src/memoize.ts index 96bdf1a..e303e5d 100644 --- a/src/memoize.ts +++ b/src/memoize.ts @@ -1,10 +1,10 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ export default function memoize) => any>( fn: T, ): (...funcArgs: Parameters) => ReturnType { - let lastArgs: Array; - let lastResult: any; - return (...args: Array): any => { + let lastArgs: Parameters; + let lastResult: ReturnType; + return (...args: Parameters): ReturnType => { if ( lastArgs && args.length === lastArgs.length && diff --git a/src/migration.ts b/src/migration.ts index 3566e76..5177fd2 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -99,7 +99,7 @@ export const generatePlaceholderReplacement = memoize( ); // So memoization above holds from compilePlaceholders -const contextObj = memoize((database) => ({ database })); +const contextObj = memoize((database: string) => ({ database })); export function compilePlaceholders( parsedSettings: ParsedSettings, @@ -131,7 +131,7 @@ async function verifyGraphileMigrateSchema(pgClient: Client): Promise { // Verify that graphile_migrate schema exists const { rows: [graphileMigrateSchema], - } = await pgClient.query( + } = await pgClient.query<{ oid: string }>( `select oid from pg_namespace where nspname = 'graphile_migrate';`, ); if (!graphileMigrateSchema) { @@ -144,7 +144,7 @@ async function verifyGraphileMigrateSchema(pgClient: Client): Promise { // Check that table exists const { rows: [table], - } = await pgClient.query( + } = await pgClient.query<{ oid: string }>( `select oid from pg_class where relnamespace = ${graphileMigrateSchema.oid} and relname = '${tableName}' and relkind = 'r'`, ); if (!table) { @@ -154,7 +154,10 @@ async function verifyGraphileMigrateSchema(pgClient: Client): Promise { } // Check that it has the right number of columns - const { rows: columns } = await pgClient.query( + const { rows: columns } = await pgClient.query<{ + attrelid: string; + attname: string; + }>( `select attrelid, attname from pg_attribute where attrelid = ${table.oid} and attnum > 0`, ); if (columns.length !== expected.columnCount) { @@ -278,7 +281,12 @@ export async function getLastMigration( const { rows: [row], - } = await pgClient.query( + } = await pgClient.query<{ + filename: string; + previousHash: string | null; + hash: string; + date: Date; + }>( `select filename, previous_hash as "previousHash", hash, date from graphile_migrate.migrations order by filename desc limit 1`, ); return (row as DbMigration) || null; diff --git a/src/pgReal.ts b/src/pgReal.ts index 5c1290e..4c95fdf 100644 --- a/src/pgReal.ts +++ b/src/pgReal.ts @@ -85,7 +85,12 @@ function getPoolDetailsFromConnectionString( clearTimeout(this._timer); this._timer = undefined; pool.end = end; - pool.end(); + pool.end().catch((e) => { + // eslint-disable-next-line no-console + console.error("Error occurred whilst releasing pool:"); + // eslint-disable-next-line no-console + console.dir(e); + }); poolDetailsByConnectionString.delete(connectionString); }, }; @@ -150,9 +155,10 @@ export async function withAdvisoryLock( } const { rows: [{ locked }], - } = await pgClient.query("select pg_try_advisory_lock($1) as locked", [ - ADVISORY_LOCK_MIGRATE, - ]); + } = await pgClient.query<{ locked: boolean }>( + "select pg_try_advisory_lock($1) as locked", + [ADVISORY_LOCK_MIGRATE], + ); if (!locked) { throw new Error("Failed to get exclusive lock"); } diff --git a/src/settings.ts b/src/settings.ts index 28b6e3b..97ae7c0 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -125,8 +125,10 @@ export async function parseSettings( const value = settings[key]; try { return await callback(value); - } catch (e: any) { - errors.push(`Setting '${key}': ${e.message}`); + } catch (e) { + errors.push( + `Setting '${key}': ${e instanceof Error ? e.message : String(e)}`, + ); return void 0 as never; } }