Name
global.OneApiExecutionUtil
Description
No description available
Script
var OneApiExecutionUtil = Class.create();
OneApiExecutionUtil.prototype = {
initialize: function() {
},
makeExecutionServicePlan: function(inputs, outputs) {
// find service plan features
var planFeatureGr = new GlideRecord("one_api_service_plan_feature");
planFeatureGr.addActiveQuery();
planFeatureGr.addQuery("service_plan", inputs.service_plan.getUniqueValue());
planFeatureGr.query();
var featureList = [];
var planObj = {};
var planInput = inputs.input;
this.validatePlanInput(planInput, inputs.service_plan.name);
if (!this.isObject(planInput)) {
planInput = this.escapeJSONString(planInput);
}
while (planFeatureGr.next()) {
var feature = {
"sys_id" : new String(planFeatureGr.feature.sys_id),
"name": planFeatureGr.feature.name,
"app_request_id" : planInput.features[new String(planFeatureGr.feature.name)].requestId,
"timeout_sec": planFeatureGr.timeout_sec,
};
featureList.push(feature);
planObj[planFeatureGr.feature.name] = this.generateDependencies(planFeatureGr.depends_on.feature.name.toString());
}
// run algorithm that sorts features according to dependencies
var executionPlanArray = this.determinePlanOrder(planObj);
var metadata = this.escapeJSONString(inputs.input).meta || {};
var servicePlanIsBlocking = metadata.blocking === true || metadata.blocking === 'true';
// Create the one_api_service_plan_invocation record
var planInvocationRecord = new GlideRecord("one_api_service_plan_invocation");
planInvocationRecord.flow_context = FlowScriptAPI.getContextID();
planInvocationRecord.service_plan = inputs.service_plan.sys_id;
planInvocationRecord.input = inputs.input;
planInvocationRecord.execution_plan = JSON.stringify(executionPlanArray);
planInvocationRecord.app_document_table = planInput.documentTable;
planInvocationRecord.app_document = planInput.documentId;
planInvocationRecord.app_transaction_id = planInput.transactionId;
planInvocationRecord.execution_mode = (true === servicePlanIsBlocking ? "sync": "async");
var planInvocationSysId = planInvocationRecord.insert();
var featuresById = this.arrayToMap(featureList, "sys_id");
var executionPlanMap = this.arrayToMap(executionPlanArray, "feature");
// create the one_api_service_plan_feature_invocation records
var featureProviderIdsByFeatureIds = this.getFeatureProvidersByFeatureId(featureList);
for (var byFeatureId in featureProviderIdsByFeatureIds) {
if (Object.prototype.hasOwnProperty.call(featureProviderIdsByFeatureIds, byFeatureId)) {
var byFeature = featureProviderIdsByFeatureIds[byFeatureId];
var featureInvocationGr = new GlideRecord("one_api_service_plan_feature_invocation");
var hasDependentFeature = true === executionPlanMap[byFeature.feature_name].has_dependent;
var featureIsBlocking = servicePlanIsBlocking || hasDependentFeature;
featureInvocationGr.active = true;
featureInvocationGr.service_plan_invocation = planInvocationSysId;
featureInvocationGr.execution_mode = (true === featureIsBlocking ? "blocking": "non-blocking");
featureInvocationGr.feature_provider = byFeature.feature_provider_sys_id;
featureInvocationGr.app_request_id = featuresById[byFeatureId].app_request_id;
featureInvocationGr.timeout_sec = featuresById[byFeatureId].timeout_sec;
featureInvocationGr.timeout_policy = featuresById[byFeatureId].timeout_policy;
featureInvocationGr.insert();
}
}
// set the outputs
outputs.service_plan_invocation = planInvocationRecord;
outputs.feature_execution_plan = executionPlanArray;
},
prepareFeature: function(inputs, outputs) {
var featureInvocationGR = new GlideRecord("one_api_service_plan_feature_invocation");
featureInvocationGR.addQuery("service_plan_invocation", inputs.service_plan_invocation.sys_id);
featureInvocationGR.addQuery("feature_provider.feature.name", inputs.feature_name);
featureInvocationGR.query();
featureInvocationGR.next();
var completedAt = new global.GlideDateTime();
// Short-circuit if this feature is NOT applicable
if (!this.isApplicable(inputs, outputs)) {
completedAt = new global.GlideDateTime();
featureInvocationGR.completed_at = completedAt;
featureInvocationGR.completed_at_millis = completedAt.getNumericValue();
featureInvocationGR.output = JSON.stringify({ status: 'N/A' });
featureInvocationGR.sequence = this.escapeJSONString(inputs.sequence);
featureInvocationGR.status = 'N/A';
featureInvocationGR.update();
outputs.skipped = true;
this.logFeatureInvocation(featureInvocationGR);
return;
}
// Parse the feature's inputs, then adapt the inputs required by the target FDIH Action (or Subflow)
var featureInput = inputs.service_plan_input;
var featureInputText = inputs.service_plan_input;
while (!this.isObject(featureInput)) {
featureInputText = featureInput;
var parsed = this.escapeJSONString(featureInput);
if (parsed == null || parsed === featureInput) break;
featureInput = parsed;
}
var rawFeatureInput = featureInput.features[inputs.feature_name].payload;
featureInvocationGR.raw_input = JSON.stringify(rawFeatureInput);
var startedAt = new global.GlideDateTime();
featureInvocationGR.started_at = startedAt;
featureInvocationGR.started_at_millis = startedAt.getNumericValue();
var statusCheckGR = new GlideRecord("one_api_service_plan_feature_invocation");
statusCheckGR.addQuery("service_plan_invocation", inputs.service_plan_invocation.sys_id);
statusCheckGR.addQuery("status", 'timed_out');
statusCheckGR.addQuery("timeout_policy", "exit_service_plan_execution");
statusCheckGR.query();
if (statusCheckGR.next()) {
// previous step timed out, exit early
completedAt = new global.GlideDateTime();
featureInvocationGR.completed_at = completedAt;
featureInvocationGR.completed_at_millis = completedAt.getNumericValue();
featureInvocationGR.output = JSON.stringify({ warning: 'skipped' });
featureInvocationGR.sequence = this.escapeJSONString(inputs.sequence);
featureInvocationGR.status = 'skipped';
featureInvocationGR.update();
outputs.skipped = true;
this.logFeatureInvocation(featureInvocationGR);
return;
}
if (!this.isFeatureInputValid(featureInput, inputs.feature_name)) {
completedAt = new global.GlideDateTime();
featureInvocationGR.completed_at = completedAt;
featureInvocationGR.completed_at_millis = completedAt.getNumericValue();
featureInvocationGR.output = JSON.stringify({ error: 'Missing input' });
featureInvocationGR.sequence = this.escapeJSONString(inputs.sequence);
featureInvocationGR.status = 'error';
featureInvocationGR.update();
this.logFeatureInvocation(featureInvocationGR);
throw new Error('Missing feature input for: Service Plan ['
+ inputs.service_plan_invocation.service_plan.name + '], '
+ 'Feature [' + inputs.feature_name + ']');
}
var fdihDocSysId = featureInvocationGR.feature_provider.type_document;
var featureService = this.getFeatureService(featureInvocationGR.feature_provider);
var adaptedFeatureInput = featureService.toProviderInput(rawFeatureInput);
var adaptedFeatureInputText = JSON.stringify(adaptedFeatureInput);
featureInvocationGR.input = adaptedFeatureInputText;
featureInvocationGR.update();
if (!this.isObject(adaptedFeatureInput)) {
completedAt = new global.GlideDateTime();
featureInvocationGR.completed_at = completedAt;
featureInvocationGR.completed_at_millis = completedAt.getNumericValue();
featureInvocationGR.output = JSON.stringify({ error: 'Invalid JSON' });
featureInvocationGR.sequence = this.escapeJSONString(inputs.sequence);
featureInvocationGR.status = 'error';
featureInvocationGR.update();
this.logFeatureInvocation(featureInvocationGR);
throw new Error('Feature service produced invalid JSON for: Service Plan ['
+ inputs.service_plan_invocation.service_plan.name + '], '
+ 'Feature [' + inputs.feature_name + ']');
}
outputs.feature_input = adaptedFeatureInputText;
outputs.timeout_sec = featureInvocationGR.timeout_sec;
outputs.skipped = false;
outputs.feature_invocation_id = featureInvocationGR.getUniqueValue();
var subflowGR = new GlideRecord('sys_hub_flow');
subflowGR.get(fdihDocSysId);
outputs.subflow = subflowGR;
},
completeFeature: function(inputs, outputs) {
var featureInvocationGR = new GlideRecord('one_api_service_plan_feature_invocation');
featureInvocationGR.get(inputs.invocation_id);
var featureService = this.getFeatureService(featureInvocationGR.feature_provider);
var rawFeatureOutputs = inputs.feature_outputs;
var rawFeatureOutputsText = JSON.stringify(rawFeatureOutputs);
var featureOutputs = featureService.fromProviderOutput(rawFeatureOutputs);
var featureOutputsText = featureOutputs;
var validJson = true;
if (this.isObject(featureOutputsText)) {
featureOutputsText = JSON.stringify(featureOutputsText);
} else {
validJson = false;
}
outputs.output = featureOutputs;
var completedAt = new global.GlideDateTime();
featureInvocationGR.completed_at = completedAt;
featureInvocationGR.completed_at_millis = completedAt.getNumericValue();
featureInvocationGR.raw_output = rawFeatureOutputsText;
featureInvocationGR.output = featureOutputsText;
featureInvocationGR.flow_context = inputs.context_id;
featureInvocationGR.status = 'success';
featureInvocationGR.update();
this.logFeatureInvocation(featureInvocationGR);
if (!validJson) {
gs.warn('One API - Invalid JSON returned from provider for feature invocation [' + inputs.invocation_id + ']');
}
},
executeFeature: function(inputs, outputs) {
var MILLISECONDS = 1000;
var featureInput = inputs.featureInput;
var featureName = inputs.featureScope + '.' + inputs.featureName;
var timeout = inputs.timeout;
if (timeout) {
timeout = timeout * MILLISECONDS;
}
var execution = sn_fd.FlowAPI.getRunner()
.subflow(featureName)
.inForeground()
.timeout(timeout)
.withInputs({ input: featureInput });
var result;
try {
result = execution.run();
} catch (e) {
if (e.toString().startsWith('com.glide.sys.ExecutionTimeoutException')) {
outputs.timed_out = true;
return;
}
throw e;
}
if (result.getOutputs()) {
outputs.output = result.getOutputs().output;
}
outputs.timed_out = false;
outputs.context_id = result.getContextId();
},
gatherAllOutputs: function(inputs, outputs) {
var gr = new GlideRecord("one_api_service_plan_feature_invocation");
gr.addQuery("service_plan_invocation", inputs.service_plan_invocation.sys_id);
gr.addNotNullQuery("completed_at");
gr.query();
var output =
{
"service_plan" : new String(inputs.service_plan_invocation.service_plan.sys_id),
"contextId": new String(inputs.service_plan_invocation.flow_context.sys_id),
"output": {
"status": "COMPLETE",
"error": "",
"transactionId": new String(inputs.service_plan_invocation.app_transaction_id),
"documentId": new String(inputs.service_plan_invocation.app_document),
"documentTable": new String(inputs.service_plan_invocation.app_document_table),
"features": {},
"completedAt": new global.GlideDateTime().getDisplayValue()
}
};
var failureDetected = false;
while (gr.next()) {
var theResultOutput = gr.output;
if (!this.isString(theResultOutput)) {
theResultOutput = this.escapeJSONString(theResultOutput.toString());
}
var featureResult = {
"requestId": new String(gr.app_request_id),
"contextId": new String(gr.flow_context.sys_id),
"sequence": parseInt(gr.sequence),
"completedAt": new String(gr.completed_at),
"status" : new String(gr.status),
"result": theResultOutput,
"meta" : {},
"errors" : []
};
if (gr.status == 'timed_out' && gr.timeout_policy == 'exit_service_plan_execution') {
output.output.status = 'TIMED_OUT';
}
if (gr.status == 'error') {
failureDetected = true;
}
output.output.features[new String(gr.feature_provider.feature.name)] = featureResult;
}
if (failureDetected) {
output.output.status = 'COMPLETE_WITH_ERRORS';
}
outputs.output = JSON.stringify(output);
return output;
},
determinePlanOrder: function(plan) {
// associate each node with a level
var util = this;
function findLevels(tree) {
var levels = {};
Object.keys(tree).forEach(function(node) {
findLevel(levels, tree, node, {});
});
return levels;
}
// associate given node with a level
function findLevel(levels, tree, node, history) {
if (tree[node] == null) {
levels[node] = 0;
history[node] = true;
return 0;
}
if (levels[node]) return levels[node];
if (history[node]) {
throw new Error('Feature cycle detected.');
}
var dependencyLevels = [];
history[node] = true;
tree[node].forEach(function(dependency) {
dependencyLevels.push(findLevel(levels, tree, dependency, history));
});
var level = util.max(dependencyLevels) + 1;
levels[node] = level;
return level;
}
// get a complete map of nodes which other nodes depend on
function findDependencyNodes(tree) {
var dependencies = {};
util.objectValues(tree).forEach(function(nodeDependencies) {
if (nodeDependencies) {
nodeDependencies.forEach(function(node) { dependencies[node] = true; });
}
});
return dependencies;
}
// generate a sorting score which prioritizes level, then lack of dependents
function generateSortingScore(level, hasDependent) {
return level + (hasDependent ? .5 : 0);
}
var levels = findLevels(plan);
var dependencies = findDependencyNodes(plan);
var order = [];
this.objectEntries(levels).forEach(function(level) {
var node = level[0];
var baseScore = level[1];
has_dependent = !!dependencies[node];
var score = generateSortingScore(baseScore, has_dependent);
if (plan.hasOwnProperty(node)) {
order.push({ feature: node, has_dependent: has_dependent, score: score });
}
});
order.sort(function(a, b) { return (a.score - b.score); });
order.forEach(function(feature) { delete feature.score; });
return order;
},
/** convert feature's depends_on into a list of dependencies */
generateDependencies: function(dependsOn) {
if (!dependsOn) return null;
var dependencies = dependsOn.split(',');
var results = [];
dependencies.forEach(function(dependency) {
results.push(dependency.trim());
});
return results;
},
getFeatureProvidersByFeatureId: function(featureList) {
var featureSysIds = featureList.map(function(f){return f.sys_id;});
var gr = new GlideRecord("one_api_feature_provider");
gr.addActiveQuery();
gr.addQuery("feature", "IN", featureSysIds);
gr.query();
var featureProvidersByFeatureId = {};
while (gr.next()) {
var featureSysId = new String(gr.feature.sys_id);
featureProvidersByFeatureId[featureSysId] = {
"feature_name": new String(gr.feature.name),
"feature_sys_id": featureSysId,
"feature_provider_sys_id": new String(gr.sys_id)
};
}
return featureProvidersByFeatureId;
},
getFeatureService: function(featureProviderSysId) {
var featureProviderGr = new GlideRecord("one_api_feature_provider");
featureProviderGr.get(featureProviderSysId);
var evaluator = new GlideScopedEvaluator();
var featureService = evaluator.evaluateScript(featureProviderGr, 'feature_service_factory', null);
return featureService;
},
isApplicable: function(inputs, outputs) {
var servicePlanSysId = inputs.service_plan_invocation.service_plan.sys_id;
var planFeatureGr = new GlideRecord("one_api_service_plan_feature");
var query = "active=true^applicability_typeANYTHING^applicability_scriptISNOTEMPTY^" +
"ORapplicability_conditionISNOTEMPTY^service_plan=" + servicePlanSysId + "^feature.name=" + inputs.feature_name;
planFeatureGr.addEncodedQuery(query);
planFeatureGr.query();
if (planFeatureGr.next()) {
if ("script" == planFeatureGr.applicability_type) {
return this.evaluateScriptApplicability(inputs, outputs, planFeatureGr);
} else if ("condition" == planFeatureGr.applicability_type) {
return this.evaluateConditionApplicability(inputs, outputs, planFeatureGr);
}
}
return true;
},
evaluateScriptApplicability: function(inputs, outputs, planFeatureGr) {
try {
var previousFeatureOutputs;
if (!gs.nil(planFeatureGr.depends_on)) {
previousFeatureOutputs = this.gatherAllOutputs(inputs, outputs).output.features;
}
var servicePlanInput = this.escapeJSONString(inputs.service_plan_input);
var evaluator = new GlideScopedEvaluator();
evaluator.putVariable('featureInput', servicePlanInput.features[inputs.feature_name].payload);
evaluator.putVariable('previousFeatureOutputs', previousFeatureOutputs);
var isApplicable = evaluator.evaluateScript(planFeatureGr, 'applicability_script', null);
return true === isApplicable;
} catch (e) {
gs.warn('Exception while evaluating OneAPI feature applicability ' + JSON.stringify(e));
return false;
}
},
evaluateConditionApplicability: function(inputs, outputs, planFeatureGr) {
var gr = new GlideRecord(planFeatureGr.applicability_condition_table);
gr.addEncodedQuery(planFeatureGr.applicability_condition);
gr.query();
return gr.next();
},
validatePlanInput: function(planInput, plan) {
var input;
try {
input = this.escapeJSONString(planInput);
} catch (e) {
throw new Error('Invalid JSON provided for service plan ['
+ plan + '].', { cause: e });
}
if (!input || !this.isObject(input)) {
throw new Error('Invalid JSON provided for service plan ['
+ plan + '].');
}
if (!input.features) {
throw new Error('Invalid JSON provided for service plan ['
+ plan + ']: missing "features" key.');
}
},
isFeatureInputValid: function(servicePlanInput, featureName) {
if (servicePlanInput == null) return false;
if (servicePlanInput.features == null) return false;
return this.isObject(servicePlanInput.features[featureName]);
},
isString: function(val) {
return !this.isObject(val) && Object.prototype.toString.call(val) === '[object String]';
},
isObject: function(val) {
var type = typeof val;
return type === 'function' || type === 'object' && !!val;
},
/** equivalent of Math.max(...array) */
max: function(values) {
var maximum = 0;
values.forEach(function(value) {
maximum = Math.max(maximum, value);
});
return maximum;
},
/** equivalent of Object.entries(object) */
objectEntries: function(object) {
var keys = Object.keys(object);
var values = this.objectValues(object);
var result = [];
var index;
for (index = 0; index < keys.length; index++) {
result.push([keys[index], values[index]]);
}
return result;
},
arrayToMap: function(arr, arrayKeyName) {
var asMap = {};
for (var i = 0; i < arr.length; ++i){
var arrElement = arr[i];
asMap[arrElement[arrayKeyName]] = arrElement;
}
return asMap;
},
/** equivalent of Object.values(object) */
objectValues: function(object) {
var keys = Object.keys(object);
var result = [];
var index;
for (index = 0; index < keys.length; index++) {
result.push(object[[keys[index]]]);
}
return result;
},
logFeatureInvocation: function(featureInvocationGR) {
if (gs.nil(featureInvocationGR)) return;
var logProp = gs.getProperty("com.glide.oneapi.feature.invocation.node_logging_enabled", "true");
var enabled = (true == logProp || "true" == logProp);
if (!enabled) return;
var feature = featureInvocationGR.feature_provider.feature;
var servicePlanInvocation = featureInvocationGR.service_plan_invocation;
var context = this.newLoggingContext();
context.api_type = "ONE_API_FEATURE";
context.api_id = feature.sys_id;
context.api_name = feature.name;
context.service_plan = servicePlanInvocation.service_plan.sys_id;
context.service_plan_name = servicePlanInvocation.service_plan.name;
context.status = featureInvocationGR.status;
context.mode = servicePlanInvocation.execution_mode;
context.context_id = featureInvocationGR.flow_context.sys_id;
context.app_document = servicePlanInvocation.app_document;
context.app_document_table = servicePlanInvocation.app_document_table;
context.transaction_id = servicePlanInvocation.app_transaction_id;
context.started_at = servicePlanInvocation.started_at.getDisplayValue();
context.started_at_millis = servicePlanInvocation.started_at_millis;
if (!gs.nil(featureInvocationGR.completed_at)) {
context.completed_at = featureInvocationGR.completed_at.getDisplayValue();
context.completed_at_millis = featureInvocationGR.completed_at_millis;
context.feature_duration = (featureInvocationGR.completed_at_millis - featureInvocationGR.started_at_millis);
context.feature_in_plan_duration =
(featureInvocationGR.completed_at_millis - servicePlanInvocation.started_at_millis);
}
new sn_log.GlideLogger("com.glide.oneapi", context, Object.keys(context)).info("Feature Invocation Result");
},
escapeJSONString: function(JSONString) {
var EscapedJSONString = JSONString.replace(/\\n/g, "\\n")
.replace(/\\'/g, "\\'")
.replace(/\\"/g, '\\"')
.replace(/\\&/g, "\\&")
.replace(/\\r/g, "\\r")
.replace(/\\t/g, "\\t")
.replace(/\\b/g, "\\b")
.replace(/\\f/g, "\\f");
return JSON.parse(EscapedJSONString);
},
newLoggingContext: function() {
return {"app": "CI", "track": "ONE_API", "layer":"FDIH"};
},
type: 'OneApiExecutionUtil'
};
Sys ID
37bf98c62dda0110fa9bb8e1eb575758