Jelajahi Sumber

feat(core): Group api options in VendureConfig

Closes #327

BREAKING CHANGE: Options in the VendureConfig related to the API have been moved into a new location: `VendureConfig.apiOptions`. The affected options are `hostname`, `port`, `adminApiPath`, `shopApiPath`, `channelTokenKey`, `cors`, `middleware` and `apolloServerPlugins`.

```TypeScript
// before
const config: VendureConfig = {
  port: 3000,
  middleware: [/*...*/],
  // ...
}

// after
const config: VendureConfig = {
  apiOptions: {
      port: 3000,
      middleware: [/*...*/],
  },
  // ...
}
```

This also applies to the `ConfigService`, in case you are using it in a custom plugin.
Michael Bromley 5 tahun lalu
induk
melakukan
6904743f78

+ 8 - 5
packages/admin-ui-plugin/src/plugin.ts

@@ -21,7 +21,7 @@ import fs from 'fs-extra';
 import { Server } from 'http';
 import path from 'path';
 
-import { DEFAULT_APP_PATH, defaultAvailableLanguages, defaultLanguage, loggerCtx } from './constants';
+import { defaultAvailableLanguages, defaultLanguage, DEFAULT_APP_PATH, loggerCtx } from './constants';
 
 /**
  * @description
@@ -137,7 +137,7 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
         } else {
             port = this.options.port;
         }
-        config.middleware.push({
+        config.apiOptions.middleware.push({
             handler: createProxyHandler({
                 hostname: this.options.hostname,
                 port,
@@ -148,7 +148,7 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
             route,
         });
         if (this.isDevModeApp(app)) {
-            config.middleware.push({
+            config.apiOptions.middleware.push({
                 handler: createProxyHandler({
                     hostname: this.options.hostname,
                     port,
@@ -244,9 +244,12 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
             return partialConfig ? (partialConfig as AdminUiConfig)[prop] || defaultVal : defaultVal;
         };
         return {
-            adminApiPath: propOrDefault('adminApiPath', this.configService.adminApiPath),
+            adminApiPath: propOrDefault('adminApiPath', this.configService.apiOptions.adminApiPath),
             apiHost: propOrDefault('apiHost', AdminUiPlugin.options.apiHost || 'http://localhost'),
-            apiPort: propOrDefault('apiPort', AdminUiPlugin.options.apiPort || this.configService.port),
+            apiPort: propOrDefault(
+                'apiPort',
+                AdminUiPlugin.options.apiPort || this.configService.apiOptions.port,
+            ),
             tokenMethod: propOrDefault('tokenMethod', authOptions.tokenMethod || 'cookie'),
             authTokenHeaderKey: propOrDefault(
                 'authTokenHeaderKey',

+ 3 - 1
packages/asset-server-plugin/e2e/asset-server-plugin.e2e-spec.ts

@@ -21,7 +21,9 @@ describe('AssetServerPlugin', () => {
 
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig, {
-            port: 5050,
+            apiOptions: {
+                port: 5050,
+            },
             workerOptions: {
                 options: {
                     port: 5055,

+ 1 - 1
packages/asset-server-plugin/src/plugin.ts

@@ -155,7 +155,7 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
         config.assetOptions.assetStorageStrategy = this.assetStorage;
         config.assetOptions.assetNamingStrategy =
             this.options.namingStrategy || new HashedAssetNamingStrategy();
-        config.middleware.push({
+        config.apiOptions.middleware.push({
             handler: createProxyHandler({ ...this.options, label: 'Asset Server' }),
             route: this.options.route,
         });

+ 9 - 5
packages/core/e2e/apollo-server-plugin.e2e-spec.ts

@@ -1,3 +1,4 @@
+import { mergeConfig } from '@vendure/core';
 import {
     ApolloServerPlugin,
     GraphQLRequestContext,
@@ -8,7 +9,7 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 import { createTestEnvironment } from '../../testing/lib/create-test-environment';
 
 class MyApolloServerPlugin implements ApolloServerPlugin {
@@ -38,10 +39,13 @@ class MyApolloServerPlugin implements ApolloServerPlugin {
 }
 
 describe('custom apolloServerPlugins', () => {
-    const { server, adminClient, shopClient } = createTestEnvironment({
-        ...testConfig,
-        apolloServerPlugins: [new MyApolloServerPlugin()],
-    });
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            apiOptions: {
+                apolloServerPlugins: [new MyApolloServerPlugin()],
+            },
+        }),
+    );
 
     beforeAll(async () => {
         await server.init({

+ 4 - 4
packages/core/e2e/plugin.e2e-spec.ts

@@ -5,7 +5,7 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import { TestPluginWithAllLifecycleHooks } from './fixtures/test-plugins/with-all-lifecycle-hooks';
 import { TestAPIExtensionPlugin } from './fixtures/test-plugins/with-api-extensions';
@@ -129,7 +129,7 @@ describe('Plugins', () => {
     });
 
     describe('REST plugins', () => {
-        const restControllerUrl = `http://localhost:${testConfig.port}/test`;
+        const restControllerUrl = `http://localhost:${testConfig.apiOptions.port}/test`;
 
         it('public route', async () => {
             const response = await shopClient.fetch(restControllerUrl + '/public');
@@ -164,7 +164,7 @@ describe('Plugins', () => {
     describe('processContext', () => {
         it('server context', async () => {
             const response = await shopClient.fetch(
-                `http://localhost:${testConfig.port}/process-context/server`,
+                `http://localhost:${testConfig.apiOptions.port}/process-context/server`,
             );
             const body = await response.text();
 
@@ -172,7 +172,7 @@ describe('Plugins', () => {
         });
         it('worker context', async () => {
             const response = await shopClient.fetch(
-                `http://localhost:${testConfig.port}/process-context/worker`,
+                `http://localhost:${testConfig.apiOptions.port}/process-context/worker`,
             );
             const body = await response.text();
 

+ 2 - 2
packages/core/src/api/api.module.ts

@@ -27,7 +27,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
         ShopApiModule,
         configureGraphQLModule(configService => ({
             apiType: 'shop',
-            apiPath: configService.shopApiPath,
+            apiPath: configService.apiOptions.shopApiPath,
             typePaths: ['type', 'shop-api', 'common'].map(p =>
                 path.join(__dirname, 'schema', p, '*.graphql'),
             ),
@@ -35,7 +35,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
         })),
         configureGraphQLModule(configService => ({
             apiType: 'admin',
-            apiPath: configService.adminApiPath,
+            apiPath: configService.apiOptions.adminApiPath,
             typePaths: ['type', 'admin-api', 'common'].map(p =>
                 path.join(__dirname, 'schema', p, '*.graphql'),
             ),

+ 2 - 2
packages/core/src/api/common/request-context.service.ts

@@ -54,7 +54,7 @@ export class RequestContextService {
     }
 
     private getChannelToken(req: Request): string {
-        const tokenKey = this.configService.channelTokenKey;
+        const tokenKey = this.configService.apiOptions.channelTokenKey;
         let channelToken = '';
 
         if (req && req.query && req.query[tokenKey]) {
@@ -86,7 +86,7 @@ export class RequestContextService {
             return false;
         }
         const permissionsOnChannel = user.roles
-            .filter((role) => role.channels.find((c) => idsAreEqual(c.id, channel.id)))
+            .filter(role => role.channels.find(c => idsAreEqual(c.id, channel.id)))
             .reduce((output, role) => [...output, ...role.permissions], [] as Permission[]);
         return this.arraysIntersect(permissions, permissionsOnChannel);
     }

+ 1 - 1
packages/core/src/api/config/configure-graphql-module.ts

@@ -146,7 +146,7 @@ async function createGraphQLOptions(
             new IdCodecPlugin(idCodecService),
             new TranslateErrorsPlugin(i18nService),
             new AssetInterceptorPlugin(configService),
-            ...configService.apolloServerPlugins,
+            ...configService.apiOptions.apolloServerPlugins,
         ],
     } as GqlModuleOptions;
 

+ 3 - 3
packages/core/src/app.module.ts

@@ -38,7 +38,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
     }
 
     configure(consumer: MiddlewareConsumer) {
-        const { adminApiPath, shopApiPath } = this.configService;
+        const { adminApiPath, shopApiPath, middleware } = this.configService.apiOptions;
         const i18nextHandler = this.i18nService.handle();
         const defaultMiddleware: Array<{ handler: RequestHandler; route?: string }> = [
             { handler: i18nextHandler, route: adminApiPath },
@@ -53,7 +53,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
             defaultMiddleware.push({ handler: cookieHandler, route: adminApiPath });
             defaultMiddleware.push({ handler: cookieHandler, route: shopApiPath });
         }
-        const allMiddleware = defaultMiddleware.concat(this.configService.middleware);
+        const allMiddleware = defaultMiddleware.concat(middleware);
         const middlewareByRoute = this.groupMiddlewareByRoute(allMiddleware);
         for (const [route, handlers] of Object.entries(middlewareByRoute)) {
             consumer.apply(...handlers).forRoutes(route);
@@ -76,7 +76,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
     ): { [route: string]: RequestHandler[] } {
         const result = {} as { [route: string]: RequestHandler[] };
         for (const middleware of middlewareArray) {
-            const route = middleware.route || this.configService.adminApiPath;
+            const route = middleware.route || this.configService.apiOptions.adminApiPath;
             if (!result[route]) {
                 result[route] = [];
             }

+ 32 - 9
packages/core/src/bootstrap.ts

@@ -43,14 +43,15 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
     // config, so that they are available when the AppModule decorator is evaluated.
     // tslint:disable-next-line:whitespace
     const appModule = await import('./app.module');
+    const { hostname, port, cors } = config.apiOptions;
     DefaultLogger.hideNestBoostrapLogs();
     const app = await NestFactory.create(appModule.AppModule, {
-        cors: config.cors,
+        cors,
         logger: new Logger(),
     });
     DefaultLogger.restoreOriginalLogLevel();
     app.useLogger(new Logger());
-    await app.listen(config.port, config.hostname);
+    await app.listen(port, hostname || '');
     app.enableShutdownHooks();
     if (config.workerOptions.runInMainProcess) {
         try {
@@ -142,8 +143,9 @@ async function bootstrapWorkerInternal(
  */
 export async function preBootstrapConfig(
     userConfig: Partial<VendureConfig>,
-): Promise<ReadOnlyRequired<VendureConfig>> {
+): Promise<Readonly<RuntimeVendureConfig>> {
     if (userConfig) {
+        checkForDeprecatedOptions(userConfig);
         setConfig(userConfig);
     }
 
@@ -207,10 +209,10 @@ export async function getAllEntities(userConfig: Partial<VendureConfig>): Promis
  * If the 'bearer' tokenMethod is being used, then we automatically expose the authTokenHeaderKey header
  * in the CORS options, making sure to preserve any user-configured exposedHeaders.
  */
-function setExposedHeaders(config: ReadOnlyRequired<VendureConfig>) {
+function setExposedHeaders(config: Readonly<RuntimeVendureConfig>) {
     if (config.authOptions.tokenMethod === 'bearer') {
         const authTokenHeaderKey = config.authOptions.authTokenHeaderKey as string;
-        const corsOptions = config.cors;
+        const corsOptions = config.apiOptions.cors;
         if (typeof corsOptions !== 'boolean') {
             const { exposedHeaders } = corsOptions;
             let exposedHeadersWithAuthKey: string[];
@@ -257,17 +259,18 @@ function workerWelcomeMessage(config: VendureConfig) {
     Logger.info(`Vendure Worker started${transportString}${connectionString}`);
 }
 
-function logWelcomeMessage(config: VendureConfig) {
+function logWelcomeMessage(config: RuntimeVendureConfig) {
     let version: string;
     try {
         version = require('../package.json').version;
     } catch (e) {
         version = ' unknown';
     }
+    const { port, shopApiPath, adminApiPath } = config.apiOptions;
     Logger.info(`=================================================`);
-    Logger.info(`Vendure server (v${version}) now running on port ${config.port}`);
-    Logger.info(`Shop API: http://localhost:${config.port}/${config.shopApiPath}`);
-    Logger.info(`Admin API: http://localhost:${config.port}/${config.adminApiPath}`);
+    Logger.info(`Vendure server (v${version}) now running on port ${port}`);
+    Logger.info(`Shop API: http://localhost:${port}/${shopApiPath}`);
+    Logger.info(`Admin API: http://localhost:${port}/${adminApiPath}`);
     logProxyMiddlewares(config);
     Logger.info(`=================================================`);
 }
@@ -284,3 +287,23 @@ function disableSynchronize(userConfig: ReadOnlyRequired<VendureConfig>): ReadOn
     } as ConnectionOptions;
     return config;
 }
+
+function checkForDeprecatedOptions(config: Partial<VendureConfig>) {
+    const deprecatedApiOptions = [
+        'hostname',
+        'port',
+        'adminApiPath',
+        'shopApiPath',
+        'channelTokenKey',
+        'cors',
+        'middleware',
+        'apolloServerPlugins',
+    ];
+    const deprecatedOptionsUsed = deprecatedApiOptions.filter(option => config.hasOwnProperty(option));
+    if (deprecatedOptionsUsed.length) {
+        throw new Error(
+            `The following VendureConfig options are deprecated: ${deprecatedOptionsUsed.join(', ')}\n` +
+                `They have been moved to the "apiOptions" object. Please update your configuration.`,
+        );
+    }
+}

+ 12 - 8
packages/core/src/config/config.service.mock.ts

@@ -5,13 +5,18 @@ import { ConfigService } from './config.service';
 import { EntityIdStrategy, PrimaryKeyType } from './entity-id-strategy/entity-id-strategy';
 
 export class MockConfigService implements MockClass<ConfigService> {
+    apiOptions = {
+        channelTokenKey: 'vendure-token',
+        adminApiPath: 'admin-api',
+        shopApiPath: 'shop-api',
+        port: 3000,
+        cors: false,
+        middleware: [],
+        apolloServerPlugins: [],
+    };
     authOptions: {};
     defaultChannelToken: 'channel-token';
-    channelTokenKey: 'vendure-token';
-    adminApiPath = 'admin-api';
-    shopApiPath = 'shop-api';
-    port = 3000;
-    cors = false;
+
     defaultLanguageCode: jest.Mock<any>;
     roundingStrategy: {};
     entityIdStrategy = new MockIdStrategy();
@@ -35,10 +40,9 @@ export class MockConfigService implements MockClass<ConfigService> {
     orderOptions = {};
     workerOptions = {};
     customFields = {};
-    middleware = [];
-    logger = {} as any;
-    apolloServerPlugins = [];
+
     plugins = [];
+    logger = {} as any;
     jobQueueOptions = {};
 }
 

+ 7 - 29
packages/core/src/config/config.service.ts

@@ -10,8 +10,10 @@ import { CustomFields } from './custom-field/custom-field-types';
 import { EntityIdStrategy } from './entity-id-strategy/entity-id-strategy';
 import { Logger, VendureLogger } from './logger/vendure-logger';
 import {
+    ApiOptions,
     AssetOptions,
-    AuthOptions, CatalogOptions,
+    AuthOptions,
+    CatalogOptions,
     ImportExportOptions,
     JobQueueOptions,
     OrderOptions,
@@ -36,6 +38,10 @@ export class ConfigService implements VendureConfig {
         }
     }
 
+    get apiOptions(): Required<ApiOptions> {
+        return this.activeConfig.apiOptions;
+    }
+
     get authOptions(): Required<AuthOptions> {
         return this.activeConfig.authOptions;
     }
@@ -48,30 +54,10 @@ export class ConfigService implements VendureConfig {
         return this.activeConfig.defaultChannelToken;
     }
 
-    get channelTokenKey(): string {
-        return this.activeConfig.channelTokenKey;
-    }
-
     get defaultLanguageCode(): LanguageCode {
         return this.activeConfig.defaultLanguageCode;
     }
 
-    get adminApiPath(): string {
-        return this.activeConfig.adminApiPath;
-    }
-
-    get shopApiPath(): string {
-        return this.activeConfig.shopApiPath;
-    }
-
-    get port(): number {
-        return this.activeConfig.port;
-    }
-
-    get cors(): boolean | CorsOptions {
-        return this.activeConfig.cors;
-    }
-
     get entityIdStrategy(): EntityIdStrategy {
         return this.activeConfig.entityIdStrategy;
     }
@@ -112,14 +98,6 @@ export class ConfigService implements VendureConfig {
         return this.activeConfig.customFields;
     }
 
-    get middleware(): Array<{ handler: RequestHandler; route: string }> {
-        return this.activeConfig.middleware;
-    }
-
-    get apolloServerPlugins(): PluginDefinition[] {
-        return this.activeConfig.apolloServerPlugins;
-    }
-
     get plugins(): Array<DynamicModule | Type<any>> {
         return this.activeConfig.plugins;
     }

+ 14 - 11
packages/core/src/config/default-config.ts

@@ -29,16 +29,22 @@ import { RuntimeVendureConfig } from './vendure-config';
  * @docsCategory configuration
  */
 export const defaultConfig: RuntimeVendureConfig = {
-    channelTokenKey: 'vendure-token',
     defaultChannelToken: null,
     defaultLanguageCode: LanguageCode.en,
-    hostname: '',
-    port: 3000,
-    cors: {
-        origin: true,
-        credentials: true,
-    },
     logger: new DefaultLogger(),
+    apiOptions: {
+        hostname: '',
+        port: 3000,
+        adminApiPath: 'admin-api',
+        shopApiPath: 'shop-api',
+        channelTokenKey: 'vendure-token',
+        cors: {
+            origin: true,
+            credentials: true,
+        },
+        middleware: [],
+        apolloServerPlugins: [],
+    },
     authOptions: {
         disableAuth: false,
         tokenMethod: 'cookie',
@@ -51,8 +57,7 @@ export const defaultConfig: RuntimeVendureConfig = {
     catalogOptions: {
         collectionFilters: defaultCollectionFilters,
     },
-    adminApiPath: 'admin-api',
-    shopApiPath: 'shop-api',
+
     entityIdStrategy: new AutoIncrementIdStrategy(),
     assetOptions: {
         assetNamingStrategy: new DefaultAssetNamingStrategy(),
@@ -116,7 +121,5 @@ export const defaultConfig: RuntimeVendureConfig = {
         ProductVariant: [],
         User: [],
     },
-    middleware: [],
-    apolloServerPlugins: [],
     plugins: [],
 };

+ 74 - 58
packages/core/src/config/vendure-config.ts

@@ -29,6 +29,78 @@ import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibili
 import { TaxCalculationStrategy } from './tax/tax-calculation-strategy';
 import { TaxZoneStrategy } from './tax/tax-zone-strategy';
 
+/**
+ * @description
+ * The ApiOptions define how the Vendure GraphQL APIs are exposed, as well as allowing the API layer
+ * to be extended with middleware.
+ *
+ * @docsCategory configuration
+ */
+export interface ApiOptions {
+    /**
+     * @description
+     * Set the hostname of the server. If not set, the server will be available on localhost.
+     *
+     * @default ''
+     */
+    hostname?: string;
+    /**
+     * @description
+     * Which port the Vendure server should listen on.
+     *
+     * @default 3000
+     */
+    port: number;
+    /**
+     * @description
+     * The path to the admin GraphQL API.
+     *
+     * @default 'admin-api'
+     */
+    adminApiPath?: string;
+    /**
+     * @description
+     * The path to the admin GraphQL API.
+     *
+     * @default 'shop-api'
+     */
+    shopApiPath?: string;
+    /**
+     * @description
+     * The name of the property which contains the token of the
+     * active channel. This property can be included either in
+     * the request header or as a query string.
+     *
+     * @default 'vendure-token'
+     */
+    channelTokenKey?: string;
+    /**
+     * @description
+     * Set the CORS handling for the server. See the [express CORS docs](https://github.com/expressjs/cors#configuration-options).
+     *
+     * @default { origin: true, credentials: true }
+     */
+    cors?: boolean | CorsOptions;
+    /**
+     * @description
+     * Custom Express middleware for the server.
+     *
+     * @default []
+     */
+    middleware?: Array<{ handler: RequestHandler; route: string }>;
+    /**
+     * @description
+     * Custom [ApolloServerPlugins](https://www.apollographql.com/docs/apollo-server/integrations/plugins/) which
+     * allow the extension of the Apollo Server, which is the underlying GraphQL server used by Vendure.
+     *
+     * Apollo plugins can be used e.g. to perform custom data transformations on incoming operations or outgoing
+     * data.
+     *
+     * @default []
+     */
+    apolloServerPlugins?: PluginDefinition[];
+}
+
 /**
  * @description
  * The AuthOptions define how authentication is managed.
@@ -431,18 +503,9 @@ export interface JobQueueOptions {
 export interface VendureConfig {
     /**
      * @description
-     * The path to the admin GraphQL API.
-     *
-     * @default 'admin-api'
-     */
-    adminApiPath?: string;
-    /**
-     * @description
-     * The path to the admin GraphQL API.
      *
-     * @default 'shop-api'
      */
-    shopApiPath?: string;
+    apiOptions: ApiOptions;
     /**
      * @description
      * Configuration for the handling of Assets.
@@ -458,22 +521,6 @@ export interface VendureConfig {
      * Configuration for Products and Collections.
      */
     catalogOptions?: CatalogOptions;
-    /**
-     * @description
-     * The name of the property which contains the token of the
-     * active channel. This property can be included either in
-     * the request header or as a query string.
-     *
-     * @default 'vendure-token'
-     */
-    channelTokenKey?: string;
-    /**
-     * @description
-     * Set the CORS handling for the server. See the [express CORS docs](https://github.com/expressjs/cors#configuration-options).
-     *
-     * @default { origin: true, credentials: true }
-     */
-    cors?: boolean | CorsOptions;
     /**
      * @description
      * Defines custom fields which can be used to extend the built-in entities.
@@ -511,13 +558,6 @@ export interface VendureConfig {
      * @default new AutoIncrementIdStrategy()
      */
     entityIdStrategy?: EntityIdStrategy<any>;
-    /**
-     * @description
-     * Set the hostname of the server. If not set, the server will be available on localhost.
-     *
-     * @default ''
-     */
-    hostname?: string;
     /**
      * @description
      * Configuration settings for data import and export.
@@ -528,24 +568,6 @@ export interface VendureConfig {
      * Configuration settings governing how orders are handled.
      */
     orderOptions?: OrderOptions;
-    /**
-     * @description
-     * Custom Express middleware for the server.
-     *
-     * @default []
-     */
-    middleware?: Array<{ handler: RequestHandler; route: string }>;
-    /**
-     * @description
-     * Custom [ApolloServerPlugins](https://www.apollographql.com/docs/apollo-server/integrations/plugins/) which
-     * allow the extension of the Apollo Server, which is the underlying GraphQL server used by Vendure.
-     *
-     * Apollo plugins can be used e.g. to perform custom data transformations on incoming operations or outgoing
-     * data.
-     *
-     * @default []
-     */
-    apolloServerPlugins?: PluginDefinition[];
     /**
      * @description
      * Configures available payment processing methods.
@@ -558,13 +580,6 @@ export interface VendureConfig {
      * @default []
      */
     plugins?: Array<DynamicModule | Type<any>>;
-    /**
-     * @description
-     * Which port the Vendure server should listen on.
-     *
-     * @default 3000
-     */
-    port: number;
     /**
      * @description
      * Configures the Conditions and Actions available when creating Promotions.
@@ -607,6 +622,7 @@ export interface VendureConfig {
  * @docsCategory configuration
  */
 export interface RuntimeVendureConfig extends Required<VendureConfig> {
+    apiOptions: Required<ApiOptions>;
     assetOptions: Required<AssetOptions>;
     authOptions: Required<AuthOptions>;
     customFields: Required<CustomFields>;

+ 8 - 7
packages/core/src/plugin/plugin-utils.ts

@@ -1,7 +1,7 @@
 import { RequestHandler } from 'express';
 import { createProxyMiddleware } from 'http-proxy-middleware';
 
-import { Logger, VendureConfig } from '../config';
+import { Logger, RuntimeVendureConfig, VendureConfig } from '../config';
 
 /**
  * @description
@@ -17,7 +17,7 @@ import { Logger, VendureConfig } from '../config';
  * // route of the main Vendure server.
  * \@VendurePlugin({
  *   configure: (config: Required<VendureConfig>) => {
- *       config.middleware.push({
+ *       config.apiOptions.middleware.push({
  *           handler: createProxyHandler({
  *               label: 'Admin UI',
  *               route: 'my-plugin',
@@ -110,15 +110,16 @@ export interface ProxyOptions {
 /**
  * If any proxy middleware has been set up using the createProxyHandler function, log this information.
  */
-export function logProxyMiddlewares(config: VendureConfig) {
-    for (const middleware of config.middleware || []) {
+export function logProxyMiddlewares(config: RuntimeVendureConfig) {
+    const {} = config.apiOptions;
+    for (const middleware of config.apiOptions.middleware || []) {
         if ((middleware.handler as any).proxyMiddleware) {
             const { port, hostname, label, route, basePath } = (middleware.handler as any)
                 .proxyMiddleware as ProxyOptions;
             Logger.info(
-                `${label}: http://${config.hostname || 'localhost'}:${config.port}/${route}/ -> http://${
-                    hostname || 'localhost'
-                }:${port}${basePath ? `/${basePath}` : ''}`,
+                `${label}: http://${config.apiOptions.hostname || 'localhost'}:${
+                    config.apiOptions.port
+                }/${route}/ -> http://${hostname || 'localhost'}:${port}${basePath ? `/${basePath}` : ''}`,
             );
         }
     }

+ 5 - 3
packages/create/templates/vendure-config.hbs

@@ -26,12 +26,14 @@ const path = require('path');
 {{/if}}
 
 {{#if isTs}}export {{/if}}const config{{#if isTs}}: VendureConfig{{/if}} = {
+    apiOptions: {
+        port: 3000,
+        adminApiPath: 'admin-api',
+        shopApiPath: 'shop-api',
+    },
     authOptions: {
         sessionSecret: '{{ sessionSecret }}',
     },
-    port: 3000,
-    adminApiPath: 'admin-api',
-    shopApiPath: 'shop-api',
     dbConnectionOptions: {
         type: '{{ dbType }}',
         {{#if requiresConnection}}

+ 4 - 3
packages/dev-server/dev-config.ts

@@ -19,14 +19,15 @@ import { ConnectionOptions } from 'typeorm';
  * Config settings used during development
  */
 export const devConfig: VendureConfig = {
+    apiOptions: {
+        port: API_PORT,
+        adminApiPath: ADMIN_API_PATH,
+    },
     authOptions: {
         disableAuth: false,
         sessionSecret: 'some-secret',
         requireVerification: true,
     },
-    port: API_PORT,
-    adminApiPath: ADMIN_API_PATH,
-    shopApiPath: SHOP_API_PATH,
     dbConnectionOptions: {
         synchronize: false,
         logging: false,

+ 3 - 1
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -77,7 +77,9 @@ if (process.env.CI) {
 describe('Elasticsearch plugin', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig, {
-            port: 4050,
+            apiOptions: {
+                port: 4050,
+            },
             workerOptions: {
                 options: {
                     port: 4055,

+ 6 - 6
packages/email-plugin/src/plugin.ts

@@ -143,7 +143,7 @@ import {
     imports: [PluginCommonModule],
     providers: [{ provide: EMAIL_PLUGIN_OPTIONS, useFactory: () => EmailPlugin.options }],
     workers: [EmailProcessorController],
-    configuration: (config) => EmailPlugin.configure(config),
+    configuration: config => EmailPlugin.configure(config),
 })
 export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
     private static options: EmailPluginOptions | EmailPluginDevModeOptions;
@@ -172,7 +172,7 @@ export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
     static configure(config: RuntimeVendureConfig): RuntimeVendureConfig {
         if (isDevModeOptions(this.options) && this.options.mailboxPort !== undefined) {
             const route = 'mailbox';
-            config.middleware.push({
+            config.apiOptions.middleware.push({
                 handler: createProxyHandler({ port: this.options.mailboxPort, route, label: 'Dev Mailbox' }),
                 route,
             });
@@ -201,10 +201,10 @@ export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
             this.jobQueue = this.jobQueueService.createQueue({
                 name: 'send-email',
                 concurrency: 5,
-                process: (job) => {
+                process: job => {
                     this.workerService.send(new EmailWorkerMessage(job.data)).subscribe({
                         complete: () => job.complete(),
-                        error: (err) => job.fail(err),
+                        error: err => job.fail(err),
                     });
                 },
             });
@@ -220,7 +220,7 @@ export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
 
     private async setupEventSubscribers() {
         for (const handler of EmailPlugin.options.handlers) {
-            this.eventBus.ofType(handler.event).subscribe((event) => {
+            this.eventBus.ofType(handler.event).subscribe(event => {
                 return this.handleEvent(handler, event);
             });
         }
@@ -237,7 +237,7 @@ export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
                 (event as EventWithAsyncData<EventWithContext, any>).data = await handler._loadDataFn({
                     event,
                     connection: this.connection,
-                    inject: (t) => this.moduleRef.get(t, { strict: false }),
+                    inject: t => this.moduleRef.get(t, { strict: false }),
                 });
             }
             const result = await handler.handle(event as any, EmailPlugin.options.globalTemplateVars);

+ 6 - 4
packages/testing/src/config/test-config.ts

@@ -28,10 +28,12 @@ export const E2E_DEFAULT_CHANNEL_TOKEN = 'e2e-default-channel';
  * @docsCategory testing
  */
 export const testConfig: Required<VendureConfig> = mergeConfig(defaultConfig, {
-    port: 3050,
-    adminApiPath: ADMIN_API_PATH,
-    shopApiPath: SHOP_API_PATH,
-    cors: true,
+    apiOptions: {
+        port: 3050,
+        adminApiPath: ADMIN_API_PATH,
+        shopApiPath: SHOP_API_PATH,
+        cors: true,
+    },
     defaultChannelToken: E2E_DEFAULT_CHANNEL_TOKEN,
     authOptions: {
         sessionSecret: 'some-secret',

+ 3 - 8
packages/testing/src/create-test-environment.ts

@@ -59,14 +59,9 @@ export interface TestEnvironment {
  */
 export function createTestEnvironment(config: Required<VendureConfig>): TestEnvironment {
     const server = new TestServer(config);
-    const adminClient = new SimpleGraphQLClient(
-        config,
-        `http://localhost:${config.port}/${config.adminApiPath}`,
-    );
-    const shopClient = new SimpleGraphQLClient(
-        config,
-        `http://localhost:${config.port}/${config.shopApiPath}`,
-    );
+    const { port, adminApiPath, shopApiPath } = config.apiOptions;
+    const adminClient = new SimpleGraphQLClient(config, `http://localhost:${port}/${adminApiPath}`);
+    const shopClient = new SimpleGraphQLClient(config, `http://localhost:${port}/${shopApiPath}`);
     return {
         server,
         adminClient,

+ 1 - 1
packages/testing/src/data-population/populate-customers.ts

@@ -13,7 +13,7 @@ export async function populateCustomers(
     logging: boolean = false,
     simpleGraphQLClient = new SimpleGraphQLClient(
         config,
-        `http://localhost:${config.port}/${config.adminApiPath}`,
+        `http://localhost:${config.apiOptions.port}/${config.apiOptions.adminApiPath}`,
     ),
 ) {
     const client = simpleGraphQLClient;

+ 4 - 2
packages/testing/src/simple-graphql-client.ts

@@ -53,7 +53,9 @@ export class SimpleGraphQLClient {
      */
     setChannelToken(token: string | null) {
         this.channelToken = token;
-        this.headers[this.vendureConfig.channelTokenKey] = this.channelToken;
+        if (this.vendureConfig.apiOptions.channelTokenKey) {
+            this.headers[this.vendureConfig.apiOptions.channelTokenKey] = this.channelToken;
+        }
     }
 
     /**
@@ -228,7 +230,7 @@ export class SimpleGraphQLClient {
             curl.setOpt(Curl.option.HTTPPOST, processedPostData);
             curl.setOpt(Curl.option.HTTPHEADER, [
                 `Authorization: Bearer ${this.authToken}`,
-                `${this.vendureConfig.channelTokenKey}: ${this.channelToken}`,
+                `${this.vendureConfig.apiOptions.channelTokenKey}: ${this.channelToken}`,
             ]);
             curl.perform();
             curl.on('end', (statusCode: any, body: any) => {

+ 3 - 3
packages/testing/src/test-server.ts

@@ -71,7 +71,7 @@ export class TestServer {
      */
     async destroy() {
         // allow a grace period of any outstanding async tasks to complete
-        await new Promise((resolve) => global.setTimeout(resolve, 500));
+        await new Promise(resolve => global.setTimeout(resolve, 500));
         await this.app.close();
         if (this.worker) {
             await this.worker.close();
@@ -130,11 +130,11 @@ export class TestServer {
         try {
             DefaultLogger.hideNestBoostrapLogs();
             const app = await NestFactory.create(appModule.AppModule, {
-                cors: config.cors,
+                cors: config.apiOptions.cors,
                 logger: new Logger(),
             });
             let worker: INestMicroservice | undefined;
-            await app.listen(config.port);
+            await app.listen(config.apiOptions.port);
             if (config.workerOptions.runInMainProcess) {
                 const workerModule = await import('@vendure/core/dist/worker/worker.module');
                 worker = await NestFactory.createMicroservice(workerModule.WorkerModule, {