@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
JavaScript
;
/* 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