const KnexMigrator = require('knex-migrator'); const errors = require('@tryghost/errors'); const logging = require('@tryghost/logging'); const metrics = require('@tryghost/metrics'); const config = require('../../../shared/config'); const sentry = require('../../../shared/sentry'); const states = { READY: 0, NEEDS_INITIALISATION: 1, NEEDS_MIGRATION: 2, ERROR: 3, }; const printState = ({ state }) => { if (state === states.READY) { logging.info('Database is in a ready state.'); } if (state === states.NEEDS_INITIALISATION) { logging.warn('Database state requires initialisation.'); } if (state === states.NEEDS_MIGRATION) { logging.warn('Database state requires migration.'); } if (state === states.ERROR) { logging.error('Database is in an error state.'); } }; const createMissingInDevelopmentTables = async () => { const { inDevelopment } = require('../schema'); const db = require('./index'); await inDevelopment.createMissingInDevelopmentTables(db.knex); }; class DatabaseStateManager { constructor({ knexMigratorFilePath }) { this.knexMigrator = new KnexMigrator({ knexMigratorFilePath, }); this.warning = null; } async getState() { let state = states.READY; try { await this.knexMigrator.isDatabaseOK(); return state; } catch (error) { // CASE: database has not yet been initialized if (error.code === 'DB_NOT_INITIALISED') { state = states.NEEDS_INITIALISATION; return state; } // CASE: there's no migration table so we can't understand if (error.code === 'MIGRATION_TABLE_IS_MISSING') { state = states.NEEDS_INITIALISATION; return state; } // CASE: database needs migrations if (error.code === 'DB_NEEDS_MIGRATION') { state = states.NEEDS_MIGRATION; return state; } // CASE: database ran migrations this code doesn't have, e.g. after switching branches in development if (error.code === 'MIGRATION_STATE_ERROR' && config.get('env') === 'development') { this.warning = 'Database has migrations this code does not know about. Run `pnpm reset:db` for a fresh database.'; return state; } // CASE: database connection errors, unknown cases let errorToThrow = error; if (!errors.utils.isGhostError(errorToThrow)) { errorToThrow = new errors.InternalServerError({ code: 'DATABASE_ERROR', message: errorToThrow.message, err: errorToThrow, }); } sentry.captureException(errorToThrow); throw errorToThrow; } } async makeReady() { try { let state = await this.getState(); printState({ state }); if (state === states.READY) { await createMissingInDevelopmentTables(); return; } if (state === states.NEEDS_INITIALISATION) { const beforeInitializationTime = Date.now(); await this.knexMigrator.init(); metrics.metric('migrations', { value: Date.now() - beforeInitializationTime, type: 'initialization', }); } if (state === states.NEEDS_MIGRATION) { const beforeMigrationTime = Date.now(); await this.knexMigrator.migrate(); metrics.metric('migrations', { value: Date.now() - beforeMigrationTime, type: 'migrations', }); } state = await this.getState(); printState({ state }); await createMissingInDevelopmentTables(); } catch (error) { let errorToThrow = error; if (!errors.utils.isGhostError(error)) { errorToThrow = new errors.InternalServerError({ code: 'DATABASE_ERROR', message: errorToThrow.message, err: errorToThrow, }); } sentry.captureException(errorToThrow); throw errorToThrow; } } } module.exports = DatabaseStateManager;