Skip to content

Commit

Permalink
feat(core): Rewrite plugin system to use Nest modules
Browse files Browse the repository at this point in the history
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 }) ],
    ```
  • Loading branch information
michaelbromley committed Jul 25, 2019
1 parent 81b3dc8 commit 7ec309b
Show file tree
Hide file tree
Showing 24 changed files with 800 additions and 511 deletions.
302 changes: 180 additions & 122 deletions packages/core/e2e/default-search-plugin.e2e-spec.ts

Large diffs are not rendered by default.

128 changes: 78 additions & 50 deletions packages/core/e2e/fixtures/test-plugins.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
TestPluginWithAllLifecycleHooks.onBootstrapFn();
}
onVendureWorkerBootstrap(): void | Promise<void> {
TestPluginWithAllLifecycleHooks.onWorkerBootstrapFn();
}
onVendureClose(): void | Promise<void> {
TestPluginWithAllLifecycleHooks.onCloseFn();
}
onVendureWorkerClose(): void | Promise<void> {
TestPluginWithAllLifecycleHooks.onWorkerCloseFn();
}
}

Expand All @@ -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'];
Expand All @@ -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<VendureConfig>): Required<VendureConfig> {
@VendurePlugin({
imports: [ConfigModule],
configuration(config: Required<VendureConfig>): Required<VendureConfig> {
// 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);
}
}
43 changes: 34 additions & 9 deletions packages/core/e2e/plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(
Expand All @@ -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,
],
},
);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
});
});
});
29 changes: 17 additions & 12 deletions packages/core/e2e/shop-auth.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
/* 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';
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 {
Expand Down Expand Up @@ -58,7 +60,7 @@ describe('Shop auth & accounts', () => {
customerCount: 2,
},
{
plugins: [new TestEmailPlugin()],
plugins: [TestEmailPlugin],
},
);
await shopClient.init();
Expand Down Expand Up @@ -517,7 +519,7 @@ describe('Expiring tokens', () => {
customerCount: 1,
},
{
plugins: [new TestEmailPlugin()],
plugins: [TestEmailPlugin],
authOptions: {
verificationTokenDuration: '1ms',
},
Expand Down Expand Up @@ -607,7 +609,7 @@ describe('Registration without email verification', () => {
customerCount: 1,
},
{
plugins: [new TestEmailPlugin()],
plugins: [TestEmailPlugin],
authOptions: {
requireVerification: false,
},
Expand Down Expand Up @@ -683,7 +685,7 @@ describe('Updating email address without email verification', () => {
customerCount: 1,
},
{
plugins: [new TestEmailPlugin()],
plugins: [TestEmailPlugin],
authOptions: {
requireVerification: false,
},
Expand Down Expand Up @@ -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);
});
}
Expand Down
9 changes: 5 additions & 4 deletions packages/core/e2e/test-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -108,13 +108,14 @@ export class TestServer {
/**
* Bootstraps an instance of the Vendure server for testing against.
*/
private async bootstrapForTesting(userConfig: Partial<VendureConfig>): Promise<[INestApplication, INestMicroservice | undefined]> {
private async bootstrapForTesting(
userConfig: Partial<VendureConfig>,
): 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');
Expand Down
19 changes: 10 additions & 9 deletions packages/core/src/api/api-internal-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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 {}
Expand All @@ -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 {}

0 comments on commit 7ec309b

Please sign in to comment.