UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

363 lines 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.bootstrap = bootstrap; exports.bootstrapWorker = bootstrapWorker; exports.preBootstrapConfig = preBootstrapConfig; exports.runPluginConfigurations = runPluginConfigurations; exports.getAllEntities = getAllEntities; exports.configureSessionCookies = configureSessionCookies; const core_1 = require("@nestjs/core"); const typeorm_1 = require("@nestjs/typeorm"); const shared_constants_1 = require("@vendure/common/lib/shared-constants"); const cookieSession = require("cookie-session"); const semver_1 = require("semver"); const errors_1 = require("./common/error/errors"); const config_helpers_1 = require("./config/config-helpers"); const default_logger_1 = require("./config/logger/default-logger"); const vendure_logger_1 = require("./config/logger/vendure-logger"); const administrator_entity_1 = require("./entity/administrator/administrator.entity"); const entities_1 = require("./entity/entities"); const register_custom_entity_fields_1 = require("./entity/register-custom-entity-fields"); const run_entity_metadata_modifiers_1 = require("./entity/run-entity-metadata-modifiers"); const set_entity_id_strategy_1 = require("./entity/set-entity-id-strategy"); const set_money_strategy_1 = require("./entity/set-money-strategy"); const validate_custom_fields_config_1 = require("./entity/validate-custom-fields-config"); const plugin_metadata_1 = require("./plugin/plugin-metadata"); const plugin_utils_1 = require("./plugin/plugin-utils"); const process_context_1 = require("./process-context/process-context"); const version_1 = require("./version"); const vendure_worker_1 = require("./worker/vendure-worker"); /** * @description * Bootstraps the Vendure server. This is the entry point to the application. * * @example * ```ts * import { bootstrap } from '\@vendure/core'; * import { config } from './vendure-config'; * * bootstrap(config).catch(err => { * console.log(err); * process.exit(1); * }); * ``` * * ### Passing additional options * * Since v2.2.0, you can pass additional options to the NestJs application via the `options` parameter. * For example, to integrate with the [Nest Devtools](https://docs.nestjs.com/devtools/overview), you need to * pass the `snapshot` option: * * ```ts * import { bootstrap } from '\@vendure/core'; * import { config } from './vendure-config'; * * bootstrap(config, { * // highlight-start * nestApplicationOptions: { * snapshot: true, * } * // highlight-end * }).catch(err => { * console.log(err); * process.exit(1); * }); * ``` * * ### Ignoring compatibility errors for plugins * * Since v3.1.0, you can ignore compatibility errors for specific plugins by passing the `ignoreCompatibilityErrorsForPlugins` option. * * This should be used with caution, only if you are sure that the plugin will still work as expected with the current version of Vendure. * * @example * ```ts * import { bootstrap } from '\@vendure/core'; * import { config } from './vendure-config'; * import { MyPlugin } from './plugins/my-plugin'; * * bootstrap(config, { * // Let's say that `MyPlugin` is not yet compatible with the current version of Vendure * // but we know that it will still work as expected, and we are not able to publish * // a new version of the plugin right now. * ignoreCompatibilityErrorsForPlugins: [MyPlugin], * }); * ``` * * @docsCategory common * @docsPage bootstrap * @docsWeight 0 * */ async function bootstrap(userConfig, options) { const config = await preBootstrapConfig(userConfig); vendure_logger_1.Logger.useLogger(config.logger); vendure_logger_1.Logger.info(`Bootstrapping Vendure Server (pid: ${process.pid})...`); checkPluginCompatibility(config, options === null || options === void 0 ? void 0 : options.ignoreCompatibilityErrorsForPlugins); // 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. // eslint-disable-next-line const appModule = await import('./app.module.js'); (0, process_context_1.setProcessContext)('server'); const { hostname, port, cors, middleware } = config.apiOptions; default_logger_1.DefaultLogger.hideNestBoostrapLogs(); const app = await core_1.NestFactory.create(appModule.AppModule, Object.assign({ cors, logger: new vendure_logger_1.Logger() }, options === null || options === void 0 ? void 0 : options.nestApplicationOptions)); default_logger_1.DefaultLogger.restoreOriginalLogLevel(); app.useLogger(new vendure_logger_1.Logger()); const { tokenMethod } = config.authOptions; const usingCookie = tokenMethod === 'cookie' || (Array.isArray(tokenMethod) && tokenMethod.includes('cookie')); if (usingCookie) { configureSessionCookies(app, config); } const earlyMiddlewares = middleware.filter(mid => mid.beforeListen); earlyMiddlewares.forEach(mid => { app.use(mid.route, mid.handler); }); await app.listen(port, hostname || ''); app.enableShutdownHooks(); logWelcomeMessage(config); return app; } /** * @description * Bootstraps a Vendure worker. Resolves to a {@link VendureWorker} object containing a reference to the underlying * NestJs [standalone application](https://docs.nestjs.com/standalone-applications) as well as convenience * methods for starting the job queue and health check server. * * Read more about the [Vendure Worker](/guides/developer-guide/worker-job-queue/). * * @example * ```ts * import { bootstrapWorker } from '\@vendure/core'; * import { config } from './vendure-config'; * * bootstrapWorker(config) * .then(worker => worker.startJobQueue()) * .then(worker => worker.startHealthCheckServer({ port: 3020 })) * .catch(err => { * console.log(err); * process.exit(1); * }); * ``` * @docsCategory worker * @docsPage bootstrapWorker * @docsWeight 0 * */ async function bootstrapWorker(userConfig, options) { var _a, _b; const vendureConfig = await preBootstrapConfig(userConfig); const config = disableSynchronize(vendureConfig); (_b = (_a = config.logger).setDefaultContext) === null || _b === void 0 ? void 0 : _b.call(_a, 'Vendure Worker'); vendure_logger_1.Logger.useLogger(config.logger); vendure_logger_1.Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`); checkPluginCompatibility(config, options === null || options === void 0 ? void 0 : options.ignoreCompatibilityErrorsForPlugins); (0, process_context_1.setProcessContext)('worker'); default_logger_1.DefaultLogger.hideNestBoostrapLogs(); const WorkerModule = await import('./worker/worker.module.js').then(m => m.WorkerModule); const workerApp = await core_1.NestFactory.createApplicationContext(WorkerModule, Object.assign({ logger: new vendure_logger_1.Logger() }, options === null || options === void 0 ? void 0 : options.nestApplicationContextOptions)); default_logger_1.DefaultLogger.restoreOriginalLogLevel(); workerApp.useLogger(new vendure_logger_1.Logger()); workerApp.enableShutdownHooks(); await validateDbTablesForWorker(workerApp); vendure_logger_1.Logger.info('Vendure Worker is ready'); return new vendure_worker_1.VendureWorker(workerApp); } /** * Setting the global config must be done prior to loading the AppModule. */ async function preBootstrapConfig(userConfig) { var _a, _b, _c; if (userConfig) { await (0, config_helpers_1.setConfig)(userConfig); } const entities = getAllEntities(userConfig); const { coreSubscribersMap } = await import('./entity/subscribers.js'); await (0, config_helpers_1.setConfig)({ dbConnectionOptions: { entities, subscribers: [ ...((_b = (_a = userConfig.dbConnectionOptions) === null || _a === void 0 ? void 0 : _a.subscribers) !== null && _b !== void 0 ? _b : []), ...Object.values(coreSubscribersMap), ], }, }); let config = (0, config_helpers_1.getConfig)(); // The logger is set here so that we are able to log any messages prior to the final // logger (which may depend on config coming from a plugin) being set. vendure_logger_1.Logger.useLogger(config.logger); config = await runPluginConfigurations(config); const entityIdStrategy = (_c = config.entityOptions.entityIdStrategy) !== null && _c !== void 0 ? _c : config.entityIdStrategy; (0, set_entity_id_strategy_1.setEntityIdStrategy)(entityIdStrategy, entities); const moneyStrategy = config.entityOptions.moneyStrategy; (0, set_money_strategy_1.setMoneyStrategy)(moneyStrategy, entities); const customFieldValidationResult = (0, validate_custom_fields_config_1.validateCustomFieldsConfig)(config.customFields, entities); if (!customFieldValidationResult.valid) { process.exitCode = 1; throw new Error('CustomFields config error:\n- ' + customFieldValidationResult.errors.join('\n- ')); } (0, register_custom_entity_fields_1.registerCustomEntityFields)(config); await (0, run_entity_metadata_modifiers_1.runEntityMetadataModifiers)(config); setExposedHeaders(config); return config; } function checkPluginCompatibility(config, ignoredPlugins = []) { for (const plugin of config.plugins) { const compatibility = (0, plugin_metadata_1.getCompatibility)(plugin); const pluginName = plugin.name; if (!compatibility) { vendure_logger_1.Logger.info(`The plugin "${pluginName}" does not specify a compatibility range, so it is not guaranteed to be compatible with this version of Vendure.`); } else { if (!(0, semver_1.satisfies)(version_1.VENDURE_VERSION, compatibility, { loose: true, includePrerelease: true })) { const compatibilityErrorMessage = `Plugin "${pluginName}" is not compatible with this version of Vendure. ` + `It specifies a semver range of "${compatibility}" but the current version is "${version_1.VENDURE_VERSION}".`; if (ignoredPlugins.includes(plugin)) { vendure_logger_1.Logger.warn(compatibilityErrorMessage + `However, this plugin has been explicitly marked as ignored using the 'ignoreCompatibilityErrorsForPlugins' bootstrap option,` + ` so it will be loaded anyway.`); continue; } else { vendure_logger_1.Logger.error(compatibilityErrorMessage); throw new errors_1.InternalServerError(`Plugin "${pluginName}" is not compatible with this version of Vendure.`); } } } } } /** * Run the configuration functions of all plugins and return the final config object. */ async function runPluginConfigurations(config) { for (const plugin of config.plugins) { const configFn = (0, plugin_metadata_1.getConfigurationFunction)(plugin); if (typeof configFn === 'function') { const result = await configFn(config); Object.assign(config, result); } } return config; } /** * Returns an array of core entities and any additional entities defined in plugins. */ function getAllEntities(userConfig) { const coreEntities = Object.values(entities_1.coreEntitiesMap); const pluginEntities = (0, plugin_metadata_1.getEntitiesFromPlugins)(userConfig.plugins); const allEntities = coreEntities; // Check to ensure that no plugins are defining entities with names // which conflict with existing entities. for (const pluginEntity of pluginEntities) { if (allEntities.find(e => e.name === pluginEntity.name)) { throw new errors_1.InternalServerError('error.entity-name-conflict', { entityName: pluginEntity.name }); } else { allEntities.push(pluginEntity); } } return allEntities; } /** * 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) { const { tokenMethod } = config.authOptions; const isUsingBearerToken = tokenMethod === 'bearer' || (Array.isArray(tokenMethod) && tokenMethod.includes('bearer')); if (isUsingBearerToken) { const authTokenHeaderKey = config.authOptions.authTokenHeaderKey; const corsOptions = config.apiOptions.cors; if (typeof corsOptions !== 'boolean') { const { exposedHeaders } = corsOptions; let exposedHeadersWithAuthKey; if (!exposedHeaders) { exposedHeadersWithAuthKey = [authTokenHeaderKey]; } else if (typeof exposedHeaders === 'string') { exposedHeadersWithAuthKey = exposedHeaders .split(',') .map(x => x.trim()) .concat(authTokenHeaderKey); } else { exposedHeadersWithAuthKey = exposedHeaders.concat(authTokenHeaderKey); } corsOptions.exposedHeaders = exposedHeadersWithAuthKey; } } } function logWelcomeMessage(config) { const { port, shopApiPath, adminApiPath, hostname } = config.apiOptions; const apiCliGreetings = []; const pathToUrl = (path) => `http://${hostname || 'localhost'}:${port}/${path}`; apiCliGreetings.push(['Shop API', pathToUrl(shopApiPath)]); apiCliGreetings.push(['Admin API', pathToUrl(adminApiPath)]); apiCliGreetings.push(...(0, plugin_utils_1.getPluginStartupMessages)().map(({ label, path }) => [label, pathToUrl(path)])); const columnarGreetings = arrangeCliGreetingsInColumns(apiCliGreetings); const title = `Vendure server (v${version_1.VENDURE_VERSION}) now running on port ${port}`; const maxLineLength = Math.max(title.length, ...columnarGreetings.map(l => l.length)); const titlePadLength = title.length < maxLineLength ? Math.floor((maxLineLength - title.length) / 2) : 0; vendure_logger_1.Logger.info('='.repeat(maxLineLength)); vendure_logger_1.Logger.info(title.padStart(title.length + titlePadLength)); vendure_logger_1.Logger.info('-'.repeat(maxLineLength).padStart(titlePadLength)); columnarGreetings.forEach(line => vendure_logger_1.Logger.info(line)); vendure_logger_1.Logger.info('='.repeat(maxLineLength)); } function arrangeCliGreetingsInColumns(lines) { const columnWidth = Math.max(...lines.map(l => l[0].length)) + 2; return lines.map(l => `${(l[0] + ':').padEnd(columnWidth)}${l[1]}`); } /** * Fix race condition when modifying DB * See: https://github.com/vendure-ecommerce/vendure/issues/152 */ function disableSynchronize(userConfig) { const config = Object.assign(Object.assign({}, userConfig), { dbConnectionOptions: Object.assign(Object.assign({}, userConfig.dbConnectionOptions), { synchronize: false }) }); return config; } /** * Check that the Database tables exist. When running Vendure server & worker * concurrently for the first time, the worker will attempt to access the * DB tables before the server has populated them (assuming synchronize = true * in config). This method will use polling to check the existence of a known table * before allowing the rest of the worker bootstrap to continue. * @param worker */ async function validateDbTablesForWorker(worker) { const connection = worker.get((0, typeorm_1.getConnectionToken)()); await new Promise(async (resolve, reject) => { const checkForTables = async () => { try { const adminCount = await connection.getRepository(administrator_entity_1.Administrator).count(); return 0 < adminCount; } catch (e) { return false; } }; const pollIntervalMs = 5000; let attempts = 0; const maxAttempts = 10; let validTableStructure = false; vendure_logger_1.Logger.verbose('Checking for expected DB table structure...'); while (!validTableStructure && attempts < maxAttempts) { attempts++; validTableStructure = await checkForTables(); if (validTableStructure) { vendure_logger_1.Logger.verbose('Table structure verified'); resolve(); return; } vendure_logger_1.Logger.verbose(`Table structure could not be verified, trying again after ${pollIntervalMs}ms (attempt ${attempts} of ${maxAttempts})`); await new Promise(resolve1 => setTimeout(resolve1, pollIntervalMs)); } reject('Could not validate DB table structure. Aborting bootstrap.'); }); } function configureSessionCookies(app, userConfig) { var _a; const { cookieOptions } = userConfig.authOptions; // Globally set the cookie session middleware const cookieName = typeof (cookieOptions === null || cookieOptions === void 0 ? void 0 : cookieOptions.name) !== 'string' ? (_a = cookieOptions.name) === null || _a === void 0 ? void 0 : _a.shop : cookieOptions.name; app.use(cookieSession(Object.assign(Object.assign({}, cookieOptions), { name: cookieName !== null && cookieName !== void 0 ? cookieName : shared_constants_1.DEFAULT_COOKIE_NAME }))); } //# sourceMappingURL=bootstrap.js.map