From 7ec309b899de9b59c40649f594e7d93fc62cd9fe Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 22 Jul 2019 17:28:23 +0200 Subject: [PATCH] feat(core): Rewrite plugin system to use Nest modules Relates to #123 BREAKING CHANGE: Vendure plugins are now defined as Nestjs modules. For existing installations, the VendureConfig will need to be modified so that plugins are not instantiated, but use the static .init() method to pass options to the plugin, e.g.: ``` // before plugins: [ new AdminUiPlugin({ port: 3002 }) ], // after plugins: [ AdminUiPlugin.init({ port: 3002 }) ], ``` --- .../e2e/default-search-plugin.e2e-spec.ts | 302 +++++++++++------- packages/core/e2e/fixtures/test-plugins.ts | 128 +++++--- packages/core/e2e/plugin.e2e-spec.ts | 43 ++- packages/core/e2e/shop-auth.e2e-spec.ts | 29 +- packages/core/e2e/test-server.ts | 9 +- packages/core/src/api/api-internal-modules.ts | 19 +- .../api/config/configure-graphql-module.ts | 18 +- packages/core/src/app.module.ts | 15 +- packages/core/src/bootstrap.ts | 48 +-- packages/core/src/config/config.service.ts | 5 +- packages/core/src/config/vendure-config.ts | 4 +- packages/core/src/event-bus/index.ts | 1 + .../plugin/default-search-plugin/constants.ts | 2 +- .../default-search-plugin.ts | 87 ++--- .../indexer/indexer.controller.ts | 83 +++-- .../src/plugin/dynamic-plugin-api.module.ts | 53 +++ packages/core/src/plugin/index.ts | 2 + .../core/src/plugin/plugin-common.module.ts | 32 ++ packages/core/src/plugin/plugin-metadata.ts | 90 ++++++ packages/core/src/plugin/plugin-utils.ts | 27 +- packages/core/src/plugin/plugin.module.ts | 161 ++++++---- packages/core/src/plugin/vendure-plugin.ts | 120 +++---- packages/core/src/service/service.module.ts | 16 +- packages/core/src/worker/worker.module.ts | 17 +- 24 files changed, 800 insertions(+), 511 deletions(-) create mode 100644 packages/core/src/plugin/dynamic-plugin-api.module.ts create mode 100644 packages/core/src/plugin/plugin-common.module.ts create mode 100644 packages/core/src/plugin/plugin-metadata.ts diff --git a/packages/core/e2e/default-search-plugin.e2e-spec.ts b/packages/core/e2e/default-search-plugin.e2e-spec.ts index ff342f0f12..e4b5c67839 100644 --- a/packages/core/e2e/default-search-plugin.e2e-spec.ts +++ b/packages/core/e2e/default-search-plugin.e2e-spec.ts @@ -47,7 +47,7 @@ describe('Default search plugin', () => { customerCount: 1, }, { - plugins: [new DefaultSearchPlugin()], + plugins: [DefaultSearchPlugin], }, ); await adminClient.init(); @@ -59,30 +59,39 @@ describe('Default search plugin', () => { }); async function testGroupByProduct(client: SimpleGraphQLClient) { - const result = await client.query(SEARCH_PRODUCTS_SHOP, { - input: { - groupByProduct: true, + const result = await client.query( + SEARCH_PRODUCTS_SHOP, + { + input: { + groupByProduct: true, + }, }, - }); + ); expect(result.search.totalItems).toBe(20); } async function testNoGrouping(client: SimpleGraphQLClient) { - const result = await client.query(SEARCH_PRODUCTS_SHOP, { - input: { - groupByProduct: false, + const result = await client.query( + SEARCH_PRODUCTS_SHOP, + { + input: { + groupByProduct: false, + }, }, - }); + ); expect(result.search.totalItems).toBe(34); } async function testMatchSearchTerm(client: SimpleGraphQLClient) { - const result = await client.query(SEARCH_PRODUCTS_SHOP, { - input: { - term: 'camera', - groupByProduct: true, + const result = await client.query( + SEARCH_PRODUCTS_SHOP, + { + input: { + term: 'camera', + groupByProduct: true, + }, }, - }); + ); expect(result.search.items.map(i => i.productName)).toEqual([ 'Instant Camera', 'Camera Lens', @@ -91,12 +100,15 @@ describe('Default search plugin', () => { } async function testMatchFacetIds(client: SimpleGraphQLClient) { - const result = await client.query(SEARCH_PRODUCTS_SHOP, { - input: { - facetValueIds: ['T_1', 'T_2'], - groupByProduct: true, + const result = await client.query( + SEARCH_PRODUCTS_SHOP, + { + input: { + facetValueIds: ['T_1', 'T_2'], + groupByProduct: true, + }, }, - }); + ); expect(result.search.items.map(i => i.productName)).toEqual([ 'Laptop', 'Curvy Monitor', @@ -108,12 +120,15 @@ describe('Default search plugin', () => { } async function testMatchCollectionId(client: SimpleGraphQLClient) { - const result = await client.query(SEARCH_PRODUCTS_SHOP, { - input: { - collectionId: 'T_2', - groupByProduct: true, + const result = await client.query( + SEARCH_PRODUCTS_SHOP, + { + input: { + collectionId: 'T_2', + groupByProduct: true, + }, }, - }); + ); expect(result.search.items.map(i => i.productName)).toEqual([ 'Spiky Cactus', 'Orchid', @@ -122,12 +137,15 @@ describe('Default search plugin', () => { } async function testSinglePrices(client: SimpleGraphQLClient) { - const result = await client.query(SEARCH_GET_PRICES, { - input: { - groupByProduct: false, - take: 3, - } as SearchInput, - }); + const result = await client.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: false, + take: 3, + } as SearchInput, + }, + ); expect(result.search.items).toEqual([ { price: { value: 129900 }, @@ -145,12 +163,15 @@ describe('Default search plugin', () => { } async function testPriceRanges(client: SimpleGraphQLClient) { - const result = await client.query(SEARCH_GET_PRICES, { - input: { - groupByProduct: true, - take: 3, - } as SearchInput, - }); + const result = await client.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: true, + take: 3, + } as SearchInput, + }, + ); expect(result.search.items).toEqual([ { price: { min: 129900, max: 229900 }, @@ -183,11 +204,14 @@ describe('Default search plugin', () => { it('price ranges', () => testPriceRanges(shopClient)); it('returns correct facetValues when not grouped by product', async () => { - const result = await shopClient.query(SEARCH_GET_FACET_VALUES, { - input: { - groupByProduct: false, + const result = await shopClient.query( + SEARCH_GET_FACET_VALUES, + { + input: { + groupByProduct: false, + }, }, - }); + ); expect(result.search.facetValues).toEqual([ { count: 21, facetValue: { id: 'T_1', name: 'electronics' } }, { count: 17, facetValue: { id: 'T_2', name: 'computers' } }, @@ -199,11 +223,14 @@ describe('Default search plugin', () => { }); it('returns correct facetValues when grouped by product', async () => { - const result = await shopClient.query(SEARCH_GET_FACET_VALUES, { - input: { - groupByProduct: true, + const result = await shopClient.query( + SEARCH_GET_FACET_VALUES, + { + input: { + groupByProduct: true, + }, }, - }); + ); expect(result.search.facetValues).toEqual([ { count: 10, facetValue: { id: 'T_1', name: 'electronics' } }, { count: 6, facetValue: { id: 'T_2', name: 'computers' } }, @@ -215,18 +242,22 @@ describe('Default search plugin', () => { }); it('omits facetValues of private facets', async () => { - const { createFacet } = await adminClient.query(CREATE_FACET, { - input: { - code: 'profit-margin', - isPrivate: true, - translations: [ - { languageCode: LanguageCode.en, name: 'Profit Margin' }, - ], - values: [ - { code: 'massive', translations: [{ languageCode: LanguageCode.en, name: 'massive' }] }, - ], + const { createFacet } = await adminClient.query( + CREATE_FACET, + { + input: { + code: 'profit-margin', + isPrivate: true, + translations: [{ languageCode: LanguageCode.en, name: 'Profit Margin' }], + values: [ + { + code: 'massive', + translations: [{ languageCode: LanguageCode.en, name: 'massive' }], + }, + ], + }, }, - }); + ); await adminClient.query(UPDATE_PRODUCT, { input: { id: 'T_2', @@ -235,11 +266,14 @@ describe('Default search plugin', () => { }, }); - const result = await shopClient.query(SEARCH_GET_FACET_VALUES, { - input: { - groupByProduct: true, + const result = await shopClient.query( + SEARCH_GET_FACET_VALUES, + { + input: { + groupByProduct: true, + }, }, - }); + ); expect(result.search.facetValues).toEqual([ { count: 10, facetValue: { id: 'T_1', name: 'electronics' } }, { count: 6, facetValue: { id: 'T_2', name: 'computers' } }, @@ -251,44 +285,52 @@ describe('Default search plugin', () => { }); it('encodes the productId and productVariantId', async () => { - const result = await shopClient.query(SEARCH_PRODUCTS_SHOP, { - input: { - groupByProduct: false, - take: 1, - }, - }); - expect(pick(result.search.items[0], ['productId', 'productVariantId'])).toEqual( + const result = await shopClient.query( + SEARCH_PRODUCTS_SHOP, { - productId: 'T_1', - productVariantId: 'T_1', + input: { + groupByProduct: false, + take: 1, + }, }, ); + expect(pick(result.search.items[0], ['productId', 'productVariantId'])).toEqual({ + productId: 'T_1', + productVariantId: 'T_1', + }); }); it('omits results for disabled ProductVariants', async () => { - await adminClient.query(UPDATE_PRODUCT_VARIANTS, { - input: [ - { id: 'T_3', enabled: false }, - ], - }); + await adminClient.query( + UPDATE_PRODUCT_VARIANTS, + { + input: [{ id: 'T_3', enabled: false }], + }, + ); await awaitRunningJobs(); - const result = await shopClient.query(SEARCH_PRODUCTS_SHOP, { - input: { - groupByProduct: false, - take: 3, + const result = await shopClient.query( + SEARCH_PRODUCTS_SHOP, + { + input: { + groupByProduct: false, + take: 3, + }, }, - }); + ); expect(result.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_4']); }); it('encodes collectionIds', async () => { - const result = await shopClient.query(SEARCH_PRODUCTS_SHOP, { - input: { - groupByProduct: false, - term: 'cactus', - take: 1, + const result = await shopClient.query( + SEARCH_PRODUCTS_SHOP, + { + input: { + groupByProduct: false, + term: 'cactus', + take: 1, + }, }, - }); + ); expect(result.search.items[0].collectionIds).toEqual(['T_2']); }); @@ -386,7 +428,7 @@ describe('Default search plugin', () => { const { createCollection } = await adminClient.query< CreateCollection.Mutation, CreateCollection.Variables - >(CREATE_COLLECTION, { + >(CREATE_COLLECTION, { input: { translations: [ { @@ -442,12 +484,15 @@ describe('Default search plugin', () => { }, }); await awaitRunningJobs(); - const result = await adminClient.query(SEARCH_GET_PRICES, { - input: { - groupByProduct: true, - term: 'laptop', - } as SearchInput, - }); + const result = await adminClient.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: true, + term: 'laptop', + } as SearchInput, + }, + ); expect(result.search.items).toEqual([ { price: { min: 129900, max: 229900 }, @@ -457,12 +502,15 @@ describe('Default search plugin', () => { }); it('returns disabled field when not grouped', async () => { - const result = await adminClient.query(SEARCH_PRODUCTS, { - input: { - groupByProduct: false, - take: 3, + const result = await adminClient.query( + SEARCH_PRODUCTS, + { + input: { + groupByProduct: false, + take: 3, + }, }, - }); + ); expect(result.search.items.map(pick(['productVariantId', 'enabled']))).toEqual([ { productVariantId: 'T_1', enabled: true }, { productVariantId: 'T_2', enabled: true }, @@ -471,19 +519,22 @@ describe('Default search plugin', () => { }); it('when grouped, disabled is false if at least one variant is enabled', async () => { - await adminClient.query(UPDATE_PRODUCT_VARIANTS, { - input: [ - { id: 'T_1', enabled: false }, - { id: 'T_2', enabled: false }, - ], - }); + await adminClient.query( + UPDATE_PRODUCT_VARIANTS, + { + input: [{ id: 'T_1', enabled: false }, { id: 'T_2', enabled: false }], + }, + ); await awaitRunningJobs(); - const result = await adminClient.query(SEARCH_PRODUCTS, { - input: { - groupByProduct: true, - take: 3, + const result = await adminClient.query( + SEARCH_PRODUCTS, + { + input: { + groupByProduct: true, + take: 3, + }, }, - }); + ); expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([ { productId: 'T_1', enabled: true }, { productId: 'T_2', enabled: true }, @@ -492,18 +543,22 @@ describe('Default search plugin', () => { }); it('when grouped, disabled is true if all variants are disabled', async () => { - await adminClient.query(UPDATE_PRODUCT_VARIANTS, { - input: [ - { id: 'T_4', enabled: false }, - ], - }); + await adminClient.query( + UPDATE_PRODUCT_VARIANTS, + { + input: [{ id: 'T_4', enabled: false }], + }, + ); await awaitRunningJobs(); - const result = await adminClient.query(SEARCH_PRODUCTS, { - input: { - groupByProduct: true, - take: 3, + const result = await adminClient.query( + SEARCH_PRODUCTS, + { + input: { + groupByProduct: true, + take: 3, + }, }, - }); + ); expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([ { productId: 'T_1', enabled: false }, { productId: 'T_2', enabled: true }, @@ -519,12 +574,15 @@ describe('Default search plugin', () => { }, }); await awaitRunningJobs(); - const result = await adminClient.query(SEARCH_PRODUCTS, { - input: { - groupByProduct: true, - take: 3, + const result = await adminClient.query( + SEARCH_PRODUCTS, + { + input: { + groupByProduct: true, + take: 3, + }, }, - }); + ); expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([ { productId: 'T_1', enabled: false }, { productId: 'T_2', enabled: true }, diff --git a/packages/core/e2e/fixtures/test-plugins.ts b/packages/core/e2e/fixtures/test-plugins.ts index 98874dad37..83d0f6e28f 100644 --- a/packages/core/e2e/fixtures/test-plugins.ts +++ b/packages/core/e2e/fixtures/test-plugins.ts @@ -1,31 +1,43 @@ +import { Injectable, OnApplicationBootstrap, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { Query, Resolver } from '@nestjs/graphql'; import { LanguageCode } from '@vendure/common/lib/generated-types'; import gql from 'graphql-tag'; -import { APIExtensionDefinition, InjectorFn, VendureConfig, VendurePlugin } from '../../src/config'; +import { VendureConfig } from '../../src/config'; +import { ConfigModule } from '../../src/config/config.module'; import { ConfigService } from '../../src/config/config.service'; +import { + OnVendureBootstrap, + OnVendureClose, + OnVendureWorkerBootstrap, + OnVendureWorkerClose, + VendurePlugin, +} from '../../src/plugin/vendure-plugin'; -export class TestAPIExtensionPlugin implements VendurePlugin { - extendShopAPI(): APIExtensionDefinition { - return { - resolvers: [TestShopPluginResolver], - schema: gql` - extend type Query { - baz: [String]! - } - `, - }; +export class TestPluginWithAllLifecycleHooks + implements OnVendureBootstrap, OnVendureWorkerBootstrap, OnVendureClose, OnVendureWorkerClose { + private static onBootstrapFn: any; + private static onWorkerBootstrapFn: any; + private static onCloseFn: any; + private static onWorkerCloseFn: any; + static init(bootstrapFn: any, workerBootstrapFn: any, closeFn: any, workerCloseFn: any) { + this.onBootstrapFn = bootstrapFn; + this.onWorkerBootstrapFn = workerBootstrapFn; + this.onCloseFn = closeFn; + this.onWorkerCloseFn = workerCloseFn; + return this; } - - extendAdminAPI(): APIExtensionDefinition { - return { - resolvers: [TestAdminPluginResolver], - schema: gql` - extend type Query { - foo: [String]! - } - `, - }; + onVendureBootstrap(): void | Promise { + TestPluginWithAllLifecycleHooks.onBootstrapFn(); + } + onVendureWorkerBootstrap(): void | Promise { + TestPluginWithAllLifecycleHooks.onWorkerBootstrapFn(); + } + onVendureClose(): void | Promise { + TestPluginWithAllLifecycleHooks.onCloseFn(); + } + onVendureWorkerClose(): void | Promise { + TestPluginWithAllLifecycleHooks.onWorkerCloseFn(); } } @@ -45,23 +57,27 @@ export class TestShopPluginResolver { } } -export class TestPluginWithProvider implements VendurePlugin { - extendShopAPI(): APIExtensionDefinition { - return { - resolvers: [TestResolverWithInjection], - schema: gql` - extend type Query { - names: [String]! - } - `, - }; - } - - defineProviders() { - return [NameService]; - } -} +@VendurePlugin({ + shopApiExtensions: { + resolvers: [TestShopPluginResolver], + schema: gql` + extend type Query { + baz: [String]! + } + `, + }, + adminApiExtensions: { + resolvers: [TestAdminPluginResolver], + schema: gql` + extend type Query { + foo: [String]! + } + `, + }, +}) +export class TestAPIExtensionPlugin {} +@Injectable() export class NameService { getNames(): string[] { return ['seon', 'linda', 'hong']; @@ -78,24 +94,36 @@ export class TestResolverWithInjection { } } -export class TestPluginWithConfigAndBootstrap implements VendurePlugin { - constructor(private boostrapWasCalled: (arg: any) => void) {} +@VendurePlugin({ + providers: [NameService], + shopApiExtensions: { + resolvers: [TestResolverWithInjection], + schema: gql` + extend type Query { + names: [String]! + } + `, + }, +}) +export class TestPluginWithProvider {} - configure(config: Required): Required { +@VendurePlugin({ + imports: [ConfigModule], + configuration(config: Required): Required { // tslint:disable-next-line:no-non-null-assertion config.defaultLanguageCode = LanguageCode.zh; return config; + }, +}) +export class TestPluginWithConfigAndBootstrap implements OnVendureBootstrap { + private static boostrapWasCalled: any; + static setup(boostrapWasCalled: (arg: any) => void) { + TestPluginWithConfigAndBootstrap.boostrapWasCalled = boostrapWasCalled; + return TestPluginWithConfigAndBootstrap; } + constructor(private configService: ConfigService) {} - onBootstrap(inject: InjectorFn) { - const configService = inject(ConfigService); - this.boostrapWasCalled(configService); - } -} - -export class TestPluginWithOnClose implements VendurePlugin { - constructor(private onCloseCallback: () => void) {} - onClose() { - this.onCloseCallback(); + onVendureBootstrap() { + TestPluginWithConfigAndBootstrap.boostrapWasCalled(this.configService); } } diff --git a/packages/core/e2e/plugin.e2e-spec.ts b/packages/core/e2e/plugin.e2e-spec.ts index ff1a346de0..23919da0f0 100644 --- a/packages/core/e2e/plugin.e2e-spec.ts +++ b/packages/core/e2e/plugin.e2e-spec.ts @@ -7,8 +7,8 @@ import { ConfigService } from '../src/config/config.service'; import { TEST_SETUP_TIMEOUT_MS } from './config/test-config'; import { TestAPIExtensionPlugin, + TestPluginWithAllLifecycleHooks, TestPluginWithConfigAndBootstrap, - TestPluginWithOnClose, TestPluginWithProvider, } from './fixtures/test-plugins'; import { TestAdminClient, TestShopClient } from './test-client'; @@ -19,7 +19,10 @@ describe('Plugins', () => { const shopClient = new TestShopClient(); const server = new TestServer(); const bootstrapMockFn = jest.fn(); + const onBootstrapFn = jest.fn(); + const onWorkerBootstrapFn = jest.fn(); const onCloseFn = jest.fn(); + const onWorkerCloseFn = jest.fn(); beforeAll(async () => { const token = await server.init( @@ -29,10 +32,15 @@ describe('Plugins', () => { }, { plugins: [ - new TestPluginWithConfigAndBootstrap(bootstrapMockFn), - new TestAPIExtensionPlugin(), - new TestPluginWithProvider(), - new TestPluginWithOnClose(onCloseFn), + TestPluginWithAllLifecycleHooks.init( + onBootstrapFn, + onWorkerBootstrapFn, + onCloseFn, + onWorkerCloseFn, + ), + TestPluginWithConfigAndBootstrap.setup(bootstrapMockFn), + TestAPIExtensionPlugin, + TestPluginWithProvider, ], }, ); @@ -44,7 +52,15 @@ describe('Plugins', () => { await server.destroy(); }); - it('can modify the config in configure() and inject in onBootstrap()', () => { + it('calls onVendureBootstrap once only', () => { + expect(onBootstrapFn.mock.calls.length).toBe(1); + }); + + it('calls onWorkerVendureBootstrap once only', () => { + expect(onWorkerBootstrapFn.mock.calls.length).toBe(1); + }); + + it('can modify the config in configure()', () => { expect(bootstrapMockFn).toHaveBeenCalled(); const configService: ConfigService = bootstrapMockFn.mock.calls[0][0]; expect(configService instanceof ConfigService).toBe(true); @@ -78,8 +94,17 @@ describe('Plugins', () => { expect(result.names).toEqual(['seon', 'linda', 'hong']); }); - it('calls onClose method when app is closed', async () => { - await server.destroy(); - expect(onCloseFn).toHaveBeenCalled(); + describe('on app close', () => { + beforeAll(async () => { + await server.destroy(); + }); + + it('calls onVendureClose once only', () => { + expect(onCloseFn.mock.calls.length).toBe(1); + }); + + it('calls onWorkerVendureClose once only', () => { + expect(onWorkerCloseFn.mock.calls.length).toBe(1); + }); }); }); diff --git a/packages/core/e2e/shop-auth.e2e-spec.ts b/packages/core/e2e/shop-auth.e2e-spec.ts index b689faa5ee..a8ca7a7f46 100644 --- a/packages/core/e2e/shop-auth.e2e-spec.ts +++ b/packages/core/e2e/shop-auth.e2e-spec.ts @@ -1,4 +1,5 @@ /* tslint:disable:no-non-null-assertion */ +import { OnModuleInit } from '@nestjs/common'; import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types'; import { pick } from '@vendure/common/lib/pick'; import { DocumentNode } from 'graphql'; @@ -6,11 +7,12 @@ import gql from 'graphql-tag'; import path from 'path'; import { EventBus } from '../src/event-bus/event-bus'; +import { EventBusModule } from '../src/event-bus/event-bus.module'; import { AccountRegistrationEvent } from '../src/event-bus/events/account-registration-event'; import { IdentifierChangeEvent } from '../src/event-bus/events/identifier-change-event'; import { IdentifierChangeRequestEvent } from '../src/event-bus/events/identifier-change-request-event'; import { PasswordResetEvent } from '../src/event-bus/events/password-reset-event'; -import { InjectorFn, VendurePlugin } from '../src/plugin/vendure-plugin'; +import { VendurePlugin } from '../src/plugin/vendure-plugin'; import { TEST_SETUP_TIMEOUT_MS } from './config/test-config'; import { @@ -58,7 +60,7 @@ describe('Shop auth & accounts', () => { customerCount: 2, }, { - plugins: [new TestEmailPlugin()], + plugins: [TestEmailPlugin], }, ); await shopClient.init(); @@ -517,7 +519,7 @@ describe('Expiring tokens', () => { customerCount: 1, }, { - plugins: [new TestEmailPlugin()], + plugins: [TestEmailPlugin], authOptions: { verificationTokenDuration: '1ms', }, @@ -607,7 +609,7 @@ describe('Registration without email verification', () => { customerCount: 1, }, { - plugins: [new TestEmailPlugin()], + plugins: [TestEmailPlugin], authOptions: { requireVerification: false, }, @@ -683,7 +685,7 @@ describe('Updating email address without email verification', () => { customerCount: 1, }, { - plugins: [new TestEmailPlugin()], + plugins: [TestEmailPlugin], authOptions: { requireVerification: false, }, @@ -731,19 +733,22 @@ describe('Updating email address without email verification', () => { * This mock plugin simulates an EmailPlugin which would send emails * on the registration & password reset events. */ -class TestEmailPlugin implements VendurePlugin { - onBootstrap(inject: InjectorFn) { - const eventBus = inject(EventBus); - eventBus.subscribe(AccountRegistrationEvent, event => { +@VendurePlugin({ + imports: [EventBusModule], +}) +class TestEmailPlugin implements OnModuleInit { + constructor(private eventBus: EventBus) {} + onModuleInit() { + this.eventBus.subscribe(AccountRegistrationEvent, event => { sendEmailFn(event); }); - eventBus.subscribe(PasswordResetEvent, event => { + this.eventBus.subscribe(PasswordResetEvent, event => { sendEmailFn(event); }); - eventBus.subscribe(IdentifierChangeRequestEvent, event => { + this.eventBus.subscribe(IdentifierChangeRequestEvent, event => { sendEmailFn(event); }); - eventBus.subscribe(IdentifierChangeEvent, event => { + this.eventBus.subscribe(IdentifierChangeEvent, event => { sendEmailFn(event); }); } diff --git a/packages/core/e2e/test-server.ts b/packages/core/e2e/test-server.ts index fc4990dd48..d654bdf07f 100644 --- a/packages/core/e2e/test-server.ts +++ b/packages/core/e2e/test-server.ts @@ -7,7 +7,7 @@ import { ConnectionOptions } from 'typeorm'; import { SqljsConnectionOptions } from 'typeorm/driver/sqljs/SqljsConnectionOptions'; import { populateForTesting, PopulateOptions } from '../mock-data/populate-for-testing'; -import { preBootstrapConfig, runPluginOnBootstrapMethods } from '../src/bootstrap'; +import { preBootstrapConfig } from '../src/bootstrap'; import { Mutable } from '../src/common/types/common-types'; import { Logger } from '../src/config/logger/vendure-logger'; import { VendureConfig } from '../src/config/vendure-config'; @@ -108,13 +108,14 @@ export class TestServer { /** * Bootstraps an instance of the Vendure server for testing against. */ - private async bootstrapForTesting(userConfig: Partial): Promise<[INestApplication, INestMicroservice | undefined]> { + private async bootstrapForTesting( + userConfig: Partial, + ): Promise<[INestApplication, INestMicroservice | undefined]> { const config = await preBootstrapConfig(userConfig); const appModule = await import('../src/app.module'); try { - const app = await NestFactory.create(appModule.AppModule, {cors: config.cors, logger: false}); + const app = await NestFactory.create(appModule.AppModule, { cors: config.cors, logger: false }); let worker: INestMicroservice | undefined; - await runPluginOnBootstrapMethods(config, app); await app.listen(config.port); if (config.workerOptions.runInMainProcess) { const workerModule = await import('../src/worker/worker.module'); diff --git a/packages/core/src/api/api-internal-modules.ts b/packages/core/src/api/api-internal-modules.ts index 4c2847a9f9..0944201cc8 100644 --- a/packages/core/src/api/api-internal-modules.ts +++ b/packages/core/src/api/api-internal-modules.ts @@ -37,7 +37,10 @@ import { OrderLineEntityResolver } from './resolvers/entity/order-line-entity.re import { PaymentEntityResolver } from './resolvers/entity/payment-entity.resolver'; import { ProductEntityResolver } from './resolvers/entity/product-entity.resolver'; import { ProductOptionGroupEntityResolver } from './resolvers/entity/product-option-group-entity.resolver'; -import { ProductVariantAdminEntityResolver, ProductVariantEntityResolver } from './resolvers/entity/product-variant-entity.resolver'; +import { + ProductVariantAdminEntityResolver, + ProductVariantEntityResolver, +} from './resolvers/entity/product-variant-entity.resolver'; import { RefundEntityResolver } from './resolvers/entity/refund-entity.resolver'; import { ShopAuthResolver } from './resolvers/shop/shop-auth.resolver'; import { ShopCustomerResolver } from './resolvers/shop/shop-customer.resolver'; @@ -92,9 +95,7 @@ export const entityResolvers = [ RefundEntityResolver, ]; -export const adminEntityResolvers = [ - ProductVariantAdminEntityResolver, -]; +export const adminEntityResolvers = [ProductVariantAdminEntityResolver]; /** * The internal module containing some shared providers used by more than @@ -111,8 +112,8 @@ export class ApiSharedModule {} * The internal module containing the Admin GraphQL API resolvers */ @Module({ - imports: [ApiSharedModule, PluginModule.forRoot(), ServiceModule.forRoot(), DataImportModule], - providers: [...adminResolvers, ...entityResolvers, ...adminEntityResolvers, ...PluginModule.adminApiResolvers()], + imports: [ApiSharedModule, ServiceModule.forRoot(), DataImportModule, PluginModule.forAdmin()], + providers: [...adminResolvers, ...entityResolvers, ...adminEntityResolvers], exports: [...adminResolvers], }) export class AdminApiModule {} @@ -121,8 +122,8 @@ export class AdminApiModule {} * The internal module containing the Shop GraphQL API resolvers */ @Module({ - imports: [ApiSharedModule, PluginModule.forRoot(), ServiceModule.forRoot()], - providers: [...shopResolvers, ...entityResolvers, ...PluginModule.shopApiResolvers()], - exports: shopResolvers, + imports: [ApiSharedModule, ServiceModule.forRoot(), PluginModule.forShop()], + providers: [...shopResolvers, ...entityResolvers], + exports: [...shopResolvers], }) export class ShopApiModule {} diff --git a/packages/core/src/api/config/configure-graphql-module.ts b/packages/core/src/api/config/configure-graphql-module.ts index 966b293995..d9decd93c3 100644 --- a/packages/core/src/api/config/configure-graphql-module.ts +++ b/packages/core/src/api/config/configure-graphql-module.ts @@ -1,6 +1,7 @@ import { DynamicModule } from '@nestjs/common'; import { GqlModuleOptions, GraphQLModule, GraphQLTypesLoader } from '@nestjs/graphql'; import { StockMovementType } from '@vendure/common/lib/generated-types'; +import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; import { GraphQLUpload } from 'apollo-server-core'; import { extendSchema, printSchema } from 'graphql'; import { GraphQLDateTime } from 'graphql-iso-date'; @@ -11,14 +12,19 @@ import { ConfigModule } from '../../config/config.module'; import { ConfigService } from '../../config/config.service'; import { I18nModule } from '../../i18n/i18n.module'; import { I18nService } from '../../i18n/i18n.service'; -import { getPluginAPIExtensions } from '../../plugin/plugin-utils'; +import { getDynamicGraphQlModulesForPlugins } from '../../plugin/dynamic-plugin-api.module'; +import { getPluginAPIExtensions } from '../../plugin/plugin-metadata'; import { ApiSharedModule } from '../api-internal-modules'; import { IdCodecService } from '../common/id-codec.service'; import { IdEncoderExtension } from '../middleware/id-encoder-extension'; import { TranslateErrorExtension } from '../middleware/translate-errors-extension'; import { generateListOptions } from './generate-list-options'; -import { addGraphQLCustomFields, addOrderLineCustomFieldsInput, addServerConfigCustomFields } from './graphql-custom-fields'; +import { + addGraphQLCustomFields, + addOrderLineCustomFieldsInput, + addServerConfigCustomFields, +} from './graphql-custom-fields'; export interface GraphQLApiOptions { apiType: 'shop' | 'admin'; @@ -106,7 +112,7 @@ async function createGraphQLOptions( return { path: '/' + options.apiPath, typeDefs: await createTypeDefs(options.apiType), - include: [options.resolverModule], + include: [options.resolverModule, ...getDynamicGraphQlModulesForPlugins(options.apiType)], resolvers: { JSON: GraphQLJSON, DateTime: GraphQLDateTime, @@ -157,9 +163,9 @@ async function createGraphQLOptions( schema = addGraphQLCustomFields(schema, customFields, apiType === 'shop'); schema = addServerConfigCustomFields(schema, customFields); schema = addOrderLineCustomFieldsInput(schema, customFields.OrderLine || []); - const pluginSchemaExtensions = getPluginAPIExtensions(configService.plugins, apiType).map( - e => e.schema, - ); + const pluginSchemaExtensions = getPluginAPIExtensions(configService.plugins, apiType) + .map(e => e.schema) + .filter(notNullOrUndefined); for (const documentNode of pluginSchemaExtensions) { schema = extendSchema(schema, documentNode); diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index 47303e4e68..6da1d44e32 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module, NestModule, OnApplicationShutdown, OnModuleDestroy } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule, OnApplicationShutdown } from '@nestjs/common'; import cookieSession = require('cookie-session'); import { RequestHandler } from 'express'; @@ -12,9 +12,8 @@ import { I18nService } from './i18n/i18n.service'; @Module({ imports: [ConfigModule, I18nModule, ApiModule], }) -export class AppModule implements NestModule, OnModuleDestroy, OnApplicationShutdown { - constructor(private configService: ConfigService, - private i18nService: I18nService) {} +export class AppModule implements NestModule, OnApplicationShutdown { + constructor(private configService: ConfigService, private i18nService: I18nService) {} configure(consumer: MiddlewareConsumer) { const { adminApiPath, shopApiPath } = this.configService; @@ -39,14 +38,6 @@ export class AppModule implements NestModule, OnModuleDestroy, OnApplicationShut } } - async onModuleDestroy() { - for (const plugin of this.configService.plugins) { - if (plugin.onClose) { - await plugin.onClose(); - } - } - } - onApplicationShutdown(signal?: string) { if (signal) { Logger.info('Received shutdown signal:' + signal); diff --git a/packages/core/src/bootstrap.ts b/packages/core/src/bootstrap.ts index b1450051e2..0be48a25ab 100644 --- a/packages/core/src/bootstrap.ts +++ b/packages/core/src/bootstrap.ts @@ -12,6 +12,12 @@ import { Logger } from './config/logger/vendure-logger'; import { VendureConfig } from './config/vendure-config'; import { registerCustomEntityFields } from './entity/register-custom-entity-fields'; import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config'; +import { + getConfigurationFunction, + getEntitiesFromPlugins, + getPluginModules, + hasLifecycleMethod, +} from './plugin/plugin-metadata'; import { logProxyMiddlewares } from './plugin/plugin-utils'; export type VendureBootstrapFunction = (config: VendureConfig) => Promise; @@ -48,7 +54,6 @@ export async function bootstrap(userConfig: Partial): Promise, ): Promise> { for (const plugin of config.plugins) { - if (plugin.configure) { - config = (await plugin.configure(config)) as ReadOnlyRequired; + const configFn = getConfigurationFunction(plugin); + if (typeof configFn === 'function') { + config = await configFn(config); } } return config; } -/** - * Run the onBootstrap() method of any configured plugins. - */ -export async function runPluginOnBootstrapMethods( - config: ReadOnlyRequired, - app: INestApplication, -): Promise { - function inject(type: Type): T { - return app.get(type); - } - - for (const plugin of config.plugins) { - if (plugin.onBootstrap) { - await plugin.onBootstrap(inject); - const pluginName = plugin.constructor && plugin.constructor.name || '(anonymous plugin)'; - Logger.verbose(`Bootstrapped plugin ${pluginName}`); - } - } -} - /** * Returns an array of core entities and any additional entities defined in plugins. */ async function getAllEntities(userConfig: Partial): Promise>> { const { coreEntitiesMap } = await import('./entity/entities'); const coreEntities = Object.values(coreEntitiesMap) as Array>; - const pluginEntities = getEntitiesFromPlugins(userConfig); + const pluginEntities = getEntitiesFromPlugins(userConfig.plugins); const allEntities: Array> = coreEntities; @@ -203,18 +189,6 @@ async function getAllEntities(userConfig: Partial): Promise): Array> { - if (!userConfig.plugins) { - return []; - } - return userConfig.plugins - .map(p => (p.defineEntities ? p.defineEntities() : [])) - .reduce((all, entities) => [...all, ...entities], []); -} - /** * Monkey-patches the app's .close() method to also close the worker microservice * instance too. diff --git a/packages/core/src/config/config.service.ts b/packages/core/src/config/config.service.ts index 2583611c3a..696a8fbd23 100644 --- a/packages/core/src/config/config.service.ts +++ b/packages/core/src/config/config.service.ts @@ -1,11 +1,10 @@ -import { Injectable } from '@nestjs/common'; +import { DynamicModule, Injectable, Type } from '@nestjs/common'; import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; import { LanguageCode } from '@vendure/common/lib/generated-types'; import { RequestHandler } from 'express'; import { ConnectionOptions } from 'typeorm'; import { ReadOnlyRequired } from '../common/types/common-types'; -import { VendurePlugin } from '../plugin/vendure-plugin'; import { getConfig } from './config-helpers'; import { CustomFields } from './custom-field/custom-field-types'; @@ -112,7 +111,7 @@ export class ConfigService implements VendureConfig { return this.activeConfig.middleware; } - get plugins(): VendurePlugin[] { + get plugins(): Array> { return this.activeConfig.plugins; } diff --git a/packages/core/src/config/vendure-config.ts b/packages/core/src/config/vendure-config.ts index 3e089dbdd1..280ec231e0 100644 --- a/packages/core/src/config/vendure-config.ts +++ b/packages/core/src/config/vendure-config.ts @@ -1,3 +1,4 @@ +import { DynamicModule, Type } from '@nestjs/common'; import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; import { ClientOptions, Transport } from '@nestjs/microservices'; import { LanguageCode } from '@vendure/common/lib/generated-types'; @@ -7,7 +8,6 @@ import { ConnectionOptions } from 'typeorm'; import { Transitions } from '../common/finite-state-machine'; import { Order } from '../entity/order/order.entity'; -import { VendurePlugin } from '../plugin/vendure-plugin'; import { OrderState } from '../service/helpers/order-state-machine/order-state'; import { AssetNamingStrategy } from './asset-naming-strategy/asset-naming-strategy'; @@ -489,7 +489,7 @@ export interface VendureConfig { * * @default [] */ - plugins?: VendurePlugin[]; + plugins?: Array>; /** * @description * Which port the Vendure server should listen on. diff --git a/packages/core/src/event-bus/index.ts b/packages/core/src/event-bus/index.ts index 4ccd74b993..8d7436af6a 100644 --- a/packages/core/src/event-bus/index.ts +++ b/packages/core/src/event-bus/index.ts @@ -1,4 +1,5 @@ export * from './event-bus'; +export * from './event-bus.module'; export * from './vendure-event'; export * from './events/account-registration-event'; export * from './events/catalog-modification-event'; diff --git a/packages/core/src/plugin/default-search-plugin/constants.ts b/packages/core/src/plugin/default-search-plugin/constants.ts index c308812b80..dad0fe3522 100644 --- a/packages/core/src/plugin/default-search-plugin/constants.ts +++ b/packages/core/src/plugin/default-search-plugin/constants.ts @@ -1,4 +1,4 @@ -export const loggerCtx = 'DefaultSearchPlugin'; +export const workerLoggerCtx = 'DefaultSearchPlugin Worker'; export enum Message { Reindex = 'Reindex', UpdateVariantsById = 'UpdateVariantsById', diff --git a/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts b/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts index 843f0c88a1..33a4fe4177 100644 --- a/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts +++ b/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts @@ -1,11 +1,7 @@ -import { Provider } from '@nestjs/common'; +import { OnApplicationBootstrap } from '@nestjs/common'; import { SearchReindexResponse } from '@vendure/common/lib/generated-types'; -import { CREATING_VENDURE_APP } from '@vendure/common/lib/shared-constants'; -import { Type } from '@vendure/common/lib/shared-types'; -import gql from 'graphql-tag'; import { idsAreEqual } from '../../common/utils'; -import { APIExtensionDefinition, VendurePlugin } from '../../config'; import { ProductVariant } from '../../entity/product-variant/product-variant.entity'; import { Product } from '../../entity/product/product.entity'; import { EventBus } from '../../event-bus/event-bus'; @@ -13,6 +9,8 @@ import { CatalogModificationEvent } from '../../event-bus/events/catalog-modific import { CollectionModificationEvent } from '../../event-bus/events/collection-modification-event'; import { TaxRateModificationEvent } from '../../event-bus/events/tax-rate-modification-event'; import { SearchService } from '../../service/services/search.service'; +import { PluginCommonModule } from '../plugin-common.module'; +import { VendurePlugin } from '../vendure-plugin'; import { AdminFulltextSearchResolver, ShopFulltextSearchResolver } from './fulltext-search.resolver'; import { FulltextSearchService } from './fulltext-search.service'; @@ -54,72 +52,37 @@ export interface DefaultSearchReindexResponse extends SearchReindexResponse { * * @docsCategory DefaultSearchPlugin */ -export class DefaultSearchPlugin implements VendurePlugin { +@VendurePlugin({ + imports: [PluginCommonModule], + providers: [ + FulltextSearchService, + SearchIndexService, + { provide: SearchService, useClass: FulltextSearchService }, + ], + exports: [{ provide: SearchService, useClass: FulltextSearchService }], + adminApiExtensions: { resolvers: [AdminFulltextSearchResolver] }, + shopApiExtensions: { resolvers: [ShopFulltextSearchResolver] }, + entities: [SearchIndexItem], + workers: [IndexerController], +}) +export class DefaultSearchPlugin implements OnApplicationBootstrap { + constructor(private eventBus: EventBus, private searchIndexService: SearchIndexService) {} /** @internal */ - async onBootstrap(inject: (type: Type) => T): Promise { - const eventBus = inject(EventBus); - const searchIndexService = inject(SearchIndexService); - eventBus.subscribe(CatalogModificationEvent, event => { + onApplicationBootstrap() { + this.eventBus.subscribe(CatalogModificationEvent, event => { if (event.entity instanceof Product || event.entity instanceof ProductVariant) { - return searchIndexService.updateProductOrVariant(event.ctx, event.entity).start(); + return this.searchIndexService.updateProductOrVariant(event.ctx, event.entity).start(); } }); - eventBus.subscribe(CollectionModificationEvent, event => { - return searchIndexService.updateVariantsById(event.ctx, event.productVariantIds).start(); + this.eventBus.subscribe(CollectionModificationEvent, event => { + return this.searchIndexService.updateVariantsById(event.ctx, event.productVariantIds).start(); }); - eventBus.subscribe(TaxRateModificationEvent, event => { + this.eventBus.subscribe(TaxRateModificationEvent, event => { const defaultTaxZone = event.ctx.channel.defaultTaxZone; if (defaultTaxZone && idsAreEqual(defaultTaxZone.id, event.taxRate.zone.id)) { - return searchIndexService.reindex(event.ctx).start(); + return this.searchIndexService.reindex(event.ctx).start(); } }); } - - /** @internal */ - extendAdminAPI(): APIExtensionDefinition { - return { - resolvers: [AdminFulltextSearchResolver], - schema: gql` - extend type SearchReindexResponse { - timeTaken: Int! - indexedItemCount: Int! - } - `, - }; - } - - /** @internal */ - extendShopAPI(): APIExtensionDefinition { - return { - resolvers: [ShopFulltextSearchResolver], - schema: gql` - extend type SearchReindexResponse { - timeTaken: Int! - indexedItemCount: Int! - } - `, - }; - } - - /** @internal */ - defineEntities(): Array> { - return [SearchIndexItem]; - } - - /** @internal */ - defineProviders(): Provider[] { - return [ - FulltextSearchService, - SearchIndexService, - { provide: SearchService, useClass: FulltextSearchService }, - ]; - } - - /** @internal */ - defineWorkers(): Array> { - return [ - IndexerController, - ]; - } } diff --git a/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts b/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts index e75617a74b..bad99291d0 100644 --- a/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts +++ b/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts @@ -17,7 +17,7 @@ import { translateDeep } from '../../../service/helpers/utils/translate-entity'; import { ProductVariantService } from '../../../service/services/product-variant.service'; import { TaxRateService } from '../../../service/services/tax-rate.service'; import { AsyncQueue } from '../async-queue'; -import { loggerCtx, Message } from '../constants'; +import { Message, workerLoggerCtx } from '../constants'; import { SearchIndexItem } from '../search-index-item.entity'; export const BATCH_SIZE = 1000; @@ -57,17 +57,19 @@ export class IndexerController { const timeStart = Date.now(); const qb = this.getSearchIndexQueryBuilder(); const count = await qb.where('variants__product.deletedAt IS NULL').getCount(); - Logger.verbose(`Reindexing ${count} variants`, loggerCtx); + Logger.verbose(`Reindexing ${count} variants`, workerLoggerCtx); const batches = Math.ceil(count / BATCH_SIZE); // Ensure tax rates are up-to-date. await this.taxRateService.updateActiveTaxRates(); - await this.connection.getRepository(SearchIndexItem).delete({ languageCode: ctx.languageCode }); - Logger.verbose('Deleted existing index items', loggerCtx); + await this.connection + .getRepository(SearchIndexItem) + .delete({ languageCode: ctx.languageCode }); + Logger.verbose('Deleted existing index items', workerLoggerCtx); for (let i = 0; i < batches; i++) { - Logger.verbose(`Processing batch ${i + 1} of ${batches}`, loggerCtx); + Logger.verbose(`Processing batch ${i + 1} of ${batches}`, workerLoggerCtx); const variants = await qb .where('variants__product.deletedAt IS NULL') @@ -82,7 +84,7 @@ export class IndexerController { duration: +new Date() - timeStart, }); } - Logger.verbose(`Completed reindexing!`); + Logger.verbose(`Completed reindexing`, workerLoggerCtx); observer.next({ total: count, completed: count, @@ -94,7 +96,13 @@ export class IndexerController { } @MessagePattern(Message.UpdateVariantsById) - updateVariantsById({ ctx: rawContext, ids }: { ctx: any, ids: ID[] }): Observable { + updateVariantsById({ + ctx: rawContext, + ids, + }: { + ctx: any; + ids: ID[]; + }): Observable { const ctx = RequestContext.fromObject(rawContext); return new Observable(observer => { @@ -109,9 +117,11 @@ export class IndexerController { const end = begin + BATCH_SIZE; Logger.verbose(`Updating ids from index ${begin} to ${end}`); const batchIds = ids.slice(begin, end); - const batch = await this.connection.getRepository(ProductVariant).findByIds(batchIds, { - relations: variantRelations, - }); + const batch = await this.connection + .getRepository(ProductVariant) + .findByIds(batchIds, { + relations: variantRelations, + }); const variants = this.hydrateVariants(ctx, batch); await this.saveVariants(ctx, variants); observer.next({ @@ -136,7 +146,15 @@ export class IndexerController { * Updates the search index only for the affected entities. */ @MessagePattern(Message.UpdateProductOrVariant) - updateProductOrVariant({ ctx: rawContext, productId, variantId }: { ctx: any, productId?: ID, variantId?: ID }): Observable { + updateProductOrVariant({ + ctx: rawContext, + productId, + variantId, + }: { + ctx: any; + productId?: ID; + variantId?: ID; + }): Observable { const ctx = RequestContext.fromObject(rawContext); let updatedVariants: ProductVariant[] = []; let removedVariantIds: ID[] = []; @@ -155,7 +173,7 @@ export class IndexerController { relations: variantRelations, }); if (product.enabled === false) { - updatedVariants.forEach(v => v.enabled = false); + updatedVariants.forEach(v => (v.enabled = false)); } } } @@ -167,7 +185,7 @@ export class IndexerController { updatedVariants = [variant]; } } - Logger.verbose(`Updating ${updatedVariants.length} variants`, loggerCtx); + Logger.verbose(`Updating ${updatedVariants.length} variants`, workerLoggerCtx); updatedVariants = this.hydrateVariants(ctx, updatedVariants); if (updatedVariants.length) { await this.saveVariants(ctx, updatedVariants); @@ -198,25 +216,26 @@ export class IndexerController { } private async saveVariants(ctx: RequestContext, variants: ProductVariant[]) { - const items = variants.map((v: ProductVariant) => - new SearchIndexItem({ - sku: v.sku, - enabled: v.enabled, - slug: v.product.slug, - price: v.price, - priceWithTax: v.priceWithTax, - languageCode: ctx.languageCode, - productVariantId: v.id, - productId: v.product.id, - productName: v.product.name, - description: v.product.description, - productVariantName: v.name, - productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '', - productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '', - facetIds: this.getFacetIds(v), - facetValueIds: this.getFacetValueIds(v), - collectionIds: v.collections.map(c => c.id.toString()), - }), + const items = variants.map( + (v: ProductVariant) => + new SearchIndexItem({ + sku: v.sku, + enabled: v.enabled, + slug: v.product.slug, + price: v.price, + priceWithTax: v.priceWithTax, + languageCode: ctx.languageCode, + productVariantId: v.id, + productId: v.product.id, + productName: v.product.name, + description: v.product.description, + productVariantName: v.name, + productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '', + productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '', + facetIds: this.getFacetIds(v), + facetValueIds: this.getFacetValueIds(v), + collectionIds: v.collections.map(c => c.id.toString()), + }), ); await this.queue.push(() => this.connection.getRepository(SearchIndexItem).save(items)); } diff --git a/packages/core/src/plugin/dynamic-plugin-api.module.ts b/packages/core/src/plugin/dynamic-plugin-api.module.ts new file mode 100644 index 0000000000..a2b9f77888 --- /dev/null +++ b/packages/core/src/plugin/dynamic-plugin-api.module.ts @@ -0,0 +1,53 @@ +import { DynamicModule } from '@nestjs/common'; + +import { Type } from '../../../common/lib/shared-types'; +import { notNullOrUndefined } from '../../../common/lib/shared-utils'; +import { getConfig } from '../config/config-helpers'; + +import { getModuleMetadata, graphQLResolversFor, isDynamicModule } from './plugin-metadata'; + +const dynamicApiModuleClassMap: { [name: string]: Type } = {}; + +/** + * This function dynamically creates a Nest module to house any GraphQL resolvers defined by + * any configured plugins. + */ +export function createDynamicGraphQlModulesForPlugins(apiType: 'shop' | 'admin'): DynamicModule[] { + return getConfig() + .plugins.map(plugin => { + const pluginModule = isDynamicModule(plugin) ? plugin.module : plugin; + const resolvers = graphQLResolversFor(plugin, apiType) || []; + + if (resolvers.length) { + const className = dynamicClassName(pluginModule, apiType); + dynamicApiModuleClassMap[className] = class {}; + Object.defineProperty(dynamicApiModuleClassMap[className], 'name', { value: className }); + const { imports, providers } = getModuleMetadata(pluginModule); + return { + module: dynamicApiModuleClassMap[className], + imports, + providers: [...providers, ...resolvers], + }; + } + }) + .filter(notNullOrUndefined); +} + +/** + * This function retrieves any dynamic modules which were created with createDynamicGraphQlModulesForPlugins. + */ +export function getDynamicGraphQlModulesForPlugins(apiType: 'shop' | 'admin'): Array> { + return getConfig() + .plugins.map(plugin => { + const pluginModule = isDynamicModule(plugin) ? plugin.module : plugin; + const resolvers = graphQLResolversFor(plugin, apiType) || []; + + const className = dynamicClassName(pluginModule, apiType); + return dynamicApiModuleClassMap[className]; + }) + .filter(notNullOrUndefined); +} + +function dynamicClassName(module: Type, apiType: 'shop' | 'admin'): string { + return module.name + `Dynamic` + (apiType === 'shop' ? 'Shop' : 'Admin') + 'Module'; +} diff --git a/packages/core/src/plugin/index.ts b/packages/core/src/plugin/index.ts index 5cfc56d3b7..3fbfc5f7d7 100644 --- a/packages/core/src/plugin/index.ts +++ b/packages/core/src/plugin/index.ts @@ -1,2 +1,4 @@ export * from './default-search-plugin/default-search-plugin'; +export * from './vendure-plugin'; +export * from './plugin-common.module'; export { createProxyHandler, ProxyOptions } from './plugin-utils'; diff --git a/packages/core/src/plugin/plugin-common.module.ts b/packages/core/src/plugin/plugin-common.module.ts new file mode 100644 index 0000000000..5f12fb0483 --- /dev/null +++ b/packages/core/src/plugin/plugin-common.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { ClientProxyFactory } from '@nestjs/microservices'; + +import { ConfigModule } from '../config/config.module'; +import { ConfigService } from '../config/config.service'; +import { EventBusModule } from '../event-bus/event-bus.module'; +import { ServiceModule } from '../service/service.module'; +import { VENDURE_WORKER_CLIENT } from '../worker/constants'; + +/** + * This module provides the common services, configuration, and event bus capabilities + * required by a typical plugin. It should be imported into plugins to avoid having to + * repeat the same boilerplate for each individual plugin. + */ +@Module({ + imports: [EventBusModule, ConfigModule, ServiceModule.forPlugin()], + providers: [ + { + provide: VENDURE_WORKER_CLIENT, + useFactory: (configService: ConfigService) => { + return ClientProxyFactory.create({ + transport: configService.workerOptions.transport as any, + options: configService.workerOptions.options as any, + }); + }, + inject: [ConfigService], + }, + // TODO: Provide an injectable which defines whether in main or worker context + ], + exports: [EventBusModule, ConfigModule, ServiceModule.forPlugin(), VENDURE_WORKER_CLIENT], +}) +export class PluginCommonModule {} diff --git a/packages/core/src/plugin/plugin-metadata.ts b/packages/core/src/plugin/plugin-metadata.ts new file mode 100644 index 0000000000..f749804845 --- /dev/null +++ b/packages/core/src/plugin/plugin-metadata.ts @@ -0,0 +1,90 @@ +import { DynamicModule } from '@nestjs/common'; +import { METADATA } from '@nestjs/common/constants'; +import { Type } from '@vendure/common/lib/shared-types'; + +import { notNullOrUndefined } from '../../../common/lib/shared-utils'; + +import { APIExtensionDefinition, PluginConfigurationFn, PluginLifecycleMethods } from './vendure-plugin'; + +export const PLUGIN_METADATA = { + CONFIGURATION: 'configuration', + SHOP_API_EXTENSIONS: 'shopApiExtensions', + ADMIN_API_EXTENSIONS: 'adminApiExtensions', + WORKERS: 'workers', + ENTITIES: 'entities', +}; + +export function getEntitiesFromPlugins(plugins?: Array | DynamicModule>): Array> { + if (!plugins) { + return []; + } + return plugins + .map(p => reflectMetadata(p, PLUGIN_METADATA.ENTITIES)) + .reduce((all, entities) => [...all, ...(entities || [])], []); +} + +export function getModuleMetadata(module: Type) { + return { + controllers: Reflect.getMetadata(METADATA.CONTROLLERS, module) || [], + providers: Reflect.getMetadata(METADATA.PROVIDERS, module) || [], + imports: Reflect.getMetadata(METADATA.IMPORTS, module) || [], + exports: Reflect.getMetadata(METADATA.EXPORTS, module) || [], + }; +} + +export function getPluginAPIExtensions( + plugins: Array | DynamicModule>, + apiType: 'shop' | 'admin', +): APIExtensionDefinition[] { + const extensions = + apiType === 'shop' + ? plugins.map(p => reflectMetadata(p, PLUGIN_METADATA.SHOP_API_EXTENSIONS)) + : plugins.map(p => reflectMetadata(p, PLUGIN_METADATA.ADMIN_API_EXTENSIONS)); + + return extensions.filter(notNullOrUndefined); +} + +export function getPluginModules(plugins: Array | DynamicModule>): Array> { + return plugins.map(p => (isDynamicModule(p) ? p.module : p)); +} + +export function hasLifecycleMethod( + plugin: any, + lifecycleMethod: M, +): plugin is { [key in M]: PluginLifecycleMethods[M] } { + return typeof (plugin as any)[lifecycleMethod] === 'function'; +} + +export function getWorkerControllers(plugin: Type | DynamicModule) { + return reflectMetadata(plugin, PLUGIN_METADATA.WORKERS); +} + +export function getConfigurationFunction( + plugin: Type | DynamicModule, +): PluginConfigurationFn | undefined { + return reflectMetadata(plugin, PLUGIN_METADATA.CONFIGURATION); +} + +export function graphQLResolversFor( + plugin: Type | DynamicModule, + apiType: 'shop' | 'admin', +): Array> { + const apiExtensions = + apiType === 'shop' + ? reflectMetadata(plugin, PLUGIN_METADATA.SHOP_API_EXTENSIONS) + : reflectMetadata(plugin, PLUGIN_METADATA.ADMIN_API_EXTENSIONS); + + return apiExtensions ? apiExtensions.resolvers : []; +} + +function reflectMetadata(metatype: Type | DynamicModule, metadataKey: string) { + if (isDynamicModule(metatype)) { + return Reflect.getMetadata(metadataKey, metatype.module); + } else { + return Reflect.getMetadata(metadataKey, metatype); + } +} + +export function isDynamicModule(input: Type | DynamicModule): input is DynamicModule { + return !!(input as DynamicModule).module; +} diff --git a/packages/core/src/plugin/plugin-utils.ts b/packages/core/src/plugin/plugin-utils.ts index 0af8f00ab0..d9c7aa69b2 100644 --- a/packages/core/src/plugin/plugin-utils.ts +++ b/packages/core/src/plugin/plugin-utils.ts @@ -1,8 +1,7 @@ -import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; import { RequestHandler } from 'express'; import proxy from 'http-proxy-middleware'; -import { APIExtensionDefinition, Logger, VendureConfig, VendurePlugin } from '../config'; +import { Logger, VendureConfig } from '../config'; /** * @description @@ -104,23 +103,13 @@ export function createProxyHandler(options: ProxyOptions): RequestHandler { export function logProxyMiddlewares(config: VendureConfig) { for (const middleware of config.middleware || []) { if ((middleware.handler as any).proxyMiddleware) { - const { port, hostname, label, route } = (middleware.handler as any).proxyMiddleware as ProxyOptions; - Logger.info(`${label}: http://${config.hostname || 'localhost'}:${config.port}/${route}/ -> http://${hostname || 'localhost'}:${port}`); + const { port, hostname, label, route } = (middleware.handler as any) + .proxyMiddleware as ProxyOptions; + Logger.info( + `${label}: http://${config.hostname || 'localhost'}:${ + config.port + }/${route}/ -> http://${hostname || 'localhost'}:${port}`, + ); } } } - -/** - * Given an array of VendurePlugins, returns a flattened array of all APIExtensionDefinitions. - */ -export function getPluginAPIExtensions( - plugins: VendurePlugin[], - apiType: 'shop' | 'admin', -): APIExtensionDefinition[] { - const extensions = - apiType === 'shop' - ? plugins.map(p => (p.extendShopAPI ? p.extendShopAPI() : undefined)) - : plugins.map(p => (p.extendAdminAPI ? p.extendAdminAPI() : undefined)); - - return extensions.filter(notNullOrUndefined); -} diff --git a/packages/core/src/plugin/plugin.module.ts b/packages/core/src/plugin/plugin.module.ts index 3c61414249..912ce6f642 100644 --- a/packages/core/src/plugin/plugin.module.ts +++ b/packages/core/src/plugin/plugin.module.ts @@ -1,86 +1,139 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { ClientProxyFactory } from '@nestjs/microservices'; -import { Type } from '@vendure/common/lib/shared-types'; -import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; +import { DynamicModule, Inject, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; import { getConfig } from '../config/config-helpers'; import { ConfigModule } from '../config/config.module'; import { ConfigService } from '../config/config.service'; -import { EventBusModule } from '../event-bus/event-bus.module'; -import { ServiceModule } from '../service/service.module'; -import { VENDURE_WORKER_CLIENT } from '../worker/constants'; +import { Logger } from '../config/logger/vendure-logger'; -import { getPluginAPIExtensions } from './plugin-utils'; +import { createDynamicGraphQlModulesForPlugins } from './dynamic-plugin-api.module'; +import { + getPluginModules, + getWorkerControllers, + hasLifecycleMethod, + isDynamicModule, +} from './plugin-metadata'; +import { PluginLifecycleMethods } from './vendure-plugin'; -const pluginProviders = getPluginProviders(); +export enum PluginProcessContext { + Main, + Worker, +} + +const PLUGIN_PROCESS_CONTEXT = 'PLUGIN_PROCESS_CONTEXT'; /** * This module collects and re-exports all providers defined in plugins so that they can be used in other - * modules. + * modules and in responsible for executing any lifecycle methods defined by the plugins. */ @Module({ - imports: [ - EventBusModule, - ConfigModule, - ], - providers: [ - { - provide: VENDURE_WORKER_CLIENT, - useFactory: (configService: ConfigService) => { - return ClientProxyFactory.create({ - transport: configService.workerOptions.transport as any, - options: configService.workerOptions.options as any, - }); - }, - inject: [ConfigService], - }, - ...pluginProviders, - ], - exports: pluginProviders, + imports: [ConfigModule], }) -export class PluginModule { - static shopApiResolvers(): Array> { - return graphQLResolversFor('shop'); +export class PluginModule implements OnModuleInit, OnModuleDestroy { + static forShop(): DynamicModule { + return { + module: PluginModule, + providers: [{ provide: PLUGIN_PROCESS_CONTEXT, useValue: PluginProcessContext.Main }], + imports: [...getConfig().plugins, ...createDynamicGraphQlModulesForPlugins('shop')], + }; } - static adminApiResolvers(): Array> { - return graphQLResolversFor('admin'); + static forAdmin(): DynamicModule { + return { + module: PluginModule, + providers: [{ provide: PLUGIN_PROCESS_CONTEXT, useValue: PluginProcessContext.Main }], + imports: [...getConfig().plugins, ...createDynamicGraphQlModulesForPlugins('admin')], + }; } static forRoot(): DynamicModule { return { module: PluginModule, - imports: [ServiceModule.forRoot()], + providers: [{ provide: PLUGIN_PROCESS_CONTEXT, useValue: PluginProcessContext.Main }], + imports: [...getConfig().plugins], }; } static forWorker(): DynamicModule { return { module: PluginModule, - imports: [ServiceModule.forWorker()], + providers: [{ provide: PLUGIN_PROCESS_CONTEXT, useValue: PluginProcessContext.Worker }], + imports: [...pluginsWithWorkerControllers()], }; } -} -function getPluginProviders() { - const plugins = getConfig().plugins; - return plugins - .map(p => (p.defineProviders ? p.defineProviders() : undefined)) - .filter(notNullOrUndefined) - .reduce((flattened, providers) => flattened.concat(providers), []); -} + private static mainBootstrapHasRun = false; + private static mainCloseHasRun = false; + private static workerBootstrapHasRun = false; + private static workerCloseHasRun = false; + + constructor( + @Inject(PLUGIN_PROCESS_CONTEXT) private processContext: PluginProcessContext, + private moduleRef: ModuleRef, + private configService: ConfigService, + ) {} + + async onModuleInit() { + if (!PluginModule.mainBootstrapHasRun && this.processContext === PluginProcessContext.Main) { + PluginModule.mainBootstrapHasRun = true; + this.runPluginLifecycleMethods('onVendureBootstrap', instance => { + const pluginName = instance.constructor.name || '(anonymous plugin)'; + Logger.verbose(`Bootstrapped plugin ${pluginName}`); + }); + } + if (!PluginModule.workerBootstrapHasRun && this.processContext === PluginProcessContext.Worker) { + PluginModule.workerBootstrapHasRun = true; + this.runPluginLifecycleMethods('onVendureWorkerBootstrap'); + } + } + + async onModuleDestroy() { + if (!PluginModule.mainCloseHasRun && this.processContext === PluginProcessContext.Main) { + PluginModule.mainCloseHasRun = true; + await this.runPluginLifecycleMethods('onVendureClose'); + } + if (!PluginModule.workerCloseHasRun && this.processContext === PluginProcessContext.Worker) { + PluginModule.workerCloseHasRun = true; + this.runPluginLifecycleMethods('onVendureWorkerClose'); + } + } -function getWorkerControllers() { - const plugins = getConfig().plugins; - return plugins - .map(p => (p.defineWorkers ? p.defineWorkers() : undefined)) - .filter(notNullOrUndefined) - .reduce((flattened, providers) => flattened.concat(providers), []); + private async runPluginLifecycleMethods( + lifecycleMethod: keyof PluginLifecycleMethods, + afterRun?: (instance: any) => void, + ) { + for (const plugin of getPluginModules(this.configService.plugins)) { + let instance: any; + try { + instance = this.moduleRef.get(plugin, { strict: false }); + } catch (e) { + Logger.error(`Could not find ${plugin.name}`, undefined, e.stack); + } + if (instance) { + if (hasLifecycleMethod(instance, lifecycleMethod)) { + await instance[lifecycleMethod](); + } + if (typeof afterRun === 'function') { + afterRun(instance); + } + } + } + } } -function graphQLResolversFor(apiType: 'shop' | 'admin'): Array> { - const plugins = getConfig().plugins; - return getPluginAPIExtensions(plugins, apiType) - .map(extension => extension.resolvers) - .reduce((flattened, r) => [...flattened, ...r], []); +function pluginsWithWorkerControllers(): DynamicModule[] { + return getConfig().plugins.map(plugin => { + const controllers = getWorkerControllers(plugin); + if (isDynamicModule(plugin)) { + return { + ...plugin, + controllers, + }; + } else { + return { + module: plugin, + controllers, + }; + } + }); } diff --git a/packages/core/src/plugin/vendure-plugin.ts b/packages/core/src/plugin/vendure-plugin.ts index 2bba43c12d..d2ef9e7769 100644 --- a/packages/core/src/plugin/vendure-plugin.ts +++ b/packages/core/src/plugin/vendure-plugin.ts @@ -1,16 +1,13 @@ -import { Provider } from '@nestjs/common'; +import { Module } from '@nestjs/common'; +import { METADATA } from '@nestjs/common/constants'; +import { ModuleMetadata } from '@nestjs/common/interfaces'; +import { pick } from '@vendure/common/lib/pick'; import { Type } from '@vendure/common/lib/shared-types'; import { DocumentNode } from 'graphql'; import { VendureConfig } from '../config/vendure-config'; -/** - * @description - * A function which allows any injectable provider to be injected into the `onBootstrap` method of a {@link VendurePlugin}. - * - * @docsCategory plugin - */ -export type InjectorFn = (type: Type) => T; +import { PLUGIN_METADATA } from './plugin-metadata'; /** * @description @@ -21,7 +18,7 @@ export type InjectorFn = (type: Type) => T; export interface APIExtensionDefinition { /** * @description - * The schema extensions. + * Extensions to the schema. * * @example * ```TypeScript @@ -31,7 +28,7 @@ export interface APIExtensionDefinition { * }`; * ``` */ - schema: DocumentNode; + schema?: DocumentNode; /** * @description * An array of resolvers for the schema extensions. Should be defined as Nest GraphQL resolver @@ -42,67 +39,82 @@ export interface APIExtensionDefinition { /** * @description - * A VendurePlugin is a means of configuring and/or extending the functionality of the Vendure server. In its simplest form, - * a plugin simply modifies the VendureConfig object. Although such configuration can be directly supplied to the bootstrap - * function, using a plugin allows one to abstract away a set of related configuration. - * - * As well as configuring the app, a plugin may also extend the GraphQL schema by extending existing types or adding - * entirely new types. Database entities and resolvers can also be defined to handle the extended GraphQL types. - * - * @docsCategory plugin + * This method is called before the app bootstraps, and can modify the VendureConfig object and perform + * other (potentially async) tasks needed. */ -export interface VendurePlugin { - /** - * @description - * This method is called before the app bootstraps, and can modify the VendureConfig object and perform - * other (potentially async) tasks needed. - */ - configure?(config: Required): Required | Promise>; - - /** - * @description - * This method is called after the app has bootstrapped. In this method, instances of services may be injected - * into the plugin. For example, the ProductService can be injected in order to enable operations on Product - * entities. - */ - onBootstrap?(inject: InjectorFn): void | Promise; - - /** - * @description - * This method is called when the app closes. It can be used for any clean-up logic such as stopping servers. - */ - onClose?(): void | Promise; +export type PluginConfigurationFn = ( + config: Required, +) => Required | Promise>; +export interface VendurePluginMetadata extends ModuleMetadata { + configuration?: PluginConfigurationFn; /** * @description - * The plugin may extend the default Vendure GraphQL shop api by implementing this method and providing extended + * The plugin may extend the default Vendure GraphQL shop api by providing extended * schema definitions and any required resolvers. */ - extendShopAPI?(): APIExtensionDefinition; - + shopApiExtensions?: APIExtensionDefinition; /** * @description - * The plugin may extend the default Vendure GraphQL admin api by implementing this method and providing extended + * The plugin may extend the default Vendure GraphQL admin api by providing extended * schema definitions and any required resolvers. */ - extendAdminAPI?(): APIExtensionDefinition; - - /** - * @description - * The plugin may define custom providers which can then be injected via the Nest DI container. - */ - defineProviders?(): Provider[]; - + adminApiExtensions?: APIExtensionDefinition; /** * @description * The plugin may define providers which are run in the Worker context, i.e. Nest microservice controllers. */ - defineWorkers?(): Array>; - + workers?: Array>; /** * @description * The plugin may define custom database entities, which should be defined as classes annotated as per the * TypeORM documentation. */ - defineEntities?(): Array>; + entities?: Array>; } + +/** + * @description + * A VendurePlugin is a means of configuring and/or extending the functionality of the Vendure server. A Vendure Plugin is + * a Nestjs Module, with optional additional metadata defining things like extensions to the GraphQL API, custom + * configuration or new database entities. + * + * As well as configuring the app, a plugin may also extend the GraphQL schema by extending existing types or adding + * entirely new types. Database entities and resolvers can also be defined to handle the extended GraphQL types. + * + * @docsCategory plugin + */ +export function VendurePlugin(pluginMetadata: VendurePluginMetadata): ClassDecorator { + // tslint:disable-next-line:ban-types + return (target: Function) => { + for (const metadataProperty of Object.values(PLUGIN_METADATA)) { + const property = metadataProperty as keyof VendurePluginMetadata; + if (pluginMetadata[property] != null) { + Reflect.defineMetadata(property, pluginMetadata[property], target); + } + } + const nestModuleMetadata = pick(pluginMetadata, Object.values(METADATA) as any); + Module(nestModuleMetadata)(target); + }; +} + +export interface OnVendureBootstrap { + onVendureBootstrap(): void | Promise; +} + +export interface OnVendureWorkerBootstrap { + onVendureWorkerBootstrap(): void | Promise; +} + +export interface OnVendureClose { + onVendureClose(): void | Promise; +} + +export interface OnVendureWorkerClose { + onVendureWorkerClose(): void | Promise; +} + +export type PluginLifecycleMethods = OnVendureBootstrap & + OnVendureWorkerBootstrap & + OnVendureClose & + OnVendureWorkerClose; diff --git a/packages/core/src/service/service.module.ts b/packages/core/src/service/service.module.ts index 28a4aefa28..3127663fcb 100644 --- a/packages/core/src/service/service.module.ts +++ b/packages/core/src/service/service.module.ts @@ -88,10 +88,7 @@ let workerTypeOrmModule: DynamicModule; * into a format suitable for the service layer logic. */ @Module({ - imports: [ - ConfigModule, - EventBusModule, - ], + imports: [ConfigModule, EventBusModule], providers: [ ...exportedProviders, PasswordCiper, @@ -148,9 +145,7 @@ export class ServiceModule implements OnModuleInit { } return { module: ServiceModule, - imports: [ - defaultTypeOrmModule, - ], + imports: [defaultTypeOrmModule], }; } @@ -183,4 +178,11 @@ export class ServiceModule implements OnModuleInit { imports: [workerTypeOrmModule], }; } + + static forPlugin(): DynamicModule { + return { + module: ServiceModule, + imports: [TypeOrmModule.forFeature()], + }; + } } diff --git a/packages/core/src/worker/worker.module.ts b/packages/core/src/worker/worker.module.ts index 15c82dd141..11913973a0 100644 --- a/packages/core/src/worker/worker.module.ts +++ b/packages/core/src/worker/worker.module.ts @@ -1,8 +1,6 @@ import { Module, OnApplicationShutdown, OnModuleDestroy } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; -import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; -import { getConfig } from '../config/config-helpers'; import { ConfigModule } from '../config/config.module'; import { Logger } from '../config/logger/vendure-logger'; import { PluginModule } from '../plugin/plugin.module'; @@ -12,11 +10,7 @@ import { MessageInterceptor } from './message-interceptor'; import { WorkerMonitor } from './worker-monitor'; @Module({ - imports: [ - ConfigModule, - ServiceModule.forWorker(), - PluginModule.forWorker(), - ], + imports: [ConfigModule, ServiceModule.forWorker(), PluginModule.forWorker()], providers: [ WorkerMonitor, { @@ -24,7 +18,6 @@ import { WorkerMonitor } from './worker-monitor'; useClass: MessageInterceptor, }, ], - controllers: getWorkerControllers(), }) export class WorkerModule implements OnModuleDestroy, OnApplicationShutdown { constructor(private monitor: WorkerMonitor) {} @@ -38,11 +31,3 @@ export class WorkerModule implements OnModuleDestroy, OnApplicationShutdown { } } } - -function getWorkerControllers() { - const plugins = getConfig().plugins; - return plugins - .map(p => (p.defineWorkers ? p.defineWorkers() : undefined)) - .filter(notNullOrUndefined) - .reduce((flattened, controllers) => flattened.concat(controllers), []); -}