UNPKG

devebot

Version:

Nodejs Microservice Framework

834 lines (832 loc) 26.5 kB
"use strict"; const assert = require("assert"); const Promise = require("bluebird"); const lodash = require("lodash"); const constx = require("../utils/constx"); const chores = require("../utils/chores"); const nodash = require("../utils/nodash"); const errors = require("../utils/errors"); const getenv = require("../utils/getenv"); const BeanProxy = require("../utils/proxy"); const blockRef = chores.getBlockRef(__filename); const _isUpgradeSupported = chores.isUpgradeSupported.bind(chores); const FRAMEWORK_NAMESPACE_UCASE = lodash.toUpper(constx.FRAMEWORK.NAMESPACE); const FRAMEWORK_BRIDGE_LABEL = "bridge"; const FRAMEWORK_PLUGIN_LABEL = "plugin"; const NOOP = function () {}; const MODE = getenv([FRAMEWORK_NAMESPACE_UCASE + "_NODE_ENV", "NODE_ENV"]) === "test" ? null : "direct"; function ObjectDecorator(params = {}) { const loggingFactory = params.loggingFactory.branch(blockRef); const nameResolver = params.nameResolver; const L = loggingFactory.getLogger(); const T = loggingFactory.getTracer(); const C = lodash.assign({ L, T }, lodash.pick(params, ["issueInspector", "schemaValidator"])); const decoratorCfg = lodash.get(params, ["profileConfig", "decorator"], {}); const textureStore = lodash.get(params, ["textureConfig"]); const instanceId = T.get("instanceId"); const streamId = extractStreamId(decoratorCfg.logging, params.appInfo, instanceId); this.wrapBridgeDialect = function (beanConstructor, opts) { if (!_isUpgradeSupported("bean-decorator")) { return beanConstructor; } const textureOfBean = getTextureOfBridge({ textureStore: textureStore, pluginCode: opts.pluginCode || nameResolver.getDefaultAliasOf(opts.pluginName, FRAMEWORK_PLUGIN_LABEL), bridgeCode: opts.bridgeCode || nameResolver.getDefaultAliasOf(opts.bridgeName, FRAMEWORK_BRIDGE_LABEL), dialectName: opts.dialectName }); const fullObjectName = getBridgeFullname({ pluginName: opts.pluginName || nameResolver.getOriginalNameOf(opts.pluginCode, FRAMEWORK_PLUGIN_LABEL), bridgeCode: opts.bridgeCode || nameResolver.getDefaultAliasOf(opts.bridgeName, FRAMEWORK_BRIDGE_LABEL), dialectName: opts.dialectName }); return wrapConstructor(C, beanConstructor, lodash.assign({ textureOfBean, objectName: fullObjectName, streamId }, lodash.pick(opts, ["logger", "tracer", "supportAllMethods", "useDefaultTexture"]))); }; this.wrapPluginGadget = function (beanConstructor, opts) { if (!_isUpgradeSupported("bean-decorator")) { return beanConstructor; } const textureOfBean = getTextureOfPlugin({ textureStore: textureStore, pluginCode: opts.pluginCode || nameResolver.getDefaultAliasOf(opts.pluginName, FRAMEWORK_PLUGIN_LABEL), gadgetType: opts.gadgetType, gadgetName: opts.gadgetName }); const fullObjectName = getPluginFullname({ pluginName: opts.pluginName || nameResolver.getOriginalNameOf(opts.pluginCode, FRAMEWORK_PLUGIN_LABEL), gadgetName: opts.gadgetName }); const supportAllMethods = determineOptionValue("supportAllMethods", ["services"], opts); const useDefaultTexture = determineOptionValue("useDefaultTexture", ["services", "triggers"], opts); return wrapConstructor(C, beanConstructor, lodash.assign({ textureOfBean, supportAllMethods, useDefaultTexture, objectName: fullObjectName, streamId }, lodash.pick(opts, ["logger", "tracer"]))); }; } ObjectDecorator.argumentSchema = { "$id": "objectDecorator", "type": "object", "properties": { "appInfo": { "type": "object" }, "issueInspector": { "type": "object" }, "loggingFactory": { "type": "object" }, "nameResolver": { "type": "object" }, "schemaValidator": { "type": "object" }, "profileConfig": { "type": "object" }, "textureConfig": { "type": "object" } } }; module.exports = ObjectDecorator; function determineOptionValue(fieldName, conditional, opts) { if (opts) { if (fieldName in opts) { return opts[fieldName]; } return conditional.indexOf(opts.gadgetType) >= 0; } return false; } function wrapConstructor(refs, constructor, opts) { opts = opts || {}; if (!opts.textureOfBean || opts.textureOfBean.enabled === false) { return constructor; } return new Proxy(constructor, { construct: function (target, argumentsList, newTarget) { function F() { return target.apply(this, argumentsList); } F.prototype = target.prototype; // F = target.bind.apply(target, [target].concat(argumentsList)); // F = Function.prototype.bind.apply(target, [target].concat(argumentsList)); return wrapObject(refs, new F(), opts); }, apply: function (target, thisArg, argumentsList) { const createdObject = target.apply(thisArg, argumentsList) || thisArg; return wrapObject(refs, createdObject, opts); } }); } function wrapObject(refs, object, opts) { if (!lodash.isObject(object) || lodash.isArray(object)) { return object; } opts = opts || {}; refs = refs || {}; const { L, T } = refs; const cachedFields = {}; const cached = {}; return new BeanProxy(object, { get(target, property, receiver) { const node = Reflect.get(target, property, receiver); false && L && L.has("dunce") && L.log("dunce", T && T.add({ path: this.path, property, itemType: typeof node }).toMessage({ text: "#{path} / #{property} -> #{itemType}" })); if (chores.isOwnOrInheritedProperty(target, property)) { if (lodash.isFunction(node) || lodash.isObject(node)) { const lane = this.slug; if (lane) { cachedFields[lane] = cachedFields[lane] || this.wrap(node); return cachedFields[lane]; } return this.wrap(node); } } return node; }, apply(target, thisArg, argList) { const methodName = lodash.get(this.path, this.path.length - 1); const fieldChain = lodash.slice(this.path, 0, this.path.length - 1); const methodPath = this.path.join("."); L && L.has("dunce") && L.log("dunce", T && T.add({ objectName: opts.objectName, fieldChain, methodName, methodPath }).toMessage({ text: "Method: #{objectName}.#{methodPath} is invoked", info: argList })); if (!cached[methodPath]) { const { textureOfBean } = opts; const supportAllMethods = chores.getFirstDefinedValue(textureOfBean && textureOfBean.supportAllMethods, opts.supportAllMethods); const emptyTexture = supportAllMethods ? {} : null; const texture = getTextureByPath({ textureOfBean, fieldChain, methodName }) || emptyTexture; if (lodash.isObject(texture)) { const useDefaultTexture = chores.getFirstDefinedValue(texture.useDefaultTexture, textureOfBean && textureOfBean.useDefaultTexture, opts.useDefaultTexture); if (useDefaultTexture) { const SELECTED = opts.streamId ? DEFAULT_TEXTURE_WITH_STREAM_ID : DEFAULT_TEXTURE; lodash.defaultsDeep(texture, SELECTED); } } const owner = thisArg || (fieldChain.length > 0 ? lodash.get(object, fieldChain) : object); const ownerName = opts.objectName && [opts.objectName].concat(fieldChain).join(".") || null; cached[methodPath] = { method: wrapMethod(refs, target, { texture: texture, object: owner, objectName: ownerName, methodName: methodName, streamId: opts.streamId, logger: opts.logger || object.logger, tracer: opts.tracer || object.tracer }), spread: isProxyRecursive(texture) }; } const node = cached[methodPath].method.apply(thisArg, argList); if (node === thisArg) return node; if (cached[methodPath].spread && !isPromise(node)) { if (lodash.isFunction(node) || lodash.isObject(node)) { return this.wrap(node); } } return node; } }); } function wrapMethod(refs, target, opts) { if (!lodash.isFunction(target)) return target; const texture = lodash.get(opts, "texture", null); const loggingEnabled = isLoggingEnabled(texture); const mockingEnabled = isMockingEnabled(texture); let method = target; if (mockingEnabled || loggingEnabled) { const { objectName, methodName, streamId } = opts || {}; const object = lodash.get(opts, "object", null); const logger = opts && opts.logger || refs && refs.L; const tracer = opts && opts.tracer || refs && refs.T; if (mockingEnabled) { const mockingProxy = new MockingInterceptor({ texture, object, objectName, method, methodName, logger, tracer }); method = mockingProxy.capsule; } if (loggingEnabled) { const loggingProxy = new LoggingInterceptor({ texture, object, objectName, method, methodName, streamId, logger, tracer }); method = loggingProxy.capsule; } } return method; } function MockingInterceptor(params) { const { logger, tracer, texture, method, methodName, objectName } = params; const enabled = isMockingEnabled(texture) && !lodash.isEmpty(texture.mocking.mappings); let capsule; Object.defineProperty(this, "capsule", { get: function () { if (!enabled) return method; return capsule = capsule || new Proxy(method, { apply: function (target, thisArg, argumentsList) { const output = {}; const generate = getMockingGenerator(texture, thisArg, argumentsList); if (generate) { try { output.result = generate.apply(thisArg, argumentsList); } catch (error) { output.exception = error; } if (logger && tracer) { const requestId = detectRequestId(argumentsList); logger.has("info") && logger.log("info", tracer.add({ objectName, methodName, requestId }).toMessage({ text: "Req[#{requestId}] #{objectName}.#{methodName} has been mocked" }, MODE)); } if (texture.methodType === "callback") { const pair = extractCallback(argumentsList); if (pair.callback) { return pair.callback.apply(null, [output.exception].concat(output.result)); } } if (texture.methodType === "promise") { if (output.exception) { return Promise.reject(output.exception); } return Promise.resolve(output.result); } if (output.exception) { throw output.exception; } return output.result; } else { if (texture.mocking.unmatched === "exception") { const MockNotFoundError = errors.assertConstructor("MockNotFoundError"); output.exception = new MockNotFoundError("All of selectors are unmatched"); if (texture.methodType === "promise") { return Promise.reject(output.exception); } if (texture.methodType === "callback") { const pair = extractCallback(argumentsList); if (pair.callback) { return pair.callback(output.exception); } } throw output.exception; } else { return method.apply(thisArg, argumentsList); } } } }); } }); } function getMockingGenerator(texture, thisArg, argumentsList) { for (const name in texture.mocking.mappings) { const rule = texture.mocking.mappings[name]; if (!lodash.isFunction(rule.selector)) continue; if (!lodash.isFunction(rule.generate)) continue; if (rule.selector.apply(thisArg, argumentsList)) { return rule.generate; } } return null; } function LoggingInterceptor(params = {}) { const { logger, tracer, preciseThreshold } = params; const { texture, object, objectName, method, methodName, streamId } = params; const counter = { promise: 0, callback: 0, general: 0 }; const pointer = { current: null, actionFlow: null, preciseThreshold }; assert.ok(lodash.isObject(logger)); assert.ok(lodash.isObject(tracer)); assert.ok(lodash.isObject(texture)); assert.ok(lodash.isFunction(method)); // pre-processing logging texture const methodType = texture.methodType; pointer.preciseThreshold = pointer.preciseThreshold || 5; function createListener(texture, eventName) { const onEventName = "on" + eventName; const onEvent = texture.logging && texture.logging[onEventName]; if (!isEnabled(onEvent)) return NOOP; return function (logState, data, extra) { // determine the requestId if (lodash.isFunction(onEvent.getRequestId) && eventName === "Request") { let reqId = null; if (onEvent.getRequestId === DEFAULT_TEXTURE.logging[onEventName].getRequestId) { reqId = onEvent.getRequestId(data); } else { try { reqId = onEvent.getRequestId.call(logState.reqContext, data, extra); } catch (fatal) { reqId = "getRequestId-throw-an-error"; } } if (reqId) { logState.requestId = reqId; logState.requestType = "link"; } else { logState.requestId = chores.getUUID(); logState.requestType = "head"; } } // determine the action type (i.e. explicit or implicit) if (pointer.actionFlow) { logState.actionFlow = pointer.actionFlow; } const msgObj = { text: "Req[#{requestId}] #{objectName}.#{methodName}" }; switch (eventName) { case "Request": msgObj.text += " is invoked"; break; case "Success": msgObj.text += " has done"; break; case "Failure": msgObj.text += " has failed"; break; } if (lodash.isFunction(onEvent.extractInfo)) { if (onEvent.extractInfo === DEFAULT_TEXTURE.logging[onEventName].extractInfo) { msgObj.info = onEvent.extractInfo(data); } else { try { msgObj.info = onEvent.extractInfo.call(logState.reqContext, data, extra); } catch (fatal) { msgObj.info = { event: onEventName, errorName: fatal.name, errorMessage: fatal.message }; } } } if (lodash.isString(onEvent.template)) { msgObj.text = onEvent.template; } msgObj.tags = onEvent.tags || texture.tags; if (!lodash.isArray(msgObj.tags) && lodash.isEmpty(msgObj.tags)) { delete msgObj.tags; } const logLevel = onEvent.logLevel || (eventName === "Failure" ? "error" : "debug"); logger.has(logLevel) && logger.log(logLevel, tracer.add(logState).toMessage(msgObj, MODE)); }; } const CALLED_EVENTS = ["Request", "Success", "Failure"]; const logOnEvent = lodash.mapValues(lodash.keyBy(CALLED_EVENTS), function (value) { return createListener(texture, value); }); const __state__ = { object, method, methodType, counter, pointer }; let capsule; Object.defineProperty(this, "capsule", { get: function () { return capsule = capsule || new Proxy(method, { apply: function (target, thisArg, argumentsList) { const logState = { streamId, objectName, methodName, reqContext: {} }; return callMethod(__state__, argumentsList, logOnEvent, logState); } }); } }); this.getState = function () { return lodash.cloneDeep(lodash.pick(__state__, ["methodType", "counter", "pointer"])); }; } function callMethod(refs, argumentsList, logOnEvent, logState) { const { object, method, methodType, counter, pointer } = refs; function _detect(argumentsList) { let result = null; let exception = null; let found = false; const pair = proxifyCallback(argumentsList, logOnEvent, logState, function () { hitMethodType(pointer, counter, "callback"); }); try { logOnEvent.Request(logState, pair.parameters); result = method.apply(object, pair.parameters); if (isPromise(result)) { found = true; hitMethodType(pointer, counter, "promise"); result = Promise.resolve(result).then(function (value) { logOnEvent.Success(logState, value); return value; }).catch(function (error) { logOnEvent.Failure(logState, error); return Promise.reject(error); }); } else { logOnEvent.Success(logState, result); } } catch (error) { exception = error; logOnEvent.Failure(logState, exception); } if (!found) { hitMethodType(pointer, counter, "general"); } // return both result & exception return { result, exception }; } function _invoke(argumentsList) { let result, exception; switch (methodType) { case "promise": { result = Promise.resolve().then(function () { logOnEvent.Request(logState, argumentsList); return method.apply(object, argumentsList); }).then(function (value) { logOnEvent.Success(logState, value); return value; }).catch(function (error) { logOnEvent.Failure(logState, error); return Promise.reject(error); }); break; } case "callback": { try { const pair = proxifyCallback(argumentsList, logOnEvent, logState); logOnEvent.Request(logState, pair.parameters); result = method.apply(object, pair.parameters); } catch (error) { exception = error; logOnEvent.Failure(logState, exception); } break; } default: { try { logOnEvent.Request(logState, argumentsList); result = method.apply(object, argumentsList); logOnEvent.Success(logState, result); } catch (error) { exception = error; logOnEvent.Failure(logState, exception); } break; } } return { result, exception }; } argumentsList = chores.argumentsToArray(argumentsList); let output = null; if (methodType) { pointer.actionFlow = "explicit"; output = _invoke(argumentsList); } else { pointer.actionFlow = "implicit"; output = _detect(argumentsList); refs.methodType = suggestMethodType(pointer, counter, methodType); } // an error is occurred if (output.exception) { throw output.exception; } return output.result; } function hitMethodType(pointer, counter, methodType) { if (methodType) { if (methodType === "callback") { counter[methodType]++; } else { if (methodType !== pointer.current && pointer.current) { for (const name in counter) { counter[name] = 0; } } pointer.current = methodType; counter[methodType]++; } } else { counter.total = counter.total || 0; counter.total++; } } function suggestMethodType(pointer, counter, methodType) { const threshold = pointer.preciseThreshold || 100; let max = "promise"; let min = "general"; if (counter[max] < counter[min]) { let tmp = max; max = min; min = tmp; } if (counter[max] >= threshold) { if (counter["callback"] / counter[max] > 0.7) { return "callback"; } if (counter[min] === 0) { return max; } } return methodType; } function isPromise(p) { return lodash.isObject(p) && lodash.isFunction(p.then); } function extractCallback(argumentsList) { const r = {}; r.callback = argumentsList.length > 0 && argumentsList[argumentsList.length - 1] || null; if (typeof r.callback === "function") { r.parameters = Array.prototype.slice.call(argumentsList, 0, argumentsList.length - 1); } else { r.parameters = argumentsList; delete r.callback; } return r; } function proxifyCallback(argumentsList, logOnEvent, logState, checker) { const pair = extractCallback(argumentsList); if (pair.callback) { pair.parameters.push(new Proxy(pair.callback, { apply: function (target, thisArg, callbackArgs) { typeof checker === "function" && checker(); const error = callbackArgs[0]; if (error) { logOnEvent.Failure(logState, error); } else { logOnEvent.Success(logState, ...Array.prototype.slice.call(callbackArgs, 1)); } return pair.callback.apply(thisArg, callbackArgs); } })); } return pair; } function isEnabled(section) { return section && section.enabled !== false; } function isLoggingEnabled(texture) { return isEnabled(texture) && isEnabled(texture.logging); } function isMockingEnabled(texture) { return isEnabled(texture) && isEnabled(texture.mocking); } function getTextureByPath({ textureOfBean, fieldChain, methodName }) { let texture = null; if (nodash.isObject(textureOfBean)) { const beanToMethod = []; if (nodash.isArray(fieldChain) && fieldChain.length > 0) { Array.prototype.push.apply(beanToMethod, fieldChain); } if (methodName) { beanToMethod.push(methodName); } texture = lodash.get(textureOfBean, ["methods"].concat(beanToMethod)); texture = texture || lodash.get(textureOfBean, ["methods", beanToMethod.join(".")], texture); } return propagateEnabled(texture, textureOfBean); } function getBridgeFullname({ pluginName, bridgeCode, dialectName }) { return pluginName + chores.getSeparator() + bridgeCode + "#" + dialectName; } function getTextureOfBridge({ textureStore, pluginCode, bridgeCode, dialectName, dialectPath }) { const rootToBean = []; if (lodash.isArray(dialectPath) && !lodash.isEmpty(dialectPath)) { rootToBean.push("bridges"); Array.prototype.push.apply(rootToBean, dialectPath); } else { if (pluginCode && bridgeCode && dialectName) { rootToBean.push("bridges", bridgeCode, pluginCode, dialectName); } } const textureOfBean = rootToBean.length > 0 ? lodash.get(textureStore, rootToBean) : textureStore; return propagateEnabled(textureOfBean, textureStore); } function getPluginFullname({ pluginName, gadgetType, gadgetName }) { return pluginName + chores.getSeparator() + gadgetName; } function getTextureOfPlugin({ textureStore, pluginCode, gadgetType, gadgetName }) { const rootToBean = []; if (pluginCode) { if (chores.isSpecialBundle(pluginCode)) { rootToBean.push(pluginCode); } else { rootToBean.push("plugins", pluginCode); } if (gadgetType) { rootToBean.push(gadgetType); if (gadgetName) { rootToBean.push(gadgetName); } } } const textureOfBean = rootToBean.length > 0 ? lodash.get(textureStore, rootToBean) : textureStore; return propagateEnabled(textureOfBean, textureStore); } function propagateEnabled(childTexture, parentTexture) { if (parentTexture && parentTexture.enabled === false) { if (childTexture && childTexture.enabled == null) { childTexture.enabled = parentTexture.enabled; } } return childTexture; } function detectRequestId(argumentsList) { let reqId; if (argumentsList && argumentsList.length > 0) { for (let k = argumentsList.length - 1; k >= 0; k--) { const o = argumentsList[k]; reqId = o && (o.requestId || o.reqId); if (typeof reqId === "string") break; } } return reqId; } function isProxyRecursive(texture) { if (!texture) return false; const fields = ["recursive", "spread", "outspread", "nested", "wrapped"]; for (const i in fields) { if (texture[fields[i]]) return true; } return false; } function extractStreamId(logging, appInfo, instanceId) { if (lodash.isObject(logging)) { if (logging.enabled === false) { return undefined; } if (lodash.isString(logging.streamIdExpression)) { return chores.formatTemplate(logging.streamIdExpression, appInfo); } if (lodash.isFunction(logging.streamIdExtractor)) { try { const streamId = logging.streamIdExtractor(lodash.pick(appInfo, ["name", "version", "framework.name", "framework.version"]), instanceId); if (lodash.isString(streamId)) return streamId; } catch (fatal) { return instanceId || "streamIdExtractor-throw-an-error"; } } } return instanceId; } const DEFAULT_TEXTURE = { logging: { onRequest: { getRequestId: detectRequestId, extractInfo: function (argumentsList) { return chores.extractObjectInfo(chores.argumentsToArray(argumentsList)); }, template: "Req[#{requestId}] #{objectName}.#{methodName}() #{requestType}" }, onSuccess: { extractInfo: function (result) { return chores.extractObjectInfo(result); }, template: "Req[#{requestId}] #{objectName}.#{methodName}() completed" }, onFailure: { extractInfo: function (error) { error = error || { code: "null" }; return { errorName: error.name, errorCode: error.code, errorMessage: error.message }; }, template: "Req[#{requestId}] #{objectName}.#{methodName}() failed" } } }; const DEFAULT_TEXTURE_WITH_STREAM_ID = lodash.defaultsDeep({ logging: { onRequest: { template: "Req[#{requestId}/#{streamId}] #{objectName}.#{methodName}() #{requestType}" }, onSuccess: { template: "Req[#{requestId}/#{streamId}] #{objectName}.#{methodName}() completed" }, onFailure: { template: "Req[#{requestId}/#{streamId}] #{objectName}.#{methodName}() failed" } } }, DEFAULT_TEXTURE);