UNPKG

@axway/api-builder-runtime

Version:

API Builder Runtime

342 lines (311 loc) 10 kB
const fs = require('fs'); const path = require('path'); const util = require('util'); const apiBuilderConfig = require('@axway/api-builder-config'); const { FlowManager } = require('@axway/flow'); const utils = require('./utils'); const readdirAsync = util.promisify(fs.readdir); const accessAsync = util.promisify(fs.access); // The regexes to determine the module type. const PLUGIN_REGEX = /^(?:@(.+)\/)?api-builder-plugin-.+$/; /** * Plugin error message * @param {string} name plugin name * @param {object|string} err error or error message */ function errorMsg (name, err) { return `Error loading plugin: ${name}. ${err && err.message || err}`; } /** * Check if module is a dataconnector module. * @private * @param {*} mod The export from the module. * @returns {boolean} true if this module is a data connector. */ function isDataConnector(module) { return module && typeof module === 'object' && typeof module.create === 'function'; } /** * Check if a module is a plugin. * @private * @param {*} mod The export from the module. * @returns {boolean} true if this module is a plugin. */ function isPlugin(module) { return typeof module === 'function'; } /** * Determine the type of the plugin. * @private * @param {object} plugin Plugin object * @returns {string} The type of the module. Possible values: ['dataconnector', 'plugin']. */ function classify(plugin, logger) { if (isDataConnector(plugin.module)) { return 'dataconnector'; } else if (isPlugin(plugin.module)) { return 'plugin'; } else { logger.warn(errorMsg(plugin.name, 'Plugins should export a function.')); } } async function resolveModule(plugin, apibuilder) { const { config = {}, logger } = apibuilder; const scopedConfig = getScopedConfig(config, plugin); try { plugin.module = pluginsAPI.loadPlugin(plugin.path); } catch (err) { logger.error(errorMsg(plugin.name, err)); throw err; } if (isDataConnector(plugin.module)) { plugin.type = 'dataconnector'; return plugin; } else if (isPlugin(plugin.module)) { plugin.type = 'plugin'; const options = { appDir: config.dir, service: apibuilder._internal.getServiceInfo(), // Freeze the scoped logger, to prevent malicious changes logger: Object.freeze(logger.scope(null, { prefix: `[${plugin.name}]` })) }; try { // execute the plugin function. plugin.module = await plugin.module(scopedConfig, options); } catch (err) { let errResp; if (err instanceof Error) { errResp = err; errResp.message = `There was a problem loading '${plugin.path}':\n${err.message}`; } else { errResp = new Error(`There was a problem loading '${plugin.path}':\n${err}`); } // Reject the plugin even if only one of the swaggers fails to compile to // components throw errResp; } return plugin; } else { throw new Error(errorMsg(plugin.name, 'Plugins should export a function.')); } } /** * Classifies and resolves the plugin module. * @param {object} plugin - The plugin descriptor * @param {object} config - The service config * @param {object} logger - The api builder config * @returns {Promise} A promise that resolves the plugin's module and returns the resolved * plugin object. */ async function resolveModuleOld(plugin, apibuilder) { const { config = {}, logger } = apibuilder; try { plugin.module = pluginsAPI.loadPlugin(plugin.path); } catch (err) { logger.warn(errorMsg(plugin.name, err)); return plugin; } plugin.type = classify(plugin, logger); if (plugin.type !== 'plugin') { return plugin; } const scopedConfig = getScopedConfig(config, plugin); const options = { appDir: config.dir, service: apibuilder._internal.getServiceInfo(), // Freeze the scoped logger, to prevent malicious changes logger: Object.freeze(logger.scope(null, { prefix: `[${plugin.name}]` })) }; // execute the plugin function. try { plugin.module = await plugin.module(scopedConfig, options); } catch (err) { logger.warn(errorMsg(plugin.name, err)); plugin.module = undefined; } return plugin; } /** * Classifies and resolves the plugin config. * @param {object} plugin - The plugin descriptor * @param {object} config - The service config * @returns {object} The plugin config object. */ function getScopedConfig(config, plugin) { if (apiBuilderConfig.flags.enableScopedConfig) { config.pluginConfig = config.pluginConfig || {}; return Object.freeze({ proxy: config.proxy, ...config.pluginConfig[plugin.name] }); } const scopedConfig = config.pluginConfig ? { proxy: config.proxy, ...config.pluginConfig[plugin.name] } : config; return scopedConfig; } /** * Walk the specified path and list the modules within the path, and within sub-scopes. * @private * @param {string} pathToWalk The folder to walk. * @returns {Promise} When resolved a list containing module descriptors { name, path } */ async function walkPath(pathToWalk) { const modules = []; try { await accessAsync(pathToWalk, fs.constants.R_OK); } catch (e) { return modules; } const dirs = await readdirAsync(pathToWalk); for (const module of dirs) { if (module.startsWith('@')) { // Scoped const scoped = await readdirAsync(path.join(pathToWalk, module)); modules.push(...scoped.map(sub => ({ name: `${module}/${sub}`, path: path.join(pathToWalk, module, sub) }))); } // Not scoped modules.push({ name: module, path: path.join(pathToWalk, module) }); } return modules; } /** * Find all the plugin modules on the search path. * @param {object} config - The configuration to use when loading the plugin. * @param {object} logger - The api builder logger. * @returns {Promise} When resolved a map of all the available plugins grouped * by type: { dataconnector: { name1: {name1, path1, version1, description1}, * ... nameN: {nameN, pathN, versionN, descriptionN} }, }. */ async function findPlugins(apibuilder) { const { config = {}, logger } = apibuilder; // Walk the node_modules and filter the results const modules = await walkPath(path.join(config.dir || process.cwd(), 'node_modules')); const apiBuilderPlugins = {}; pluginsAPI.emptyComponents = { dataconnector: null, schema: {}, flownodes: {}, triggers: {}, apiSpecs: {}, middlewares: {} }; // NOTE: on some systems, dir entries can be in any order, which makes the // order of plugins unpredictable. Currently, they are all independent and // can be loaded in any order. By sorting alphabetically here, we are not // solving the dependency problem - just making it a bit more predictable. // In the event that we do ever have plugins that have dependencies either // on plugins or specific start/stop tasks, please see RDPP-7203. const sortedPlugins = modules .filter(item => pluginsAPI.isPluginName(item.name)) .sort((a, b) => a.name.localeCompare(b.name)); for (const plugin of sortedPlugins) { // Add metadata let pkginfo; try { // Load metadata from package.json pkginfo = require(`${plugin.path}/package.json`); plugin.version = pkginfo.version; // The repository property in package.json is optional plugin.repository = pkginfo.repository && pkginfo.repository.url; // The description property in package.json is optional plugin.description = pkginfo.description || ''; plugin.tags = ( pkginfo.keywords && pkginfo.keywords.filter(a => a === 'community') ) || []; plugin.support = (pkginfo.bugs && pkginfo.bugs.url) || pkginfo.homepage || plugin.repository || ''; // Will help us determine if a plugin is supported. plugin.engines = pkginfo.engines || {}; } catch (ex) { logger.trace(ex); plugin.version = ''; plugin.description = ''; logger.warn(`Unable to find package.json in ${plugin.path}.`); } if (pkginfo) { // validate the plugin is compatible with this version of runtime try { utils.validateModuleCompatibility(pkginfo, apibuilder); } catch (err) { // Keep respect the exitOnPluginFailure flag while it exists. if (apiBuilderConfig.flags.exitOnPluginFailure) { throw err; } else { // Warn and go to the next plugin logger.warn(err.message); continue; } } } // Classify and resolve module as plugin if (apiBuilderConfig.flags.exitOnPluginFailure) { await resolveModule(plugin, apibuilder); } else { await resolveModuleOld(plugin, apibuilder); } // check if the plugins were loaded correctly; if (!plugin.module || !plugin.type) { continue; } // Restructure the module into components. The component structure has // moved to `pluginsAPI.emptyComponents` for easier maintenance. plugin.components = JSON.parse(JSON.stringify(pluginsAPI.emptyComponents)); if (plugin.type === 'plugin') { const { flownodes, schema, triggers, apiSpecs, middlewares } = plugin.module; if (flownodes) { for (const name in flownodes) { const node = flownodes[name]; node.type = FlowManager.formatNodeHandlerUri(plugin.name, name); plugin.components.flownodes[node.type] = node; } } if (schema) { for (const item of schema) { // schema without id will clash here with an "undefined" key // axway-schema will catch it out later. plugin.components.schema[item.id] = item; } } if (triggers) { plugin.components.triggers = triggers; } if (apiSpecs) { plugin.components.apiSpecs = apiSpecs; } if (middlewares) { plugin.components.middlewares = middlewares; } } else { // data connector plugins only contain a data connector plugin.components.dataconnector = plugin.module; } delete plugin.module; apiBuilderPlugins[plugin.name] = plugin; logger.info(`Registered plugin: ${plugin.name}`); } return apiBuilderPlugins; } function loadPlugin(name) { return require(name); } function isPluginName(name) { return PLUGIN_REGEX.test(name); } const pluginsAPI = { findPlugins, loadPlugin, isPluginName }; module.exports = pluginsAPI;