From 8e90f8a1ca12f9b596f92bae811d9d92266aab7a Mon Sep 17 00:00:00 2001 From: Alberto Schiabel Date: Fri, 24 Jun 2022 10:21:22 +0200 Subject: [PATCH] feat(sdk): improved error reporting by adding detail to getConfig and getDmmf error (#13736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sdk: improved error reporting by adding detail to getConfig and getDmmf error * sdk: added openssl fix proposal * sdk: added comments to errorHelpers * sdk: updated structured error output in getDmmf * sdk: updated structured error output in getConfig * ci: fix tests * sdk: add tests for loadNodeAPILibrary * Update packages/sdk/src/engine-commands/getConfig.ts Co-authored-by: Joël Galeran * Update packages/sdk/src/engine-commands/getConfig.ts Co-authored-by: Joël Galeran * Update packages/sdk/src/engine-commands/getDmmf.ts Co-authored-by: Joël Galeran * Update packages/sdk/src/engine-commands/getDmmf.ts Co-authored-by: Joël Galeran * ci: fix tests for getDmmf * ci: fix tests * ci: fix tests * ci: fix tests * ci: fix tests * ci: fix tests * ci: fixed getConfig tests * chore: simplified query-engine test assertions * chore: fix typo * fix: tests * internals: updated openssl error message * Update packages/internals/src/__tests__/engine-commands/queryEngineCommons.test.ts Co-authored-by: Joël Galeran Co-authored-by: Joël Galeran --- packages/cli/src/Doctor.ts | 2 +- packages/cli/src/Format.ts | 2 +- packages/cli/src/Generate.ts | 2 +- packages/cli/src/Studio.ts | 2 +- packages/cli/src/Validate.ts | 2 +- .../src/__tests__/artificial-panic.test.ts | 2 +- .../cli/src/__tests__/commands/CLI.test.ts | 2 +- packages/cli/src/bin.ts | 2 +- packages/client/scripts/get-packed-client.js | 4 +- packages/client/src/__tests__/dmmf.test.ts | 6 +- .../__tests__/generation/generator.test.ts | 14 +- .../integration/__helpers__/migrateDb.ts | 2 +- .../__tests__/engine-commands/getDmmf.test.ts | 15 +- .../queryEngineCommons.test.ts | 68 +++++++++ .../src/engine-commands/errorHelpers.ts | 3 + .../src/engine-commands/getConfig.ts | 93 +++++++++--- .../internals/src/engine-commands/getDmmf.ts | 76 ++++++++-- .../src/engine-commands/queryEngineCommons.ts | 24 ++- packages/internals/src/index.ts | 1 + .../src/utils/serializeQueryEngineName.ts | 12 ++ .../migrate/src/__tests__/MigrateDev.test.ts | 137 +++++++++--------- .../migrate/src/utils/ensureDatabaseExists.ts | 9 +- 22 files changed, 354 insertions(+), 126 deletions(-) create mode 100644 packages/internals/src/__tests__/engine-commands/queryEngineCommons.test.ts create mode 100644 packages/internals/src/utils/serializeQueryEngineName.ts diff --git a/packages/cli/src/Doctor.ts b/packages/cli/src/Doctor.ts index a8b3e36aeea7..46a56ccbfb45 100644 --- a/packages/cli/src/Doctor.ts +++ b/packages/cli/src/Doctor.ts @@ -1,5 +1,4 @@ import type { DMMF } from '@prisma/generator-helper' -import { getSchemaPathAndPrint } from '@prisma/migrate' import { arg, canConnectToDatabase, @@ -14,6 +13,7 @@ import { loadEnvFile, pick, } from '@prisma/internals' +import { getSchemaPathAndPrint } from '@prisma/migrate' import chalk from 'chalk' import equal from 'fast-deep-equal' import fs from 'fs' diff --git a/packages/cli/src/Format.ts b/packages/cli/src/Format.ts index 9061b20cd98e..11f7d1a63b8b 100644 --- a/packages/cli/src/Format.ts +++ b/packages/cli/src/Format.ts @@ -1,6 +1,6 @@ -import { getSchemaPathAndPrint } from '@prisma/migrate' import type { Command } from '@prisma/internals' import { arg, ErrorArea, format, formatms, formatSchema, getDMMF, HelpError, RustPanic } from '@prisma/internals' +import { getSchemaPathAndPrint } from '@prisma/migrate' import chalk from 'chalk' import fs from 'fs' import os from 'os' diff --git a/packages/cli/src/Generate.ts b/packages/cli/src/Generate.ts index 01170139c843..e4bd8403836d 100644 --- a/packages/cli/src/Generate.ts +++ b/packages/cli/src/Generate.ts @@ -1,5 +1,4 @@ import { enginesVersion } from '@prisma/engines' -import { getSchemaPathAndPrint } from '@prisma/migrate' import { arg, Command, @@ -19,6 +18,7 @@ import { parseEnvValue, Platform, } from '@prisma/internals' +import { getSchemaPathAndPrint } from '@prisma/migrate' import chalk from 'chalk' import fs from 'fs' import logUpdate from 'log-update' diff --git a/packages/cli/src/Studio.ts b/packages/cli/src/Studio.ts index 58e02c2af351..c39b975405f2 100644 --- a/packages/cli/src/Studio.ts +++ b/packages/cli/src/Studio.ts @@ -1,6 +1,6 @@ import { enginesVersion } from '@prisma/engines' -import { getSchemaPathAndPrint } from '@prisma/migrate' import { arg, checkUnsupportedDataProxy, Command, format, HelpError, isError, loadEnvFile } from '@prisma/internals' +import { getSchemaPathAndPrint } from '@prisma/migrate' import { StudioServer } from '@prisma/studio-server' import chalk from 'chalk' import getPort from 'get-port' diff --git a/packages/cli/src/Validate.ts b/packages/cli/src/Validate.ts index d36fe38a0eeb..245a0e5d6b12 100644 --- a/packages/cli/src/Validate.ts +++ b/packages/cli/src/Validate.ts @@ -1,6 +1,6 @@ -import { getSchemaPathAndPrint } from '@prisma/migrate' import type { Command } from '@prisma/internals' import { arg, format, getConfig, getDMMF, HelpError, loadEnvFile } from '@prisma/internals' +import { getSchemaPathAndPrint } from '@prisma/migrate' import chalk from 'chalk' import fs from 'fs' diff --git a/packages/cli/src/__tests__/artificial-panic.test.ts b/packages/cli/src/__tests__/artificial-panic.test.ts index a855be112f7a..95ef22df27b8 100644 --- a/packages/cli/src/__tests__/artificial-panic.test.ts +++ b/packages/cli/src/__tests__/artificial-panic.test.ts @@ -1,5 +1,5 @@ -import { DbPull } from '@prisma/migrate' import { isRustPanic, jestConsoleContext, jestContext } from '@prisma/internals' +import { DbPull } from '@prisma/migrate' import { Format } from '../Format' import { Validate } from '../Validate' diff --git a/packages/cli/src/__tests__/commands/CLI.test.ts b/packages/cli/src/__tests__/commands/CLI.test.ts index 13248e9c6949..db53119d6688 100644 --- a/packages/cli/src/__tests__/commands/CLI.test.ts +++ b/packages/cli/src/__tests__/commands/CLI.test.ts @@ -1,3 +1,4 @@ +import { handlePanic, jestConsoleContext, jestContext } from '@prisma/internals' import { DbCommand, DbPull, @@ -11,7 +12,6 @@ import { MigrateResolve, MigrateStatus, } from '@prisma/migrate' -import { handlePanic, jestConsoleContext, jestContext } from '@prisma/internals' import { CLI } from '../../CLI' diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 25426c434494..09460b09f0f0 100755 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -2,6 +2,7 @@ import Debug from '@prisma/debug' import { enginesVersion } from '@prisma/engines' +import { arg, handlePanic, HelpError, isCurrentBinInstalledGlobally, isError, isRustPanic } from '@prisma/internals' import { DbCommand, DbExecute, @@ -17,7 +18,6 @@ import { MigrateResolve, MigrateStatus, } from '@prisma/migrate' -import { arg, handlePanic, HelpError, isCurrentBinInstalledGlobally, isError, isRustPanic } from '@prisma/internals' import chalk from 'chalk' import path from 'path' diff --git a/packages/client/scripts/get-packed-client.js b/packages/client/scripts/get-packed-client.js index 92ebfc0cddfa..3c5907a26a35 100755 --- a/packages/client/scripts/get-packed-client.js +++ b/packages/client/scripts/get-packed-client.js @@ -9,4 +9,6 @@ async function main() { console.log(`Saving packed client to ${target}`) } -main() +main().catch((e) => { + throw e +}) diff --git a/packages/client/src/__tests__/dmmf.test.ts b/packages/client/src/__tests__/dmmf.test.ts index 00cdf965c42f..583a0b1ffd6b 100644 --- a/packages/client/src/__tests__/dmmf.test.ts +++ b/packages/client/src/__tests__/dmmf.test.ts @@ -1,3 +1,4 @@ +import { serializeQueryEngineName } from '@prisma/internals' import stripAnsi from 'strip-ansi' import { getDMMF } from '../generation/getDMMF' @@ -347,8 +348,9 @@ describe('dmmf', () => { try { await getDMMF({ datamodel }) } catch (e) { - expect(stripAnsi(e.message)).toMatchInlineSnapshot(` - Get DMMF: Schema parsing + expect(serializeQueryEngineName(stripAnsi(e.message))).toMatchInlineSnapshot(` + Get DMMF: Schema parsing - Error while interacting with query-engine-NORMALIZED + Error code: P1012 error: Error validating: You defined the enum \`PostKind\`. But the current connector does not support enums. --> schema.prisma:14 | diff --git a/packages/client/src/__tests__/generation/generator.test.ts b/packages/client/src/__tests__/generation/generator.test.ts index 6c5c36dc8a17..428a68dce110 100644 --- a/packages/client/src/__tests__/generation/generator.test.ts +++ b/packages/client/src/__tests__/generation/generator.test.ts @@ -1,4 +1,11 @@ -import { ClientEngineType, getClientEngineType, getGenerator, getPackedPackage, parseEnvValue } from '@prisma/internals' +import { + ClientEngineType, + getClientEngineType, + getGenerator, + getPackedPackage, + parseEnvValue, + serializeQueryEngineName, +} from '@prisma/internals' import fs from 'fs' import path from 'path' import rimraf from 'rimraf' @@ -116,8 +123,9 @@ describe('generator', () => { skipDownload: true, }) } catch (e) { - expect(stripAnsi(e.message)).toMatchInlineSnapshot(` - Get DMMF: Schema parsing + expect(serializeQueryEngineName(stripAnsi(e.message))).toMatchInlineSnapshot(` + Get DMMF: Schema parsing - Error while interacting with query-engine-NORMALIZED + Error code: P1012 error: Error validating model "public": The model name \`public\` is invalid. It is a reserved name. Please change it. Read more at https://pris.ly/d/naming-models --> schema.prisma:10 | diff --git a/packages/client/src/__tests__/integration/__helpers__/migrateDb.ts b/packages/client/src/__tests__/integration/__helpers__/migrateDb.ts index 1e678c9ee2c6..b02ef8f1a93c 100644 --- a/packages/client/src/__tests__/integration/__helpers__/migrateDb.ts +++ b/packages/client/src/__tests__/integration/__helpers__/migrateDb.ts @@ -1,5 +1,5 @@ -import { Migrate } from '@prisma/migrate' import { createDatabase } from '@prisma/internals' +import { Migrate } from '@prisma/migrate' export type MigrateOptions = { connectionString: string diff --git a/packages/internals/src/__tests__/engine-commands/getDmmf.test.ts b/packages/internals/src/__tests__/engine-commands/getDmmf.test.ts index eccf880f0687..8d49eb09b51b 100644 --- a/packages/internals/src/__tests__/engine-commands/getDmmf.test.ts +++ b/packages/internals/src/__tests__/engine-commands/getDmmf.test.ts @@ -12,7 +12,11 @@ if (process.env.CI) { jest.setTimeout(60_000) } -describe('getDMMF', () => { +const describeIf = (condition: boolean) => (condition ? describe : describe.skip) + +describeIf( + process.env.PRISMA_CLI_QUERY_ENGINE_TYPE === 'library' || process.env.PRISMA_CLI_QUERY_ENGINE_TYPE === undefined, +)('getDMMF', () => { test('simple model, no datasource', async () => { const dmmf = await getDMMF({ datamodel: `model A { @@ -144,7 +148,8 @@ describe('getDMMF', () => { await getDMMF({ datamodel }) } catch (e) { expect(stripAnsi(e.message)).toMatchInlineSnapshot(` - "Get DMMF: Schema parsing + "Get DMMF: Schema parsing - Error while interacting with query-engine-node-api library + Error code: P1012 error: Error parsing attribute \\"@default\\": The \`autoincrement()\` default value is used on a non-id field even though the datasource does not support this. --> schema.prisma:7 | @@ -184,7 +189,8 @@ describe('getDMMF', () => { await getDMMF({ datamodel }) } catch (e) { expect(stripAnsi(e.message)).toMatchInlineSnapshot(` - "Get DMMF: Schema parsing + "Get DMMF: Schema parsing - Error while interacting with query-engine-node-api library + Error code: P1012 error: Error parsing attribute \\"@default\\": The \`autoincrement()\` default value is used on a non-indexed field even though the datasource does not support this. --> schema.prisma:7 | @@ -340,7 +346,8 @@ describe('getDMMF', () => { await getDMMF({ datamodel }) } catch (e) { expect(stripAnsi(e.message)).toMatchInlineSnapshot(` - "Get DMMF: Schema parsing + "Get DMMF: Schema parsing - Error while interacting with query-engine-node-api library + Error code: P1012 error: Field \\"id\\" is already defined on model \\"User\\". --> schema.prisma:12 | diff --git a/packages/internals/src/__tests__/engine-commands/queryEngineCommons.test.ts b/packages/internals/src/__tests__/engine-commands/queryEngineCommons.test.ts new file mode 100644 index 000000000000..5d2133d754ae --- /dev/null +++ b/packages/internals/src/__tests__/engine-commands/queryEngineCommons.test.ts @@ -0,0 +1,68 @@ +import { BinaryType } from '@prisma/fetch-engine' +import * as E from 'fp-ts/Either' + +import { loadNodeAPILibrary } from '../../engine-commands/queryEngineCommons' +import { resolveBinary } from '../../resolveBinary' +import * as loadUtils from '../../utils/load' + +const describeIf = (condition: boolean) => (condition ? describe : describe.skip) + +describeIf(process.env.PRISMA_CLI_QUERY_ENGINE_TYPE !== 'binary')('loadNodeAPILibrary', () => { + it('error path', async () => { + const spyLoadTag = 'error-load' + const spyLoad = jest.spyOn(loadUtils, 'load').mockImplementation((id: string) => { + throw new Error(spyLoadTag) + }) + + try { + const queryEnginePath = await resolveBinary(BinaryType.libqueryEngine) + const result = await loadNodeAPILibrary(queryEnginePath)() + + expect(E.isLeft(result)).toBe(true) + + if (E.isLeft(result)) { + expect(result.left.type).toEqual('connection-error') + expect(result.left.reason).toEqual('Unable to establish a connection to query-engine-node-api library.') + expect(result.left.error).toBeTruthy() + } + } finally { + spyLoad.mockRestore() + } + }) + + it('error path, openssl', async () => { + const spyLoadTag = 'error-load, something something openssl installation' + const spyLoad = jest.spyOn(loadUtils, 'load').mockImplementation((id: string) => { + throw new Error(spyLoadTag) + }) + + try { + const queryEnginePath = await resolveBinary(BinaryType.libqueryEngine) + const result = await loadNodeAPILibrary(queryEnginePath)() + + expect(E.isLeft(result)).toBe(true) + + if (E.isLeft(result)) { + expect(result.left.type).toEqual('connection-error') + expect(result.left.reason).toEqual( + `Unable to establish a connection to query-engine-node-api library. It seems there is a problem with your OpenSSL installation!`, + ) + expect(result.left.error).toBeTruthy() + } + } finally { + spyLoad.mockRestore() + } + }) + + it('happy path', async () => { + const queryEnginePath = await resolveBinary(BinaryType.libqueryEngine) + const result = await loadNodeAPILibrary(queryEnginePath)() + + expect(E.isRight(result)).toBe(true) + + if (E.isRight(result)) { + expect(result.right.NodeAPIQueryEngineLibrary).toBeTruthy() + expect(result.right.NodeAPIQueryEngineLibrary.QueryEngine).toBeTruthy() + } + }) +}) diff --git a/packages/internals/src/engine-commands/errorHelpers.ts b/packages/internals/src/engine-commands/errorHelpers.ts index 9bfda5c0771a..1f7186227e5d 100644 --- a/packages/internals/src/engine-commands/errorHelpers.ts +++ b/packages/internals/src/engine-commands/errorHelpers.ts @@ -1,6 +1,9 @@ import { formatTable } from '../utils/formatTable' import { version } from '../utils/getVersion' +/** + * Adds `Prisma CLI Version : x.x.x` at the bottom of the error output. + */ export function addVersionDetailsToErrorMessage(message: string) { const rows = [['Prisma CLI Version', version]] return `${message} diff --git a/packages/internals/src/engine-commands/getConfig.ts b/packages/internals/src/engine-commands/getConfig.ts index 8543e3132750..403eacd12f60 100644 --- a/packages/internals/src/engine-commands/getConfig.ts +++ b/packages/internals/src/engine-commands/getConfig.ts @@ -37,9 +37,46 @@ export type GetConfigOptions = { retry?: number ignoreEnvVarErrors?: boolean } + +type GetConfigErrorInit = { + // e.g., `Schema parsing - Error while interacting with query-engine-node-api library` + reason: string + + // e.g., Error validating model "public": The model name \`public\` is invalid. + message: string +} & ( + | { + // parsed as JSON + readonly _tag: 'parsed' + + // e.g., `P1012` + errorCode?: string + } + | { + // text + readonly _tag: 'unparsed' + } +) + export class GetConfigError extends Error { - constructor(message: string, public readonly _error?: Error) { - super(addVersionDetailsToErrorMessage(`${chalk.redBright.bold('Get config: ')}${message}`)) + constructor(params: GetConfigErrorInit) { + const headline = chalk.redBright.bold('Get Config: ') + + const constructedErrorMessage = match(params) + .with({ _tag: 'parsed' }, ({ errorCode, message, reason }) => { + const errorCodeMessage = errorCode ? `Error code: ${errorCode}` : '' + return `${reason} +${errorCodeMessage} +${message}` + }) + .with({ _tag: 'unparsed' }, ({ message, reason }) => { + const detailsHeader = chalk.red.bold('Details:') + return `${reason} +${detailsHeader}${message}` + }) + .exhaustive() + + super(addVersionDetailsToErrorMessage(`${headline}${constructedErrorMessage}`)) } } @@ -67,7 +104,7 @@ async function getConfigNodeAPI(options: GetConfigOptions) { if (E.isLeft(preliminaryEither)) { const { left: e } = preliminaryEither debugErrorType(e) - throw new GetConfigError(e.reason, e.error) + throw new GetConfigError({ _tag: 'unparsed', message: e.error.message, reason: e.reason }) } const { queryEnginePath } = preliminaryEither.right debug(`Using CLI Query Engine (Node-API Library) at: ${queryEnginePath}`) @@ -129,7 +166,7 @@ async function getConfigNodeAPI(options: GetConfigOptions) { () => JSON.parse(errorOutput), () => { debug(`Coudln't apply JSON.parse to "${errorOutput}"`) - return new GetConfigError(errorOutput, e.error) + return new GetConfigError({ _tag: 'unparsed', message: errorOutput, reason: e.reason }) }, ), E.map((errorOutputAsJSON: Record) => { @@ -146,14 +183,14 @@ async function getConfigNodeAPI(options: GetConfigOptions) { return panic } - const message = match(errorOutputAsJSON) - .with({ error_code: 'P1012' }, (error: Record) => { - return chalk.redBright(`Schema Parsing ${error.error_code}\n\n`) + error.message + '\n' - }) - .otherwise((error: any) => { - return chalk.redBright(`${error.error_code}\n\n`) + error - }) - return new GetConfigError(message, e.error) + const { error_code: errorCode } = errorOutputAsJSON as { error_code: string | undefined } + + return new GetConfigError({ + _tag: 'parsed', + message: errorOutputAsJSON.message, + reason: `${chalk.redBright.bold('Schema parsing')} - ${e.reason}`, + errorCode, + }) }), E.getOrElseW(identity), ) @@ -162,7 +199,7 @@ async function getConfigNodeAPI(options: GetConfigOptions) { }) .otherwise((e) => { debugErrorType(e) - return new GetConfigError(e.reason, e.error) + return new GetConfigError({ _tag: 'unparsed', message: e.error.message, reason: e.reason }) }) throw error @@ -179,7 +216,7 @@ async function getConfigBinary(options: GetConfigOptions) { if (E.isLeft(preliminaryEither)) { const { left: e } = preliminaryEither debugErrorType(e) - throw new GetConfigError(e.reason, e.error) + throw new GetConfigError({ _tag: 'unparsed', message: e.error.message, reason: e.reason }) } const { queryEnginePath, tempDatamodelPath } = preliminaryEither.right debug(`Using CLI Query Engine (Binary) at: ${queryEnginePath}`) @@ -287,23 +324,31 @@ async function getConfigBinary(options: GetConfigOptions) { () => JSON.parse(errorOutput), () => { debug(`Coudln't apply JSON.parse to "${errorOutput}"`) - return new GetConfigError(errorOutput, e.error) + return new GetConfigError({ _tag: 'unparsed', message: errorOutput, reason: e.reason }) }, ), E.map((errorOutputAsJSON: Record) => { - const defaultMessage = `${chalk.redBright(errorOutputAsJSON.message)}\n` - const message = match(errorOutputAsJSON) - .with({ error_code: 'P1012' }, (error) => { - return chalk.redBright(`Schema Parsing ${error.error_code}\n\n`) + defaultMessage + const defaultMessage = chalk.redBright(errorOutputAsJSON.message) + const getConfigErrorInit = match(errorOutputAsJSON) + .with({ error_code: 'P1012' }, (eJSON) => { + return { + reason: `${chalk.redBright.bold('Schema parsing')} - ${e.reason}`, + errorCode: eJSON.error_code, + } }) - .with({ error_code: P.string }, (error) => { - return chalk.redBright(`${error.error_code}\n\n`) + defaultMessage + .with({ error_code: P.string }, (eJSON) => { + return { + reason: e.reason, + errorCode: eJSON.error_code, + } }) .otherwise(() => { - return defaultMessage + return { + reason: e.reason, + } }) - return new GetConfigError(message, e.error) + return new GetConfigError({ _tag: 'parsed', message: defaultMessage, ...getConfigErrorInit }) }), E.getOrElse(identity), ) @@ -311,7 +356,7 @@ async function getConfigBinary(options: GetConfigOptions) { }) .otherwise((e) => { debugErrorType(e) - return new GetConfigError(e.reason, e.error) + return new GetConfigError({ _tag: 'unparsed', message: e.error.message, reason: e.reason }) }) throw error diff --git a/packages/internals/src/engine-commands/getDmmf.ts b/packages/internals/src/engine-commands/getDmmf.ts index 1c5deb168a56..fb83e812e91d 100644 --- a/packages/internals/src/engine-commands/getDmmf.ts +++ b/packages/internals/src/engine-commands/getDmmf.ts @@ -39,9 +39,45 @@ export type GetDMMFOptions = { previewFeatures?: string[] } +type GetDmmfErrorInit = { + // e.g., `Schema parsing - Error while interacting with query-engine-node-api library` + reason: string + + // e.g., Error validating model "public": The model name \`public\` is invalid. + message: string +} & ( + | { + // JSON + readonly _tag: 'parsed' + + // e.g., `P1012` + errorCode?: string + } + | { + // text + readonly _tag: 'unparsed' + } +) + export class GetDmmfError extends Error { - constructor(message: string, public readonly _error?: Error) { - super(addVersionDetailsToErrorMessage(`${chalk.redBright.bold('Get DMMF: ')}${message}`)) + constructor(params: GetDmmfErrorInit) { + const headline = chalk.redBright.bold('Get DMMF: ') + + const constructedErrorMessage = match(params) + .with({ _tag: 'parsed' }, ({ errorCode, message, reason }) => { + const errorCodeMessage = errorCode ? `Error code: ${errorCode}` : '' + return `${reason} +${errorCodeMessage} +${message}` + }) + .with({ _tag: 'unparsed' }, ({ message, reason }) => { + const detailsHeader = chalk.red.bold('Details:') + return `${reason} +${detailsHeader}${message}` + }) + .exhaustive() + + super(addVersionDetailsToErrorMessage(`${headline}${constructedErrorMessage}`)) } } @@ -73,7 +109,7 @@ async function getDmmfNodeAPI(options: GetDMMFOptions) { if (E.isLeft(preliminaryEither)) { const { left: e } = preliminaryEither debugErrorType(e) - throw new GetDmmfError(e.reason, e.error) + throw new GetDmmfError({ _tag: 'unparsed', message: e.error.message, reason: e.reason }) } const { queryEnginePath } = preliminaryEither.right debug(`Using CLI Query Engine (Node-API Library) at: ${queryEnginePath}`) @@ -170,7 +206,7 @@ async function getDmmfNodeAPI(options: GetDMMFOptions) { () => JSON.parse(errorOutput), () => { debug(`Coudln't apply JSON.parse to "${errorOutput}"`) - return new GetDmmfError(errorOutput, e.error) + return new GetDmmfError({ _tag: 'unparsed', message: errorOutput, reason: e.reason }) }, ), E.map((errorOutputAsJSON: Record) => { @@ -188,7 +224,14 @@ async function getDmmfNodeAPI(options: GetDMMFOptions) { } const defaultMessage = addMissingOpenSSLInfo(errorOutputAsJSON.message) - return new GetDmmfError(chalk.redBright.bold('Schema parsing\n') + defaultMessage, e.error) + const { error_code: errorCode } = errorOutputAsJSON as { error_code: string | undefined } + + return new GetDmmfError({ + _tag: 'parsed', + message: defaultMessage, + reason: `${chalk.redBright.bold('Schema parsing')} - ${e.reason}`, + errorCode, + }) }), E.getOrElseW(identity), ) @@ -197,7 +240,7 @@ async function getDmmfNodeAPI(options: GetDMMFOptions) { }) .otherwise((e) => { debugErrorType(e) - return new GetDmmfError(e.reason, e.error) + return new GetDmmfError({ _tag: 'unparsed', message: e.error.message, reason: e.reason }) }) throw error @@ -214,7 +257,7 @@ async function getDmmfBinary(options: GetDMMFOptions): Promise { if (E.isLeft(preliminaryEither)) { const { left: e } = preliminaryEither debugErrorType(e) - throw new GetDmmfError(e.reason, e.error) + throw new GetDmmfError({ _tag: 'unparsed', message: e.error.message, reason: e.reason }) } const { queryEnginePath, tempDatamodelPath } = preliminaryEither.right debug(`Using CLI Query Engine (Binary) at: ${queryEnginePath}`) @@ -363,13 +406,18 @@ async function getDmmfBinary(options: GetDMMFOptions): Promise { () => JSON.parse(errorOutput), () => { debug(`Coudln't apply JSON.parse to "${errorOutput}"`) - return new GetDmmfError(errorOutput, e.error) + return new GetDmmfError({ _tag: 'unparsed', message: errorOutput, reason: e.reason }) }, ), E.map((errorOutputAsJSON: Record) => { - const defaultMessage = `${chalk.redBright(errorOutputAsJSON.message)}` - const message = addMissingOpenSSLInfo(defaultMessage) - return new GetDmmfError(chalk.redBright.bold('Schema parsing\n') + message, e.error) + const defaultMessage = addMissingOpenSSLInfo(`${chalk.redBright(errorOutputAsJSON.message)}`) + const { error_code: errorCode } = errorOutputAsJSON as { error_code: string | undefined } + return new GetDmmfError({ + _tag: 'parsed', + message: defaultMessage, + reason: `${chalk.redBright.bold('Schema parsing')} - ${e.reason}`, + errorCode, + }) }), E.getOrElse(identity), ) @@ -379,7 +427,11 @@ async function getDmmfBinary(options: GetDMMFOptions): Promise { .with({ type: 'parse-json' }, (e) => { debugErrorType(e) const message = `Problem while parsing the query engine response at ${queryEnginePath}. ${e.result.stdout}\n${e.error?.stack}` - const error = new GetDmmfError(chalk.redBright.bold('JSON parsing\n') + message, e.error) + const error = new GetDmmfError({ + _tag: 'unparsed', + message: message, + reason: `${chalk.redBright.bold('JSON parsing')} - ${e.reason}\n`, + }) return E.right(error) }) .with({ type: 'retry' }, (e) => { diff --git a/packages/internals/src/engine-commands/queryEngineCommons.ts b/packages/internals/src/engine-commands/queryEngineCommons.ts index 92c6835c60b8..8668e6838269 100644 --- a/packages/internals/src/engine-commands/queryEngineCommons.ts +++ b/packages/internals/src/engine-commands/queryEngineCommons.ts @@ -6,6 +6,7 @@ import { pipe } from 'fp-ts/lib/function' import * as TE from 'fp-ts/TaskEither' import fs from 'fs' import tmpWrite from 'temp-write' +import { match } from 'ts-pattern' import { resolveBinary } from '../resolveBinary' import { load } from '../utils/load' @@ -75,11 +76,24 @@ export function loadNodeAPILibrary(queryEnginePath: string) { return pipe( E.tryCatch( () => load(queryEnginePath), - (e) => ({ - type: 'connection-error' as const, - reason: 'Unable to establish a connection to query-engine-node-api library', - error: e as Error, - }), + (e) => { + const error = e as Error + const defaultErrorMessage = `Unable to establish a connection to query-engine-node-api library.` + const proposedErrorFixMessage = match(error.message) + .when( + (errMessage) => errMessage.includes('openssl'), + () => { + return ` It seems there is a problem with your OpenSSL installation!` + }, + ) + .otherwise(() => '') + const reason = `${defaultErrorMessage}${proposedErrorFixMessage}` + return { + type: 'connection-error' as const, + reason, + error, + } + }, ), TE.fromEither, TE.map((NodeAPIQueryEngineLibrary) => ({ NodeAPIQueryEngineLibrary })), diff --git a/packages/internals/src/index.ts b/packages/internals/src/index.ts index 61301b1d5e19..da2553241692 100644 --- a/packages/internals/src/index.ts +++ b/packages/internals/src/index.ts @@ -71,6 +71,7 @@ export { parseBinaryTargetsEnvValue, parseEnvValue } from './utils/parseEnvValue export { pick } from './utils/pick' export { platformRegex } from './utils/platformRegex' export { printConfigWarnings } from './utils/printConfigWarnings' +export { serializeQueryEngineName } from './utils/serializeQueryEngineName' export { createSpinner } from './utils/spinner' export type { Position } from './utils/trimBlocksFromSchema' export { trimBlocksFromSchema, trimNewLine } from './utils/trimBlocksFromSchema' diff --git a/packages/internals/src/utils/serializeQueryEngineName.ts b/packages/internals/src/utils/serializeQueryEngineName.ts new file mode 100644 index 000000000000..098e45a73c7c --- /dev/null +++ b/packages/internals/src/utils/serializeQueryEngineName.ts @@ -0,0 +1,12 @@ +/** + * Normalize the snapshot messages related to the interaction with Query Engine. + */ +export function serializeQueryEngineName(message: string) { + const normalizedName = 'query-engine-NORMALIZED' + if (process.env.PRISMA_CLI_QUERY_ENGINE_TYPE === 'binary') { + return message.replace(/query-engine binary/g, normalizedName) + } + + // process.env.PRISMA_CLI_QUERY_ENGINE_TYPE is 'library' by default + return message.replace(/query-engine-node-api library/g, normalizedName) +} diff --git a/packages/migrate/src/__tests__/MigrateDev.test.ts b/packages/migrate/src/__tests__/MigrateDev.test.ts index 416120b24878..bd762837f789 100644 --- a/packages/migrate/src/__tests__/MigrateDev.test.ts +++ b/packages/migrate/src/__tests__/MigrateDev.test.ts @@ -1,4 +1,4 @@ -import { jestConsoleContext, jestContext } from '@prisma/internals' +import { jestConsoleContext, jestContext, serializeQueryEngineName } from '@prisma/internals' import fs from 'fs-jetpack' import path from 'path' import prompt from 'prompts' @@ -21,6 +21,66 @@ process.env.GITHUB_ACTIONS = '1' process.env.PRISMA_MIGRATE_SKIP_GENERATE = '1' describe('common', () => { + it('invalid schema', async () => { + ctx.fixture('schema-only-sqlite') + + try { + await MigrateDev.new().parse(['--schema=./prisma/invalid.prisma']) + expect(true).toBe(false) // unreachable + } catch (error) { + expect(serializeQueryEngineName(error.message)).toMatchInlineSnapshot(` + Get Config: Schema parsing - Error while interacting with query-engine-NORMALIZED + Error code: P1012 + error: Error validating: This line is invalid. It does not start with any known Prisma schema keyword. + --> schema.prisma:10 + | + 9 | } + 10 | model Blog { + 11 | + | + + Validation Error Count: 1 + + Prisma CLI Version : 0.0.0 + `) + } + + expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot( + `Prisma schema loaded from prisma/invalid.prisma`, + ) + expect(ctx.mocked['console.log'].mock.calls).toEqual([]) + expect(ctx.mocked['console.error'].mock.calls).toEqual([]) + }) + + it('provider array should fail', async () => { + ctx.fixture('schema-only-sqlite') + + try { + await MigrateDev.new().parse(['--schema=./prisma/provider-array.prisma']) + expect(true).toBe(false) // unreachable + } catch (error) { + expect(serializeQueryEngineName(error.message)).toMatchInlineSnapshot(` + Get Config: Schema parsing - Error while interacting with query-engine-NORMALIZED + Error code: P1012 + error: Error validating datasource \`my_db\`: The provider argument in a datasource must be a string literal + --> schema.prisma:2 + | + 1 | datasource my_db { + 2 | provider = ["postgresql", "sqlite"] + | + + Validation Error Count: 1 + + Prisma CLI Version : 0.0.0 + `) + } + expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot( + `Prisma schema loaded from prisma/provider-array.prisma`, + ) + expect(ctx.mocked['console.log'].mock.calls).toEqual([]) + expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(``) + }) + it('wrong flag', async () => { const commandInstance = MigrateDev.new() const spy = jest.spyOn(commandInstance, 'help').mockImplementation(() => 'Help Me') @@ -103,33 +163,6 @@ describe('sqlite', () => { expect(ctx.mocked['console.error'].mock.calls).toEqual([]) }) - it('invalid schema', async () => { - ctx.fixture('schema-only-sqlite') - const result = MigrateDev.new().parse(['--schema=./prisma/invalid.prisma']) - await expect(result).rejects.toMatchInlineSnapshot(` - Get config: Schema Parsing P1012 - - error: Error validating: This line is invalid. It does not start with any known Prisma schema keyword. - --> schema.prisma:10 - | - 9 | } - 10 | model Blog { - 11 | - | - - Validation Error Count: 1 - - - Prisma CLI Version : 0.0.0 - `) - - expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot( - `Prisma schema loaded from prisma/invalid.prisma`, - ) - expect(ctx.mocked['console.log'].mock.calls).toEqual([]) - expect(ctx.mocked['console.error'].mock.calls).toEqual([]) - }) - it('first migration (--name)', async () => { ctx.fixture('schema-only-sqlite') const result = MigrateDev.new().parse(['--name=first']) @@ -636,14 +669,14 @@ describe('sqlite', () => { await expect(result).rejects.toMatchInlineSnapshot(` - ⚠️ We found changes that cannot be executed: + ⚠️ We found changes that cannot be executed: - • Step 0 Made the column \`fullname\` on table \`Blog\` required, but there are 1 existing NULL values. + • Step 0 Made the column \`fullname\` on table \`Blog\` required, but there are 1 existing NULL values. - You can use prisma migrate dev --create-only to create the migration file, and manually modify it to address the underlying issue(s). - Then run prisma migrate dev to apply it and verify it works. + You can use prisma migrate dev --create-only to create the migration file, and manually modify it to address the underlying issue(s). + Then run prisma migrate dev to apply it and verify it works. - `) + `) expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(` Prisma schema loaded from prisma/schema.prisma Datasource "my_db": SQLite database "dev.db" at "file:dev.db" @@ -697,10 +730,10 @@ describe('sqlite', () => { `) expect(ctx.mocked['console.log'].mock.calls.join('\n')).toMatchInlineSnapshot(` - ⚠️ Warnings for the current datasource: + ⚠️ Warnings for the current datasource: - • You are about to drop the \`Blog\` table, which is not empty (2 rows). - `) + • You are about to drop the \`Blog\` table, which is not empty (2 rows). + `) expect(ctx.mocked['console.error'].mock.calls).toEqual([]) }) @@ -719,39 +752,13 @@ describe('sqlite', () => { `) expect(ctx.mocked['console.log'].mock.calls.join('\n')).toMatchInlineSnapshot(` - ⚠️ Warnings for the current datasource: + ⚠️ Warnings for the current datasource: - • You are about to drop the \`Blog\` table, which is not empty (2 rows). - `) + • You are about to drop the \`Blog\` table, which is not empty (2 rows). + `) expect(ctx.mocked['console.error'].mock.calls).toEqual([]) }) - it('provider array should fail', async () => { - ctx.fixture('schema-only-sqlite') - const result = MigrateDev.new().parse(['--schema=./prisma/provider-array.prisma']) - - await expect(result).rejects.toMatchInlineSnapshot(` - Get config: Schema Parsing P1012 - - error: Error validating datasource \`my_db\`: The provider argument in a datasource must be a string literal - --> schema.prisma:2 - | - 1 | datasource my_db { - 2 | provider = ["postgresql", "sqlite"] - | - - Validation Error Count: 1 - - - Prisma CLI Version : 0.0.0 - `) - expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot( - `Prisma schema loaded from prisma/provider-array.prisma`, - ) - expect(ctx.mocked['console.log'].mock.calls).toEqual([]) - expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(``) - }) - // TODO: Windows: snapshot test fails because of emoji. testIf(process.platform !== 'win32')('one seed.ts file', async () => { ctx.fixture('seed-sqlite-ts') diff --git a/packages/migrate/src/utils/ensureDatabaseExists.ts b/packages/migrate/src/utils/ensureDatabaseExists.ts index 19fd32950e2e..58cca8bf80f7 100644 --- a/packages/migrate/src/utils/ensureDatabaseExists.ts +++ b/packages/migrate/src/utils/ensureDatabaseExists.ts @@ -1,5 +1,12 @@ import type { DatabaseCredentials } from '@prisma/internals' -import { canConnectToDatabase, createDatabase, getConfig, getSchema, getSchemaDir, uriToCredentials } from '@prisma/internals' +import { + canConnectToDatabase, + createDatabase, + getConfig, + getSchema, + getSchemaDir, + uriToCredentials, +} from '@prisma/internals' import chalk from 'chalk' import type execa from 'execa' import prompt from 'prompts'