import { BinaryType } from '@prisma/fetch-engine' ; import execa from 'execa' ; import fs from 'fs' ; import path from 'path' ; import { promisify } from 'util' ; import { getSchemaDir } from './cli/getSchema' ; import { protocolToConnectorType } from './convertCredentials' ; import { resolveBinary } from './resolveBinary' ; const exists = promisify ( fs . exists ) ; export enum MigrateEngineExitCode { Success = 0 , Error = 1 , Panic = 101 } export interface MigrateEngineLogLine { timestamp : string ; level : LogLevel ; fields : LogFields ; target : string ; } type LogLevel = 'INFO' | 'ERROR' | 'DEBUG' | 'WARN' ; interface LogFields { message : string ; git_hash ? : string ; migrate_action ? : 'log' ; is_panic ? : boolean ; error_code ? : string ; [ key ] : any ; } export type DatabaseErrorCodes = 'P1000' | 'P1001' | 'P1002' | 'P1003' | 'P1009' | 'P1010' ; export type ConnectionResult = true | ConnectionError ; export interface ConnectionError { message : string ; code : DatabaseErrorCodes ; } function parseJsonFromStderr ( stderr ) { const lines = stderr . split ( /\r?\n/ ) . slice ( 1 ) ; const logs = [ ] ; for ( const line of lines ) { const data = String ( line ) ; try { const json = JSON . parse ( data ) ; logs . push ( json ) ; } catch ( e ) { throw new Error ( `Could not parse migration engine response: ${ e }` ) ; } } return logs ; } export async function canConnectToDatabase ( connectionString , cwd = process . cwd ( ) , migrationEnginePath ? ) { if ( ! connectionString ) { throw new Error ( 'Connection url is empty. See https://www.prisma.io/docs/reference/database-reference/connection-urls' ) ; } const provider = protocolToConnectorType ( `${ connectionString . split ( ':' ) [ 0 ] }:` ) ; if ( provider === 'sqlite' ) { const sqliteExists = await doesSqliteDbExist ( connectionString , cwd ) ; if ( sqliteExists ) { return true ; } else { return { code : 'P1003' , message : "SQLite database file doesn't exist" , } ; } } try { await execaCommand ( { connectionString , cwd , migrationEnginePath , engineCommandName : 'can-connect-to-database' , } ) ; } catch ( _e ) { const e = _e as execa . ExecaError ; if ( e . stderr ) { const logs = parseJsonFromStderr ( e . stderr ) ; const error = logs . find ( ( it ) => it . level === 'ERROR' && it . target === 'migration_engine::logger' ) ; if ( error && error . fields . error_code && error . fields . message ) { return { code : error . fields . error_code as DatabaseErrorCodes , message : error . fields . message , } ; } else { throw new Error ( `Migration engine error:\n${ logs . map ( ( log ) => log . fields . message ) . join ( '\n' ) }` ) ; } } else { throw new Error ( `Migration engine exited.` ) ; } } return true ; } export async function createDatabase ( connectionString , cwd = process . cwd ( ) , migrationEnginePath ? ) { const dbExists = await canConnectToDatabase ( connectionString , cwd , migrationEnginePath ) ; if ( dbExists === true ) { return false ; } try { await execaCommand ( { connectionString , cwd , migrationEnginePath , engineCommandName : 'create-database' , } ) ; return true ; } catch ( _e ) { const e = _e as execa . ExecaError ; if ( e . stderr ) { const logs = parseJsonFromStderr ( e . stderr ) ; const error = logs . find ( ( it ) => it . level === 'ERROR' && it . target === 'migration_engine::logger' ) ; if ( error && error . fields . error_code && error . fields . message ) { throw new Error ( `${ error . fields . error_code }: ${ error . fields . message }` ) ; } else { throw new Error ( `Migration engine error:\n${ logs . map ( ( log ) => log . fields . message ) . join ( '\n' ) }` ) ; } } else { throw new Error ( `Migration engine exited.` ) ; } } } export async function dropDatabase ( connectionString , cwd = process . cwd ( ) , migrationEnginePath ? ) { try { const result = await execaCommand ( { connectionString , cwd , migrationEnginePath , engineCommandName : 'drop-database' , } ) ; if ( result && result . exitCode === 0 && result . stderr . includes ( 'The database was successfully dropped' ) ) { return true ; } else { throw Error ( `An error occurred during the drop: ${ JSON . stringify ( result , undefined , 2 ) }` ) ; } } catch ( e ) { if ( e . stderr ) { const logs = parseJsonFromStderr ( e . stderr ) ; throw new Error ( `Migration engine error:\n${ logs . map ( ( log ) => log . fields . message ) . join ( '\n' ) }` ) ; } else { throw new Error ( `Migration engine exited.` ) ; } } } export async function execaCommand ( { connectionString , cwd , migrationEnginePath , engineCommandName , } ) { migrationEnginePath = migrationEnginePath || ( await resolveBinary ( BinaryType . migrationEngine ) ) ; try { return await execa ( migrationEnginePath , [ 'cli' , '--datasource' , connectionString , engineCommandName ] , { cwd , env : { RUST_BACKTRACE : '1' , RUST_LOG : 'info' , } , } ) ; } catch ( _e ) { const e = _e as execa . ExecaError ; if ( e . message ) { e . message = e . message . replace ( connectionString , '<REDACTED>' ) ; } if ( e . stdout ) { e . stdout = e . stdout . replace ( connectionString , '<REDACTED>' ) ; } if ( e . stderr ) { e . stderr = e . stderr . replace ( connectionString , '<REDACTED>' ) ; } throw e ; } } export async function doesSqliteDbExist ( connectionString , schemaDir ? ) { let filePath = connectionString ; if ( filePath . startsWith ( 'file:' ) ) { filePath = filePath . slice ( 5 ) ; } else if ( filePath . startsWith ( 'sqlite:' ) ) { filePath = filePath . slice ( 7 ) ; } const cwd = schemaDir || ( await getSchemaDir ( ) ) ; if ( ! cwd ) { throw new Error ( `Could not find schema.prisma in ${ process . cwd ( ) }` ) ; } const absoluteTarget = path . resolve ( cwd , filePath ) ; return exists ( absoluteTarget ) ; }