Dynamic Action Dispatch with Script Includes - ben-vargas/servicenow-wiki GitHub Wiki
This article describes how to implement a dynamic action dispatch mechanism in ServiceNow using Script Includes and a configuration object. This approach allows for flexible and scalable handling of various actions, centralizing logic, and promoting code reusability. This solution uses a combination of a configuration object, dynamic object creation, and dynamic method calls to simplify the process of performing complex actions based on an incoming request.
The Challenge
When building complex applications or integrations, you often need to perform different actions based on various inputs or conditions. A common approach is to use a large conditional statement or switch case, which can become difficult to manage and extend over time. A more modular and dynamic solution is needed to improve scalability and maintainability.
The Solution
This solution utilizes a combination of a configuration object, dynamic instantiation of Script Include classes, and a dynamic method call to efficiently handle different actions. The primary component is an ACTIONS
object that defines the configuration for each possible action and which script include and method to execute.
Implementation Components
The solution involves these key elements:
ACTIONS
Object: A JavaScript object that defines all possible actions and their configuration settings.- Dynamic Script Include Loading: JavaScript code to dynamically instantiate Script Includes based on configuration settings.
- Dynamic Method Execution: Dynamically execute the correct method within the instantiated Script Include, providing request parameters.
- Error Handling: A try/catch block around the logic to prevent script failures from propagating, and to gracefully handle errors.
- Robust Input Validation: Validates the incoming request and user permissions.
Scripted REST Resource [sys_ws_operation]
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var ACTIONS = {
u_alert_accept_reject: {
validate_gliderecord: true,
validate_user: true,
script_include: "HSSlackUtils",
script_method: "u_alertAcceptReject"
}
};
var TAG = '[Scripted REST Resource: /api/hs/v1/dynamic_action_dispatch] ';
var log = new GSLog('com.hs.log_level', TAG.trim());
var returnObject = {};
var errorStr;
try {
var actionArray = ["u_alert_accept_reject"]; // Example - Replace with actual logic to get the action from the request.
if (!actionArray || actionArray.length === 0) {
errorStr = 'No action specified';
returnObject.error = {
'message': errorStr,
'detail': 'The `actionArray` is either null or empty.'
};
response.setStatus(400);
response.getStreamWriter().writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'validation_failed');
return;
}
var actionName = actionArray[0];
var actionConfig = ACTIONS[actionName];
if (!actionConfig) {
errorStr = 'Invalid action specified: ' + actionName;
returnObject.error = {
'message': errorStr,
'detail': 'There is no configuration associated with the action: ' + actionName
};
response.setStatus(400);
response.getStreamWriter().writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'validation_failed');
return;
}
var scriptIncludeName = actionConfig.script_include;
var scriptMethodName = actionConfig.script_method;
var payloadObj = request.body.data;
var targetGR; // Assume targetGR is loaded elsewhere. You'll need to provide your actual implementation.
var slackUserGR; // Assume slackUserGR is loaded elsewhere. You'll need to provide your actual implementation.
var responseObj; // Assume responseObj is initialized elsewhere. You'll need to provide your actual implementation.
if (!scriptIncludeName) {
errorStr = 'No Script Include provided for action ' + actionName;
returnObject.error = {
'message': errorStr,
'detail': 'There is no `script_include` property defined for the action ' + actionName
};
response.setStatus(400);
response.getStreamWriter().writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'validation_failed');
return;
}
if (!scriptMethodName) {
errorStr = 'No script method provided for action ' + actionName;
returnObject.error = {
'message': errorStr,
'detail': 'There is no `script_method` property defined for the action ' + actionName
};
response.setStatus(400);
response.getStreamWriter().writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'validation_failed');
return;
}
// Attempt to dynamically load and initialize Script Include
var dynamicLib = new global[scriptIncludeName]();
if (!dynamicLib) {
errorStr = 'Failed to instantiate Script Include: ' + scriptIncludeName;
returnObject.error = {
'message': errorStr,
'detail': 'The Script Include class with the name of ' + scriptIncludeName + ' could not be created. Confirm the script include exists.'
};
response.setStatus(400);
response.getStreamWriter().writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'exception_caught');
return;
}
// Dynamically call the specified method with the needed parameters
try {
dynamicLib[scriptMethodName](responseObj, payloadObj, slackUserGR, actionArray, targetGR);
} catch (ex) {
errorStr = 'An error occurred while attempting to execute the dynamic method ' + scriptMethodName +
' in script include ' + scriptIncludeName + '. Error: ' + ex;
returnObject.error = {
'message': errorStr,
'detail': 'An exception was thrown trying to execute the dynamic function.'
};
response.setStatus(500);
response.getStreamWriter().writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'exception_caught');
return;
}
returnObject.status = 'success';
writeLogEntry(request, returnObject, 'success');
} catch (ex) {
errorStr = 'An error occurred during dynamic dispatch: ' + ex;
returnObject.error = {
'message': errorStr,
'detail': ex.message
};
returnObject.status = 'failure';
response.setStatus(500);
response.getStreamWriter().writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'exception_caught');
} finally {
return returnObject;
}
function writeLogEntry(request, returnObject, status) {
var activityLogGR = new GlideRecord('x_hs_dynamic_action_log'); // Replace with actual table
activityLogGR.initialize();
activityLogGR.action_type = (actionArray || []).join(',');
activityLogGR.http_status = status;
activityLogGR.request_body = request.body.data;
if(returnObject.error) {
activityLogGR.response_body = JSON.stringify(returnObject.error);
} else {
activityLogGR.response_body = JSON.stringify(returnObject);
}
activityLogGR.insert();
}
})(request, response);
Detailed Explanation:
ACTIONS
Object:- This object defines the configuration for each action. The object should contain the action name as the key, and the following information about the action in the object that is the value of the key:
validate_gliderecord
: A boolean value representing if a GlideRecord should be validated by the Script Include method.validate_user
: A boolean value representing if a user should be validated by the Script Include method.script_include
: A string value that represents the Script Include to use.script_method
: A string value that represents the method of the script include class to execute.
- This object defines the configuration for each action. The object should contain the action name as the key, and the following information about the action in the object that is the value of the key:
- Error Handling:
- The entire script is wrapped in a
try...catch...finally
block to handle any potential errors that may be thrown by the execution. - Error messages provide context and debugging details.
- Response status codes are set for client-facing error scenarios.
- If an exception is thrown during the execution of the script, the method will exit early and log the error before returning.
- The entire script is wrapped in a
- Dynamic Instantiation of the Script Include:
var dynamicLib = new global[scriptIncludeName]();
dynamically loads and instantiates the class defined in the specified Script Include usingglobal[]
notation which provides dynamic access to the classes in the scope.- A null check is done on the variable
dynamicLib
to ensure the object was created successfully, if it was not an error will be returned.
- Dynamic Method Execution:
dynamicLib[scriptMethodName](responseObj, payloadObj, slackUserGR, actionArray, targetGR);
dynamically executes the specified method on thedynamicLib
object using the request data and theactionArray
used to define what method should be called.- A
try...catch
is implemented around the function call to ensure errors are properly handled.
- Logging:
- The
writeLogEntry()
function logs the request and response data to an activity log table for debugging and auditing purposes, along with the status (success or exception).
- The
How to Implement
- Create Script Include(s): Ensure the Script Include specified in the
script_include
properties are available in your instance and configured as "Client Callable". - Create Scripted REST API Resource:
- Create a new Scripted REST API resource.
- Configure the resource to accept POST requests.
- Set the path to something like
/api/hs/v1/dynamic_action_dispatch
.
- Copy and Paste Code:
- Copy the provided code and paste it into the "Script" section of your REST API resource, making sure to update the table name to use the correct log table.
- Configure
ACTIONS
Object:- Modify the
ACTIONS
object to specify the different actions you will use with the API, along with any required validations, and script include/method information.
- Modify the
- Test Thoroughly:
- Use a REST client or similar tool to test the REST API with different actions to ensure they work as expected.
Best Practices
- Well-Defined Actions: Use the
ACTIONS
object to clearly define the set of possible actions and configurations. - Modularity: By using Script Includes, and an object for configurations, the code is modular and more maintainable, and easier to debug.
- Error Handling: Always include
try...catch
blocks and error logging to gracefully handle any errors that may occur during the process. - Input Validation: Make sure to validate the inputs in the Script Include methods, to ensure there are no errors due to incorrect input.
- Logging: Use
gs.info
orgs.debug
throughout your code to track errors and successful activities. - Code Comments: Add comments in your code for future maintainability.
Security Considerations
- Script Include Access: Ensure that the script includes used in this solution are only accessible to the scopes that are required. If other scopes require access, you can use the
Accessible from
field on the script include to grant them access. - Parameter Validation: Be sure to validate all input parameters from the request in the Script Include methods and handle invalid values.
- Authentication and Authorization: Ensure that the Scripted REST API resource is protected with the appropriate security measures.
Conclusion
This solution provides a dynamic and flexible way to execute different actions within a Scripted REST API by using a configuration object, and dynamic object creation, to reduce code duplication, and improve code maintainability. By using best practices, and adding robust error handling, you can build a more robust and secure solution to handle different action requests. Remember to always test all changes thoroughly in a non-production environment before deploying to production.