Преглед на файлове

feat(core): Implement injectable lifecycle hooks for configurable ops

Relates to #303
Michael Bromley преди 5 години
родител
ревизия
16db62047f

+ 55 - 14
packages/core/e2e/lifecycle.e2e-spec.ts

@@ -1,33 +1,55 @@
-import { Injector } from '@vendure/core';
+import { AutoIncrementIdStrategy, Injector, ProductService, ShippingEligibilityChecker } 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();
+const strategyInitSpy = jest.fn();
+const strategyDestroySpy = jest.fn();
+const codInitSpy = jest.fn();
+const codDestroySpy = 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);
+        strategyInitSpy(productService.constructor.name, connection.name);
     }
 
     async destroy() {
         await new Promise(resolve => setTimeout(resolve, 100));
-        destroySpy();
+        strategyDestroySpy();
     }
 }
 
+const testShippingEligChecker = new ShippingEligibilityChecker({
+    code: 'test',
+    args: {},
+    description: [],
+    init: async injector => {
+        const productService = injector.get(ProductService);
+        const connection = injector.getConnection();
+        await new Promise(resolve => setTimeout(resolve, 100));
+        codInitSpy(productService.constructor.name, connection.name);
+    },
+    destroy: async () => {
+        await new Promise(resolve => setTimeout(resolve, 100));
+        codDestroySpy();
+    },
+    check: order => {
+        return true;
+    },
+});
+
 describe('lifecycle hooks for configurable objects', () => {
     const { server, adminClient } = createTestEnvironment({
         ...testConfig,
         entityIdStrategy: new TestIdStrategy(),
+        shippingOptions: {
+            shippingEligibilityCheckers: [testShippingEligChecker],
+        },
     });
 
     beforeAll(async () => {
@@ -39,14 +61,33 @@ describe('lifecycle hooks for configurable objects', () => {
         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');
+    describe('strategy', () => {
+        it('runs init with Injector', () => {
+            expect(strategyInitSpy).toHaveBeenCalled();
+            expect(strategyInitSpy.mock.calls[0][0]).toEqual('ProductService');
+            expect(strategyInitSpy.mock.calls[0][1]).toBe('default');
+        });
+
+        it('runs destroy', async () => {
+            await server.destroy();
+            expect(strategyDestroySpy).toHaveBeenCalled();
+        });
     });
 
-    it('runs destroy', async () => {
-        await server.destroy();
-        expect(destroySpy).toHaveBeenCalled();
+    describe('configurable operation', () => {
+        beforeAll(async () => {
+            await server.bootstrap();
+        });
+
+        it('runs init with Injector', () => {
+            expect(codInitSpy).toHaveBeenCalled();
+            expect(codInitSpy.mock.calls[0][0]).toEqual('ProductService');
+            expect(codInitSpy.mock.calls[0][1]).toBe('default');
+        });
+
+        it('runs destroy', async () => {
+            await server.destroy();
+            expect(codDestroySpy).toHaveBeenCalled();
+        });
     });
 });

+ 30 - 0
packages/core/src/app.module.ts

@@ -11,6 +11,7 @@ import cookieSession = require('cookie-session');
 import { RequestHandler } from 'express';
 
 import { ApiModule } from './api/api.module';
+import { ConfigurableOperationDef } from './common/configurable-operation';
 import { Injector } from './common/injector';
 import { InjectableStrategy } from './common/types/injectable-strategy';
 import { ConfigModule } from './config/config.module';
@@ -33,6 +34,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
 
     async onApplicationBootstrap() {
         await this.initInjectableStrategies();
+        await this.initConfigurableOperations();
     }
 
     configure(consumer: MiddlewareConsumer) {
@@ -60,6 +62,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
 
     async onApplicationShutdown(signal?: string) {
         await this.destroyInjectableStrategies();
+        await this.destroyConfigurableOperations();
         if (signal) {
             Logger.info('Received shutdown signal:' + signal);
         }
@@ -99,6 +102,19 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
         }
     }
 
+    private async initConfigurableOperations() {
+        const injector = new Injector(this.moduleRef);
+        for (const operation of this.getConfigurableOperations()) {
+            await operation.init(injector);
+        }
+    }
+
+    private async destroyConfigurableOperations() {
+        for (const operation of this.getConfigurableOperations()) {
+            await operation.destroy();
+        }
+    }
+
     private getInjectableStrategies(): InjectableStrategy[] {
         const {
             assetNamingStrategy,
@@ -120,4 +136,18 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
             entityIdStrategy,
         ];
     }
+
+    private getConfigurableOperations(): Array<ConfigurableOperationDef<any>> {
+        const { paymentMethodHandlers } = this.configService.paymentOptions;
+        // TODO: add CollectionFilters once #325 is fixed
+        const { promotionActions, promotionConditions } = this.configService.promotionOptions;
+        const { shippingCalculators, shippingEligibilityCheckers } = this.configService.shippingOptions;
+        return [
+            ...paymentMethodHandlers,
+            ...(promotionActions || []),
+            ...(promotionConditions || []),
+            ...(shippingCalculators || []),
+            ...(shippingEligibilityCheckers || []),
+        ];
+    }
 }

+ 63 - 5
packages/core/src/common/configurable-operation.ts

@@ -15,6 +15,8 @@ import { RequestContext } from '../api/common/request-context';
 
 import { DEFAULT_LANGUAGE_CODE } from './constants';
 import { InternalServerError } from './error/errors';
+import { Injector } from './injector';
+import { InjectableStrategy } from './types/injectable-strategy';
 
 /**
  * @description
@@ -106,25 +108,81 @@ export type ConfigArgValues<T extends ConfigArgs<any>> = {
 
 /**
  * @description
- * Defines a ConfigurableOperation, which is a method which can be configured
- * by the Administrator via the Admin API.
+ * Common configuration options used when creating a new instance of a
+ * {@link ConfigurableOperationDef}.
  *
  * @docsCategory common
  * @docsPage Configurable Operations
  */
-export interface ConfigurableOperationDef {
+export interface ConfigurableOperationDefOptions<T extends ConfigArgs<ConfigArgType>>
+    extends InjectableStrategy {
+    /**
+     * @description
+     * A unique code used to identify this operation.
+     */
     code: string;
-    args: ConfigArgs<any>;
+    /**
+     * @description
+     * Optional provider-specific arguments which, when specified, are
+     * editable in the admin-ui. For example, args could be used to store an API key
+     * for a payment provider service.
+     *
+     * @example
+     * ```ts
+     * args: {
+     *   apiKey: { type: 'string' },
+     * }
+     * ```
+     *
+     * See {@link ConfigArgs} for available configuration options.
+     */
+    args: T;
+    /**
+     * @description
+     * A human-readable description for the operation method.
+     */
     description: LocalizedStringArray;
 }
 
+/**
+ * @description
+ * Defines a ConfigurableOperation, which is a method which can be configured
+ * by the Administrator via the Admin API.
+ *
+ * @docsCategory common
+ * @docsPage Configurable Operations
+ */
+export class ConfigurableOperationDef<T extends ConfigArgs<ConfigArgType>> {
+    get code(): string {
+        return this.options.code;
+    }
+    get args(): T {
+        return this.options.args;
+    }
+    get description(): LocalizedStringArray {
+        return this.options.description;
+    }
+    constructor(protected options: ConfigurableOperationDefOptions<T>) {}
+
+    async init(injector: Injector) {
+        if (typeof this.options.init === 'function') {
+            await this.options.init(injector);
+        }
+    }
+    async destroy() {
+        if (typeof this.options.destroy === 'function') {
+            await this.options.destroy();
+        }
+    }
+}
+
 /**
  * Convert a ConfigurableOperationDef into a ConfigurableOperation object, typically
  * so that it can be sent via the API.
  */
 export function configurableDefToOperation(
     ctx: RequestContext,
-    def: ConfigurableOperationDef,
+    def: ConfigurableOperationDef<ConfigArgs<any>>,
 ): ConfigurableOperationDefinition {
     return {
         code: def.code,

+ 6 - 13
packages/core/src/config/collection/collection-filter.ts

@@ -1,5 +1,5 @@
 import { ConfigArg } from '@vendure/common/lib/generated-types';
-import { ConfigArgSubset, ConfigArgType } from '@vendure/common/lib/shared-types';
+import { ConfigArgSubset } from '@vendure/common/lib/shared-types';
 import { SelectQueryBuilder } from 'typeorm';
 
 import {
@@ -7,7 +7,7 @@ import {
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
-    LocalizedStringArray,
+    ConfigurableOperationDefOptions,
 } from '../../common/configurable-operation';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 
@@ -19,23 +19,16 @@ export type ApplyCollectionFilterFn<T extends CollectionFilterArgs> = (
     args: ConfigArgValues<T>,
 ) => SelectQueryBuilder<ProductVariant>;
 
-export interface CollectionFilterConfig<T extends CollectionFilterArgs> {
-    args: T;
-    code: string;
-    description: LocalizedStringArray;
+export interface CollectionFilterConfig<T extends CollectionFilterArgs>
+    extends ConfigurableOperationDefOptions<T> {
     apply: ApplyCollectionFilterFn<T>;
 }
 
-export class CollectionFilter<T extends CollectionFilterArgs = {}> implements ConfigurableOperationDef {
-    readonly code: string;
-    readonly args: CollectionFilterArgs;
-    readonly description: LocalizedStringArray;
+export class CollectionFilter<T extends CollectionFilterArgs = {}> extends ConfigurableOperationDef<T> {
     private readonly applyFn: ApplyCollectionFilterFn<T>;
 
     constructor(config: CollectionFilterConfig<T>) {
-        this.code = config.code;
-        this.description = config.description;
-        this.args = config.args;
+        super(config);
         this.applyFn = config.apply;
     }
 

+ 1 - 1
packages/core/src/config/index.ts

@@ -16,7 +16,7 @@ export * from './logger/noop-logger';
 export * from './logger/vendure-logger';
 export * from './merge-config';
 export * from './order-merge-strategy/order-merge-strategy';
-export * from './payment-method/example-payment-method-config';
+export * from './payment-method/example-payment-method-handler';
 export * from './payment-method/payment-method-handler';
 export * from './promotion/default-promotion-actions';
 export * from './promotion/default-promotion-conditions';

+ 0 - 0
packages/core/src/config/payment-method/example-payment-method-config.ts → packages/core/src/config/payment-method/example-payment-method-handler.ts


+ 7 - 38
packages/core/src/config/payment-method/payment-method-handler.ts

@@ -6,6 +6,7 @@ import {
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
+    ConfigurableOperationDefOptions,
     LocalizedStringArray,
 } from '../../common/configurable-operation';
 import { StateMachineConfig } from '../../common/finite-state-machine';
@@ -141,17 +142,8 @@ export type CreateRefundFn<T extends PaymentMethodArgs> = (
  *
  * @docsCategory payment
  */
-export interface PaymentMethodConfigOptions<T extends PaymentMethodArgs = PaymentMethodArgs> {
-    /**
-     * @description
-     * A unique code used to identify this handler.
-     */
-    code: string;
-    /**
-     * @description
-     * A human-readable description for the payment method.
-     */
-    description: LocalizedStringArray;
+export interface PaymentMethodConfigOptions<T extends PaymentMethodArgs = PaymentMethodArgs>
+    extends ConfigurableOperationDefOptions<T> {
     /**
      * @description
      * This function provides the logic for creating a payment. For example,
@@ -175,22 +167,6 @@ export interface PaymentMethodConfigOptions<T extends PaymentMethodArgs = Paymen
      * omitted and any Refunds will have to be settled manually by an administrator.
      */
     createRefund?: CreateRefundFn<T>;
-    /**
-     * @description
-     * Optional provider-specific arguments which, when specified, are
-     * editable in the admin-ui. For example, args could be used to store an API key
-     * for a payment provider service.
-     *
-     * @example
-     * ```ts
-     * args: {
-     *   apiKey: { type: 'string' },
-     * }
-     * ```
-     *
-     * See {@link ConfigArgs} for available configuration options.
-     */
-    args: T;
     /**
      * @description
      * This function, when specified, will be invoked before any transition from one {@link PaymentState} to another.
@@ -251,23 +227,16 @@ export interface PaymentMethodConfigOptions<T extends PaymentMethodArgs = Paymen
  *
  * @docsCategory payment
  */
-export class PaymentMethodHandler<T extends PaymentMethodArgs = PaymentMethodArgs>
-    implements ConfigurableOperationDef {
-    /** @internal */
-    readonly code: string;
-    /** @internal */
-    readonly description: LocalizedStringArray;
-    /** @internal */
-    readonly args: T;
+export class PaymentMethodHandler<
+    T extends PaymentMethodArgs = PaymentMethodArgs
+> extends ConfigurableOperationDef<T> {
     private readonly createPaymentFn: CreatePaymentFn<T>;
     private readonly settlePaymentFn: SettlePaymentFn<T>;
     private readonly createRefundFn?: CreateRefundFn<T>;
     private readonly onTransitionStartFn?: OnTransitionStartFn<T>;
 
     constructor(config: PaymentMethodConfigOptions<T>) {
-        this.code = config.code;
-        this.description = config.description;
-        this.args = config.args;
+        super(config);
         this.createPaymentFn = config.createPayment;
         this.settlePaymentFn = config.settlePayment;
         this.settlePaymentFn = config.settlePayment;

+ 7 - 13
packages/core/src/config/promotion/promotion-action.ts

@@ -6,7 +6,7 @@ import {
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
-    LocalizedStringArray,
+    ConfigurableOperationDefOptions,
 } from '../../common/configurable-operation';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
@@ -29,10 +29,8 @@ export type ExecutePromotionOrderActionFn<T extends PromotionActionArgs> = (
     utils: PromotionUtils,
 ) => number | Promise<number>;
 
-export interface PromotionActionConfig<T extends PromotionActionArgs> {
-    args: T;
-    code: string;
-    description: LocalizedStringArray;
+export interface PromotionActionConfig<T extends PromotionActionArgs>
+    extends ConfigurableOperationDefOptions<T> {
     priorityValue?: number;
 }
 export interface PromotionItemActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
@@ -48,17 +46,13 @@ export interface PromotionOrderActionConfig<T extends PromotionActionArgs> exten
  *
  * @docsCategory promotions
  */
-export abstract class PromotionAction<T extends PromotionActionArgs = {}>
-    implements ConfigurableOperationDef {
-    readonly code: string;
-    readonly args: PromotionActionArgs;
-    readonly description: LocalizedStringArray;
+export abstract class PromotionAction<T extends PromotionActionArgs = {}> extends ConfigurableOperationDef<
+    T
+> {
     readonly priorityValue: number;
 
     protected constructor(config: PromotionActionConfig<T>) {
-        this.code = config.code;
-        this.description = config.description;
-        this.args = config.args;
+        super(config);
         this.priorityValue = config.priorityValue || 0;
     }
 }

+ 12 - 14
packages/core/src/config/promotion/promotion-condition.ts

@@ -6,6 +6,7 @@ import {
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
+    ConfigurableOperationDefOptions,
     LocalizedStringArray,
 } from '../../common/configurable-operation';
 import { OrderLine } from '../../entity';
@@ -21,6 +22,8 @@ export type PromotionConditionArgs = ConfigArgs<PromotionConditionArgType>;
  * An object containing utility methods which may be used in promotion `check` functions
  * in order to determine whether a promotion should be applied.
  *
+ * TODO: Remove this and use the new init() method to inject providers where needed.
+ *
  * @docsCategory promotions
  */
 export interface PromotionUtils {
@@ -45,6 +48,12 @@ export type CheckPromotionConditionFn<T extends PromotionConditionArgs> = (
     utils: PromotionUtils,
 ) => boolean | Promise<boolean>;
 
+export interface PromotionConditionConfig<T extends PromotionConditionArgs>
+    extends ConfigurableOperationDefOptions<T> {
+    check: CheckPromotionConditionFn<T>;
+    priorityValue?: number;
+}
+
 /**
  * @description
  * PromotionConditions are used to create {@link Promotion}s. The purpose of a PromotionCondition
@@ -53,23 +62,12 @@ export type CheckPromotionConditionFn<T extends PromotionConditionArgs> = (
  *
  * @docsCategory promotions
  */
-export class PromotionCondition<T extends PromotionConditionArgs = {}> implements ConfigurableOperationDef {
-    readonly code: string;
-    readonly description: LocalizedStringArray;
-    readonly args: PromotionConditionArgs;
+export class PromotionCondition<T extends PromotionConditionArgs = {}> extends ConfigurableOperationDef<T> {
     readonly priorityValue: number;
     private readonly checkFn: CheckPromotionConditionFn<T>;
 
-    constructor(config: {
-        args: T;
-        check: CheckPromotionConditionFn<T>;
-        code: string;
-        description: LocalizedStringArray;
-        priorityValue?: number;
-    }) {
-        this.code = config.code;
-        this.description = config.description;
-        this.args = config.args;
+    constructor(config: PromotionConditionConfig<T>) {
+        super(config);
         this.checkFn = config.check;
         this.priorityValue = config.priorityValue || 0;
     }

+ 9 - 17
packages/core/src/config/shipping-method/shipping-calculator.ts

@@ -6,13 +6,18 @@ import {
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
-    LocalizedStringArray,
+    ConfigurableOperationDefOptions,
 } from '../../common/configurable-operation';
 import { Order } from '../../entity/order/order.entity';
 
 export type ShippingCalculatorArgType = ConfigArgSubset<'int' | 'float' | 'string' | 'boolean'>;
 export type ShippingCalculatorArgs = ConfigArgs<ShippingCalculatorArgType>;
 
+export interface ShippingCalculatorConfig<T extends ShippingCalculatorArgs>
+    extends ConfigurableOperationDefOptions<T> {
+    calculate: CalculateShippingFn<T>;
+}
+
 /**
  * @description
  * The ShippingCalculator is used by a {@link ShippingMethod} to calculate the price of shipping on a given {@link Order}.
@@ -37,24 +42,11 @@ export type ShippingCalculatorArgs = ConfigArgs<ShippingCalculatorArgType>;
  * @docsCategory shipping
  * @docsPage ShippingCalculator
  */
-export class ShippingCalculator<T extends ShippingCalculatorArgs = {}> implements ConfigurableOperationDef {
-    /** @internal */
-    readonly code: string;
-    /** @internal */
-    readonly description: LocalizedStringArray;
-    /** @internal */
-    readonly args: ShippingCalculatorArgs;
+export class ShippingCalculator<T extends ShippingCalculatorArgs = {}> extends ConfigurableOperationDef<T> {
     private readonly calculateFn: CalculateShippingFn<T>;
 
-    constructor(config: {
-        args: T;
-        calculate: CalculateShippingFn<T>;
-        code: string;
-        description: LocalizedStringArray;
-    }) {
-        this.code = config.code;
-        this.description = config.description;
-        this.args = config.args;
+    constructor(config: ShippingCalculatorConfig<T>) {
+        super(config);
         this.calculateFn = config.calculate;
     }
 

+ 10 - 17
packages/core/src/config/shipping-method/shipping-eligibility-checker.ts

@@ -4,6 +4,7 @@ import { ConfigArgSubset } from '@vendure/common/lib/shared-types';
 import {
     ConfigArgs,
     ConfigurableOperationDef,
+    ConfigurableOperationDefOptions,
     LocalizedStringArray,
 } from '../../common/configurable-operation';
 import { argsArrayToHash, ConfigArgValues } from '../../common/configurable-operation';
@@ -12,6 +13,10 @@ import { Order } from '../../entity/order/order.entity';
 export type ShippingEligibilityCheckerArgType = ConfigArgSubset<'int' | 'float' | 'string' | 'boolean'>;
 export type ShippingEligibilityCheckerArgs = ConfigArgs<ShippingEligibilityCheckerArgType>;
 
+export interface ShippingEligibilityCheckerConfig<T extends ShippingEligibilityCheckerArgs>
+    extends ConfigurableOperationDefOptions<T> {
+    check: CheckShippingEligibilityCheckerFn<T>;
+}
 /**
  * @description
  * The ShippingEligibilityChecker class is used to check whether an order qualifies for a
@@ -34,25 +39,13 @@ export type ShippingEligibilityCheckerArgs = ConfigArgs<ShippingEligibilityCheck
  * @docsCategory shipping
  * @docsPage ShippingEligibilityChecker
  */
-export class ShippingEligibilityChecker<T extends ShippingEligibilityCheckerArgs = {}>
-    implements ConfigurableOperationDef {
-    /** @internal */
-    readonly code: string;
-    /** @internal */
-    readonly description: LocalizedStringArray;
-    /** @internal */
-    readonly args: ShippingEligibilityCheckerArgs;
+export class ShippingEligibilityChecker<
+    T extends ShippingEligibilityCheckerArgs = {}
+> extends ConfigurableOperationDef<T> {
     private readonly checkFn: CheckShippingEligibilityCheckerFn<T>;
 
-    constructor(config: {
-        args: T;
-        check: CheckShippingEligibilityCheckerFn<T>;
-        code: string;
-        description: LocalizedStringArray;
-    }) {
-        this.code = config.code;
-        this.description = config.description;
-        this.args = config.args;
+    constructor(config: ShippingEligibilityCheckerConfig<T>) {
+        super(config);
         this.checkFn = config.check;
     }