Skip to content

Commit

Permalink
Don't initialize database on file require (#6003)
Browse files Browse the repository at this point in the history
  • Loading branch information
rijkvanzanten authored Jun 2, 2021
1 parent 02fc696 commit 77e00b7
Show file tree
Hide file tree
Showing 39 changed files with 220 additions and 144 deletions.
9 changes: 3 additions & 6 deletions api/src/cli/commands/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import installDatabase from '../../../database/seeds/run';
import env from '../../../env';
import logger from '../../../logger';
import { getSchema } from '../../../utils/get-schema';
import { RolesService, UsersService, SettingsService } from '../../../services';
import getDatabase, { isInstalled, hasDatabaseConnection } from '../../../database';

export default async function bootstrap(): Promise<void> {
logger.info('Initializing bootstrap...');
Expand All @@ -13,10 +15,7 @@ export default async function bootstrap(): Promise<void> {
process.exit(1);
}

const { isInstalled, default: database } = require('../../../database');
const { RolesService } = require('../../../services/roles');
const { UsersService } = require('../../../services/users');
const { SettingsService } = require('../../../services/settings');
const database = getDatabase();

if ((await isInstalled()) === false) {
logger.info('Installing Directus system tables...');
Expand Down Expand Up @@ -66,8 +65,6 @@ export default async function bootstrap(): Promise<void> {
}

async function isDatabaseAvailable() {
const { hasDatabaseConnection } = require('../../../database');

const tries = 5;
const secondsBetweenTries = 5;

Expand Down
4 changes: 3 additions & 1 deletion api/src/cli/commands/count/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import getDatabase from '../../../database';

export default async function count(collection: string): Promise<void> {
const database = require('../../../database/index').default;
const database = getDatabase();

if (!collection) {
console.error('Collection is required');
Expand Down
4 changes: 2 additions & 2 deletions api/src/cli/commands/database/install.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Knex } from 'knex';
import runMigrations from '../../../database/migrations/run';
import installSeeds from '../../../database/seeds/run';
import getDatabase from '../../../database';

export default async function start(): Promise<void> {
const database = require('../../../database/index').default as Knex;
const database = getDatabase();

try {
await installSeeds(database);
Expand Down
3 changes: 2 additions & 1 deletion api/src/cli/commands/database/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import run from '../../../database/migrations/run';
import getDatabase from '../../../database';

export default async function migrate(direction: 'latest' | 'up' | 'down'): Promise<void> {
const database = require('../../../database').default;
const database = getDatabase();

try {
console.log('✨ Running migrations...');
Expand Down
5 changes: 3 additions & 2 deletions api/src/cli/commands/roles/create.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getSchema } from '../../../utils/get-schema';
import { RolesService } from '../../../services';
import getDatabase from '../../../database';

export default async function rolesCreate({ role: name, admin }: { role: string; admin: boolean }): Promise<void> {
const { default: database } = require('../../../database/index');
const { RolesService } = require('../../../services/roles');
const database = getDatabase();

if (!name) {
console.error('Name is required');
Expand Down
5 changes: 3 additions & 2 deletions api/src/cli/commands/users/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getSchema } from '../../../utils/get-schema';
import { UsersService } from '../../../services';
import getDatabase from '../../../database';

export default async function usersCreate({
email,
Expand All @@ -9,8 +11,7 @@ export default async function usersCreate({
password?: string;
role?: string;
}): Promise<void> {
const { default: database } = require('../../../database/index');
const { UsersService } = require('../../../services/users');
const database = getDatabase();

if (!email || !password || !role) {
console.error('Email, password, role are required');
Expand Down
5 changes: 3 additions & 2 deletions api/src/cli/commands/users/passwd.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import argon2 from 'argon2';
import { getSchema } from '../../../utils/get-schema';
import { UsersService } from '../../../services';
import getDatabase from '../../../database';

export default async function usersPasswd({ email, password }: { email?: string; password?: string }): Promise<void> {
const { default: database } = require('../../../database/index');
const { UsersService } = require('../../../services/users');
const database = getDatabase();

if (!email || !password) {
console.error('Email and password are required');
Expand Down
5 changes: 3 additions & 2 deletions api/src/controllers/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { pick } from 'lodash';
import ms from 'ms';
import validate from 'uuid-validate';
import { ASSET_TRANSFORM_QUERY_KEYS, SYSTEM_ASSET_ALLOW_LIST } from '../constants';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import { ForbiddenException, InvalidQueryException, RangeNotSatisfiableException } from '../exceptions';
import useCollection from '../middleware/use-collection';
Expand Down Expand Up @@ -32,11 +32,11 @@ router.get(
* This is a little annoying. Postgres will error out if you're trying to search in `where`
* with a wrong type. In case of directus_files where id is a uuid, we'll have to verify the
* validity of the uuid ahead of time.
* @todo move this to a validation middleware function
*/
const isValidUUID = validate(id, 4);
if (isValidUUID === false) throw new ForbiddenException();

const database = getDatabase();
const file = await database.select('id', 'storage', 'filename_disk').from('directus_files').where({ id }).first();
if (!file) throw new ForbiddenException();

Expand All @@ -51,6 +51,7 @@ router.get(
const payloadService = new PayloadService('directus_settings', { schema: req.schema });
const defaults = { storage_asset_presets: [], storage_asset_transform: 'all' };

const database = getDatabase();
const savedAssetSettings = await database
.select('storage_asset_presets', 'storage_asset_transform')
.from('directus_settings')
Expand Down
132 changes: 76 additions & 56 deletions api/src/database/index.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,98 @@
import SchemaInspector from '@directus/schema';
import dotenv from 'dotenv';
import { knex, Knex } from 'knex';
import path from 'path';
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';

dotenv.config({ path: path.resolve(__dirname, '../../', '.env') });
let database: Knex | null = null;
let inspector: ReturnType<typeof SchemaInspector> | null = null;

const connectionConfig: Record<string, any> = getConfigFromEnv('DB_', [
'DB_CLIENT',
'DB_SEARCH_PATH',
'DB_CONNECTION_STRING',
'DB_POOL',
]);
export default function getDatabase() {
if (database) {
return database;
}

const poolConfig = getConfigFromEnv('DB_POOL');
const connectionConfig: Record<string, any> = getConfigFromEnv('DB_', [
'DB_CLIENT',
'DB_SEARCH_PATH',
'DB_CONNECTION_STRING',
'DB_POOL',
]);

const requiredKeys = ['DB_CLIENT'];
const poolConfig = getConfigFromEnv('DB_POOL');

if (env.DB_CLIENT && env.DB_CLIENT === 'sqlite3') {
requiredKeys.push('DB_FILENAME');
} else if (env.DB_CLIENT && env.DB_CLIENT === 'oracledb') {
requiredKeys.push('DB_USER', 'DB_PASSWORD', 'DB_CONNECT_STRING');
} else {
if (env.DB_CLIENT === 'pg') {
if (!env.DB_CONNECTION_STRING) {
requiredKeys.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER');
}
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') {
requiredEnvVars.push('DB_USER', 'DB_PASSWORD', 'DB_CONNECT_STRING');
} else {
requiredKeys.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
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(requiredKeys);

const knexConfig: Knex.Config = {
client: env.DB_CLIENT,
searchPath: env.DB_SEARCH_PATH,
connection: env.DB_CONNECTION_STRING || connectionConfig,
log: {
warn: (msg) => 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 = (conn: any, cb: any) => {
conn.run('PRAGMA foreign_keys = ON', cb);
validateEnv(requiredEnvVars);

const knexConfig: Knex.Config = {
client: env.DB_CLIENT,
searchPath: env.DB_SEARCH_PATH,
connection: env.DB_CONNECTION_STRING || connectionConfig,
log: {
warn: (msg) => 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 = (conn: any, cb: any) => {
conn.run('PRAGMA foreign_keys = ON', cb);
};
}

database = knex(knexConfig);

const times: Record<string, number> = {};

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;
}

const database = knex(knexConfig);
export function getSchemaInspector() {
if (inspector) {
return inspector;
}

const times: Record<string, number> = {};
const database = getDatabase();

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(', ')}]`);
});
inspector = SchemaInspector(database);

return inspector;
}

export async function hasDatabaseConnection(): Promise<boolean> {
const database = getDatabase();

try {
if (env.DB_CLIENT === 'oracledb') {
await database.raw('select 1 from DUAL');
Expand All @@ -93,13 +115,11 @@ export async function validateDBConnection(): Promise<void> {
}
}

export const schemaInspector = SchemaInspector(database);

export async function isInstalled(): Promise<boolean> {
const inspector = getSchemaInspector();

// The existence of a directus_collections table alone isn't a "proper" check to see if everything
// is installed correctly of course, but it's safe enough to assume that this collection only
// exists when using the installer CLI.
return await schemaInspector.hasTable('directus_collections');
return await inspector.hasTable('directus_collections');
}

export default database;
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Knex } from 'knex';
import SchemaInspector from 'knex-schema-inspector';
import { schemaInspector } from '..';
import logger from '../../logger';
import { RelationMeta } from '../../types';

Expand All @@ -22,8 +21,8 @@ export async function up(knex: Knex): Promise<void> {
for (const constraint of constraintsToAdd) {
if (!constraint.one_collection) continue;

const currentPrimaryKeyField = await schemaInspector.primary(constraint.many_collection);
const relatedPrimaryKeyField = await schemaInspector.primary(constraint.one_collection);
const currentPrimaryKeyField = await inspector.primary(constraint.many_collection);
const relatedPrimaryKeyField = await inspector.primary(constraint.one_collection);
if (!currentPrimaryKeyField || !relatedPrimaryKeyField) continue;

const rowsWithIllegalFKValues = await knex
Expand Down Expand Up @@ -67,8 +66,8 @@ export async function up(knex: Knex): Promise<void> {
// to `unsigned`, but defaults `.integer()` to `int`. This means that created m2o fields
// have the wrong type. This step will force the m2o `int` field into `unsigned`, but only
// if both types are integers, and only if we go from `int` to `int unsigned`.
const columnInfo = await schemaInspector.columnInfo(constraint.many_collection, constraint.many_field);
const relatedColumnInfo = await schemaInspector.columnInfo(constraint.one_collection!, relatedPrimaryKeyField);
const columnInfo = await inspector.columnInfo(constraint.many_collection, constraint.many_field);
const relatedColumnInfo = await inspector.columnInfo(constraint.one_collection!, relatedPrimaryKeyField);

try {
await knex.schema.alterTable(constraint.many_collection, (table) => {
Expand Down
4 changes: 2 additions & 2 deletions api/src/database/run-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Item, Query, SchemaOverview } from '../types';
import { AST, FieldNode, NestedCollectionNode } from '../types/ast';
import applyQuery from '../utils/apply-query';
import { toArray } from '../utils/to-array';
import database from './index';
import getDatabase from './index';

type RunASTOptions = {
/**
Expand Down Expand Up @@ -39,7 +39,7 @@ export default async function runAST(
): Promise<null | Item | Item[]> {
const ast = cloneDeep(originalAST);

const knex = options?.knex || database;
const knex = options?.knex || getDatabase();

if (ast.type === 'm2a') {
const results: { [collection: string]: null | Item | Item[] } = {};
Expand Down
16 changes: 16 additions & 0 deletions api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ env = processValues(env);

export default env;

/**
* When changes have been made during runtime, like in the CLI, we can refresh the env object with
* the newly created variables
*/
export function refreshEnv() {
env = {
...defaults,
...getEnv(),
...process.env,
};

process.env = env;

env = processValues(env);
}

function getEnv() {
const configPath = path.resolve(process.env.CONFIG_PATH || defaults.CONFIG_PATH);

Expand Down
4 changes: 3 additions & 1 deletion api/src/exceptions/database/dialects/mssql.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import database from '../../../database';
import getDatabase from '../../../database';
import { ContainsNullValuesException } from '../contains-null-values';
import { InvalidForeignKeyException } from '../invalid-foreign-key';
import { NotNullViolationException } from '../not-null-violation';
Expand Down Expand Up @@ -56,6 +56,8 @@ async function uniqueViolation(error: MSSQLError) {

const keyName = quoteMatches[1];

const database = getDatabase();

const constraintUsage = await database
.select('*')
.from('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE')
Expand Down
4 changes: 3 additions & 1 deletion api/src/exceptions/database/translate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import database from '../../database';
import getDatabase from '../../database';
import { extractError as mssql } from './dialects/mssql';
import { extractError as mysql } from './dialects/mysql';
import { extractError as oracle } from './dialects/oracle';
Expand All @@ -16,6 +16,8 @@ import { SQLError } from './dialects/types';
* - Value Too Long
*/
export async function translateDatabaseError(error: SQLError): Promise<any> {
const database = getDatabase();

switch (database.client.constructor.name) {
case 'Client_MySQL':
return mysql(error);
Expand Down
Loading

0 comments on commit 77e00b7

Please sign in to comment.