Robust Change Request Creation via Scripted REST API - ben-vargas/servicenow-wiki GitHub Wiki
This article outlines a solution for creating Change Requests via a Scripted REST API, emphasizing robust input validation, error handling, and flexible field mapping. The solution leverages a well-defined schema for validation and includes a check for conflicts before inserting a new record.
When creating records in ServiceNow via a REST API, it's essential to validate input data, handle errors gracefully, and provide informative responses to the calling system. This includes validating required fields, parsing different input types, and providing meaningful error messages, all while ensuring data integrity.
This solution employs a Scripted REST API resource that accepts JSON input for creating a Change Request. It leverages a predefined schema (the FIELDS
object) to:
- Define Required Fields: Enforce which fields must be present in the input.
- Handle Optional Fields: Allow for fields that are not strictly required.
- Override Values: Provide default values for specific fields when provided by the REST API.
- Validate Data Types: Validate data based on a defined type for the fields, to ensure the data is in the proper format.
- Enforce Data Integrity: Prevent invalid records by ensuring data is in the correct format.
- Check for Conflicts: After inserting the record, it runs a conflict detection check to verify no other change requests clash with the newly created change.
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Define field schema with requirements, types, and configurations
var FIELDS = {
short_description: { required: true, type: 'string' }, // Short Description
type: { required: true, type: 'string' }, // Change Type
u_cdt_justification: { required: true, type: 'string' }, // Change Category
u_parent_continuous_delivery: {
required: true,
type: 'reference',
table: 'change_request',
display: 'number',
string: 'type=Normal^u_continuous_delivery=true^approval=approved'
}, // Kind of Continuous Delivery
u_severity_level: { required: true, type: 'string' }, // Severity Level
u_hot_fix_type: { required: false, type: 'string' }, // Hot Fix Type
requested_by: { required: true, type: 'reference' }, // Requested By
u_primary_technical_contact: { required: false, type: 'reference' }, // Primary Technical Contact
assigned_to: { required: false, type: 'reference' }, // CM/RM Assigned
u_release: { required: false, type: 'reference' }, // Release
u_deployer_assigned: { required: false, type: 'reference' }, // Deployer Assigned
u_division: { required: true, type: 'reference' }, // Division
u_cmdb_ci_capability: {
required: true,
type: 'reference',
table: 'u_capability',
display: 'name',
string: 'name!=Production Support^install_status=1'
}, // Capability
u_cmdb_ci_experience: {
required: false,
type: 'reference',
table: 'u_experience',
display: 'name',
string: 'install_status=1'
}, // Experience
u_technical_dependencies: { required: true, type: 'string' }, // Technical Dependencies
risk: { required: true, type: 'string' }, // Risk
u_pci_change: { required: true, type: 'boolean' }, // PCI Change?
u_peer_reviewer: { required: 'u_pci_change', type: 'reference' }, // Peer Reviewer
description: { required: true, type: 'string' }, // Description
u_business_driver: { required: true, type: 'string' }, // Business Driver
u_geography_impacted: {
required: true,
type: 'reference',
table: 'u_temp_geo',
display: 'name',
string: 'install_status!=7'
}, // Geography Impacted
start_date: { required: true, type: 'glide_date_time' }, // Implementation Date
end_date: { required: true, type: 'glide_date_time' }, // End Date
u_outage_required: { type: 'string', override_value: "No" }, // Outage Required
u_code_change: { type: 'boolean', override_value: true }, // Change Area: Code Change
comments: { required: false, type: 'string' } // Additional comments
};
var TAG = '[Scripted REST Resource: /api/hs/v1/change_request/create_continuous_delivery.process()] ';
var log = new GSLog('com.hs.log_level', TAG.trim());
var json = new JSON();
// Modifies FIELDS.<key>.type to establish internal_type of each field.
setSupportedFieldTypes(FIELDS);
log.debug(TAG + 'FIELDS After setSupportedFieldTypes: ' + json.encode(FIELDS));
var body = request.body.data;
var returnObject = {};
var failed_validations;
var errorStr;
// Process any override_value field in FIELDS; update the body with override value.
for (var key in FIELDS) {
if (FIELDS[key].override_value)
body[key] = FIELDS[key].override_value;
}
// Instantiate the change_request, used for validation and eventual insert.
var changeGR = new GlideRecord('change_request');
changeGR.initialize();
// Validate the required fields are populated with data.
// Modifies the changeGR as fields are set and validated.
failed_validations = validateFields(body, FIELDS, changeGR);
if (failed_validations.length === 0) {
try {
changeGR.state = 100;
changeGR.contact_type = 'REST API';
changeGR.approval = 'not requested';
var sysId = changeGR.insert();
if (!sysId) {
response.setError(new sn_ws_err.InternalServerError('Failure inserting record, sys_id not returned.'));
return;
}
// Return the PD sys_id, number, and summary back to the integration for informational purposes.
returnObject.sys_id = sysId + '';
returnObject.number = changeGR.number + '';
var conflictDetector = new ChangeCheckConflicts(changeGR);
var conflictResults = conflictDetector.check();
changeGR.conflict_status = 'No Conflict';
var msg;
if (conflictResults < 0) {
changeGR.conflict_status = 'Not Run';
changeGR.update();
msg = 'Configuration Item needed for conflict analysis';
errorStr = msg;
returnObject.error = {
'message': errorStr,
'detail': ''
};
returnObject.status = 'Success';
writeLogEntry(request, returnObject, 'exception_caught');
} else if (conflictResults === 0) {
changeGR.state = 101;
changeGR.approval = 'requested';
changeGR.conflict_status = 'No Conflict';
changeGR.update();
msg = 'There are NO CONFLICTS';
returnObject.sys_id = changeGR.sys_id + '';
returnObject.number = changeGR.number + '';
writeLogEntry(request, returnObject, 'success');
} else {
msg = 'CONFLICT - this Change Request falls within a Restricted Change Window (RCW) and/or Freeze. Please follow-up with CDT Change Management';
changeGR.state = 101;
changeGR.approval = 'requested';
changeGR.conflict_status = 'Conflict';
changeGR.update();
errorStr = msg;
returnObject.error = {
'message': errorStr,
'detail': ''
};
returnObject.status = 'Success';
writeLogEntry(request, returnObject, 'exception_caught');
}
} catch (err) {
errorStr = 'Failure attempting to insert new Continuous Delivery Change: ' + err;
response.setError(new sn_ws_err.BadRequestError(errorStr));
returnObject.error = {
'message': errorStr,
'detail': ''
};
returnObject.status = 'failure';
writeLogEntry(request, returnObject, 'exception_caught');
return;
}
} else {
// Use our own stream writer for the error return as standard sn_ws_err.*Error() doesn't handle JSON well.
var writer = response.getStreamWriter();
var hdrs = {};
hdrs['Content-Type'] = 'application/json';
response.setStatus(403);
response.setHeaders(hdrs);
errorStr = 'Failure attempting to create new Continuous Delivery Change due to one or more failed validations.';
returnObject.error = {
'message': errorStr,
'detail': failed_validations
};
returnObject.status = 'failure';
writer.writeString(JSON.stringify(returnObject));
writeLogEntry(request, returnObject, 'validation_failed');
return;
}
return returnObject;
function setSupportedFieldTypes(fieldObject) {
for (var key in fieldObject) {
if (fieldObject[key].table && !fieldObject[key].type) {
fieldObject[key].type = 'reference';
} else if (!fieldObject[key].type) {
fieldObject[key].type = 'string';
}
}
}
function validateFields(body, FIELDS, changeGR) {
var invalid_fields = [];
for (var key in FIELDS) {
var bad_field = {};
if (FIELDS[key].required === true && !body[key]) {
bad_field.field = key;
bad_field.info = 'This field is required';
invalid_fields.push(bad_field);
} else if (FIELDS[key].required && typeof FIELDS[key].required === 'string' && body[FIELDS[key].required] && !body[key]) {
bad_field.field = key;
bad_field.info = 'This field is required due to another field';
invalid_fields.push(bad_field);
} else if (body[key]) {
if (FIELDS[key].type === 'reference') {
if (FIELDS[key].table) {
var ref = new GlideRecord(FIELDS[key].table);
if (ref.get(body[key])) {
changeGR[key] = body[key];
} else {
bad_field.field = key;
bad_field.info = 'Reference value not valid on table: ' + FIELDS[key].table;
invalid_fields.push(bad_field);
}
} else {
changeGR[key] = body[key];
}
} else if (FIELDS[key].type === 'glide_date_time') {
try {
var simplified_date = new Packages.java.text.SimpleDateFormat();
simplified_date.setLenient(false);
simplified_date.applyPattern(gs.getDateTimeFormat());
var mydate = simplified_date.parse(body[key]);
changeGR[key] = body[key];
} catch (err) {
bad_field.field = key;
bad_field.info = 'Date not in valid format: ' + body[key] + ' -- Expecting: ' + gs.getDateTimeFormat();
invalid_fields.push(bad_field);
}
} else if (FIELDS[key].type === 'boolean') {
if (body[key] == 'true' || body[key] === true) {
changeGR[key] = true;
} else if (body[key] == 'false' || body[key] === false) {
changeGR[key] = false;
} else {
bad_field.field = key;
bad_field.info = 'Value is not a boolean, valid value is true or false';
invalid_fields.push(bad_field);
}
} else {
changeGR[key] = body[key];
}
}
}
return invalid_fields;
}
function writeLogEntry(request, returnObject, status) {
var activityLogGR = new GlideRecord('x_hs_cd_change_log'); // Replace with actual table
activityLogGR.initialize();
activityLogGR.change_request = returnObject.sys_id;
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);
Explanation:
-
Field Schema (
FIELDS
):- Defines an object with the details of each expected field, including:
-
required
: Boolean or string that defines if a field is required for the request. -
type
: Specifies the type of field for type validation purposes. -
table
,display
,string
: If areference
type, further defines table to look up, display value, and encoded query respectively. -
override_value
: A default value that will be used if no value is provided in the input.
-
- Defines an object with the details of each expected field, including:
-
setSupportedFieldTypes()
Function:- Iterates through the
FIELDS
object and if notype
is present, it sets thetype
toreference
if a table is defined, or defaults to astring
.
- Iterates through the
-
Data Validation:
-
Override Values: The
override_value
properties are added to the input before validation. -
Required Fields: A check is done to ensure required fields are populated based on the
required
value set in the object.- If the
required
value is a string, it checks to make sure the dependent field has a value in the body.
- If the
-
Field Type Validation: Input values are validated based on the
type
field.-
reference
: If a table is specified, it validates the reference on the target table. -
glide_date_time
: It validates the data using Java'sSimpleDateFormat
, based on the user's date/time format. -
boolean
: Validates that the input values aretrue
orfalse
. - Otherwise just sets the value in the record.
-
-
Override Values: The
-
GlideRecord Initialization:
- A new GlideRecord is created for the
change_request
table, and the records values are set.
- A new GlideRecord is created for the
-
Conflict Detection:
- After inserting the record, it calls the
ChangeCheckConflicts
Script Include to verify that there are no conflicts. - Based on the results, the workflow status is updated, and a message is returned to the end user.
- After inserting the record, it calls the
-
Error Handling:
- Validation Failures: If validation fails, a custom stream writer returns a JSON response with an HTTP status code of 403, an error message, and the failed validation details.
- Insert Errors: If the insert method fails, the code gracefully catches and handles the error, logging information, and returning a specific response.
- GlideRecord.insert() Error: The code now includes a check to make sure that a sys_id is returned after using the insert method, and will return an error if not.
-
Logging:
- Activity Logs: The response (success, validation failure, exception) and the request data are logged to a custom activity log table to aid with auditing and debugging.
-
Create the 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/change_request/create_continuous_delivery
.
-
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 the
FIELDS
Object:- Adjust the
FIELDS
object to reflect your specific Change Request fields.
- Adjust the
-
Test Thoroughly:
- Use a REST client or similar tool to test your REST API. Make sure to test with both valid and invalid inputs.
-
Clear Schema: Use a well-defined schema like the
FIELDS
object to streamline validation and improve maintainability. -
Explicit Types: Using the
type
field ensures consistency and validates the input data against the expected type. -
Error Handling: Implement proper
try...catch
blocks for robust error handling. - Specific Errors: Return detailed error messages that specify the exact reason for the error, including the failed field name when possible.
- Logging: Use logging to track errors and activity details in the platform for later analysis.
- Input Sanitization: Always sanitize input values to prevent malicious code injection or unexpected script issues.
- Authorization: Add authorization checks to ensure only authorized applications or users can make API calls.
- Asynchronous Processing: For large and complex records, consider performing the actual record insertion and updating in an asynchronous process to prevent the REST call from taking too long to respond.
- Script Includes: Use Script Includes to separate and abstract logic from the REST API resource script, improving reusability and maintainability.
- Input Validation: Validate all input data to prevent SQL injection, cross-site scripting (XSS), or other security vulnerabilities.
- Authentication: Implement proper authentication and authorization to ensure that only authorized applications or users can make API calls.
- Data Handling: Handle sensitive data securely, making sure to avoid logging sensitive information. Consider using encryption or masking sensitive data when logging errors.
- Error Messaging: Avoid exposing information about the inner workings of the system when returning errors.
This solution provides a robust and flexible framework for creating Change Requests through a REST API. By focusing on input validation, comprehensive logging, and modular code, you can ensure your application is reliable, secure, and easy to maintain. Remember to always test all changes thoroughly in a non-production instance before deploying to production.