Skip to content

Commit

Permalink
feat(core): Implement injectable lifecycle hooks for strategies
Browse files Browse the repository at this point in the history
Relates to #303
  • Loading branch information
michaelbromley committed Apr 30, 2020
1 parent 65a113b commit 451caf1
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 48 deletions.
52 changes: 52 additions & 0 deletions packages/core/e2e/lifecycle.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Injector } from '@vendure/core';
import { createTestEnvironment } from '@vendure/testing';
import path from 'path';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
import { AutoIncrementIdStrategy } from '../src/config/entity-id-strategy/auto-increment-id-strategy';
import { ProductService } from '../src/service/services/product.service';

const initSpy = jest.fn();
const destroySpy = jest.fn();

class TestIdStrategy extends AutoIncrementIdStrategy {
async init(injector: Injector) {
const productService = injector.get(ProductService);
const connection = injector.getConnection();
await new Promise(resolve => setTimeout(resolve, 100));
initSpy(productService.constructor.name, connection.name);
}

async destroy() {
await new Promise(resolve => setTimeout(resolve, 100));
destroySpy();
}
}

describe('lifecycle hooks for configurable objects', () => {
const { server, adminClient } = createTestEnvironment({
...testConfig,
entityIdStrategy: new TestIdStrategy(),
});

beforeAll(async () => {
await server.init({
initialData,
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
customerCount: 1,
});
await adminClient.asSuperAdmin();
}, TEST_SETUP_TIMEOUT_MS);

it('runs init with Injector', () => {
expect(initSpy).toHaveBeenCalled();
expect(initSpy.mock.calls[0][0]).toEqual('ProductService');
expect(initSpy.mock.calls[0][1]).toBe('default');
});

it('runs destroy', async () => {
await server.destroy();
expect(destroySpy).toHaveBeenCalled();
});
});
51 changes: 43 additions & 8 deletions packages/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import cookieSession = require('cookie-session');
import { RequestHandler } from 'express';

import { ApiModule } from './api/api.module';
import { Injector } from './common/injector';
import { InjectableStrategy } from './common/types/injectable-strategy';
import { ConfigModule } from './config/config.module';
import { ConfigService } from './config/config.service';
import { Logger } from './config/logger/vendure-logger';
Expand All @@ -30,10 +32,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
) {}

async onApplicationBootstrap() {
const { jobQueueStrategy } = this.configService.jobQueueOptions;
if (typeof jobQueueStrategy.init === 'function') {
await jobQueueStrategy.init(this.moduleRef);
}
await this.initInjectableStrategies();
}

configure(consumer: MiddlewareConsumer) {
Expand All @@ -60,10 +59,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
}

async onApplicationShutdown(signal?: string) {
const { jobQueueStrategy } = this.configService.jobQueueOptions;
if (typeof jobQueueStrategy.destroy === 'function') {
await jobQueueStrategy.destroy();
}
await this.destroyInjectableStrategies();
if (signal) {
Logger.info('Received shutdown signal:' + signal);
}
Expand All @@ -85,4 +81,43 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
}
return result;
}

private async initInjectableStrategies() {
const injector = new Injector(this.moduleRef);
for (const strategy of this.getInjectableStrategies()) {
if (typeof strategy.init === 'function') {
await strategy.init(injector);
}
}
}

private async destroyInjectableStrategies() {
for (const strategy of this.getInjectableStrategies()) {
if (typeof strategy.destroy === 'function') {
await strategy.destroy();
}
}
}

private getInjectableStrategies(): InjectableStrategy[] {
const {
assetNamingStrategy,
assetPreviewStrategy,
assetStorageStrategy,
} = this.configService.assetOptions;
const { taxCalculationStrategy, taxZoneStrategy } = this.configService.taxOptions;
const { jobQueueStrategy } = this.configService.jobQueueOptions;
const { mergeStrategy } = this.configService.orderOptions;
const { entityIdStrategy } = this.configService;
return [
assetNamingStrategy,
assetPreviewStrategy,
assetStorageStrategy,
taxCalculationStrategy,
taxZoneStrategy,
jobQueueStrategy,
mergeStrategy,
entityIdStrategy,
];
}
}
3 changes: 2 additions & 1 deletion packages/core/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './async-queue';
export * from './error/errors';
export * from './injector';
export * from './utils';
export * from './async-queue';
44 changes: 44 additions & 0 deletions packages/core/src/common/injector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Type } from '@nestjs/common';
import { ContextId, ModuleRef } from '@nestjs/core';
import { getConnectionToken } from '@nestjs/typeorm';
import { Connection } from 'typeorm';

/**
* @description
* The Injector wraps the underlying Nestjs `ModuleRef`, allowing injection of providers
* known to the application's dependency injection container. This is intended to enable the injection
* of services into objects which exist outside of the Nestjs module system, e.g. the various
* Strategies which can be supplied in the VendureConfig.
*
* @docsCategory common
*/
export class Injector {
constructor(private moduleRef: ModuleRef) {}

/**
* @description
* Retrieve an instance of the given type from the app's dependency injection container.
* Wraps the Nestjs `ModuleRef.get()` method.
*/
get<T, R = T>(typeOrToken: Type<T> | string | symbol): R {
return this.moduleRef.get(typeOrToken, { strict: false });
}

/**
* @description
* Retrieve the TypeORM `Connection` instance.
*/
getConnection(): Connection {
return this.moduleRef.get(getConnectionToken() as any, { strict: false });
}

/**
* @description
* Retrieve an instance of the given scoped provider (transient or request-scoped) from the
* app's dependency injection container.
* Wraps the Nestjs `ModuleRef.resolve()` method.
*/
resolve<T, R = T>(typeOrToken: Type<T> | string | symbol, contextId?: ContextId): Promise<R> {
return this.moduleRef.resolve(typeOrToken, contextId, { strict: false });
}
}
32 changes: 32 additions & 0 deletions packages/core/src/common/types/injectable-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injector } from '../injector';

/**
* @description
* This interface defines the setup and teardown hooks available to the
* various strategies used to configure Vendure.
*
* @docsCategory common
*/
export interface InjectableStrategy {
/**
* @description
* Defines setup logic to be run during application bootstrap. Receives
* the {@link Injector} as an argument, which allows application providers
* to be used as part of the setup.
*
* @example
* ```TypeScript
* async init(injector: Injector) {
* const myService = injector.get(MyService);
* await myService.doSomething();
* }
* ```
*/
init?: (injector: Injector) => void | Promise<void>;

/**
* @description
* Defines teardown logic to be run before application shutdown.
*/
destroy?: () => void | Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { InjectableStrategy } from '../../common/types/injectable-strategy';

/**
* @description
* The AssetNamingStrategy determines how file names are generated based on the uploaded source file name,
* as well as how to handle naming conflicts.
*
* @docsCategory assets
*/
export interface AssetNamingStrategy {
export interface AssetNamingStrategy extends InjectableStrategy {
/**
* @description
* Given the original file name of the uploaded file, generate a file name to
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Stream } from 'stream';

import { InjectableStrategy } from '../../common/types/injectable-strategy';

/**
* @description
* The AssetPreviewStrategy determines how preview images for assets are created. For image
Expand All @@ -11,6 +13,6 @@ import { Stream } from 'stream';
*
* @docsCategory assets
*/
export interface AssetPreviewStrategy {
export interface AssetPreviewStrategy extends InjectableStrategy {
generatePreviewImage(mimeType: string, data: Buffer): Promise<Buffer>;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Request } from 'express';
import { Stream } from 'stream';

import { InjectableStrategy } from '../../common/types/injectable-strategy';

/**
* @description
* The AssetPersistenceStrategy determines how Asset files are physically stored
* and retrieved.
*
* @docsCategory assets
*/
export interface AssetStorageStrategy {
export interface AssetStorageStrategy extends InjectableStrategy {
/**
* @description
* Writes a buffer to the store and returns a unique identifier for that
Expand Down Expand Up @@ -57,5 +59,5 @@ export interface AssetStorageStrategy {
* (i.e. the identifier is already an absolute url) then this method
* should not be implemented.
*/
toAbsoluteUrl?(reqest: Request, identifier: string): string;
toAbsoluteUrl?(request: Request, identifier: string): string;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ID } from '@vendure/common/lib/shared-types';

import { InjectableStrategy } from '../../common/types/injectable-strategy';

/**
* @description
* Defines the type of primary key used for all entities in the database.
Expand All @@ -20,7 +22,7 @@ export type PrimaryKeyType = 'increment' | 'uuid';
* @docsCategory entities
* @docsPage Entity Configuration
* */
export interface EntityIdStrategy<T extends ID = ID> {
export interface EntityIdStrategy<T extends ID = ID> extends InjectableStrategy {
readonly primaryKeyType: PrimaryKeyType;
encodeId: (primaryKey: T) => string;
decodeId: (id: string) => T;
Expand Down
26 changes: 2 additions & 24 deletions packages/core/src/config/job-queue/job-queue-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ModuleRef } from '@nestjs/core';
import { JobListOptions } from '@vendure/common/lib/generated-types';
import { ID, PaginatedList } from '@vendure/common/lib/shared-types';

import { InjectableStrategy } from '../../common/types/injectable-strategy';
import { Job } from '../../job-queue/job';

/**
Expand All @@ -12,30 +13,7 @@ import { Job } from '../../job-queue/job';
*
* @docsCategory JobQueue
*/
export interface JobQueueStrategy {
/**
* @description
* Initialization logic to be run after the Vendure server has been initialized
* (in the Nestjs [onApplicationBootstrap hook](https://docs.nestjs.com/fundamentals/lifecycle-events)).
*
* Receives an instance of the application's ModuleRef, which can be used to inject
* providers:
*
* @example
* ```TypeScript
* init(moduleRef: ModuleRef) {
* const myService = moduleRef.get(MyService, { strict: false });
* }
* ```
*/
init?(moduleRef: ModuleRef): void | Promise<void>;

/**
* @description
* Teardown logic to be run when the Vendure server shuts down.
*/
destroy?(): void | Promise<void>;

export interface JobQueueStrategy extends InjectableStrategy {
/**
* @description
* Add a new job to the queue.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { InjectableStrategy } from '../../common/types/injectable-strategy';
import { OrderLine } from '../../entity/order-line/order-line.entity';
import { Order } from '../../entity/order/order.entity';

Expand All @@ -11,7 +12,7 @@ import { Order } from '../../entity/order/order.entity';
*
* @docsCategory orders
*/
export interface OrderMergeStrategy {
export interface OrderMergeStrategy extends InjectableStrategy {
/**
* @description
* Merges the lines of the guest Order with those of the existing Order which is associated
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/config/tax/tax-calculation-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RequestContext } from '../../api/common/request-context';
import { InjectableStrategy } from '../../common/types/injectable-strategy';
import { TaxCategory, Zone } from '../../entity';
import { TaxCalculationResult } from '../../service/helpers/tax-calculator/tax-calculator';
import { TaxRateService } from '../../service/services/tax-rate.service';
Expand All @@ -9,7 +10,7 @@ import { TaxRateService } from '../../service/services/tax-rate.service';
*
* @docsCategory tax
*/
export interface TaxCalculationStrategy {
export interface TaxCalculationStrategy extends InjectableStrategy {
calculate(args: TaxCalculationArgs): TaxCalculationResult;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/config/tax/tax-zone-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { InjectableStrategy } from '../../common/types/injectable-strategy';
import { Channel, Order, Zone } from '../../entity';

/**
Expand All @@ -6,6 +7,6 @@ import { Channel, Order, Zone } from '../../entity';
*
* @docsCategory tax
*/
export interface TaxZoneStrategy {
export interface TaxZoneStrategy extends InjectableStrategy {
determineTaxZone(zones: Zone[], channel: Channel, order?: Order): Zone | undefined;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ModuleRef } from '@nestjs/core';
import { getConnectionToken } from '@nestjs/typeorm';
import { JobListOptions, JobState } from '@vendure/common/lib/generated-types';
import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
import { Brackets, Connection, FindConditions, In, LessThan, Not } from 'typeorm';
import { Brackets, Connection, FindConditions, In, LessThan } from 'typeorm';

import { Injector } from '../../common/injector';
import { JobQueueStrategy } from '../../config/job-queue/job-queue-strategy';
import { Job } from '../../job-queue/job';
import { ProcessContext } from '../../process-context/process-context';
Expand All @@ -22,11 +21,11 @@ export class SqlJobQueueStrategy implements JobQueueStrategy {
private connection: Connection | undefined;
private listQueryBuilder: ListQueryBuilder;

init(moduleRef: ModuleRef) {
const processContext = moduleRef.get(ProcessContext, { strict: false });
init(injector: Injector) {
const processContext = injector.get(ProcessContext);
if (processContext.isServer) {
this.connection = moduleRef.get(getConnectionToken() as any, { strict: false });
this.listQueryBuilder = moduleRef.get(ListQueryBuilder, { strict: false });
this.connection = injector.getConnection();
this.listQueryBuilder = injector.get(ListQueryBuilder);
}
}

Expand Down

0 comments on commit 451caf1

Please sign in to comment.