Skip to content

Commit

Permalink
DB connection and migrations working
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle1morel committed Dec 11, 2023
1 parent 648f433 commit 62cc9b8
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 199 deletions.
101 changes: 91 additions & 10 deletions app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,25 @@ import { name as appName, version as appVersion } from './package.json';
import { DEFAULTCORS } from './src/components/constants';
import { getLogger, httpLogger } from './src/components/log';
import { getGitRevision, readIdpList } from './src/components/utils';
import DataConnection from './src/db/dataConnection';
import v1Router from './src/routes/v1';

import type { Request, Response } from 'express';

const dataConnection = new DataConnection();
const log = getLogger(module.filename);

const state = {
connections: {
data: {}
},
gitRev: getGitRevision(),
ready: true, // No dependencies so application is always ready
shutdown: false
};

let probeId;

const appRouter = express.Router();
const app = express();
app.use(compression());
Expand All @@ -34,6 +41,8 @@ app.use(helmet());

// Skip if running tests
if (process.env.NODE_ENV !== 'test') {
// Initialize connections and exit if unsuccessful
initializeConnections();
app.use(httpLogger);
}

Expand Down Expand Up @@ -135,16 +144,6 @@ process.on('exit', () => {
log.info('Exiting...');
});

/**
* @function shutdown
* Shuts down this application after at least 3 seconds.
*/
function shutdown(): void {
log.info('Received kill signal. Shutting down...');
// Wait 3 seconds before starting cleanup
if (!state.shutdown) setTimeout(cleanup, 3000);
}

/**
* @function cleanup
* Cleans up connections in this application.
Expand All @@ -155,4 +154,86 @@ function cleanup(): void {
process.exit();
}

/**
* @function checkConnections
* Checks Database connectivity
* This may force the application to exit if a connection fails
*/
function checkConnections() {
const wasReady = state.ready;
if (!state.shutdown) {
dataConnection.checkConnection().then((results) => {
state.connections.data = results;
state.ready = Object.values(state.connections).every((x) => x);
if (!wasReady && state.ready) log.info('Application ready to accept traffic', { function: 'checkConnections' });
if (wasReady && !state.ready)
log.warn('Application not ready to accept traffic', { function: 'checkConnections' });
log.silly('App state', { function: 'checkConnections', state: state });
if (!state.ready) notReadyHandler();
});
}
}

/**
* @function fatalErrorHandler
* Forces the application to shutdown
*/
function fatalErrorHandler() {
process.exitCode = 1;
shutdown();
}

/**
* @function initializeConnections
* Initializes the database connections
* This may force the application to exit if it fails
*/
function initializeConnections() {
// Initialize connections and exit if unsuccessful
dataConnection
.checkAll()
.then((results) => {
state.connections.data = results;

if (state.connections.data) {
log.info('DataConnection Reachable', { function: 'initializeConnections' });
}
})
.catch((error) => {
console.log(error);
log.error(`Initialization failed: Database OK = ${state.connections.data}`, {
function: 'initializeConnections'
});
log.error('Connection initialization failure', error.message, { function: 'initializeConnections' });
if (!state.ready) notReadyHandler();
})
.finally(() => {
state.ready = Object.values(state.connections).every((x) => x);
if (state.ready) log.info('Application ready to accept traffic', { function: 'initializeConnections' });

// Start periodic 10 second connection probes
probeId = setInterval(checkConnections, 10000);
});
}

/**
* @function notReadyHandler
* Forces an application shutdown if `server.hardReset` is defined.
* Otherwise will flush and attempt to reset the connection pool.
*/
function notReadyHandler() {
if (config.has('server.hardReset')) fatalErrorHandler();
else dataConnection.resetConnection();
}

/**
* @function shutdown
* Shuts down this application after at least 3 seconds.
*/
function shutdown(): void {
log.info('Received kill signal. Shutting down...');
// Wait 3 seconds before starting cleanup
if (!state.shutdown) setTimeout(cleanup, 3000);
}

export default app;
8 changes: 8 additions & 0 deletions app/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
"server": {
"apiPath": "/api/v1",
"bodyLimit": "30mb",
"db": {
"database": "pns",
"host": "localhost",
"port": "5432",
"poolMin": "2",
"poolMax": "10",
"username": "app"
},
"logLevel": "http",
"port": "8080"
}
Expand Down
23 changes: 13 additions & 10 deletions app/src/knexfile.ts → app/knexfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import config from 'config';
import { format, parseJSON } from 'date-fns';
import { Knex } from 'knex';

import { getLogger } from './components/log';
import { getLogger } from './src/components/log';
const log = getLogger(module.filename);

/**
Expand Down Expand Up @@ -36,11 +36,11 @@ const logWrapper = (level: string, msg: string) => log.log(level, msg);
export const knexConfig: Knex.Config = {
client: 'pg',
connection: {
host: config.get('db.host'),
user: config.get('db.username'),
password: config.get('db.password'),
database: config.get('db.database'),
port: config.get('db.port')
host: config.get('server.db.host'),
user: config.get('server.db.username'),
password: config.get('server.db.password'),
database: config.get('server.db.database'),
port: config.get('server.db.port')
},
debug: ['silly', 'debug'].includes(config.get('server.logLevel')),
log: {
Expand All @@ -50,16 +50,19 @@ export const knexConfig: Knex.Config = {
warn: (msg) => logWrapper('warn', msg)
},
migrations: {
directory: __dirname + '/src/db/migrations'
extension: 'ts',
directory: './src/db/migrations'
},
pool: {
min: parseInt(config.get('db.poolMin')),
max: parseInt(config.get('db.poolMax'))
min: parseInt(config.get('server.db.poolMin')),
max: parseInt(config.get('server.db.poolMax'))
// This shouldn't be here: https://github.com/knex/knex/issues/3455#issuecomment-535554401
// propagateCreateError: false
},
searchPath: ['public'], // Define postgres schemas to match on
seeds: {
directory: __dirname + '/src/db/seeds'
directory: './src/db/seeds'
}
};

export default knexConfig;
8 changes: 8 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
"purge": "rimraf node_modules",
"rebuild": "npm run clean && npm run build",
"reinstall": "npm run purge && npm install",
"migrate": "npm run migrate:latest",
"migrate:down": "knex migrate:down",
"migrate:latest": "knex migrate:latest",
"migrate:make": "knex migrate:make",
"migrate:rollback": "knex migrate:rollback",
"migrate:rollback:all": "knex migrate:rollback --all",
"migrate:up": "knex migrate:up",
"seed": "knex seed:run",
"serve": "ts-node-dev --respawn --transpile-only --rs --watch bin,config,dist ./bin/www",
"start": "ts-node --transpile-only ./bin/www",
"test": "jest --verbose --forceExit --detectOpenHandles",
Expand Down
80 changes: 0 additions & 80 deletions app/src/components/crypt.ts

This file was deleted.

31 changes: 16 additions & 15 deletions app/src/db/dataConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Knex, knex } from 'knex';
import { Model } from 'objection';

import { getLogger } from '../components/log';
import { knexConfig } from '../knexfile';
import models from './models';
import { knexConfig } from '../../knexfile';
import { models } from './models';

const log = getLogger(module.filename);

Expand All @@ -13,7 +13,7 @@ export default class DataConnection {
* @class
*/
private static _instance: DataConnection;
private _knex: Knex;
private _knex: Knex | undefined;
private _connected: boolean = false;

constructor() {
Expand Down Expand Up @@ -88,7 +88,7 @@ export default class DataConnection {
*/
async checkConnection() {
try {
const data = await this._knex.raw('show transaction_read_only');
const data = await this._knex?.raw('show transaction_read_only');
const result = data?.rows[0]?.transaction_read_only === 'off';
if (result) {
log.debug('Database connection ok', { function: 'checkConnection' });
Expand All @@ -97,7 +97,7 @@ export default class DataConnection {
}
this._connected = result;
return result;
} catch (err) {
} catch (err: any) {
log.error(`Error with database connection: ${err.message}`, { function: 'checkConnection' });
this._connected = false;
return false;
Expand All @@ -109,20 +109,21 @@ export default class DataConnection {
* Queries the knex connection to check for the existence of the expected schema tables
* @returns {boolean} True if schema is ok, otherwise false
*/
checkSchema() {
async checkSchema() {
try {
const tables = Object.values(models).map((model) => model.tableName);
return Promise.all(
tables.map((table) =>
Promise.all(knexConfig.searchPath.map((schema) => this._knex.schema.withSchema(schema).hasTable(table)))
)
models.map((table) => {
return Array.isArray(knexConfig.searchPath)
? Promise.all(knexConfig.searchPath.map((schema) => this._knex?.schema.withSchema(schema).hasTable(table)))
: this._knex?.schema.withSchema(knexConfig.searchPath as string).hasTable(table);
})
)
.then((exists) => exists.every((table) => table.some((exist) => exist)))
.then((exists: Array<any>) => exists.every((table) => table.some((exist: any) => exist)))
.then((result) => {
if (result) log.debug('Database schema ok', { function: 'checkSchema' });
return result;
});
} catch (err) {
} catch (err: any) {
log.error(`Error with database schema: ${err.message}`, { function: 'checkSchema' });
log.error(err);
return false;
Expand All @@ -139,7 +140,7 @@ export default class DataConnection {
Model.knex(this._knex);
log.debug('Database models ok', { function: 'checkModel' });
return true;
} catch (err) {
} catch (err: any) {
log.error(`Error attaching Model to connection: ${err.message}`, { function: 'checkModel' });
log.error(err);
return false;
Expand All @@ -151,7 +152,7 @@ export default class DataConnection {
* Will close the DataConnection
* @param {function} [cb] Optional callback
*/
close(cb = undefined) {
close(cb = () => {}) {
if (this._knex) {
this._knex.destroy(() => {
this._knex = undefined;
Expand All @@ -169,7 +170,7 @@ export default class DataConnection {
if (this._knex) {
log.warn('Attempting to reset database connection pool', { function: 'resetConnection' });
this._knex.destroy(() => {
this._knex.initialize();
this._knex?.initialize();
});
}
}
Expand Down
Loading

0 comments on commit 62cc9b8

Please sign in to comment.