Skip to content

Commit

Permalink
feat(core): Add static lifecycle hooks to run before bootstrap
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed May 19, 2020
1 parent ebf78a2 commit c92c21b
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
import { INestApplication, INestMicroservice } from '@nestjs/common';
import {
BeforeVendureBootstrap,
BeforeVendureWorkerBootstrap,
OnVendureBootstrap,
OnVendureClose,
OnVendureWorkerBootstrap,
OnVendureWorkerClose,
} from '@vendure/core';

export class TestPluginWithAllLifecycleHooks
implements OnVendureBootstrap, OnVendureWorkerBootstrap, OnVendureClose, OnVendureWorkerClose {
implements
BeforeVendureBootstrap,
BeforeVendureWorkerBootstrap,
OnVendureBootstrap,
OnVendureWorkerBootstrap,
OnVendureClose,
OnVendureWorkerClose {
private static onConstructorFn: any;
private static onBeforeBootstrapFn: any;
private static onBeforeWorkerBootstrapFn: any;
private static onBootstrapFn: any;
private static onWorkerBootstrapFn: any;
private static onCloseFn: any;
private static onWorkerCloseFn: any;

static init(
constructorFn: any,
beforeBootstrapFn: any,
beforeWorkerBootstrapFn: any,
bootstrapFn: any,
workerBootstrapFn: any,
closeFn: any,
workerCloseFn: any,
) {
this.onConstructorFn = constructorFn;
this.onBeforeBootstrapFn = beforeBootstrapFn;
this.onBeforeWorkerBootstrapFn = beforeWorkerBootstrapFn;
this.onBootstrapFn = bootstrapFn;
this.onWorkerBootstrapFn = workerBootstrapFn;
this.onCloseFn = closeFn;
Expand All @@ -32,6 +47,14 @@ export class TestPluginWithAllLifecycleHooks
TestPluginWithAllLifecycleHooks.onConstructorFn();
}

static beforeVendureBootstrap(app: INestApplication): void | Promise<void> {
TestPluginWithAllLifecycleHooks.onBeforeBootstrapFn(app);
}

static beforeVendureWorkerBootstrap(app: INestMicroservice): void | Promise<void> {
TestPluginWithAllLifecycleHooks.onBeforeWorkerBootstrapFn(app);
}

onVendureBootstrap(): void | Promise<void> {
TestPluginWithAllLifecycleHooks.onBootstrapFn();
}
Expand Down
16 changes: 15 additions & 1 deletion packages/core/e2e/plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import gql from 'graphql-tag';
import path from 'path';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';

import { TestPluginWithAllLifecycleHooks } from './fixtures/test-plugins/with-all-lifecycle-hooks';
import { TestAPIExtensionPlugin } from './fixtures/test-plugins/with-api-extensions';
Expand All @@ -18,6 +18,8 @@ import { TestProcessContextPlugin } from './fixtures/test-plugins/with-worker-co
describe('Plugins', () => {
const bootstrapMockFn = jest.fn();
const onConstructorFn = jest.fn();
const beforeBootstrapFn = jest.fn();
const beforeWorkerBootstrapFn = jest.fn();
const onBootstrapFn = jest.fn();
const onWorkerBootstrapFn = jest.fn();
const onCloseFn = jest.fn();
Expand All @@ -28,6 +30,8 @@ describe('Plugins', () => {
plugins: [
TestPluginWithAllLifecycleHooks.init(
onConstructorFn,
beforeBootstrapFn,
beforeWorkerBootstrapFn,
onBootstrapFn,
onWorkerBootstrapFn,
onCloseFn,
Expand Down Expand Up @@ -59,6 +63,16 @@ describe('Plugins', () => {
expect(onConstructorFn).toHaveBeenCalledTimes(2);
});

it('calls beforeVendureBootstrap', () => {
expect(beforeBootstrapFn).toHaveBeenCalledTimes(1);
expect(beforeBootstrapFn).toHaveBeenCalledWith(server.app);
});

it('calls beforeVendureWorkerBootstrap', () => {
expect(beforeWorkerBootstrapFn).toHaveBeenCalledTimes(1);
expect(beforeWorkerBootstrapFn).toHaveBeenCalledWith(server.worker);
});

it('calls onVendureBootstrap', () => {
expect(onBootstrapFn).toHaveBeenCalledTimes(1);
});
Expand Down
51 changes: 41 additions & 10 deletions packages/core/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { setEntityIdStrategy } from './entity/set-entity-id-strategy';
import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config';
import { getConfigurationFunction, getEntitiesFromPlugins } from './plugin/plugin-metadata';
import { getProxyMiddlewareCliGreetings } from './plugin/plugin-utils';
import { BeforeVendureBootstrap, BeforeVendureWorkerBootstrap } from './plugin/vendure-plugin';

export type VendureBootstrapFunction = (config: VendureConfig) => Promise<INestApplication>;

Expand Down Expand Up @@ -51,6 +52,7 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
});
DefaultLogger.restoreOriginalLogLevel();
app.useLogger(new Logger());
await runBeforeBootstrapHooks(config, app);
await app.listen(port, hostname || '');
app.enableShutdownHooks();
if (config.workerOptions.runInMainProcess) {
Expand Down Expand Up @@ -101,7 +103,7 @@ export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promi
}

async function bootstrapWorkerInternal(
vendureConfig: ReadOnlyRequired<VendureConfig>,
vendureConfig: Readonly<RuntimeVendureConfig>,
): Promise<INestMicroservice> {
const config = disableSynchronize(vendureConfig);
if (!config.workerOptions.runInMainProcess && (config.logger as any).setDefaultContext) {
Expand All @@ -120,7 +122,7 @@ async function bootstrapWorkerInternal(
DefaultLogger.restoreOriginalLogLevel();
workerApp.useLogger(new Logger());
workerApp.enableShutdownHooks();

await runBeforeWorkerBootstrapHooks(config, workerApp);
// A work-around to correctly handle errors when attempting to start the
// microservice server listening.
// See https://github.com/nestjs/nest/issues/2777
Expand Down Expand Up @@ -196,7 +198,7 @@ export async function getAllEntities(userConfig: Partial<VendureConfig>): Promis
// Check to ensure that no plugins are defining entities with names
// which conflict with existing entities.
for (const pluginEntity of pluginEntities) {
if (allEntities.find(e => e.name === pluginEntity.name)) {
if (allEntities.find((e) => e.name === pluginEntity.name)) {
throw new InternalServerError(`error.entity-name-conflict`, { entityName: pluginEntity.name });
} else {
allEntities.push(pluginEntity);
Expand All @@ -221,7 +223,7 @@ function setExposedHeaders(config: Readonly<RuntimeVendureConfig>) {
} else if (typeof exposedHeaders === 'string') {
exposedHeadersWithAuthKey = exposedHeaders
.split(',')
.map(x => x.trim())
.map((x) => x.trim())
.concat(authTokenHeaderKey);
} else {
exposedHeadersWithAuthKey = exposedHeaders.concat(authTokenHeaderKey);
Expand All @@ -231,6 +233,35 @@ function setExposedHeaders(config: Readonly<RuntimeVendureConfig>) {
}
}

export async function runBeforeBootstrapHooks(config: Readonly<RuntimeVendureConfig>, app: INestApplication) {
function hasBeforeBootstrapHook(
plugin: any,
): plugin is { beforeVendureBootstrap: BeforeVendureBootstrap } {
return typeof plugin.beforeVendureBootstrap === 'function';
}
for (const plugin of config.plugins) {
if (hasBeforeBootstrapHook(plugin)) {
await plugin.beforeVendureBootstrap(app);
}
}
}

export async function runBeforeWorkerBootstrapHooks(
config: Readonly<RuntimeVendureConfig>,
worker: INestMicroservice,
) {
function hasBeforeBootstrapHook(
plugin: any,
): plugin is { beforeVendureWorkerBootstrap: BeforeVendureWorkerBootstrap } {
return typeof plugin.beforeVendureWorkerBootstrap === 'function';
}
for (const plugin of config.plugins) {
if (hasBeforeBootstrapHook(plugin)) {
await plugin.beforeVendureWorkerBootstrap(worker);
}
}
}

/**
* Monkey-patches the app's .close() method to also close the worker microservice
* instance too.
Expand Down Expand Up @@ -273,25 +304,25 @@ function logWelcomeMessage(config: RuntimeVendureConfig) {
apiCliGreetings.push(...getProxyMiddlewareCliGreetings(config));
const columnarGreetings = arrangeCliGreetingsInColumns(apiCliGreetings);
const title = `Vendure server (v${version}) now running on port ${port}`;
const maxLineLength = Math.max(title.length, ...columnarGreetings.map(l => l.length));
const maxLineLength = Math.max(title.length, ...columnarGreetings.map((l) => l.length));
const titlePadLength = title.length < maxLineLength ? Math.floor((maxLineLength - title.length) / 2) : 0;
Logger.info(`=`.repeat(maxLineLength));
Logger.info(title.padStart(title.length + titlePadLength));
Logger.info('-'.repeat(maxLineLength).padStart(titlePadLength));
columnarGreetings.forEach(line => Logger.info(line));
columnarGreetings.forEach((line) => Logger.info(line));
Logger.info(`=`.repeat(maxLineLength));
}

function arrangeCliGreetingsInColumns(lines: Array<[string, string]>): string[] {
const columnWidth = Math.max(...lines.map(l => l[0].length)) + 2;
return lines.map(l => `${(l[0] + ':').padEnd(columnWidth)}${l[1]}`);
const columnWidth = Math.max(...lines.map((l) => l[0].length)) + 2;
return lines.map((l) => `${(l[0] + ':').padEnd(columnWidth)}${l[1]}`);
}

/**
* Fix race condition when modifying DB
* See: https://github.com/vendure-ecommerce/vendure/issues/152
*/
function disableSynchronize(userConfig: ReadOnlyRequired<VendureConfig>): ReadOnlyRequired<VendureConfig> {
function disableSynchronize(userConfig: Readonly<RuntimeVendureConfig>): Readonly<RuntimeVendureConfig> {
const config = { ...userConfig };
config.dbConnectionOptions = {
...userConfig.dbConnectionOptions,
Expand All @@ -311,7 +342,7 @@ function checkForDeprecatedOptions(config: Partial<VendureConfig>) {
'middleware',
'apolloServerPlugins',
];
const deprecatedOptionsUsed = deprecatedApiOptions.filter(option => config.hasOwnProperty(option));
const deprecatedOptionsUsed = deprecatedApiOptions.filter((option) => config.hasOwnProperty(option));
if (deprecatedOptionsUsed.length) {
throw new Error(
`The following VendureConfig options are deprecated: ${deprecatedOptionsUsed.join(', ')}\n` +
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/plugin/vendure-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { INestApplication, INestMicroservice, Module } from '@nestjs/common';
import { MODULE_METADATA } from '@nestjs/common/constants';
import { ModuleMetadata } from '@nestjs/common/interfaces';
import { pick } from '@vendure/common/lib/pick';
Expand Down Expand Up @@ -139,6 +139,28 @@ export function VendurePlugin(pluginMetadata: VendurePluginMetadata): ClassDecor
};
}

/**
* @description
* A plugin which implements a static `beforeVendureBootstrap` method with this type can define logic to run
* before the Vendure server (and the underlying Nestjs application) is bootstrapped. This is called
* _after_ the Nestjs application has been created, but _before_ the `app.listen()` method is invoked.
*
* @docsCategory plugin
* @docsPage Plugin Lifecycle Methods
*/
export type BeforeVendureBootstrap = (app: INestApplication) => void | Promise<void>;

/**
* @description
* A plugin which implements a static `beforeVendureWorkerBootstrap` method with this type can define logic to run
* before the Vendure worker (and the underlying Nestjs microservice) is bootstrapped. This is called
* _after_ the Nestjs microservice has been created, but _before_ the `microservice.listen()` method is invoked.
*
* @docsCategory plugin
* @docsPage Plugin Lifecycle Methods
*/
export type BeforeVendureWorkerBootstrap = (app: INestMicroservice) => void | Promise<void>;

/**
* @description
* A plugin which implements this interface can define logic to run when the Vendure server is initialized.
Expand Down
10 changes: 8 additions & 2 deletions packages/testing/src/test-server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { INestApplication, INestMicroservice } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DefaultLogger, Logger, VendureConfig } from '@vendure/core';
import { preBootstrapConfig } from '@vendure/core/dist/bootstrap';
import {
preBootstrapConfig,
runBeforeBootstrapHooks,
runBeforeWorkerBootstrapHooks,
} from '@vendure/core/dist/bootstrap';

import { populateForTesting } from './data-population/populate-for-testing';
import { getInitializerFor } from './initializers/initializers';
Expand Down Expand Up @@ -71,7 +75,7 @@ export class TestServer {
*/
async destroy() {
// allow a grace period of any outstanding async tasks to complete
await new Promise(resolve => global.setTimeout(resolve, 500));
await new Promise((resolve) => global.setTimeout(resolve, 500));
await this.app.close();
if (this.worker) {
await this.worker.close();
Expand Down Expand Up @@ -134,6 +138,7 @@ export class TestServer {
logger: new Logger(),
});
let worker: INestMicroservice | undefined;
await runBeforeBootstrapHooks(config, app);
await app.listen(config.apiOptions.port);
if (config.workerOptions.runInMainProcess) {
const workerModule = await import('@vendure/core/dist/worker/worker.module');
Expand All @@ -142,6 +147,7 @@ export class TestServer {
logger: new Logger(),
options: config.workerOptions.options,
});
await runBeforeWorkerBootstrapHooks(config, worker);
await worker.listenAsync();
}
DefaultLogger.restoreOriginalLogLevel();
Expand Down

0 comments on commit c92c21b

Please sign in to comment.