import SchemaInspector from '@directus/schema' ; import { knex , Knex } from 'knex' ; import { performance } from 'perf_hooks' ; import env from '../env' ; import logger from '../logger' ; import { getConfigFromEnv } from '../utils/get-config-from-env' ; import { validateEnv } from '../utils/validate-env' ; import fse from 'fs-extra' ; import path from 'path' ; import { merge } from 'lodash' ; import { promisify } from 'util' ; import { getHelpers } from './helpers' ; let database = null ; let inspector = null ; export default function getDatabase ( ) { if ( database ) { return database ; } const connectionConfig = getConfigFromEnv ( 'DB_' , [ 'DB_CLIENT' , 'DB_SEARCH_PATH' , 'DB_CONNECTION_STRING' , 'DB_POOL' , 'DB_EXCLUDE_TABLES' , 'DB_VERSION' , ] ) ; const poolConfig = getConfigFromEnv ( 'DB_POOL' ) ; const requiredEnvVars = [ 'DB_CLIENT' ] ; if ( env . DB_CLIENT && env . DB_CLIENT === 'sqlite3' ) { requiredEnvVars . push ( 'DB_FILENAME' ) ; } else if ( env . DB_CLIENT && env . DB_CLIENT === 'oracledb' ) { if ( ! env . DB_CONNECT_STRING ) { requiredEnvVars . push ( 'DB_HOST' , 'DB_PORT' , 'DB_DATABASE' , 'DB_USER' , 'DB_PASSWORD' ) ; } else { requiredEnvVars . push ( 'DB_USER' , 'DB_PASSWORD' , 'DB_CONNECT_STRING' ) ; } } else { if ( env . DB_CLIENT === 'pg' ) { if ( ! env . DB_CONNECTION_STRING ) { requiredEnvVars . push ( 'DB_HOST' , 'DB_PORT' , 'DB_DATABASE' , 'DB_USER' ) ; } } else { requiredEnvVars . push ( 'DB_HOST' , 'DB_PORT' , 'DB_DATABASE' , 'DB_USER' , 'DB_PASSWORD' ) ; } } validateEnv ( requiredEnvVars ) ; const knexConfig = { client : env . DB_CLIENT , version : env . DB_VERSION , searchPath : env . DB_SEARCH_PATH , connection : env . DB_CONNECTION_STRING || connectionConfig , log : { warn : ( msg ) => { if ( msg . startsWith ( '.returning()' ) ) return ; if ( msg . startsWith ( 'Transaction was implicitly committed, do not mix transactions and DDL with MySQL' ) ) return ; return logger . warn ( msg ) ; } , error : ( msg ) => logger . error ( msg ) , deprecate : ( msg ) => logger . info ( msg ) , debug : ( msg ) => logger . debug ( msg ) , } , pool : poolConfig , } ; if ( env . DB_CLIENT === 'sqlite3' ) { knexConfig . useNullAsDefault = true ; poolConfig . afterCreate = async ( conn , callback ) => { logger . trace ( 'Enabling SQLite Foreign Keys support...' ) ; const run = promisify ( conn . run . bind ( conn ) ) ; await run ( 'PRAGMA foreign_keys = ON' ) ; callback ( null , conn ) ; } ; } if ( env . DB_CLIENT === 'mssql' ) { merge ( knexConfig , { connection : { options : { useUTC : false } } } ) ; } if ( env . DB_CLIENT === 'mysql' && ! env . DB_CHARSET ) { logger . warn ( `DB_CHARSET hasn't been set. Please make sure DB_CHARSET matches your database's collation.` ) ; } database = knex ( knexConfig ) ; const times = { } ; database . on ( 'query' , ( queryInfo ) => { times [ queryInfo . __knexUid ] = performance . now ( ) ; } ) . on ( 'query-response' , ( response , queryInfo ) => { const delta = performance . now ( ) - times [ queryInfo . __knexUid ] ; logger . trace ( `[${ delta . toFixed ( 3 ) }ms] ${ queryInfo . sql } [${ queryInfo . bindings . join ( ', ' ) }]` ) ; delete times [ queryInfo . __knexUid ] ; } ) ; return database ; } export function getSchemaInspector ( ) { if ( inspector ) { return inspector ; } const database = getDatabase ( ) ; inspector = SchemaInspector ( database ) ; return inspector ; } export async function hasDatabaseConnection ( database ? ) { database = database ?? getDatabase ( ) ; try { if ( getDatabaseClient ( database ) === 'oracle' ) { await database . raw ( 'select 1 from DUAL' ) ; } else { await database . raw ( 'SELECT 1' ) ; } return true ; } catch { return false ; } } export async function validateDatabaseConnection ( database ? ) { database = database ?? getDatabase ( ) ; try { if ( getDatabaseClient ( database ) === 'oracle' ) { await database . raw ( 'select 1 from DUAL' ) ; } else { await database . raw ( 'SELECT 1' ) ; } } catch ( error ) { logger . error ( `Can't connect to the database.` ) ; logger . error ( error ) ; process . exit ( 1 ) ; } } export function getDatabaseClient ( database ? ) { database = database ?? getDatabase ( ) ; switch ( database . client . constructor . name ) { case 'Client_MySQL' : return 'mysql' ; case 'Client_PG' : return 'postgres' ; case 'Client_SQLite3' : return 'sqlite' ; case 'Client_Oracledb' : case 'Client_Oracle' : return 'oracle' ; case 'Client_MSSQL' : return 'mssql' ; case 'Client_Redshift' : return 'redshift' ; } throw new Error ( `Couldn't extract database client` ) ; } export async function isInstalled ( ) { const inspector = getSchemaInspector ( ) ; return await inspector . hasTable ( 'directus_collections' ) ; } export async function validateMigrations ( ) { const database = getDatabase ( ) ; try { let migrationFiles = await fse . readdir ( path . join ( __dirname , 'migrations' ) ) ; const customMigrationsPath = path . resolve ( env . EXTENSIONS_PATH , 'migrations' ) ; let customMigrationFiles = ( ( await fse . pathExists ( customMigrationsPath ) ) && ( await fse . readdir ( customMigrationsPath ) ) ) || [ ] ; migrationFiles = migrationFiles . filter ( ( file ) => file . startsWith ( 'run' ) === false && file . endsWith ( '.d.ts' ) === false ) ; customMigrationFiles = customMigrationFiles . filter ( ( file ) => file . endsWith ( '.js' ) ) ; migrationFiles . push ( ... customMigrationFiles ) ; const requiredVersions = migrationFiles . map ( ( filePath ) => filePath . split ( '-' ) [ 0 ] ) ; const completedVersions = ( await database . select ( 'version' ) . from ( 'directus_migrations' ) ) . map ( ( { version } ) => version ) ; return requiredVersions . every ( ( version ) => completedVersions . includes ( version ) ) ; } catch ( error ) { logger . error ( `Database migrations cannot be found` ) ; logger . error ( error ) ; throw process . exit ( 1 ) ; } } /**
 * These database extensions should be optional, so we don't throw or return any problem states when they don't
 */ export async function validateDatabaseExtensions ( ) { const database = getDatabase ( ) ; const client = getDatabaseClient ( database ) ; const helpers = getHelpers ( database ) ; const geometrySupport = await helpers . st . supported ( ) ; if ( ! geometrySupport ) { switch ( client ) { case 'postgres' : logger . warn ( `PostGIS isn't installed. Geometry type support will be limited.` ) ; break ; case 'sqlite' : logger . warn ( `Spatialite isn't installed. Geometry type support will be limited.` ) ; break ; default : logger . warn ( `Geometry type not supported on ${ client }` ) ; } } }