Skip to content

Commit

Permalink
feat(core): Set up worker architecture based on Nest microservices
Browse files Browse the repository at this point in the history
Relates to #115
  • Loading branch information
michaelbromley committed Jun 17, 2019
1 parent 973acad commit 508bafd
Show file tree
Hide file tree
Showing 26 changed files with 704 additions and 662 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@nestjs/common": "^6.3.1",
"@nestjs/core": "^6.3.1",
"@nestjs/graphql": "^6.2.4",
"@nestjs/microservices": "^6.3.1",
"@nestjs/platform-express": "^6.3.1",
"@nestjs/testing": "^6.3.1",
"@nestjs/typeorm": "^6.0.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/api/api-internal-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,17 @@ export class ApiSharedModule {}
* The internal module containing the Admin GraphQL API resolvers
*/
@Module({
imports: [ApiSharedModule, PluginModule, ServiceModule, DataImportModule],
imports: [ApiSharedModule, PluginModule.forRoot(), ServiceModule.forRoot(), DataImportModule],
providers: [...adminResolvers, ...entityResolvers, ...adminEntityResolvers, ...PluginModule.adminApiResolvers()],
exports: adminResolvers,
exports: [...adminResolvers],
})
export class AdminApiModule {}

/**
* The internal module containing the Shop GraphQL API resolvers
*/
@Module({
imports: [ApiSharedModule, PluginModule, ServiceModule],
imports: [ApiSharedModule, PluginModule.forRoot(), ServiceModule.forRoot()],
providers: [...shopResolvers, ...entityResolvers, ...PluginModule.shopApiResolvers()],
exports: shopResolvers,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { IdInterceptor } from './middleware/id-interceptor';
*/
@Module({
imports: [
ServiceModule,
ServiceModule.forRoot(),
DataImportModule,
ApiSharedModule,
AdminApiModule,
Expand Down
113 changes: 113 additions & 0 deletions packages/core/src/api/common/request-context.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { CurrencyCode, LanguageCode } from '@vendure/common/lib/generated-types';

import { Channel } from '../../entity/channel/channel.entity';
import { Order } from '../../entity/order/order.entity';
import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
import { Session } from '../../entity/session/session.entity';
import { User } from '../../entity/user/user.entity';
import { Zone } from '../../entity/zone/zone.entity';

import { RequestContext } from './request-context';

describe('RequestContext', () => {

describe('fromObject()', () => {

let original: RequestContext;
let ctxObject: object;
let session: Session;
let user: User;
let channel: Channel;
let activeOrder: Order;
let zone: Zone;

beforeAll(() => {
activeOrder = new Order({
id: '55555',
active: true,
code: 'ADAWDJAWD',
});
user = new User({
id: '8833774',
verified: true,
});
session = new AuthenticatedSession({
id: '1234',
token: '2d37187e9e8fc47807fe4f58ca',
activeOrder,
user,
});
zone = new Zone({
id: '62626',
name: 'Europe',
});
channel = new Channel({
token: 'oiajwodij09au3r',
id: '995859',
code: '__default_channel__',
currencyCode: CurrencyCode.EUR,
pricesIncludeTax: true,
defaultLanguageCode: LanguageCode.en,
defaultShippingZone: zone,
defaultTaxZone: zone,
});
original = new RequestContext({
apiType: 'admin',
languageCode: LanguageCode.en,
channel,
session,
isAuthorized: true,
authorizedAsOwnerOnly: false,
});

ctxObject = JSON.parse(JSON.stringify(original));
});

it('apiType', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.apiType).toBe(original.apiType);
});

it('channelId', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.channelId).toBe(original.channelId);
});

it('languageCode', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.languageCode).toBe(original.languageCode);
});

it('activeUserId', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.activeUserId).toBe(original.activeUserId);
});

it('isAuthorized', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.isAuthorized).toBe(original.isAuthorized);
});

it('authorizedAsOwnerOnly', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.authorizedAsOwnerOnly).toBe(original.authorizedAsOwnerOnly);
});

it('channel', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.channel).toEqual(original.channel);
});

it('session', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.session).toEqual(original.session);
});

it('activeUser', () => {
const result = RequestContext.fromObject(ctxObject);
expect(result.activeUser).toEqual(original.activeUser);
});

});

});
29 changes: 29 additions & 0 deletions packages/core/src/api/common/request-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import i18next from 'i18next';

import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
import { Channel } from '../../entity/channel/channel.entity';
import { AnonymousSession } from '../../entity/session/anonymous-session.entity';
import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
import { Session } from '../../entity/session/session.entity';
import { User } from '../../entity/user/user.entity';
Expand Down Expand Up @@ -49,6 +50,34 @@ export class RequestContext {
this._translationFn = translationFn || (((key: string) => key) as any);
}

/**
* @description
* Creates a new RequestContext object from a plain object which is the result of
* a JSON serialization - deserialization operation.
*/
static fromObject(ctxObject: any): RequestContext {
let session: Session | undefined;
if (ctxObject._session) {
if (ctxObject._session.user) {
const user = new User(ctxObject._session.user);
session = new AuthenticatedSession({
...ctxObject._session,
user,
});
} else {
session = new AnonymousSession(ctxObject._session);
}
}
return new RequestContext({
apiType: ctxObject._apiType,
channel: new Channel(ctxObject._channel),
session,
languageCode: ctxObject._languageCode,
isAuthorized: ctxObject._isAuthorized,
authorizedAsOwnerOnly: ctxObject._authorizedAsOwnerOnly,
});
}

get apiType(): ApiType {
return this._apiType;
}
Expand Down
35 changes: 30 additions & 5 deletions packages/core/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { INestApplication } from '@nestjs/common';
import { INestApplication, INestMicroservice } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { Type } from '@vendure/common/lib/shared-types';
import { worker } from 'cluster';
import { EntitySubscriberInterface } from 'typeorm';

import { InternalServerError } from './common/error/errors';
Expand All @@ -19,26 +21,50 @@ export type VendureBootstrapFunction = (config: VendureConfig) => Promise<INestA
*/
export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INestApplication> {
const config = await preBootstrapConfig(userConfig);
Logger.info(`Bootstrapping Vendure Server...`);
Logger.useLogger(config.logger);
Logger.info(`Bootstrapping Vendure Server (pid: ${process.pid})...`);

// The AppModule *must* be loaded only after the entities have been set in the
// config, so that they are available when the AppModule decorator is evaluated.
// tslint:disable-next-line:whitespace
const appModule = await import('./app.module');
DefaultLogger.hideNestBoostrapLogs();
let app: INestApplication;
app = await NestFactory.create(appModule.AppModule, {
const app = await NestFactory.create(appModule.AppModule, {
cors: config.cors,
logger: new Logger(),
});
DefaultLogger.restoreOriginalLogLevel();
app.useLogger(new Logger());
await runPluginOnBootstrapMethods(config, app);
await app.listen(config.port, config.hostname);
if (config.workerOptions.runInMainProcess) {
await bootstrapWorker(config);
}
logWelcomeMessage(config);
return app;
}

export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promise<INestMicroservice> {
const config = await preBootstrapConfig(userConfig);
if ((config.logger as any).setDefaultContext) {
(config.logger as any).setDefaultContext('Vendure Worker');
}
Logger.useLogger(config.logger);
Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);

const workerModule = await import('./worker/worker.module');
DefaultLogger.hideNestBoostrapLogs();
const workerApp = await NestFactory.createMicroservice(workerModule.WorkerModule, {
transport: config.workerOptions.transport,
logger: new Logger(),
options: config.workerOptions.options,
});
DefaultLogger.restoreOriginalLogLevel();
workerApp.useLogger(new Logger());
await workerApp.listenAsync();
return workerApp;
}

/**
* Setting the global config must be done prior to loading the AppModule.
*/
Expand All @@ -64,7 +90,6 @@ export async function preBootstrapConfig(
});

let config = getConfig();
Logger.useLogger(config.logger);
config = await runPluginConfigurations(config);
registerCustomEntityFields(config);
return config;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/config.service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class MockConfigService implements MockClass<ConfigService> {
emailOptions: {};
importExportOptions: {};
orderOptions = {};
workerOptions = {};
customFields = {};
middleware = [];
logger = {} as any;
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ShippingOptions,
TaxOptions,
VendureConfig,
WorkerOptions,
} from './vendure-config';
import { VendurePlugin } from './vendure-plugin/vendure-plugin';

Expand Down Expand Up @@ -120,4 +121,8 @@ export class ConfigService implements VendureConfig {
get logger(): VendureLogger {
return this.activeConfig.logger;
}

get workerOptions(): WorkerOptions {
return this.activeConfig.workerOptions as Required<WorkerOptions>;
}
}
7 changes: 6 additions & 1 deletion packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Transport } from '@nestjs/microservices';
import { LanguageCode } from '@vendure/common/lib/generated-types';
import { ADMIN_API_PATH, API_PORT } from '@vendure/common/lib/shared-constants';
import { CustomFields } from '@vendure/common/lib/shared-types';

import { ReadOnlyRequired } from '../common/types/common-types';
Expand Down Expand Up @@ -80,6 +80,11 @@ export const defaultConfig: ReadOnlyRequired<VendureConfig> = {
importExportOptions: {
importAssetsDir: __dirname,
},
workerOptions: {
runInMainProcess: true,
transport: Transport.TCP,
options: {},
},
customFields: {
Address: [],
Collection: [],
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/config/logger/default-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class DefaultLogger implements VendureLogger {
/** @internal */
level: LogLevel = LogLevel.Info;
private readonly timestamp: boolean;
private defaultContext = DEFAULT_CONTEXT;
private readonly localeStringOptions = {
year: '2-digit',
hour: 'numeric',
Expand Down Expand Up @@ -74,6 +75,10 @@ export class DefaultLogger implements VendureLogger {
}
}

setDefaultContext(defaultContext: string) {
this.defaultContext = defaultContext;
}

error(message: string, context?: string, trace?: string | undefined): void {
if (this.level >= LogLevel.Error) {
this.logMessage(
Expand Down Expand Up @@ -131,7 +136,7 @@ export class DefaultLogger implements VendureLogger {
}

private logContext(context?: string) {
return chalk.cyan(`[${context || DEFAULT_CONTEXT}]`);
return chalk.cyan(`[${context || this.defaultContext}]`);
}

private logTimestamp() {
Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
import { ClientOptions, Transport } from '@nestjs/microservices';
import { LanguageCode } from '@vendure/common/lib/generated-types';
import { CustomFields } from '@vendure/common/lib/shared-types';
import { RequestHandler } from 'express';
Expand Down Expand Up @@ -313,6 +314,40 @@ export interface ImportExportOptions {
importAssetsDir?: string;
}

/**
* @description
* Options related to the Vendure Worker.
*
* @docsCategory worker
*/
export interface WorkerOptions {
/**
* @description
* If set to `true`, the Worker will run be bootstrapped as part of the main Vendure server (when invoking the
* `bootstrap()` function) and will run in the same process. This mode is intended only for development and
* testing purposes, not for production, since running the Worker in the main process negates the benefits
* of having long-running or expensive tasks run in the background.
*
* @default true
*/
runInMainProcess?: boolean;
/**
* @description
* Sets the transport protocol used to communicate with the Worker. Options include TCP, Redis, gPRC and more. See the
* [NestJS microservices documentation](https://docs.nestjs.com/microservices/basics) for a full list.
*
* @default Transport.TCP
*/
transport?: Transport;
/**
* @description
* Additional options related to the chosen transport method. See See the
* [NestJS microservices documentation](https://docs.nestjs.com/microservices/basics) for details on the options relating to each of the
* transport methods.
*/
options?: ClientOptions['options'];
}

/**
* @description
* All possible configuration options are defined by the
Expand Down Expand Up @@ -464,4 +499,9 @@ export interface VendureConfig {
* Configures how taxes are calculated on products.
*/
taxOptions?: TaxOptions;
/**
* @description
* Configures the Vendure Worker, which is used for long-running background tasks.
*/
workerOptions?: WorkerOptions;
}
Loading

0 comments on commit 508bafd

Please sign in to comment.