UNPKG

@mediarithmics/plugins-nodejs-sdk

Version:

This is the mediarithmics nodejs to help plugin developers bootstrapping their plugin without having to deal with most of the plugin boilerplate

387 lines 17.4 kB
"use strict"; /* eslint-disable @typescript-eslint/require-await */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BasePlugin = exports.PropertiesWrapper = exports.ResourceNotFoundError = void 0; const body_parser_1 = __importDefault(require("body-parser")); const express_1 = __importDefault(require("express")); const lodash_1 = __importDefault(require("lodash")); const memory_cache_1 = __importDefault(require("memory-cache")); const request_promise_native_1 = __importDefault(require("request-promise-native")); const toobusy_js_1 = __importDefault(require("toobusy-js")); const winston_1 = __importDefault(require("winston")); const PluginPropertyInterface_1 = require("../../api/core/plugin/PluginPropertyInterface"); const utils_1 = require("../../utils"); const Normalizer_1 = require("../../utils/Normalizer"); const index_1 = require("./index"); class ResourceNotFoundError extends Error { constructor(message) { super(message); this.name = 'ResourceNotFoundError'; } } exports.ResourceNotFoundError = ResourceNotFoundError; class PropertiesWrapper { constructor(values) { this.values = values; this.get = (key) => this.normalized[key]; this.ofType = (typeName) => lodash_1.default.find(this.values, (p) => p.property_type === typeName); this.findAssetFileProperty = (key) => { const p = key ? this.get(key) : this.ofType('ASSET') || this.ofType('ASSET_FILE'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asAssetFileProperty); }; this.findAssetFolderProperty = (key) => { const p = key ? this.get(key) : this.ofType('ASSET_FOLDER'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asAssetFolderProperty); }; this.findDataFileProperty = (key) => { const p = key ? this.get(key) : this.ofType('DATA_FILE'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asDataFileProperty); }; this.findUrlProperty = (key) => { const p = key ? this.get(key) : this.ofType('URL'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asUrlProperty); }; this.findStringProperty = (key) => { const p = key ? this.get(key) : this.ofType('STRING'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asStringProperty); }; this.findAdLayoutProperty = (key) => { const p = key ? this.get(key) : this.ofType('AD_LAYOUT'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asAdLayoutProperty); }; this.findBooleanProperty = (key) => { const p = key ? this.get(key) : this.ofType('BOOLEAN'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asBooleanProperty); }; this.findDoubleProperty = (key) => { const p = key ? this.get(key) : this.ofType('DOUBLE'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asDoubleProperty); }; this.findIntProperty = (key) => { const p = key ? this.get(key) : this.ofType('INT'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asIntProperty); }; this.findNativeDataProperty = (key) => { const p = key ? this.get(key) : this.ofType('NATIVE_DATA'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asNativeDataProperty); }; this.findNativeTitleProperty = (key) => { const p = key ? this.get(key) : this.ofType('NATIVE_TITLE'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asNativeTitleProperty); }; this.findNativeImageProperty = (key) => { const p = key ? this.get(key) : this.ofType('NATIVE_IMAGE'); return (0, utils_1.flatMap)(p, PluginPropertyInterface_1.asNativeImageProperty); }; this.normalized = (0, Normalizer_1.normalizeArray)(values, 'technical_name'); } } exports.PropertiesWrapper = PropertiesWrapper; class BasePlugin { // The idea here is to add a random part in the instance cache expiration -> we add +0-10% variablity constructor(enableThrottling = false) { this.multiThread = false; // Default cache is now 10 min to give some breathing to the Gateway // Note: This will be private or completly remove in the next major release as a breaking change // TODO: in 0.8.x+, make this private or remove it completly (this should no longer be overriden in plugin impl., // or we should implement a minimum threshold pattern) this.INSTANCE_CONTEXT_CACHE_EXPIRATION = 600000; this._transport = request_promise_native_1.default; this.enableThrottling = false; // Log level update implementation // Plugin Init implementation this.asyncMiddleware = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; if (enableThrottling) { this.enableThrottling = enableThrottling; } const gatewayHost = process.env.GATEWAY_HOST; if (gatewayHost) { this.gatewayHost = gatewayHost; } else { this.gatewayHost = 'plugin-gateway.platform'; } const gatewayPort = process.env.GATEWAY_PORT; if (gatewayPort) { this.gatewayPort = parseInt(gatewayPort); } else { this.gatewayPort = 8080; } this.outboundPlatformUrl = `http://${this.gatewayHost}:${this.gatewayPort}`; this.app = (0, express_1.default)(); if (this.enableThrottling) { this.app.use((req, res, next) => { if ( // eslint-disable-next-line @typescript-eslint/no-unsafe-call (0, toobusy_js_1.default)() && !(req.path === '/v1/init' || req.path === '/v1/status' || req.path === '/v1/shutdown' || req.path === '/v1/log_level')) { res.status(429).json({ status: 'RETRY', message: "I'm busy right now, sorry." }); } else { next(); } }); } this.app.use(body_parser_1.default.raw({ type: 'application/ion', limit: '5mb' })); this.app.use(body_parser_1.default.json({ type: '*/*', limit: '5mb' })); this.logger = winston_1.default.createLogger({ transports: [ new winston_1.default.transports.Console({ format: winston_1.default.format.json(), }), ], }); this.pluginCache = memory_cache_1.default; this.pluginCache.clear(); if (!process.env.PLUGIN_WORKER_ID) { throw new Error('Missing worker id in the environment!'); } if (!process.env.PLUGIN_AUTHENTICATION_TOKEN) { throw new Error('Missing auth token in the environment!'); } this.credentials = { authentication_token: process.env.PLUGIN_AUTHENTICATION_TOKEN, worker_id: process.env.PLUGIN_WORKER_ID, }; this.initInitRoute(); this.initStatusRoute(); this.initLogLevelUpdateRoute(); this.initLogLevelGetRoute(); this.initMetadataRoute(); } // The objective is to stop having 'synchronized' instance context re-build that are putting some stress on the gateway due to burst of API calls getInstanceContextCacheExpiration() { return this.INSTANCE_CONTEXT_CACHE_EXPIRATION * (1 + 0.1 * Math.random()); } // Log level update implementation onLogLevelUpdate(level) { const logLevel = level.toLowerCase(); this.logger.info('Setting log level to ' + logLevel); this.logger.level = logLevel; } fetchDataFile(uri) { return this.requestGatewayHelper('GET', `${this.outboundPlatformUrl}/v1/data_file/data`, undefined, { uri: uri }, false, true); } async fetchConfigurationFileOptional(fileName) { try { return await this.fetchConfigurationFile(fileName); } catch (e) { const err = e; if (err instanceof ResourceNotFoundError) { return undefined; } throw e; } } // Log level update implementation fetchConfigurationFile(fileName) { return this.requestGatewayHelper('GET', `${this.outboundPlatformUrl}/v1/configuration/technical_name=${fileName}`, undefined, undefined, false, true); } upsertConfigurationFile(fileName, fileContent) { return this.requestGatewayHelper('PUT', `${this.outboundPlatformUrl}/v1/configuration/technical_name=${fileName}`, fileContent, undefined, false, true); } async requestGatewayHelper(method, uri, body, qs, isJson, isBinary) { const options = { method: method, uri: uri, auth: { user: this.credentials.worker_id, pass: this.credentials.authentication_token, sendImmediately: true, }, proxy: false, }; // Set the body if provided options.body = body !== undefined ? body : undefined; // Set the querystring if provided options.qs = qs !== undefined ? qs : undefined; // Set the json flag if provided options.json = isJson !== undefined ? isJson : true; // Set the encoding to null if it is binary options.encoding = isBinary !== undefined && isBinary ? null : undefined; this.logger.silly(`Doing gateway call with ${JSON.stringify(options)}`); try { return (await this._transport(options)); } catch (e) { const error = e; if (error.name === 'StatusCodeError') { const bodyString = (isJson !== undefined && !isJson ? body : JSON.stringify(body)); const message = `Error while calling ${method} '${uri}' with the request body '${bodyString || ''}', the qs '${JSON.stringify(qs) || ''}', the auth user '${(0, utils_1.obfuscateString)(options.auth ? options.auth.user : undefined) || ''}', the auth password '${(0, utils_1.obfuscateString)(options.auth ? options.auth.pass : undefined) || ''}': got a ${error.response.statusCode} ${error.response.statusMessage} with the response body ${JSON.stringify(error.response.body)}`; if (error.response.statusCode === 404) { throw new ResourceNotFoundError(message); } else { throw new Error(message); } } else { this.logger.error(`Got an issue while doing a Gateway call: ${e.message} - ${e.stack ? e.stack : 'stack undefined'}`); throw e; } } } // Health Status implementation async requestPublicMicsApiHelper(apiToken, options) { const tweakedOptions = { ...options, headers: { ...options.headers, Authorization: apiToken, }, proxy: false, }; try { return (await this._transport(tweakedOptions)); } catch (e) { const error = e; if (error.name === 'StatusCodeError') { throw new Error(`Error while calling ${options.method} '${options.uri}' with the header body '${JSON.stringify(options.headers)}': got a ${error.response.statusCode} ${error.response.statusMessage} with the response body ${JSON.stringify(error.response.body)}`); } else { this.logger.error(`Got an issue while doing a mediarithmics API call: ${e.message} - ${e.stack ? e.stack : 'stack undefined'}`); throw e; } } } async fetchDatamarts(apiToken, organisationId) { const options = { method: 'GET', uri: 'https://api.mediarithmics.com/v1/datamarts', qs: { organisation_id: organisationId, allow_administrator: 'false' }, json: true, }; return this.requestPublicMicsApiHelper(apiToken, options); } async fetchDatamartCompartments(apiToken, datamartId) { const options = { method: 'GET', uri: `https://api.mediarithmics.com/v1/datamarts/${datamartId}/user_account_compartments`, json: true, }; return this.requestPublicMicsApiHelper(apiToken, options); } onInitRequest(creds) { this.credentials.authentication_token = creds.authentication_token; this.credentials.worker_id = creds.worker_id; this.logger.info('Update authentication_token with %s', this.credentials.authentication_token); } // Method to start the plugin // eslint-disable-next-line @typescript-eslint/no-empty-function start() { } httpIsReady() { return this.credentials && this.credentials.worker_id && this.credentials.authentication_token; } // This method can be overridden by any subclass onLogLevelUpdateHandler(req, res) { const body = req.body; if (body && body.level) { const { level } = body; if (this.multiThread) { const msg = { value: level.toLowerCase(), cmd: index_1.MsgCmd.LOG_LEVEL_UPDATE_FROM_WORKER, }; this.logger.debug(`Sending DEBUG_LEVEL_UPDATE_FROM_WORKER from worker ${process.pid} to master with value: ${msg.value ? msg.value : 'undefiend'}`); if (typeof process.send === 'function') { process.send(msg); } // We have to assume that everything went fine in the propagation... res.status(200).end(); } else { // Lowering case this.onLogLevelUpdate(level); return res.status(200).end(); } } else { this.logger.error('Incorrect body : Cannot change log level, actual: ' + this.logger.level); res.status(400).end(); } } // This method can be overridden by any subclass onLogLevelRequest(req, res) { res.send({ level: this.logger.level.toUpperCase() }); } // This method can be overridden by any subclass onStatusRequest(req, res) { //Route used by the plugin manager to check if the plugin is UP and running this.logger.silly('GET /v1/status'); if (this.httpIsReady()) { res.status(200).end(); } else { this.logger.error(`Plugin is not inialized yet, we don't have any worker_id & authentification_token`); res.status(503).end(); } } setErrorHandler() { this.app.use((err, req, res, next) => { this.logger.error(`Something bad happened : ${err.message} - ${err.stack ? err.stack : 'stack undefined'}`); return res.status(500).send(`${err.message} \n ${err.stack ? err.stack : 'stack undefined'}`); }); } initLogLevelUpdateRoute() { //Route used by the plugin manager to check if the plugin is UP and running this.app.put('/v1/log_level', this.asyncMiddleware(async (req, res) => { this.onLogLevelUpdateHandler(req, res); })); } initLogLevelGetRoute() { this.app.get('/v1/log_level', this.asyncMiddleware(async (req, res) => { this.onLogLevelRequest(req, res); })); } initStatusRoute() { this.app.get('/v1/status', this.asyncMiddleware(async (req, res) => { this.onStatusRequest(req, res); })); } initInitRoute() { this.app.post('/v1/init', this.asyncMiddleware(async (req, res) => { // not useful anymore, keep it during the migration phase res.status(200).end(); })); } getMetadata() { var _a; const dependencies = {}; try { dependencies['@mediarithmics/plugins-nodejs-sdk'] = (_a = require(process.cwd() + '/node_modules/@mediarithmics/plugins-nodejs-sdk/package.json')) === null || _a === void 0 ? void 0 : _a.version; } catch (err) { } return { runtime: 'node', runtime_version: process.version, group_id: process.env.PLUGIN_GROUP_ID, artifact_id: process.env.PLUGIN_ARTIFACT_ID, plugin_type: process.env.PLUGIN_TYPE, plugin_version: process.env.PLUGIN_VERSION, plugin_build: process.env.PLUGIN_BUILD, dependencies, }; } initMetadataRoute() { this.app.get('/v1/metadata', this.asyncMiddleware(async (req, res) => { try { res.status(200).send(this.getMetadata()); } catch (err) { res.status(500).send({ status: 'error', message: `${err.message}` }); } })); } } exports.BasePlugin = BasePlugin; //# sourceMappingURL=BasePlugin.js.map