Browse Source

feat(server): Extend options, expose bootstrap function

Michael Bromley 7 years ago
parent
commit
0e5fb346b7

+ 25 - 0
server/index-dev.ts

@@ -0,0 +1,25 @@
+import { bootstrap } from './src';
+import { IntegerIdStrategy, StringIdStrategy } from './src/config/entity-id-strategy';
+
+/**
+ * This bootstraps the dev server, used for testing Vendure during development.
+ */
+bootstrap({
+    port: 3000,
+    cors: true,
+    dbConnectionOptions: {
+        type: 'mysql',
+        entities: ['./src/**/entity/**/*.entity.ts'],
+        synchronize: true,
+        logging: true,
+        host: '192.168.99.100',
+        port: 3306,
+        username: 'root',
+        password: '',
+        database: 'test',
+    },
+    // entityIdStrategy: new MadBastardIdStrategy(),
+}).catch(err => {
+    // tslint:disable-next-line
+    console.log(err);
+});

+ 1 - 1
server/nodemon-debug.json

@@ -2,5 +2,5 @@
   "watch": ["src"],
   "ext": "ts",
   "ignore": ["src/**/*.spec.ts", "mock-data/**/*"],
-  "exec": "node --inspect=5858 -r ts-node/register src/main.ts"
+  "exec": "node --inspect=5858 -r ts-node/register index-dev.ts"
 }

+ 11 - 6
server/src/app.module.ts

@@ -22,7 +22,7 @@ import { ProductOptionService } from './service/product-option.service';
 import { ProductVariantService } from './service/product-variant.service';
 import { ProductService } from './service/product.service';
 
-const connectionOptions = getConfig().connectionOptions;
+const connectionOptions = getConfig().dbConnectionOptions;
 
 @Module({
     imports: [GraphQLModule, TypeOrmModule.forRoot(connectionOptions)],
@@ -48,7 +48,7 @@ const connectionOptions = getConfig().connectionOptions;
     ],
 })
 export class AppModule implements NestModule {
-    constructor(private readonly graphQLFactory: GraphQLFactory) {}
+    constructor(private readonly graphQLFactory: GraphQLFactory, private configService: ConfigService) {}
 
     configure(consumer: MiddlewareConsumer) {
         const schema = this.createSchema();
@@ -56,17 +56,22 @@ export class AppModule implements NestModule {
         consumer
             .apply(
                 graphiqlExpress({
-                    endpointURL: '/graphql',
+                    endpointURL: this.configService.apiPath,
                     subscriptionsEndpoint: `ws://localhost:3001/subscriptions`,
                 }),
             )
             .forRoutes('/graphiql')
             .apply(graphqlExpress(req => ({ schema, rootValue: req })))
-            .forRoutes('/graphql');
+            .forRoutes(this.configService.apiPath);
     }
 
     createSchema() {
-        const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql');
-        return this.graphQLFactory.createSchema({ typeDefs });
+        const typeDefs = this.graphQLFactory.mergeTypesByPaths(__dirname + '/**/*.graphql');
+        return this.graphQLFactory.createSchema({
+            typeDefs,
+            resolverValidationOptions: {
+                requireResolversForResolveType: false,
+            },
+        });
     }
 }

+ 29 - 0
server/src/bootstrap.ts

@@ -0,0 +1,29 @@
+import { NestFactory } from '@nestjs/core';
+import { getConfig, setConfig, VendureConfig } from './config/vendure-config';
+
+/**
+ * Bootstrap the Vendure server.
+ */
+export async function bootstrap(userConfig?: Partial<VendureConfig>) {
+    if (userConfig) {
+        setConfig(userConfig);
+    }
+
+    // Entities *must* be loaded after the user config is set in order for the
+    // base VendureEntity to be correctly configured with the primary key type
+    // specified in the EntityIdStrategy.
+    const entities = await import('./entity/entities');
+    setConfig({
+        dbConnectionOptions: {
+            entities: entities.coreEntities,
+        },
+    });
+
+    // 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.
+    const appModule = await import('./app.module');
+    const config = getConfig();
+
+    const app = await NestFactory.create(appModule.AppModule, { cors: config.cors });
+    await app.listen(config.port);
+}

+ 50 - 0
server/src/config/merge-config.spec.ts

@@ -0,0 +1,50 @@
+import { LanguageCode } from '../locale/language-code';
+import { mergeConfig } from './merge-config';
+import { VendureConfig } from './vendure-config';
+
+describe('mergeConfig()', () => {
+    it('merges top-level properties', () => {
+        const input: any = {
+            a: 1,
+            b: 2,
+        };
+
+        mergeConfig(input, { b: 3, c: 5 } as any);
+        expect(input).toEqual({
+            a: 1,
+            b: 3,
+            c: 5,
+        });
+    });
+
+    it('merges deep properties', () => {
+        const input: any = {
+            a: 1,
+            b: { c: 2 },
+        };
+
+        mergeConfig(input, { b: { c: 5 } } as any);
+        expect(input).toEqual({
+            a: 1,
+            b: { c: 5 },
+        });
+    });
+
+    it('replaces class instances rather than merging their properties', () => {
+        class Foo {
+            name = 'foo';
+        }
+        class Bar {
+            name = 'bar';
+        }
+
+        const input: any = {
+            class: new Foo(),
+        };
+
+        mergeConfig(input, { class: new Bar() } as any);
+
+        expect(input.class instanceof Bar).toBe(true);
+        expect(input.class.name).toBe('bar');
+    });
+});

+ 44 - 0
server/src/config/merge-config.ts

@@ -0,0 +1,44 @@
+import { DeepPartial } from '../common/common-types';
+import { VendureConfig } from './vendure-config';
+
+/**
+ * Simple object check.
+ * From https://stackoverflow.com/a/34749873/772859
+ */
+function isObject(item: any): item is object {
+    return item && typeof item === 'object' && !Array.isArray(item);
+}
+
+function isClassInstance(item: any): boolean {
+    return isObject(item) && item.constructor.name !== 'Object';
+}
+
+/**
+ * Deep merge config objects. Based on the solution from https://stackoverflow.com/a/34749873/772859
+ * but modified so that it does not overwrite fields of class instances, rather it overwrites
+ * the entire instance.
+ */
+export function mergeConfig(target: VendureConfig, source: DeepPartial<VendureConfig>) {
+    if (!source) {
+        return target;
+    }
+
+    if (isObject(target) && isObject(source)) {
+        for (const key in source) {
+            if (isObject(source[key])) {
+                if (!target[key]) {
+                    Object.assign(target, { [key]: {} });
+                }
+                if (!isClassInstance(source[key])) {
+                    mergeConfig(target[key], source[key]);
+                } else {
+                    target[key] = source[key];
+                }
+            } else {
+                Object.assign(target, { [key]: source[key] });
+            }
+        }
+    }
+
+    return target;
+}

+ 43 - 8
server/src/config/vendure-config.ts

@@ -1,32 +1,67 @@
+import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
 import { ConnectionOptions } from 'typeorm';
+import { DeepPartial } from '../common/common-types';
 import { LanguageCode } from '../locale/language-code';
 import { AutoIncrementIdStrategy } from './auto-increment-id-strategy';
 import { EntityIdStrategy } from './entity-id-strategy';
+import { mergeConfig } from './merge-config';
 
 export interface VendureConfig {
+    /**
+     * The default languageCode of the app.
+     */
     defaultLanguageCode: LanguageCode;
+    /**
+     * The path to the GraphQL API.
+     */
+    apiPath: string;
+    /**
+     * Set the CORS handling for the server.
+     */
+    cors: boolean | CorsOptions;
+    /**
+     * Which port the Vendure server should listen on.
+     */
+    port: number;
+    /**
+     * Defines the strategy used for both storing the primary keys of entities
+     * in the database, and the encoding & decoding of those ids when exposing
+     * entities via the API. The default uses a simple auto-increment integer
+     * strategy.
+     */
     entityIdStrategy: EntityIdStrategy<any>;
-    connectionOptions: ConnectionOptions;
+    /**
+     * The connection options used by TypeORM to connect to the database.
+     */
+    dbConnectionOptions: ConnectionOptions;
 }
 
 const defaultConfig: VendureConfig = {
     defaultLanguageCode: LanguageCode.EN,
+    port: 3000,
+    cors: false,
+    apiPath: '/api',
     entityIdStrategy: new AutoIncrementIdStrategy(),
-    connectionOptions: {
+    dbConnectionOptions: {
         type: 'mysql',
     },
 };
 
 let activeConfig = defaultConfig;
 
-export function setConfig(userConfig: Partial<VendureConfig>): void {
-    activeConfig = Object.assign({}, defaultConfig, userConfig);
+/**
+ * Override the default config by merging in the supplied values. Should only be used prior to
+ * bootstrapping the app.
+ */
+export function setConfig(userConfig: DeepPartial<VendureConfig>): void {
+    activeConfig = mergeConfig(activeConfig, userConfig);
 }
 
+/**
+ * Returns the app config object. In general this function should only be
+ * used before bootstrapping the app. In all other contexts, the {@link ConfigService}
+ * should be used to access config settings.
+ */
 export function getConfig(): VendureConfig {
     return activeConfig;
 }
-
-export function getEntityIdStrategy(): EntityIdStrategy {
-    return getConfig().entityIdStrategy;
-}

+ 4 - 3
server/src/entity/base/base.entity.ts

@@ -1,6 +1,8 @@
 import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
 import { DeepPartial, ID } from '../../common/common-types';
-import { getEntityIdStrategy } from '../../config/vendure-config';
+import { getConfig } from '../../config/vendure-config';
+
+const primaryKeyType = getConfig().entityIdStrategy.primaryKeyType as any;
 
 /**
  * This is the base class from which all entities inherit.
@@ -14,8 +16,7 @@ export abstract class VendureEntity {
         }
     }
 
-    @PrimaryGeneratedColumn(getEntityIdStrategy().primaryKeyType as any)
-    id: ID;
+    @PrimaryGeneratedColumn(primaryKeyType) id: ID;
 
     @CreateDateColumn() createdAt: string;
 

+ 27 - 0
server/src/entity/entities.ts

@@ -0,0 +1,27 @@
+import { Address } from './address/address.entity';
+import { Administrator } from './administrator/administrator.entity';
+import { Customer } from './customer/customer.entity';
+import { ProductOptionGroupTranslation } from './product-option-group/product-option-group-translation.entity';
+import { ProductOptionGroup } from './product-option-group/product-option-group.entity';
+import { ProductOptionTranslation } from './product-option/product-option-translation.entity';
+import { ProductOption } from './product-option/product-option.entity';
+import { ProductVariantTranslation } from './product-variant/product-variant-translation.entity';
+import { ProductVariant } from './product-variant/product-variant.entity';
+import { ProductTranslation } from './product/product-translation.entity';
+import { Product } from './product/product.entity';
+import { User } from './user/user.entity';
+
+export const coreEntities = [
+    Address,
+    Administrator,
+    Customer,
+    Product,
+    ProductTranslation,
+    ProductOption,
+    ProductOptionTranslation,
+    ProductOptionGroup,
+    ProductOptionGroupTranslation,
+    ProductVariant,
+    ProductVariantTranslation,
+    User,
+];

+ 1 - 0
server/src/index.ts

@@ -0,0 +1 @@
+export { bootstrap } from './bootstrap';

+ 0 - 16
server/src/main.hmr.ts

@@ -1,16 +0,0 @@
-import { NestFactory } from '@nestjs/core';
-import { AppModule } from './app.module';
-
-declare const module: any;
-
-async function bootstrap() {
-    const app = await NestFactory.create(AppModule);
-    await app.listen(3000);
-
-    if (module.hot) {
-        module.hot.accept();
-        module.hot.dispose(() => app.close());
-    }
-}
-// tslint:disable:no-floating-promises
-bootstrap();

+ 0 - 9
server/src/main.ts

@@ -1,9 +0,0 @@
-import { NestFactory } from '@nestjs/core';
-import { AppModule } from './app.module';
-
-async function bootstrap() {
-    const app = await NestFactory.create(AppModule, { cors: true });
-    await app.listen(3000);
-}
-// tslint:disable:no-floating-promises
-bootstrap();

+ 4 - 1
server/src/service/config.service.mock.ts

@@ -4,9 +4,12 @@ import { MockClass } from '../testing/testing-types';
 import { ConfigService } from './config.service';
 
 export class MockConfigService implements MockClass<ConfigService> {
+    apiPath = 'api';
+    port = 3000;
+    cors = false;
     defaultLanguageCode: jest.Mock<any>;
     entityIdStrategy = new MockIdStrategy();
-    connectionOptions = {};
+    dbConnectionOptions = {};
 }
 
 export const ENCODED = 'encoded';

+ 15 - 2
server/src/service/config.service.ts

@@ -1,4 +1,5 @@
 import { Injectable } from '@nestjs/common';
+import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
 import { ConnectionOptions } from 'typeorm';
 import { EntityIdStrategy } from '../config/entity-id-strategy';
 import { getConfig, VendureConfig } from '../config/vendure-config';
@@ -10,12 +11,24 @@ export class ConfigService implements VendureConfig {
         return this.activeConfig.defaultLanguageCode;
     }
 
+    get apiPath(): string {
+        return this.activeConfig.apiPath;
+    }
+
+    get port(): number {
+        return this.activeConfig.port;
+    }
+
+    get cors(): boolean | CorsOptions {
+        return this.activeConfig.cors;
+    }
+
     get entityIdStrategy(): EntityIdStrategy {
         return this.activeConfig.entityIdStrategy;
     }
 
-    get connectionOptions(): ConnectionOptions {
-        return this.activeConfig.connectionOptions;
+    get dbConnectionOptions(): ConnectionOptions {
+        return this.activeConfig.dbConnectionOptions;
     }
 
     private activeConfig: VendureConfig;