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.

The Challenge

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.

The Solution

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:

  1. Define Required Fields: Enforce which fields must be present in the input.
  2. Handle Optional Fields: Allow for fields that are not strictly required.
  3. Override Values: Provide default values for specific fields when provided by the REST API.
  4. Validate Data Types: Validate data based on a defined type for the fields, to ensure the data is in the proper format.
  5. Enforce Data Integrity: Prevent invalid records by ensuring data is in the correct format.
  6. 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.

Scripted REST Resource [sys_ws_operation]

(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:

  1. 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 a reference 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.
  2. setSupportedFieldTypes() Function:
    • Iterates through the FIELDS object and if no type is present, it sets the type to reference if a table is defined, or defaults to a string.
  3. 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.
    • 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's SimpleDateFormat, based on the user's date/time format.
      • boolean: Validates that the input values are true or false.
      • Otherwise just sets the value in the record.
  4. GlideRecord Initialization:
    • A new GlideRecord is created for the change_request table, and the records values are set.
  5. 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.
  6. 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.
  7. 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.

How to Implement

  1. 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.
  2. 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.
  3. Configure the FIELDS Object:
    • Adjust the FIELDS object to reflect your specific Change Request fields.
  4. Test Thoroughly:
    • Use a REST client or similar tool to test your REST API. Make sure to test with both valid and invalid inputs.

Best Practices

  • 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.

Security Considerations

  • 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.

Conclusion

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.

⚠️ **GitHub.com Fallback** ⚠️