diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/README.md b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/README.md
new file mode 100644
index 0000000000..b9d46d23e3
--- /dev/null
+++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/README.md
@@ -0,0 +1,200 @@
+# Modern JavaScript Patterns for ServiceNow Client Scripts
+
+This collection demonstrates modern JavaScript ES6+ patterns and best practices specifically adapted for ServiceNow client scripts, providing clean, maintainable, and performance-optimized code for form interactions.
+
+## 📋 Table of Contents
+
+- [Async/Await API Integration](#asyncawait-api-integration)
+- [Promise-Based Form Operations](#promise-based-form-operations)
+- [ES6+ Form Field Management](#es6-form-field-management)
+- [Modern Event Handling](#modern-event-handling)
+- [Advanced State Management](#advanced-state-management)
+
+## 🚀 Modern JavaScript Features for ServiceNow
+
+### Async/Await for GlideAjax
+Replace callback-heavy GlideAjax patterns with modern async/await syntax for cleaner, more readable code.
+
+### Promise-Based Operations
+Implement Promise patterns for form operations, field updates, and user interactions.
+
+### ES6+ Syntax Enhancements
+- Template literals for dynamic string building
+- Destructuring for clean data extraction
+- Arrow functions for concise callback handling
+- Classes for reusable form components
+
+### Modern Event Handling
+- Event delegation patterns
+- Debounced input handling
+- Custom event systems
+
+## 🎯 Pattern Categories
+
+### API Integration Patterns
+- **Async GlideAjax**: Modern promise-based server communication
+- **Fetch-Style Operations**: Consistent API interaction patterns
+- **Error Handling**: Comprehensive error management with try/catch
+
+### Form Interaction Patterns
+- **Reactive Fields**: Field dependencies with modern observers
+- **State Management**: Form state tracking and management
+- **Validation**: Real-time validation with debouncing
+
+### User Experience Patterns
+- **Progressive Enhancement**: Graceful degradation strategies
+- **Loading States**: User feedback during async operations
+- **Responsive Design**: Mobile-friendly form interactions
+
+### Performance Patterns
+- **Debouncing**: Optimize frequent operations
+- **Memoization**: Cache expensive calculations
+- **Lazy Loading**: Load data and components on demand
+
+## 🔧 Implementation Guidelines
+
+### Modern JavaScript in ServiceNow
+- Use ES6+ features available in modern browsers
+- Implement fallbacks for legacy browser support
+- Leverage ServiceNow's client-side APIs effectively
+
+### Performance Best Practices
+- Minimize DOM manipulations
+- Use efficient event handling patterns
+- Implement proper cleanup for memory management
+
+### Code Organization
+- Modular function design
+- Reusable component patterns
+- Clear separation of concerns
+
+## 📊 Pattern Examples
+
+### Before (Traditional)
+```javascript
+function onChange(control, oldValue, newValue, isLoading) {
+ if (isLoading || newValue == '') {
+ return;
+ }
+
+ var ga = new GlideAjax('MyScriptInclude');
+ ga.addParam('sysparm_name', 'getData');
+ ga.addParam('sysparm_value', newValue);
+ ga.getXML(function(response) {
+ var answer = response.responseXML.documentElement.getAttribute("answer");
+ if (answer) {
+ var data = JSON.parse(answer);
+ g_form.setValue('field1', data.value1);
+ g_form.setValue('field2', data.value2);
+ }
+ });
+}
+```
+
+### After (Modern)
+```javascript
+async function onChange(control, oldValue, newValue, isLoading) {
+ if (isLoading || !newValue) return;
+
+ try {
+ const data = await fetchData(newValue);
+ updateFormFields(data);
+ } catch (error) {
+ handleError(error);
+ }
+}
+
+const fetchData = (value) => {
+ return new Promise((resolve, reject) => {
+ const ga = new GlideAjax('MyScriptInclude');
+ ga.addParam('sysparm_name', 'getData');
+ ga.addParam('sysparm_value', value);
+ ga.getXML(response => {
+ try {
+ const answer = response.responseXML.documentElement.getAttribute("answer");
+ resolve(JSON.parse(answer));
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+};
+
+const updateFormFields = ({ value1, value2 }) => {
+ g_form.setValue('field1', value1);
+ g_form.setValue('field2', value2);
+};
+```
+
+## 🛡️ Error Handling Patterns
+
+Modern error handling with comprehensive logging and user feedback:
+
+```javascript
+const withErrorHandling = (fn) => {
+ return async (...args) => {
+ try {
+ return await fn(...args);
+ } catch (error) {
+ console.error('Operation failed:', error);
+ g_form.addErrorMessage('An error occurred. Please try again.');
+ throw error;
+ }
+ };
+};
+```
+
+## 🔄 State Management Patterns
+
+Implement reactive state management for complex form interactions:
+
+```javascript
+class FormStateManager {
+ constructor() {
+ this.state = new Proxy({}, {
+ set: this.handleStateChange.bind(this)
+ });
+ }
+
+ handleStateChange(target, property, value) {
+ target[property] = value;
+ this.notifySubscribers(property, value);
+ return true;
+ }
+}
+```
+
+## 📈 Performance Optimization
+
+### Debouncing Pattern
+```javascript
+const debounce = (func, delay) => {
+ let timeoutId;
+ return (...args) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func.apply(this, args), delay);
+ };
+};
+```
+
+### Memoization Pattern
+```javascript
+const memoize = (fn) => {
+ const cache = new Map();
+ return (...args) => {
+ const key = JSON.stringify(args);
+ if (cache.has(key)) {
+ return cache.get(key);
+ }
+ const result = fn(...args);
+ cache.set(key, result);
+ return result;
+ };
+};
+```
+
+## 🔗 Related Documentation
+
+- [ServiceNow Client Scripts Documentation](https://developer.servicenow.com/dev.do#!/learn/learning-plans/tokyo/new_to_servicenow/app_store_learnv2_automatingapps_tokyo_client_scripts)
+- [Modern JavaScript (ES6+) Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide)
+- [ServiceNow GlideForm API](https://developer.servicenow.com/dev.do#!/reference/api/tokyo/client/c_GlideFormAPI)
diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/async_glideajax_patterns.js b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/async_glideajax_patterns.js
new file mode 100644
index 0000000000..367ffcc106
--- /dev/null
+++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/async_glideajax_patterns.js
@@ -0,0 +1,626 @@
+/**
+ * Modern Async/Await GlideAjax Patterns
+ *
+ * This client script demonstrates modern promise-based and async/await patterns
+ * for ServiceNow GlideAjax operations, providing cleaner and more maintainable
+ * code compared to traditional callback approaches.
+ *
+ * Type: Client Script (All types: onLoad, onChange, onSubmit, onCellEdit)
+ * Table: Any table
+ *
+ * @author: ServiceNow Community
+ * @version: 1.0
+ * @category: Client Scripts/Modern JavaScript
+ */
+
+/**
+ * Modern GlideAjax wrapper with Promise support
+ */
+class ModernGlideAjax {
+ constructor(scriptInclude) {
+ this.scriptInclude = scriptInclude;
+ this.defaultTimeout = 30000; // 30 seconds
+ this.retryAttempts = 3;
+ this.retryDelay = 1000; // 1 second
+ }
+
+ /**
+ * Execute server-side function with promise support
+ * @param {string} functionName - Server-side function name
+ * @param {Object} params - Parameters to pass
+ * @param {Object} options - Execution options
+ * @returns {Promise} Promise resolving to server response
+ */
+ async execute(functionName, params = {}, options = {}) {
+ const config = {
+ timeout: options.timeout || this.defaultTimeout,
+ retry: options.retry !== false,
+ showLoading: options.showLoading !== false,
+ ...options
+ };
+
+ if (config.showLoading) {
+ this._showLoadingIndicator();
+ }
+
+ try {
+ const result = await this._executeWithRetry(functionName, params, config);
+ return result;
+ } finally {
+ if (config.showLoading) {
+ this._hideLoadingIndicator();
+ }
+ }
+ }
+
+ /**
+ * Execute with retry logic
+ * @param {string} functionName - Function name
+ * @param {Object} params - Parameters
+ * @param {Object} config - Configuration
+ * @returns {Promise} Promise resolving to result
+ * @private
+ */
+ async _executeWithRetry(functionName, params, config) {
+ let lastError;
+
+ for (let attempt = 1; attempt <= (config.retry ? this.retryAttempts : 1); attempt++) {
+ try {
+ return await this._executeRequest(functionName, params, config.timeout);
+ } catch (error) {
+ lastError = error;
+
+ if (attempt < this.retryAttempts && this._isRetryableError(error)) {
+ console.warn(`GlideAjax attempt ${attempt} failed, retrying...`, error);
+ await this._delay(this.retryDelay * attempt);
+ } else {
+ break;
+ }
+ }
+ }
+
+ throw lastError;
+ }
+
+ /**
+ * Execute single GlideAjax request
+ * @param {string} functionName - Function name
+ * @param {Object} params - Parameters
+ * @param {number} timeout - Timeout in milliseconds
+ * @returns {Promise} Promise resolving to parsed response
+ * @private
+ */
+ _executeRequest(functionName, params, timeout) {
+ return new Promise((resolve, reject) => {
+ const ga = new GlideAjax(this.scriptInclude);
+ ga.addParam('sysparm_name', functionName);
+
+ // Add all parameters
+ Object.keys(params).forEach(key => {
+ ga.addParam(key, params[key]);
+ });
+
+ // Set up timeout
+ const timeoutId = setTimeout(() => {
+ reject(new Error(`Request timeout after ${timeout}ms`));
+ }, timeout);
+
+ ga.getXML(response => {
+ clearTimeout(timeoutId);
+
+ try {
+ const answer = response.responseXML.documentElement.getAttribute('answer');
+
+ if (!answer) {
+ reject(new Error('Empty response from server'));
+ return;
+ }
+
+ // Try to parse as JSON, fallback to string
+ let parsedResponse;
+ try {
+ parsedResponse = JSON.parse(answer);
+ } catch (e) {
+ parsedResponse = answer;
+ }
+
+ // Check for server-side errors
+ if (parsedResponse && parsedResponse.error) {
+ reject(new Error(parsedResponse.error));
+ return;
+ }
+
+ resolve(parsedResponse);
+
+ } catch (error) {
+ reject(new Error(`Failed to parse response: ${error.message}`));
+ }
+ });
+ });
+ }
+
+ /**
+ * Check if error is retryable
+ * @param {Error} error - Error to check
+ * @returns {boolean} True if retryable
+ * @private
+ */
+ _isRetryableError(error) {
+ const retryableErrors = [
+ 'timeout',
+ 'network',
+ 'connection',
+ 'server error'
+ ];
+
+ const errorMessage = error.message.toLowerCase();
+ return retryableErrors.some(retryable => errorMessage.includes(retryable));
+ }
+
+ /**
+ * Delay execution
+ * @param {number} ms - Milliseconds to delay
+ * @returns {Promise} Promise resolving after delay
+ * @private
+ */
+ _delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ /**
+ * Show loading indicator
+ * @private
+ */
+ _showLoadingIndicator() {
+ if (typeof g_form !== 'undefined') {
+ // ServiceNow form loading indicator
+ g_form.addInfoMessage('Loading...');
+ }
+ }
+
+ /**
+ * Hide loading indicator
+ * @private
+ */
+ _hideLoadingIndicator() {
+ if (typeof g_form !== 'undefined') {
+ g_form.clearMessages();
+ }
+ }
+}
+
+/**
+ * Async form field manager with modern patterns
+ */
+class AsyncFormManager {
+ constructor() {
+ this.ajax = new ModernGlideAjax('YourScriptInclude');
+ this.cache = new Map();
+ this.pendingRequests = new Map();
+ this.observers = new Map();
+ }
+
+ /**
+ * Get data with caching and deduplication
+ * @param {string} key - Cache key
+ * @param {Function} dataFetcher - Function to fetch data
+ * @param {Object} options - Options
+ * @returns {Promise} Promise resolving to data
+ */
+ async getCachedData(key, dataFetcher, options = {}) {
+ const { ttl = 300000, force = false } = options; // 5 minutes default TTL
+
+ // Check cache first
+ if (!force && this.cache.has(key)) {
+ const cached = this.cache.get(key);
+ if (Date.now() - cached.timestamp < ttl) {
+ return cached.data;
+ }
+ }
+
+ // Check for pending request
+ if (this.pendingRequests.has(key)) {
+ return await this.pendingRequests.get(key);
+ }
+
+ // Create new request
+ const request = dataFetcher().then(data => {
+ this.cache.set(key, {
+ data,
+ timestamp: Date.now()
+ });
+ this.pendingRequests.delete(key);
+ return data;
+ }).catch(error => {
+ this.pendingRequests.delete(key);
+ throw error;
+ });
+
+ this.pendingRequests.set(key, request);
+ return await request;
+ }
+
+ /**
+ * Update form field with validation
+ * @param {string} fieldName - Field name
+ * @param {any} value - Field value
+ * @param {Object} options - Update options
+ * @returns {Promise} Promise resolving when update complete
+ */
+ async updateField(fieldName, value, options = {}) {
+ const { validate = true, cascade = true } = options;
+
+ try {
+ // Pre-validation
+ if (validate) {
+ await this.validateFieldValue(fieldName, value);
+ }
+
+ // Update field
+ g_form.setValue(fieldName, value);
+
+ // Trigger cascading updates if enabled
+ if (cascade) {
+ await this.processCascadingUpdates(fieldName, value);
+ }
+
+ // Notify observers
+ this.notifyObservers(fieldName, value);
+
+ } catch (error) {
+ console.error(`Failed to update field ${fieldName}:`, error);
+ g_form.addErrorMessage(`Failed to update ${fieldName}: ${error.message}`);
+ throw error;
+ }
+ }
+
+ /**
+ * Validate field value against server rules
+ * @param {string} fieldName - Field name
+ * @param {any} value - Value to validate
+ * @returns {Promise} Promise resolving if valid
+ */
+ async validateFieldValue(fieldName, value) {
+ const validationResult = await this.ajax.execute('validateField', {
+ table: g_form.getTableName(),
+ field: fieldName,
+ value: value,
+ record_id: g_form.getUniqueValue()
+ });
+
+ if (!validationResult.valid) {
+ throw new Error(validationResult.message || 'Validation failed');
+ }
+
+ return validationResult;
+ }
+
+ /**
+ * Process cascading field updates
+ * @param {string} triggerField - Field that triggered the update
+ * @param {any} value - New value
+ * @returns {Promise} Promise resolving when cascading complete
+ */
+ async processCascadingUpdates(triggerField, value) {
+ try {
+ const cascadeRules = await this.getCachedData(
+ `cascade_rules_${g_form.getTableName()}`,
+ () => this.ajax.execute('getCascadeRules', {
+ table: g_form.getTableName(),
+ trigger_field: triggerField
+ })
+ );
+
+ if (cascadeRules && cascadeRules.length > 0) {
+ const updates = await Promise.allSettled(
+ cascadeRules.map(rule => this.applyCascadeRule(rule, value))
+ );
+
+ // Handle any failed updates
+ const failures = updates.filter(result => result.status === 'rejected');
+ if (failures.length > 0) {
+ console.warn('Some cascade updates failed:', failures);
+ }
+ }
+
+ } catch (error) {
+ console.error('Failed to process cascading updates:', error);
+ // Don't throw - cascading failures shouldn't block the main update
+ }
+ }
+
+ /**
+ * Apply individual cascade rule
+ * @param {Object} rule - Cascade rule
+ * @param {any} triggerValue - Value that triggered the cascade
+ * @returns {Promise} Promise resolving when rule applied
+ */
+ async applyCascadeRule(rule, triggerValue) {
+ const { target_field, calculation_type, parameters } = rule;
+
+ let newValue;
+
+ switch (calculation_type) {
+ case 'lookup':
+ newValue = await this.performLookup(parameters, triggerValue);
+ break;
+ case 'calculation':
+ newValue = await this.performCalculation(parameters, triggerValue);
+ break;
+ case 'clear':
+ newValue = '';
+ break;
+ default:
+ throw new Error(`Unknown calculation type: ${calculation_type}`);
+ }
+
+ g_form.setValue(target_field, newValue);
+ return { field: target_field, value: newValue };
+ }
+
+ /**
+ * Perform lookup operation
+ * @param {Object} parameters - Lookup parameters
+ * @param {any} triggerValue - Trigger value
+ * @returns {Promise} Promise resolving to lookup result
+ */
+ async performLookup(parameters, triggerValue) {
+ return await this.ajax.execute('performLookup', {
+ ...parameters,
+ trigger_value: triggerValue
+ });
+ }
+
+ /**
+ * Add field observer
+ * @param {string} fieldName - Field to observe
+ * @param {Function} callback - Callback function
+ */
+ addObserver(fieldName, callback) {
+ if (!this.observers.has(fieldName)) {
+ this.observers.set(fieldName, new Set());
+ }
+ this.observers.get(fieldName).add(callback);
+ }
+
+ /**
+ * Remove field observer
+ * @param {string} fieldName - Field name
+ * @param {Function} callback - Callback to remove
+ */
+ removeObserver(fieldName, callback) {
+ if (this.observers.has(fieldName)) {
+ this.observers.get(fieldName).delete(callback);
+ }
+ }
+
+ /**
+ * Notify field observers
+ * @param {string} fieldName - Field name
+ * @param {any} value - New value
+ */
+ notifyObservers(fieldName, value) {
+ if (this.observers.has(fieldName)) {
+ this.observers.get(fieldName).forEach(callback => {
+ try {
+ callback(fieldName, value);
+ } catch (error) {
+ console.error('Observer callback failed:', error);
+ }
+ });
+ }
+ }
+}
+
+// Create global instances for use in form scripts
+const modernAjax = new ModernGlideAjax('YourScriptInclude');
+const formManager = new AsyncFormManager();
+
+/**
+ * Modern onChange handler example
+ */
+async function modernOnChange(control, oldValue, newValue, isLoading, isTemplate) {
+ if (isLoading || isTemplate || newValue === oldValue) {
+ return;
+ }
+
+ const fieldName = control.name;
+
+ try {
+ // Example: Update related fields based on selection
+ if (fieldName === 'category') {
+ await updateCategoryDependentFields(newValue);
+ }
+
+ // Example: Validate field value
+ if (fieldName === 'priority') {
+ await validatePrioritySelection(newValue);
+ }
+
+ // Example: Load additional data
+ if (fieldName === 'assignment_group') {
+ await loadAssignmentGroupDetails(newValue);
+ }
+
+ } catch (error) {
+ console.error(`onChange failed for ${fieldName}:`, error);
+ g_form.addErrorMessage(`Failed to process ${fieldName} change: ${error.message}`);
+ }
+}
+
+/**
+ * Update category-dependent fields
+ * @param {string} categoryId - Selected category ID
+ */
+async function updateCategoryDependentFields(categoryId) {
+ if (!categoryId) {
+ g_form.clearValue('subcategory');
+ g_form.clearValue('u_category_description');
+ return;
+ }
+
+ const categoryData = await formManager.getCachedData(
+ `category_${categoryId}`,
+ () => modernAjax.execute('getCategoryDetails', { category_id: categoryId })
+ );
+
+ if (categoryData) {
+ // Clear and repopulate subcategory choices
+ g_form.clearOptions('subcategory');
+ categoryData.subcategories.forEach(sub => {
+ g_form.addOption('subcategory', sub.value, sub.label);
+ });
+
+ // Update description field
+ g_form.setValue('u_category_description', categoryData.description);
+
+ // Set field requirements based on category
+ const isRequired = categoryData.requires_subcategory;
+ g_form.setMandatory('subcategory', isRequired);
+ }
+}
+
+/**
+ * Validate priority selection
+ * @param {string} priority - Selected priority
+ */
+async function validatePrioritySelection(priority) {
+ if (!priority) return;
+
+ const validation = await modernAjax.execute('validatePriority', {
+ priority: priority,
+ caller_id: g_form.getValue('caller_id'),
+ category: g_form.getValue('category')
+ });
+
+ if (!validation.valid) {
+ g_form.addWarningMessage(validation.message);
+
+ if (validation.suggested_priority) {
+ g_form.setValue('priority', validation.suggested_priority);
+ }
+ }
+}
+
+/**
+ * Load assignment group details
+ * @param {string} groupId - Assignment group ID
+ */
+async function loadAssignmentGroupDetails(groupId) {
+ if (!groupId) {
+ g_form.clearValue('assigned_to');
+ return;
+ }
+
+ const groupDetails = await formManager.getCachedData(
+ `group_${groupId}`,
+ () => modernAjax.execute('getGroupDetails', { group_id: groupId })
+ );
+
+ if (groupDetails) {
+ // Update assigned_to choices with group members
+ g_form.clearOptions('assigned_to');
+ g_form.addOption('assigned_to', '', '-- None --');
+
+ groupDetails.members.forEach(member => {
+ g_form.addOption('assigned_to', member.sys_id, member.name);
+ });
+
+ // Set default assignee if available
+ if (groupDetails.default_assignee) {
+ g_form.setValue('assigned_to', groupDetails.default_assignee);
+ }
+ }
+}
+
+/**
+ * Modern onLoad handler example
+ */
+async function modernOnLoad() {
+ try {
+ // Initialize form enhancements
+ await initializeFormEnhancements();
+
+ // Load initial data
+ await loadInitialFormData();
+
+ // Set up field observers
+ setupFieldObservers();
+
+ } catch (error) {
+ console.error('Form initialization failed:', error);
+ g_form.addErrorMessage('Form initialization failed. Some features may not work properly.');
+ }
+}
+
+/**
+ * Initialize form enhancements
+ */
+async function initializeFormEnhancements() {
+ // Example: Load user preferences
+ const userPrefs = await modernAjax.execute('getUserPreferences', {
+ user_id: g_user.userID,
+ table: g_form.getTableName()
+ });
+
+ if (userPrefs) {
+ // Apply user preferences
+ Object.keys(userPrefs).forEach(fieldName => {
+ if (g_form.isFieldVisible(fieldName)) {
+ g_form.setValue(fieldName, userPrefs[fieldName]);
+ }
+ });
+ }
+}
+
+/**
+ * Setup field observers for reactive behavior
+ */
+function setupFieldObservers() {
+ // Add observers for related field updates
+ formManager.addObserver('priority', (field, value) => {
+ console.log(`Priority changed to: ${value}`);
+ // Additional reactive logic here
+ });
+
+ formManager.addObserver('impact', (field, value) => {
+ console.log(`Impact changed to: ${value}`);
+ // Update urgency calculation
+ updateUrgencyCalculation();
+ });
+}
+
+/**
+ * Utility function with debouncing
+ */
+const debouncedSearch = (() => {
+ let timeoutId;
+ return (searchTerm, callback, delay = 300) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => {
+ callback(searchTerm);
+ }, delay);
+ };
+})();
+
+/**
+ * Example usage in onChange for search field
+ */
+async function onSearchFieldChange(control, oldValue, newValue, isLoading) {
+ if (isLoading || newValue === oldValue) return;
+
+ debouncedSearch(newValue, async (term) => {
+ if (term.length >= 3) {
+ try {
+ const results = await modernAjax.execute('searchRecords', {
+ term: term,
+ table: 'cmdb_ci'
+ });
+
+ displaySearchResults(results);
+ } catch (error) {
+ console.error('Search failed:', error);
+ }
+ }
+ });
+}
diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/es6_field_management.js b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/es6_field_management.js
new file mode 100644
index 0000000000..1b2e636733
--- /dev/null
+++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/es6_field_management.js
@@ -0,0 +1,822 @@
+/**
+ * ES6+ Form Field Management Patterns
+ *
+ * This client script demonstrates modern ES6+ JavaScript features for
+ * form field management, including destructuring, template literals,
+ * arrow functions, and class-based field controllers.
+ *
+ * Type: Client Script (All types)
+ * Table: Any table
+ *
+ * @author: ServiceNow Community
+ * @version: 1.0
+ * @category: Client Scripts/ES6+ Patterns
+ */
+
+/**
+ * Modern form field controller using ES6+ features
+ */
+class FormFieldController {
+ constructor(formRef = g_form) {
+ this.form = formRef;
+ this.fieldStates = new Map();
+ this.validators = new Map();
+ this.watchers = new Map();
+ this.cache = new Map();
+
+ // Bind methods to preserve context
+ this.handleFieldChange = this.handleFieldChange.bind(this);
+ this.validateField = this.validateField.bind(this);
+ }
+
+ /**
+ * Initialize field controller with configuration
+ * @param {Object} config - Controller configuration
+ */
+ initialize(config = {}) {
+ const {
+ autoValidation = true,
+ cacheEnabled = true,
+ debugMode = false
+ } = config;
+
+ this.config = { autoValidation, cacheEnabled, debugMode };
+
+ if (debugMode) {
+ console.log('FormFieldController initialized with config:', config);
+ }
+ }
+
+ /**
+ * Set field value with ES6+ features
+ * @param {string} fieldName - Field name
+ * @param {any} value - Field value
+ * @param {Object} options - Set options
+ * @returns {Object} Operation result
+ */
+ setFieldValue(fieldName, value, options = {}) {
+ const {
+ displayValue = false,
+ triggerEvents = true,
+ validate = false,
+ metadata = {}
+ } = options;
+
+ try {
+ // Store previous value for comparison
+ const previousValue = this.getFieldValue(fieldName);
+
+ // Set the value
+ if (displayValue) {
+ this.form.setDisplayValue(fieldName, value);
+ } else {
+ this.form.setValue(fieldName, value, triggerEvents);
+ }
+
+ // Update field state
+ this.updateFieldState(fieldName, {
+ value,
+ previousValue,
+ displayValue,
+ timestamp: new Date().toISOString(),
+ metadata
+ });
+
+ // Trigger validation if enabled
+ if (validate && this.config.autoValidation) {
+ this.validateField(fieldName, value);
+ }
+
+ return {
+ success: true,
+ field: fieldName,
+ value,
+ previousValue,
+ metadata
+ };
+
+ } catch (error) {
+ this.logError(`Failed to set field ${fieldName}:`, error);
+ return {
+ success: false,
+ field: fieldName,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * Get field value with enhanced information
+ * @param {string} fieldName - Field name
+ * @param {Object} options - Get options
+ * @returns {Object} Field value information
+ */
+ getFieldValue(fieldName, options = {}) {
+ const {
+ includeDisplayValue = false,
+ includeMetadata = false,
+ fromCache = this.config.cacheEnabled
+ } = options;
+
+ // Check cache first
+ const cacheKey = `${fieldName}_${JSON.stringify(options)}`;
+ if (fromCache && this.cache.has(cacheKey)) {
+ return this.cache.get(cacheKey);
+ }
+
+ const result = {
+ field: fieldName,
+ value: this.form.getValue(fieldName),
+ exists: this.form.isFieldVisible(fieldName)
+ };
+
+ if (includeDisplayValue) {
+ result.displayValue = this.form.getDisplayValue(fieldName);
+ }
+
+ if (includeMetadata) {
+ result.metadata = this.getFieldMetadata(fieldName);
+ }
+
+ // Cache result if enabled
+ if (this.config.cacheEnabled) {
+ this.cache.set(cacheKey, result);
+
+ // Clear cache after 30 seconds
+ setTimeout(() => this.cache.delete(cacheKey), 30000);
+ }
+
+ return result;
+ }
+
+ /**
+ * Bulk field operations with destructuring
+ * @param {Object} fieldOperations - Field operations configuration
+ * @returns {Array} Array of operation results
+ */
+ bulkFieldOperations(fieldOperations) {
+ const {
+ set: setOperations = {},
+ clear: clearFields = [],
+ hide: hideFields = [],
+ show: showFields = [],
+ disable: disableFields = [],
+ enable: enableFields = []
+ } = fieldOperations;
+
+ const results = [];
+
+ // Set field values
+ Object.entries(setOperations).forEach(([fieldName, value]) => {
+ results.push(this.setFieldValue(fieldName, value));
+ });
+
+ // Clear fields
+ clearFields.forEach(fieldName => {
+ results.push(this.setFieldValue(fieldName, ''));
+ });
+
+ // Show/hide fields
+ [...showFields, ...hideFields].forEach(fieldName => {
+ const visible = showFields.includes(fieldName);
+ this.form.setVisible(fieldName, visible);
+ results.push({
+ success: true,
+ field: fieldName,
+ operation: visible ? 'show' : 'hide'
+ });
+ });
+
+ // Enable/disable fields
+ [...enableFields, ...disableFields].forEach(fieldName => {
+ const enabled = enableFields.includes(fieldName);
+ this.form.setReadOnly(fieldName, !enabled);
+ results.push({
+ success: true,
+ field: fieldName,
+ operation: enabled ? 'enable' : 'disable'
+ });
+ });
+
+ return results;
+ }
+
+ /**
+ * Create field watcher with modern syntax
+ * @param {string} fieldName - Field to watch
+ * @param {Function} callback - Callback function
+ * @param {Object} options - Watcher options
+ */
+ watchField(fieldName, callback, options = {}) {
+ const {
+ immediate = false,
+ debounce = 0,
+ condition = () => true
+ } = options;
+
+ // Create debounced callback if specified
+ const actualCallback = debounce > 0 ?
+ this.debounce(callback, debounce) : callback;
+
+ // Wrapper function with condition check
+ const watcherCallback = (field, oldValue, newValue) => {
+ if (condition(newValue, oldValue, field)) {
+ actualCallback(field, oldValue, newValue);
+ }
+ };
+
+ // Store watcher
+ if (!this.watchers.has(fieldName)) {
+ this.watchers.set(fieldName, new Set());
+ }
+ this.watchers.get(fieldName).add(watcherCallback);
+
+ // Execute immediately if requested
+ if (immediate) {
+ const currentValue = this.getFieldValue(fieldName).value;
+ watcherCallback(fieldName, null, currentValue);
+ }
+
+ return {
+ unwatch: () => this.unwatchField(fieldName, watcherCallback)
+ };
+ }
+
+ /**
+ * Remove field watcher
+ * @param {string} fieldName - Field name
+ * @param {Function} callback - Callback to remove
+ */
+ unwatchField(fieldName, callback) {
+ if (this.watchers.has(fieldName)) {
+ this.watchers.get(fieldName).delete(callback);
+ }
+ }
+
+ /**
+ * Handle field change with watcher notification
+ * @param {string} fieldName - Field that changed
+ * @param {any} oldValue - Previous value
+ * @param {any} newValue - New value
+ */
+ handleFieldChange(fieldName, oldValue, newValue) {
+ // Update field state
+ this.updateFieldState(fieldName, {
+ value: newValue,
+ previousValue: oldValue,
+ timestamp: new Date().toISOString()
+ });
+
+ // Notify watchers
+ if (this.watchers.has(fieldName)) {
+ this.watchers.get(fieldName).forEach(callback => {
+ try {
+ callback(fieldName, oldValue, newValue);
+ } catch (error) {
+ this.logError(`Watcher callback failed for ${fieldName}:`, error);
+ }
+ });
+ }
+
+ // Clear related cache entries
+ this.clearFieldCache(fieldName);
+ }
+
+ /**
+ * Create field validator with ES6+ patterns
+ * @param {string} fieldName - Field to validate
+ * @param {Function|Array} validatorFn - Validator function(s)
+ * @param {Object} options - Validator options
+ */
+ addValidator(fieldName, validatorFn, options = {}) {
+ const {
+ message = 'Validation failed',
+ severity = 'error',
+ async = false
+ } = options;
+
+ const validator = {
+ fn: Array.isArray(validatorFn) ? validatorFn : [validatorFn],
+ message,
+ severity,
+ async
+ };
+
+ if (!this.validators.has(fieldName)) {
+ this.validators.set(fieldName, []);
+ }
+
+ this.validators.get(fieldName).push(validator);
+ }
+
+ /**
+ * Validate field with registered validators
+ * @param {string} fieldName - Field to validate
+ * @param {any} value - Value to validate
+ * @returns {Object} Validation result
+ */
+ async validateField(fieldName, value = null) {
+ const fieldValue = value !== null ? value : this.getFieldValue(fieldName).value;
+ const validators = this.validators.get(fieldName) || [];
+
+ const results = [];
+
+ for (const validator of validators) {
+ for (const validatorFn of validator.fn) {
+ try {
+ const result = validator.async ?
+ await validatorFn(fieldValue, fieldName) :
+ validatorFn(fieldValue, fieldName);
+
+ if (!result.valid) {
+ results.push({
+ field: fieldName,
+ valid: false,
+ message: result.message || validator.message,
+ severity: validator.severity
+ });
+ }
+ } catch (error) {
+ this.logError(`Validator failed for ${fieldName}:`, error);
+ results.push({
+ field: fieldName,
+ valid: false,
+ message: 'Validation error occurred',
+ severity: 'error'
+ });
+ }
+ }
+ }
+
+ const isValid = results.length === 0;
+
+ // Display validation messages
+ if (!isValid) {
+ const errorMessages = results
+ .filter(r => r.severity === 'error')
+ .map(r => r.message);
+
+ if (errorMessages.length > 0) {
+ this.form.showFieldMsg(fieldName, errorMessages.join('; '), 'error');
+ }
+ } else {
+ this.form.hideFieldMsg(fieldName);
+ }
+
+ return {
+ valid: isValid,
+ results,
+ field: fieldName,
+ value: fieldValue
+ };
+ }
+
+ /**
+ * Get comprehensive field metadata
+ * @param {string} fieldName - Field name
+ * @returns {Object} Field metadata
+ */
+ getFieldMetadata(fieldName) {
+ return {
+ visible: this.form.isFieldVisible(fieldName),
+ mandatory: this.form.isMandatory(fieldName),
+ readOnly: this.form.isReadOnly(fieldName),
+ newRecord: this.form.isNewRecord(),
+ fieldType: this.form.getField(fieldName)?.type || 'unknown',
+ tableName: this.form.getTableName(),
+ hasChoice: this.form.hasField(fieldName) &&
+ this.form.getField(fieldName).choices !== undefined
+ };
+ }
+
+ /**
+ * Update field state with modern syntax
+ * @param {string} fieldName - Field name
+ * @param {Object} stateUpdate - State update
+ */
+ updateFieldState(fieldName, stateUpdate) {
+ const existingState = this.fieldStates.get(fieldName) || {};
+
+ // Merge state using spread operator
+ const newState = {
+ ...existingState,
+ ...stateUpdate,
+ lastUpdated: new Date().toISOString()
+ };
+
+ this.fieldStates.set(fieldName, newState);
+ }
+
+ /**
+ * Get field state
+ * @param {string} fieldName - Field name
+ * @returns {Object} Field state
+ */
+ getFieldState(fieldName) {
+ return this.fieldStates.get(fieldName) || {};
+ }
+
+ /**
+ * Debounce utility function
+ * @param {Function} func - Function to debounce
+ * @param {number} delay - Delay in milliseconds
+ * @returns {Function} Debounced function
+ */
+ debounce(func, delay) {
+ let timeoutId;
+ return (...args) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func.apply(this, args), delay);
+ };
+ }
+
+ /**
+ * Clear field-related cache entries
+ * @param {string} fieldName - Field name
+ */
+ clearFieldCache(fieldName) {
+ const keysToDelete = [];
+
+ for (const [key] of this.cache) {
+ if (key.startsWith(fieldName)) {
+ keysToDelete.push(key);
+ }
+ }
+
+ keysToDelete.forEach(key => this.cache.delete(key));
+ }
+
+ /**
+ * Log error with context
+ * @param {string} message - Error message
+ * @param {Error} error - Error object
+ */
+ logError(message, error) {
+ console.error(`FormFieldController: ${message}`, error);
+
+ if (this.config.debugMode) {
+ this.form.addErrorMessage(`Debug: ${message} - ${error.message}`);
+ }
+ }
+
+ /**
+ * Cleanup resources
+ */
+ destroy() {
+ this.fieldStates.clear();
+ this.validators.clear();
+ this.watchers.clear();
+ this.cache.clear();
+ }
+}
+
+// Create global instance with modern syntax
+const fieldController = new FormFieldController();
+
+/**
+ * Modern field validation patterns
+ */
+class ModernFieldValidators {
+ /**
+ * Email validator using modern regex
+ * @param {string} value - Email value
+ * @returns {Object} Validation result
+ */
+ static email(value) {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ const valid = !value || emailRegex.test(value);
+
+ return {
+ valid,
+ message: valid ? '' : 'Please enter a valid email address'
+ };
+ }
+
+ /**
+ * Phone number validator with modern formatting
+ * @param {string} value - Phone value
+ * @returns {Object} Validation result
+ */
+ static phoneNumber(value) {
+ const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/;
+ const cleanValue = value?.replace(/[\s\-\(\)]/g, '') || '';
+ const valid = !value || phoneRegex.test(cleanValue);
+
+ return {
+ valid,
+ message: valid ? '' : 'Please enter a valid phone number'
+ };
+ }
+
+ /**
+ * URL validator
+ * @param {string} value - URL value
+ * @returns {Object} Validation result
+ */
+ static url(value) {
+ try {
+ const valid = !value || Boolean(new URL(value));
+ return {
+ valid,
+ message: valid ? '' : 'Please enter a valid URL'
+ };
+ } catch {
+ return {
+ valid: false,
+ message: 'Please enter a valid URL'
+ };
+ }
+ }
+
+ /**
+ * Required field validator
+ * @param {any} value - Field value
+ * @param {string} fieldName - Field name
+ * @returns {Object} Validation result
+ */
+ static required(value, fieldName) {
+ const valid = value !== null && value !== undefined &&
+ value.toString().trim() !== '';
+
+ return {
+ valid,
+ message: valid ? '' : `${fieldName} is required`
+ };
+ }
+
+ /**
+ * Length validator with options
+ * @param {Object} options - Length options
+ * @returns {Function} Validator function
+ */
+ static length(options = {}) {
+ const { min = 0, max = Infinity, exact } = options;
+
+ return (value) => {
+ const length = value ? value.toString().length : 0;
+ let valid = true;
+ let message = '';
+
+ if (exact !== undefined) {
+ valid = length === exact;
+ message = `Must be exactly ${exact} characters`;
+ } else {
+ if (length < min) {
+ valid = false;
+ message = `Must be at least ${min} characters`;
+ } else if (length > max) {
+ valid = false;
+ message = `Must not exceed ${max} characters`;
+ }
+ }
+
+ return { valid, message };
+ };
+ }
+
+ /**
+ * Custom regex validator
+ * @param {RegExp} regex - Regular expression
+ * @param {string} message - Error message
+ * @returns {Function} Validator function
+ */
+ static regex(regex, message = 'Invalid format') {
+ return (value) => {
+ const valid = !value || regex.test(value);
+ return {
+ valid,
+ message: valid ? '' : message
+ };
+ };
+ }
+}
+
+/**
+ * Modern onChange handler with ES6+ features
+ */
+const modernOnChange = async (control, oldValue, newValue, isLoading, isTemplate) => {
+ if (isLoading || isTemplate || newValue === oldValue) return;
+
+ const { name: fieldName } = control;
+
+ try {
+ // Notify field controller of change
+ fieldController.handleFieldChange(fieldName, oldValue, newValue);
+
+ // Handle specific field logic with modern patterns
+ await handleFieldSpecificLogic(fieldName, newValue, oldValue);
+
+ } catch (error) {
+ console.error(`Modern onChange failed for ${fieldName}:`, error);
+ g_form.addErrorMessage(`Field processing failed: ${error.message}`);
+ }
+};
+
+/**
+ * Handle field-specific logic with pattern matching
+ * @param {string} fieldName - Field name
+ * @param {any} newValue - New value
+ * @param {any} oldValue - Old value
+ */
+const handleFieldSpecificLogic = async (fieldName, newValue, oldValue) => {
+ // Modern switch-case equivalent using object mapping
+ const fieldHandlers = {
+ priority: () => handlePriorityChange(newValue),
+ category: () => handleCategoryChange(newValue),
+ caller_id: () => handleCallerChange(newValue),
+ impact: () => handleImpactChange(newValue),
+ urgency: () => handleUrgencyChange(newValue)
+ };
+
+ const handler = fieldHandlers[fieldName];
+ if (handler) {
+ await handler();
+ }
+};
+
+/**
+ * Handle priority change with modern patterns
+ * @param {string} priority - Priority value
+ */
+const handlePriorityChange = async (priority) => {
+ // Destructuring with default values
+ const priorityMappings = {
+ '1': { urgency: '1', escalate: true, notify: true },
+ '2': { urgency: '2', escalate: false, notify: true },
+ '3': { urgency: '3', escalate: false, notify: false }
+ };
+
+ const { urgency = '3', escalate = false, notify = false } =
+ priorityMappings[priority] || {};
+
+ // Bulk field operations with modern syntax
+ const operations = {
+ set: { urgency },
+ ...(escalate && { set: { ...operations.set, u_escalated: 'true' } })
+ };
+
+ fieldController.bulkFieldOperations(operations);
+
+ if (notify) {
+ console.log(`Priority ${priority} set - notifications enabled`);
+ }
+};
+
+/**
+ * Setup modern field patterns on form load
+ */
+const setupModernFieldPatterns = () => {
+ // Initialize field controller
+ fieldController.initialize({
+ autoValidation: true,
+ cacheEnabled: true,
+ debugMode: false
+ });
+
+ // Add modern validators using arrow functions
+ fieldController.addValidator('email', ModernFieldValidators.email);
+ fieldController.addValidator('phone', ModernFieldValidators.phoneNumber);
+ fieldController.addValidator('priority', ModernFieldValidators.required);
+
+ // Add length validator for description
+ fieldController.addValidator(
+ 'short_description',
+ ModernFieldValidators.length({ min: 10, max: 160 })
+ );
+
+ // Setup field watchers with modern syntax
+ fieldController.watchField('priority', (field, oldVal, newVal) => {
+ console.log(`Priority changed: ${oldVal} → ${newVal}`);
+ }, { debounce: 300 });
+
+ // Watch multiple related fields
+ ['impact', 'urgency'].forEach(field => {
+ fieldController.watchField(field, () => updatePriorityCalculation(), {
+ debounce: 200,
+ condition: (value) => value !== ''
+ });
+ });
+};
+
+/**
+ * Modern form state management
+ */
+class FormStateManager {
+ constructor() {
+ this.state = new Proxy({}, {
+ set: (target, property, value) => {
+ const oldValue = target[property];
+ target[property] = value;
+ this.notifyStateChange(property, value, oldValue);
+ return true;
+ }
+ });
+
+ this.subscribers = new Map();
+ }
+
+ /**
+ * Subscribe to state changes
+ * @param {string} property - Property to watch
+ * @param {Function} callback - Callback function
+ */
+ subscribe(property, callback) {
+ if (!this.subscribers.has(property)) {
+ this.subscribers.set(property, new Set());
+ }
+ this.subscribers.get(property).add(callback);
+
+ return () => this.unsubscribe(property, callback);
+ }
+
+ /**
+ * Unsubscribe from state changes
+ * @param {string} property - Property name
+ * @param {Function} callback - Callback to remove
+ */
+ unsubscribe(property, callback) {
+ if (this.subscribers.has(property)) {
+ this.subscribers.get(property).delete(callback);
+ }
+ }
+
+ /**
+ * Notify subscribers of state change
+ * @param {string} property - Property that changed
+ * @param {any} newValue - New value
+ * @param {any} oldValue - Old value
+ */
+ notifyStateChange(property, newValue, oldValue) {
+ if (this.subscribers.has(property)) {
+ this.subscribers.get(property).forEach(callback => {
+ try {
+ callback(newValue, oldValue, property);
+ } catch (error) {
+ console.error('State change callback failed:', error);
+ }
+ });
+ }
+ }
+
+ /**
+ * Update state with object spread
+ * @param {Object} updates - State updates
+ */
+ updateState(updates) {
+ Object.assign(this.state, updates);
+ }
+
+ /**
+ * Get current state
+ * @returns {Object} Current state
+ */
+ getState() {
+ return { ...this.state };
+ }
+}
+
+// Create global state manager
+const formState = new FormStateManager();
+
+/**
+ * Modern template literal helpers
+ */
+const Templates = {
+ /**
+ * Create error message template
+ * @param {string} field - Field name
+ * @param {string} error - Error message
+ * @returns {string} Formatted error message
+ */
+ errorMessage: (field, error) =>
+ `⚠️ ${field.replace(/_/g, ' ').toUpperCase()}: ${error}`,
+
+ /**
+ * Create info message template
+ * @param {string} action - Action performed
+ * @param {string} field - Field name
+ * @returns {string} Formatted info message
+ */
+ infoMessage: (action, field) =>
+ `✅ ${action} completed for ${field.replace(/_/g, ' ')}`,
+
+ /**
+ * Create field label template
+ * @param {string} field - Field name
+ * @returns {string} Formatted field label
+ */
+ fieldLabel: (field) =>
+ field.split('_').map(word =>
+ word.charAt(0).toUpperCase() + word.slice(1)
+ ).join(' ')
+};
+
+// Export for use in other scripts
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ FormFieldController,
+ ModernFieldValidators,
+ FormStateManager,
+ Templates
+ };
+}
diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/promise_based_operations.js b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/promise_based_operations.js
new file mode 100644
index 0000000000..07e58ff7db
--- /dev/null
+++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/promise_based_operations.js
@@ -0,0 +1,740 @@
+/**
+ * Promise-Based Form Operations
+ *
+ * This client script demonstrates promise-based patterns for form operations,
+ * providing consistent error handling, better composability, and improved
+ * code organization for complex form interactions.
+ *
+ * Type: Client Script (onLoad, onChange, onSubmit)
+ * Table: Any table
+ *
+ * @author: ServiceNow Community
+ * @version: 1.0
+ * @category: Client Scripts/Promise Patterns
+ */
+
+/**
+ * Promise-based form operation utilities
+ */
+class PromiseFormOperations {
+ constructor() {
+ this.operationQueue = [];
+ this.isProcessing = false;
+ this.defaultTimeout = 15000;
+ }
+
+ /**
+ * Set field value with promise-based validation
+ * @param {string} fieldName - Field name
+ * @param {any} value - New value
+ * @param {Object} options - Operation options
+ * @returns {Promise} Promise resolving when operation completes
+ */
+ setValue(fieldName, value, options = {}) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const { validate = false, trigger = true, displayValue = false } = options;
+
+ // Pre-validation if requested
+ if (validate) {
+ const isValid = await this.validateFieldValue(fieldName, value);
+ if (!isValid.valid) {
+ reject(new Error(isValid.message));
+ return;
+ }
+ }
+
+ // Set the value
+ if (displayValue) {
+ g_form.setDisplayValue(fieldName, value);
+ } else {
+ g_form.setValue(fieldName, value, trigger);
+ }
+
+ // Wait for any triggered events to complete
+ if (trigger) {
+ await this.waitForFieldUpdates(fieldName);
+ }
+
+ resolve({
+ field: fieldName,
+ value: value,
+ success: true
+ });
+
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ /**
+ * Get field value with promise interface
+ * @param {string} fieldName - Field name
+ * @param {Object} options - Get options
+ * @returns {Promise} Promise resolving to field value
+ */
+ getValue(fieldName, options = {}) {
+ return new Promise((resolve, reject) => {
+ try {
+ const { displayValue = false, reference = false } = options;
+
+ let value;
+ if (displayValue) {
+ value = g_form.getDisplayValue(fieldName);
+ } else if (reference && g_form.isReferenceField(fieldName)) {
+ value = {
+ value: g_form.getValue(fieldName),
+ displayValue: g_form.getDisplayValue(fieldName),
+ tableName: g_form.getTableName(fieldName)
+ };
+ } else {
+ value = g_form.getValue(fieldName);
+ }
+
+ resolve(value);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ /**
+ * Submit form with comprehensive validation
+ * @param {Object} options - Submit options
+ * @returns {Promise} Promise resolving when submit completes
+ */
+ submitForm(options = {}) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const { validate = true, showProgress = true } = options;
+
+ if (showProgress) {
+ this.showProgress('Submitting form...');
+ }
+
+ // Pre-submit validation
+ if (validate) {
+ const validation = await this.validateAllFields();
+ if (!validation.valid) {
+ reject(new Error('Form validation failed: ' + validation.errors.join(', ')));
+ return;
+ }
+ }
+
+ // Attempt form submission
+ const submitResult = await this.performSubmit();
+
+ if (submitResult.success) {
+ resolve(submitResult);
+ } else {
+ reject(new Error(submitResult.message || 'Form submission failed'));
+ }
+
+ } catch (error) {
+ reject(error);
+ } finally {
+ if (options.showProgress) {
+ this.hideProgress();
+ }
+ }
+ });
+ }
+
+ /**
+ * Load related records with promise interface
+ * @param {string} table - Table name
+ * @param {Object} query - Query parameters
+ * @param {Object} options - Load options
+ * @returns {Promise} Promise resolving to records
+ */
+ loadRelatedRecords(table, query, options = {}) {
+ return new Promise((resolve, reject) => {
+ const { limit = 100, orderBy = 'sys_created_on' } = options;
+
+ const ga = new GlideAjax('ClientScriptHelper');
+ ga.addParam('sysparm_name', 'getRelatedRecords');
+ ga.addParam('table', table);
+ ga.addParam('query', JSON.stringify(query));
+ ga.addParam('limit', limit);
+ ga.addParam('orderBy', orderBy);
+
+ const timeout = setTimeout(() => {
+ reject(new Error('Request timeout'));
+ }, this.defaultTimeout);
+
+ ga.getXML((response) => {
+ clearTimeout(timeout);
+
+ try {
+ const answer = response.responseXML.documentElement.getAttribute('answer');
+ const result = JSON.parse(answer);
+
+ if (result.error) {
+ reject(new Error(result.error));
+ } else {
+ resolve(result.records || []);
+ }
+ } catch (error) {
+ reject(new Error('Failed to parse response: ' + error.message));
+ }
+ });
+ });
+ }
+
+ /**
+ * Update multiple fields atomically
+ * @param {Object} fieldValues - Field name/value pairs
+ * @param {Object} options - Update options
+ * @returns {Promise} Promise resolving when all updates complete
+ */
+ updateMultipleFields(fieldValues, options = {}) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const { atomic = true, validate = false } = options;
+ const updates = [];
+ const rollbackValues = {};
+
+ // Store current values for potential rollback
+ if (atomic) {
+ for (const fieldName of Object.keys(fieldValues)) {
+ rollbackValues[fieldName] = await this.getValue(fieldName);
+ }
+ }
+
+ // Process all updates
+ for (const [fieldName, value] of Object.entries(fieldValues)) {
+ try {
+ const updateResult = await this.setValue(fieldName, value, {
+ validate,
+ trigger: false // Prevent cascading during batch update
+ });
+ updates.push(updateResult);
+ } catch (error) {
+ if (atomic) {
+ // Rollback previous updates
+ await this.rollbackUpdates(rollbackValues);
+ reject(new Error(`Atomic update failed at ${fieldName}: ${error.message}`));
+ return;
+ } else {
+ console.warn(`Failed to update ${fieldName}: ${error.message}`);
+ updates.push({
+ field: fieldName,
+ value: value,
+ success: false,
+ error: error.message
+ });
+ }
+ }
+ }
+
+ // Trigger onChange events for all updated fields
+ Object.keys(fieldValues).forEach(fieldName => {
+ if (g_form.isFieldVisible(fieldName)) {
+ g_form.fieldChanged(fieldName, true);
+ }
+ });
+
+ resolve({
+ updates: updates,
+ success: true,
+ totalUpdated: updates.filter(u => u.success).length
+ });
+
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ /**
+ * Clear multiple fields with options
+ * @param {Array} fieldNames - Fields to clear
+ * @param {Object} options - Clear options
+ * @returns {Promise} Promise resolving when clearing completes
+ */
+ clearFields(fieldNames, options = {}) {
+ const clearValues = {};
+ fieldNames.forEach(fieldName => {
+ clearValues[fieldName] = '';
+ });
+
+ return this.updateMultipleFields(clearValues, options);
+ }
+
+ /**
+ * Validate field value against business rules
+ * @param {string} fieldName - Field name
+ * @param {any} value - Value to validate
+ * @returns {Promise} Promise resolving to validation result
+ */
+ validateFieldValue(fieldName, value) {
+ return new Promise((resolve, reject) => {
+ const ga = new GlideAjax('FieldValidationHelper');
+ ga.addParam('sysparm_name', 'validateField');
+ ga.addParam('table', g_form.getTableName());
+ ga.addParam('field', fieldName);
+ ga.addParam('value', value);
+ ga.addParam('record_id', g_form.getUniqueValue());
+
+ ga.getXML((response) => {
+ try {
+ const answer = response.responseXML.documentElement.getAttribute('answer');
+ const result = JSON.parse(answer);
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+ }
+
+ /**
+ * Validate all form fields
+ * @returns {Promise} Promise resolving to overall validation result
+ */
+ validateAllFields() {
+ return new Promise(async (resolve) => {
+ const errors = [];
+ const warnings = [];
+
+ // Get all visible fields
+ const fields = this.getVisibleFields();
+
+ // Validate each field
+ for (const fieldName of fields) {
+ try {
+ const value = await this.getValue(fieldName);
+ const validation = await this.validateFieldValue(fieldName, value);
+
+ if (!validation.valid) {
+ if (validation.severity === 'error') {
+ errors.push(`${fieldName}: ${validation.message}`);
+ } else {
+ warnings.push(`${fieldName}: ${validation.message}`);
+ }
+ }
+ } catch (error) {
+ errors.push(`${fieldName}: Validation failed - ${error.message}`);
+ }
+ }
+
+ resolve({
+ valid: errors.length === 0,
+ errors: errors,
+ warnings: warnings
+ });
+ });
+ }
+
+ /**
+ * Wait for field updates to complete
+ * @param {string} fieldName - Field name
+ * @param {number} timeout - Timeout in milliseconds
+ * @returns {Promise} Promise resolving when updates complete
+ */
+ waitForFieldUpdates(fieldName, timeout = 5000) {
+ return new Promise((resolve) => {
+ let checks = 0;
+ const maxChecks = timeout / 100;
+
+ const checkForUpdates = () => {
+ checks++;
+
+ // Check if any async operations are pending
+ if (checks >= maxChecks || !this.hasOpenAjaxRequests()) {
+ resolve();
+ } else {
+ setTimeout(checkForUpdates, 100);
+ }
+ };
+
+ setTimeout(checkForUpdates, 100);
+ });
+ }
+
+ /**
+ * Perform actual form submission
+ * @returns {Promise} Promise resolving to submit result
+ * @private
+ */
+ performSubmit() {
+ return new Promise((resolve) => {
+ // Override the default submit action
+ const originalAction = window.gsftSubmit;
+
+ window.gsftSubmit = function(action) {
+ // Restore original function
+ window.gsftSubmit = originalAction;
+
+ // Perform submission and resolve promise
+ try {
+ const result = originalAction.call(this, action);
+ resolve({ success: true, result: result });
+ } catch (error) {
+ resolve({ success: false, message: error.message });
+ }
+ };
+
+ // Trigger form submission
+ g_form.submit();
+ });
+ }
+
+ /**
+ * Rollback field updates
+ * @param {Object} rollbackValues - Values to restore
+ * @returns {Promise} Promise resolving when rollback completes
+ * @private
+ */
+ async rollbackUpdates(rollbackValues) {
+ for (const [fieldName, value] of Object.entries(rollbackValues)) {
+ try {
+ await this.setValue(fieldName, value, { validate: false, trigger: false });
+ } catch (error) {
+ console.error(`Failed to rollback ${fieldName}: ${error.message}`);
+ }
+ }
+ }
+
+ /**
+ * Get all visible fields on the form
+ * @returns {Array} Array of field names
+ * @private
+ */
+ getVisibleFields() {
+ const fields = [];
+ const sections = g_form.getSections();
+
+ sections.forEach(section => {
+ const sectionFields = g_form.getSectionFields(section);
+ sectionFields.forEach(field => {
+ if (g_form.isFieldVisible(field)) {
+ fields.push(field);
+ }
+ });
+ });
+
+ return fields;
+ }
+
+ /**
+ * Check if there are pending AJAX requests
+ * @returns {boolean} True if requests are pending
+ * @private
+ */
+ hasOpenAjaxRequests() {
+ // Check ServiceNow's internal AJAX queue if available
+ if (typeof CustomEvent !== 'undefined' && window.g_ajax_processors) {
+ return Object.keys(window.g_ajax_processors).length > 0;
+ }
+ return false;
+ }
+
+ /**
+ * Show progress indicator
+ * @param {string} message - Progress message
+ * @private
+ */
+ showProgress(message) {
+ g_form.addInfoMessage(message);
+ }
+
+ /**
+ * Hide progress indicator
+ * @private
+ */
+ hideProgress() {
+ g_form.clearMessages();
+ }
+}
+
+// Create global instance
+const promiseFormOps = new PromiseFormOperations();
+
+/**
+ * Promise-based field dependency manager
+ */
+class FieldDependencyManager {
+ constructor() {
+ this.dependencies = new Map();
+ this.isProcessing = false;
+ }
+
+ /**
+ * Add field dependency
+ * @param {string} sourceField - Source field that triggers the dependency
+ * @param {string} targetField - Target field that gets updated
+ * @param {Function} calculator - Function to calculate new value
+ * @param {Object} options - Dependency options
+ */
+ addDependency(sourceField, targetField, calculator, options = {}) {
+ if (!this.dependencies.has(sourceField)) {
+ this.dependencies.set(sourceField, []);
+ }
+
+ this.dependencies.get(sourceField).push({
+ targetField,
+ calculator,
+ options
+ });
+ }
+
+ /**
+ * Process dependencies for a field change
+ * @param {string} sourceField - Field that changed
+ * @param {any} newValue - New value
+ * @returns {Promise} Promise resolving when all dependencies processed
+ */
+ async processDependencies(sourceField, newValue) {
+ if (this.isProcessing) {
+ return; // Prevent recursive processing
+ }
+
+ this.isProcessing = true;
+
+ try {
+ const dependencies = this.dependencies.get(sourceField) || [];
+
+ // Process dependencies in parallel
+ const updates = await Promise.allSettled(
+ dependencies.map(dep => this.processSingleDependency(dep, newValue))
+ );
+
+ // Report any failures
+ const failures = updates.filter(result => result.status === 'rejected');
+ if (failures.length > 0) {
+ console.warn('Some dependency updates failed:', failures);
+ }
+
+ } finally {
+ this.isProcessing = false;
+ }
+ }
+
+ /**
+ * Process single dependency
+ * @param {Object} dependency - Dependency configuration
+ * @param {any} sourceValue - Source field value
+ * @returns {Promise} Promise resolving when dependency processed
+ * @private
+ */
+ async processSingleDependency(dependency, sourceValue) {
+ const { targetField, calculator, options } = dependency;
+ const { condition, delay = 0 } = options;
+
+ // Check condition if specified
+ if (condition && !condition(sourceValue)) {
+ return;
+ }
+
+ // Add delay if specified
+ if (delay > 0) {
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+
+ // Calculate new value
+ const newValue = await calculator(sourceValue, targetField);
+
+ // Update target field
+ if (newValue !== undefined) {
+ await promiseFormOps.setValue(targetField, newValue);
+ }
+ }
+}
+
+// Create global dependency manager
+const dependencyManager = new FieldDependencyManager();
+
+/**
+ * Promise-based onChange handler example
+ */
+async function promiseOnChange(control, oldValue, newValue, isLoading, isTemplate) {
+ if (isLoading || isTemplate || newValue === oldValue) {
+ return;
+ }
+
+ const fieldName = control.name;
+
+ try {
+ // Process field dependencies
+ await dependencyManager.processDependencies(fieldName, newValue);
+
+ // Handle specific field logic
+ switch (fieldName) {
+ case 'priority':
+ await handlePriorityChange(newValue);
+ break;
+ case 'category':
+ await handleCategoryChange(newValue);
+ break;
+ case 'caller_id':
+ await handleCallerChange(newValue);
+ break;
+ }
+
+ } catch (error) {
+ console.error(`Promise-based onChange failed for ${fieldName}:`, error);
+ g_form.addErrorMessage(`Failed to process ${fieldName} change: ${error.message}`);
+ }
+}
+
+/**
+ * Handle priority change with promise pattern
+ * @param {string} priority - New priority value
+ * @returns {Promise} Promise resolving when handling completes
+ */
+async function handlePriorityChange(priority) {
+ const updates = {};
+
+ // Set urgency based on priority
+ const urgencyMap = { '1': '1', '2': '2', '3': '3', '4': '3', '5': '3' };
+ updates.urgency = urgencyMap[priority] || '3';
+
+ // Update impact if priority is critical
+ if (priority === '1') {
+ updates.impact = '1';
+ updates.u_escalated = 'true';
+ }
+
+ await promiseFormOps.updateMultipleFields(updates);
+}
+
+/**
+ * Handle category change with promise pattern
+ * @param {string} category - New category value
+ * @returns {Promise} Promise resolving when handling completes
+ */
+async function handleCategoryChange(category) {
+ if (!category) {
+ await promiseFormOps.clearFields(['subcategory', 'assignment_group']);
+ return;
+ }
+
+ try {
+ // Load category data
+ const categoryData = await promiseFormOps.loadRelatedRecords('incident_category', {
+ name: category
+ });
+
+ if (categoryData.length > 0) {
+ const cat = categoryData[0];
+ const updates = {};
+
+ if (cat.default_assignment_group) {
+ updates.assignment_group = cat.default_assignment_group;
+ }
+
+ if (cat.default_priority) {
+ updates.priority = cat.default_priority;
+ }
+
+ await promiseFormOps.updateMultipleFields(updates);
+ }
+
+ } catch (error) {
+ console.error('Failed to load category data:', error);
+ }
+}
+
+/**
+ * Promise-based onSubmit handler
+ */
+async function promiseOnSubmit() {
+ try {
+ // Perform comprehensive form validation
+ const validation = await promiseFormOps.validateAllFields();
+
+ if (!validation.valid) {
+ g_form.addErrorMessage('Please correct the following errors:\n' +
+ validation.errors.join('\n'));
+ return false;
+ }
+
+ // Show warnings if any
+ if (validation.warnings.length > 0) {
+ g_form.addWarningMessage('Please review:\n' +
+ validation.warnings.join('\n'));
+ }
+
+ // Additional business logic validation
+ const businessValidation = await validateBusinessRules();
+
+ if (!businessValidation.valid) {
+ g_form.addErrorMessage(businessValidation.message);
+ return false;
+ }
+
+ return true;
+
+ } catch (error) {
+ console.error('Form submission validation failed:', error);
+ g_form.addErrorMessage('Validation failed: ' + error.message);
+ return false;
+ }
+}
+
+/**
+ * Validate business rules before submission
+ * @returns {Promise} Promise resolving to validation result
+ */
+async function validateBusinessRules() {
+ const priority = await promiseFormOps.getValue('priority');
+ const category = await promiseFormOps.getValue('category');
+ const assignmentGroup = await promiseFormOps.getValue('assignment_group');
+
+ // Example business rule: High priority incidents must have assignment group
+ if ((priority === '1' || priority === '2') && !assignmentGroup) {
+ return {
+ valid: false,
+ message: 'High priority incidents must have an assignment group'
+ };
+ }
+
+ // Example business rule: Certain categories require approval
+ const approvalRequiredCategories = ['security', 'data_breach'];
+ if (approvalRequiredCategories.includes(category)) {
+ const approval = await promiseFormOps.getValue('u_approval_status');
+ if (!approval || approval === 'pending') {
+ return {
+ valid: false,
+ message: 'This category requires approval before submission'
+ };
+ }
+ }
+
+ return { valid: true };
+}
+
+/**
+ * Setup field dependencies on form load
+ */
+function setupPromiseBasedDependencies() {
+ // Priority affects urgency
+ dependencyManager.addDependency('priority', 'urgency', async (priority) => {
+ const urgencyMap = { '1': '1', '2': '2', '3': '3', '4': '3', '5': '3' };
+ return urgencyMap[priority] || '3';
+ });
+
+ // Category affects assignment group
+ dependencyManager.addDependency('category', 'assignment_group', async (category) => {
+ if (!category) return '';
+
+ const categoryData = await promiseFormOps.loadRelatedRecords('incident_category', {
+ name: category
+ });
+
+ return categoryData.length > 0 ? categoryData[0].default_assignment_group : '';
+ });
+
+ // Caller affects location and company
+ dependencyManager.addDependency('caller_id', 'location', async (callerId) => {
+ if (!callerId) return '';
+
+ const userData = await promiseFormOps.loadRelatedRecords('sys_user', {
+ sys_id: callerId
+ });
+
+ return userData.length > 0 ? userData[0].location : '';
+ });
+}
diff --git a/Client-Side Components/UI Actions/Advanced UI Action Patterns/README.md b/Client-Side Components/UI Actions/Advanced UI Action Patterns/README.md
new file mode 100644
index 0000000000..0b33f2bf69
--- /dev/null
+++ b/Client-Side Components/UI Actions/Advanced UI Action Patterns/README.md
@@ -0,0 +1,64 @@
+# Advanced UI Action Patterns
+
+This collection demonstrates sophisticated UI Action patterns for ServiceNow, focusing on enterprise-grade implementations with robust error handling, performance optimization, and user experience enhancements.
+
+## 🎯 Features
+
+### 1. **Conditional Action Framework** (`conditional_action_framework.js`)
+- Dynamic action visibility based on complex business rules
+- Multi-condition evaluation engine
+- Role-based action control
+- State-dependent action management
+
+### 2. **Bulk Operations Manager** (`bulk_operations_manager.js`)
+- Efficient batch processing for large datasets
+- Progress tracking and user feedback
+- Transaction management and rollback capabilities
+- Memory-optimized record handling
+
+### 3. **Interactive Form Controller** (`interactive_form_controller.js`)
+- Real-time form validation and updates
+- Dynamic field dependencies
+- Progressive disclosure patterns
+- Smart defaults and auto-completion
+
+### 4. **Workflow Integration Handler** (`workflow_integration_handler.js`)
+- Seamless workflow triggering from UI actions
+- Context preservation and parameter passing
+- Asynchronous workflow monitoring
+- Status feedback and error handling
+
+## 🚀 Key Benefits
+
+- **Performance**: Optimized for large-scale operations
+- **Usability**: Enhanced user experience with real-time feedback
+- **Reliability**: Comprehensive error handling and validation
+- **Maintainability**: Modular, reusable code patterns
+- **Security**: Role-based access control integration
+
+## 📋 Implementation Guidelines
+
+1. **Error Handling**: All patterns include comprehensive error management
+2. **Performance**: Optimized queries and batch processing where applicable
+3. **User Experience**: Loading indicators, progress bars, and clear messaging
+4. **Security**: Proper ACL checks and input validation
+5. **Logging**: Detailed audit trails for troubleshooting
+
+## 🔧 Usage Requirements
+
+- ServiceNow Madrid or later
+- Appropriate user roles and permissions
+- Understanding of ServiceNow client-side scripting
+- Knowledge of UI Action configuration
+
+## 📖 Best Practices
+
+- Test all patterns in sub-production environments first
+- Follow ServiceNow coding standards
+- Implement proper error handling
+- Consider performance implications for large datasets
+- Document custom implementations thoroughly
+
+---
+
+*Part of the ServiceNow Code Snippets collection - Advanced UI Action Patterns*
diff --git a/Client-Side Components/UI Actions/Advanced UI Action Patterns/bulk_operations_manager.js b/Client-Side Components/UI Actions/Advanced UI Action Patterns/bulk_operations_manager.js
new file mode 100644
index 0000000000..1fbd7d662d
--- /dev/null
+++ b/Client-Side Components/UI Actions/Advanced UI Action Patterns/bulk_operations_manager.js
@@ -0,0 +1,401 @@
+/**
+ * Bulk Operations Manager
+ *
+ * Advanced UI Action pattern for handling bulk operations on large datasets
+ * with progress tracking, transaction management, and performance optimization.
+ *
+ * Features:
+ * - Efficient batch processing
+ * - Progress tracking and user feedback
+ * - Transaction management and rollback
+ * - Memory-optimized record handling
+ * - Error handling and recovery
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+// Client Script for Bulk Operations UI Action
+function executeBulkOperation() {
+ 'use strict';
+
+ /**
+ * Bulk Operations Manager
+ */
+ const BulkOperationsManager = {
+
+ // Configuration
+ config: {
+ batchSize: 100,
+ maxConcurrentBatches: 3,
+ progressUpdateInterval: 1000,
+ timeoutDuration: 300000 // 5 minutes
+ },
+
+ // Operation state
+ state: {
+ totalRecords: 0,
+ processedRecords: 0,
+ failedRecords: 0,
+ currentBatch: 0,
+ isRunning: false,
+ startTime: null,
+ operations: []
+ },
+
+ /**
+ * Initialize bulk operation
+ */
+ initialize: function() {
+ try {
+ this.showOperationDialog();
+ this.setupProgressTracking();
+ return true;
+ } catch (error) {
+ this.handleError('Initialization failed', error);
+ return false;
+ }
+ },
+
+ /**
+ * Show operation selection dialog
+ */
+ showOperationDialog: function() {
+ const dialog = new GlideDialogWindow('bulk_operation_dialog');
+ dialog.setTitle('Bulk Operation Manager');
+ dialog.setPreference('sysparm_operation_types', this.getAvailableOperations());
+ dialog.setPreference('sysparm_record_count', this.getSelectedRecordCount());
+ dialog.render();
+ },
+
+ /**
+ * Get available operations based on table and user permissions
+ */
+ getAvailableOperations: function() {
+ const tableName = g_form.getTableName();
+ const operations = [];
+
+ // Standard operations
+ if (g_user.hasRole('admin') || g_user.hasRole(tableName + '_admin')) {
+ operations.push({
+ id: 'bulk_update',
+ name: 'Bulk Update Fields',
+ description: 'Update multiple fields across selected records'
+ });
+
+ operations.push({
+ id: 'bulk_assign',
+ name: 'Bulk Assignment',
+ description: 'Assign multiple records to users or groups'
+ });
+
+ operations.push({
+ id: 'bulk_state_change',
+ name: 'Bulk State Change',
+ description: 'Change state of multiple records'
+ });
+ }
+
+ // Table-specific operations
+ if (tableName === 'incident') {
+ operations.push({
+ id: 'bulk_resolve',
+ name: 'Bulk Resolve',
+ description: 'Resolve multiple incidents with standard resolution'
+ });
+ }
+
+ return operations;
+ },
+
+ /**
+ * Get count of selected records
+ */
+ getSelectedRecordCount: function() {
+ // This would typically come from a list view selection
+ // For demo purposes, using a mock count
+ return 150;
+ },
+
+ /**
+ * Setup progress tracking interface
+ */
+ setupProgressTracking: function() {
+ // Create progress container
+ const progressContainer = document.createElement('div');
+ progressContainer.id = 'bulk_operation_progress';
+ progressContainer.innerHTML = `
+
+
+
+ 0 processed,
+ 0 failed,
+ 0 remaining
+
+
+ `;
+
+ // Add to page (would typically be in a modal or dedicated area)
+ document.body.appendChild(progressContainer);
+ },
+
+ /**
+ * Start bulk operation
+ */
+ startOperation: function(operationType, targetRecords, operationParams) {
+ if (this.state.isRunning) {
+ this.showError('Another operation is already running');
+ return false;
+ }
+
+ try {
+ this.state.isRunning = true;
+ this.state.startTime = new Date();
+ this.state.totalRecords = targetRecords.length;
+ this.state.processedRecords = 0;
+ this.state.failedRecords = 0;
+
+ this.logOperation('Starting bulk operation: ' + operationType);
+ this.processBatches(operationType, targetRecords, operationParams);
+
+ return true;
+ } catch (error) {
+ this.handleError('Failed to start operation', error);
+ return false;
+ }
+ },
+
+ /**
+ * Process records in batches
+ */
+ processBatches: function(operationType, records, params) {
+ const batches = this.createBatches(records);
+ let completedBatches = 0;
+
+ const processBatch = (batchIndex) => {
+ if (batchIndex >= batches.length || !this.state.isRunning) {
+ this.completeOperation();
+ return;
+ }
+
+ const batch = batches[batchIndex];
+ this.state.currentBatch = batchIndex + 1;
+
+ this.logOperation(`Processing batch ${batchIndex + 1} of ${batches.length}`);
+
+ // Process batch asynchronously
+ this.processBatchAsync(operationType, batch, params)
+ .then((result) => {
+ this.handleBatchResult(result);
+ completedBatches++;
+
+ // Process next batch with delay to prevent overwhelming server
+ setTimeout(() => processBatch(batchIndex + 1), 100);
+ })
+ .catch((error) => {
+ this.handleBatchError(batchIndex, error);
+ processBatch(batchIndex + 1); // Continue with next batch
+ });
+ };
+
+ // Start processing batches
+ for (let i = 0; i < Math.min(this.config.maxConcurrentBatches, batches.length); i++) {
+ processBatch(i);
+ }
+ },
+
+ /**
+ * Create batches from record array
+ */
+ createBatches: function(records) {
+ const batches = [];
+ const batchSize = this.config.batchSize;
+
+ for (let i = 0; i < records.length; i += batchSize) {
+ batches.push(records.slice(i, i + batchSize));
+ }
+
+ return batches;
+ },
+
+ /**
+ * Process a single batch asynchronously
+ */
+ processBatchAsync: function(operationType, batch, params) {
+ return new Promise((resolve, reject) => {
+ const ga = new GlideAjax('BulkOperationProcessor');
+ ga.addParam('sysparm_name', 'processBatch');
+ ga.addParam('sysparm_operation_type', operationType);
+ ga.addParam('sysparm_record_ids', JSON.stringify(batch.map(r => r.sys_id)));
+ ga.addParam('sysparm_operation_params', JSON.stringify(params));
+
+ ga.getXMLAnswer((response) => {
+ try {
+ const result = JSON.parse(response);
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ }
+ });
+
+ // Set timeout for batch processing
+ setTimeout(() => {
+ reject(new Error('Batch processing timeout'));
+ }, this.config.timeoutDuration);
+ });
+ },
+
+ /**
+ * Handle batch processing result
+ */
+ handleBatchResult: function(result) {
+ this.state.processedRecords += result.processed || 0;
+ this.state.failedRecords += result.failed || 0;
+
+ this.updateProgress();
+
+ if (result.errors && result.errors.length > 0) {
+ result.errors.forEach(error => {
+ this.logOperation('Error: ' + error.message, 'error');
+ });
+ }
+ },
+
+ /**
+ * Handle batch processing error
+ */
+ handleBatchError: function(batchIndex, error) {
+ const batchSize = this.config.batchSize;
+ this.state.failedRecords += batchSize;
+ this.logOperation(`Batch ${batchIndex + 1} failed: ${error.message}`, 'error');
+ this.updateProgress();
+ },
+
+ /**
+ * Update progress display
+ */
+ updateProgress: function() {
+ const progressPercent = Math.round((this.state.processedRecords / this.state.totalRecords) * 100);
+ const progressBar = document.getElementById('progress-bar');
+ const progressText = document.getElementById('progress-text');
+
+ if (progressBar && progressText) {
+ progressBar.style.width = progressPercent + '%';
+ progressText.textContent = progressPercent + '%';
+ }
+
+ // Update stats
+ this.updateStats();
+ },
+
+ /**
+ * Update operation statistics
+ */
+ updateStats: function() {
+ const processedEl = document.getElementById('processed-count');
+ const failedEl = document.getElementById('failed-count');
+ const remainingEl = document.getElementById('remaining-count');
+
+ if (processedEl) processedEl.textContent = this.state.processedRecords;
+ if (failedEl) failedEl.textContent = this.state.failedRecords;
+ if (remainingEl) {
+ remainingEl.textContent = this.state.totalRecords - this.state.processedRecords - this.state.failedRecords;
+ }
+ },
+
+ /**
+ * Log operation message
+ */
+ logOperation: function(message, type = 'info') {
+ const timestamp = new Date().toLocaleTimeString();
+ const logEntry = `[${timestamp}] ${message}`;
+
+ const logContainer = document.getElementById('operation-log');
+ if (logContainer) {
+ const logLine = document.createElement('div');
+ logLine.className = `log-entry log-${type}`;
+ logLine.textContent = logEntry;
+ logContainer.appendChild(logLine);
+ logContainer.scrollTop = logContainer.scrollHeight;
+ }
+
+ // Also log to browser console
+ console.log(logEntry);
+ },
+
+ /**
+ * Complete operation
+ */
+ completeOperation: function() {
+ this.state.isRunning = false;
+ const endTime = new Date();
+ const duration = Math.round((endTime - this.state.startTime) / 1000);
+
+ this.logOperation(`Operation completed in ${duration} seconds`);
+ this.logOperation(`Total: ${this.state.totalRecords}, Processed: ${this.state.processedRecords}, Failed: ${this.state.failedRecords}`);
+
+ // Show completion message
+ this.showCompletionDialog();
+ },
+
+ /**
+ * Cancel operation
+ */
+ cancelOperation: function() {
+ if (confirm('Are you sure you want to cancel the bulk operation?')) {
+ this.state.isRunning = false;
+ this.logOperation('Operation cancelled by user');
+ }
+ },
+
+ /**
+ * Show completion dialog
+ */
+ showCompletionDialog: function() {
+ const message = `
+ Bulk operation completed successfully!
+
+ Total Records: ${this.state.totalRecords}
+ Processed: ${this.state.processedRecords}
+ Failed: ${this.state.failedRecords}
+
+ Duration: ${Math.round((new Date() - this.state.startTime) / 1000)} seconds
+ `;
+
+ alert(message);
+ },
+
+ /**
+ * Handle errors
+ */
+ handleError: function(message, error) {
+ const errorMsg = `${message}: ${error.message || error}`;
+ this.logOperation(errorMsg, 'error');
+ g_form.addErrorMessage(errorMsg);
+ },
+
+ /**
+ * Show error message
+ */
+ showError: function(message) {
+ g_form.addErrorMessage(message);
+ this.logOperation(message, 'error');
+ }
+ };
+
+ // Initialize and start bulk operation
+ if (BulkOperationsManager.initialize()) {
+ // This would typically be called after user selects operation type and parameters
+ // BulkOperationsManager.startOperation(operationType, targetRecords, params);
+ }
+
+ // Make manager globally accessible for dialog callbacks
+ window.BulkOperationsManager = BulkOperationsManager;
+}
diff --git a/Client-Side Components/UI Actions/Advanced UI Action Patterns/conditional_action_framework.js b/Client-Side Components/UI Actions/Advanced UI Action Patterns/conditional_action_framework.js
new file mode 100644
index 0000000000..143966157f
--- /dev/null
+++ b/Client-Side Components/UI Actions/Advanced UI Action Patterns/conditional_action_framework.js
@@ -0,0 +1,255 @@
+/**
+ * Conditional Action Framework
+ *
+ * Advanced UI Action pattern that provides dynamic action visibility and behavior
+ * based on complex business rules, user roles, and record states.
+ *
+ * Features:
+ * - Multi-condition evaluation engine
+ * - Role-based action control
+ * - State-dependent action management
+ * - Performance-optimized condition checking
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+// UI Action Condition Script
+(function() {
+ 'use strict';
+
+ /**
+ * Conditional Action Framework Configuration
+ */
+ const ConditionalActionFramework = {
+
+ /**
+ * Define action visibility rules
+ */
+ visibilityRules: {
+ // Rule: Only show for specific states
+ stateBasedRules: function(current) {
+ const allowedStates = ['1', '2', '6']; // New, In Progress, Resolved
+ return allowedStates.includes(current.getValue('state'));
+ },
+
+ // Rule: Role-based visibility
+ roleBasedRules: function(current) {
+ const requiredRoles = ['incident_manager', 'itil_admin'];
+ return gs.hasRole(requiredRoles.join(','));
+ },
+
+ // Rule: Business hour restrictions
+ businessHourRules: function(current) {
+ const now = new GlideDateTime();
+ const hour = parseInt(now.getDisplayValue().split(' ')[1].split(':')[0]);
+ return hour >= 8 && hour <= 18; // 8 AM to 6 PM
+ },
+
+ // Rule: Record age restrictions
+ recordAgeRules: function(current) {
+ const createdOn = new GlideDateTime(current.getValue('sys_created_on'));
+ const now = new GlideDateTime();
+ const diffInHours = gs.dateDiff(createdOn.getDisplayValue(), now.getDisplayValue(), true) / (1000 * 60 * 60);
+ return diffInHours <= 24; // Only show within 24 hours of creation
+ },
+
+ // Rule: Field value dependencies
+ fieldDependencyRules: function(current) {
+ const priority = current.getValue('priority');
+ const category = current.getValue('category');
+
+ // High priority incidents in specific categories
+ return (priority === '1' || priority === '2') &&
+ ['hardware', 'software', 'network'].includes(category);
+ }
+ },
+
+ /**
+ * Evaluate all visibility rules
+ */
+ evaluateVisibility: function(current) {
+ try {
+ const rules = this.visibilityRules;
+
+ // All rules must pass for action to be visible
+ return rules.stateBasedRules(current) &&
+ rules.roleBasedRules(current) &&
+ rules.businessHourRules(current) &&
+ rules.recordAgeRules(current) &&
+ rules.fieldDependencyRules(current);
+
+ } catch (error) {
+ gs.error('ConditionalActionFramework: Error evaluating visibility rules: ' + error.message);
+ return false; // Fail safe - hide action on error
+ }
+ },
+
+ /**
+ * Advanced condition with caching
+ */
+ evaluateWithCache: function(current) {
+ const cacheKey = 'ui_action_visibility_' + current.getUniqueValue();
+ const cached = gs.getProperty(cacheKey);
+
+ if (cached) {
+ const cacheData = JSON.parse(cached);
+ const cacheAge = new Date().getTime() - cacheData.timestamp;
+
+ // Cache valid for 5 minutes
+ if (cacheAge < 300000) {
+ return cacheData.result === 'true';
+ }
+ }
+
+ // Evaluate and cache result
+ const result = this.evaluateVisibility(current);
+ const cacheData = {
+ result: result.toString(),
+ timestamp: new Date().getTime()
+ };
+
+ gs.setProperty(cacheKey, JSON.stringify(cacheData));
+ return result;
+ }
+ };
+
+ // Main condition evaluation
+ return ConditionalActionFramework.evaluateWithCache(current);
+})();
+
+// UI Action Client Script
+function executeConditionalAction() {
+ 'use strict';
+
+ /**
+ * Client-side conditional action execution
+ */
+ const ConditionalActionClient = {
+
+ /**
+ * Pre-execution validation
+ */
+ validateExecution: function() {
+ const validationRules = [
+ this.validateFormState,
+ this.validateUserPermissions,
+ this.validateBusinessRules
+ ];
+
+ for (let rule of validationRules) {
+ if (!rule.call(this)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Validate form state
+ */
+ validateFormState: function() {
+ if (g_form.isNewRecord()) {
+ g_form.addErrorMessage('Action not available for new records');
+ return false;
+ }
+
+ const requiredFields = ['short_description', 'caller_id', 'category'];
+ for (let field of requiredFields) {
+ if (!g_form.getValue(field)) {
+ g_form.showFieldMsg(field, 'This field is required before executing this action', 'error');
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Validate user permissions
+ */
+ validateUserPermissions: function() {
+ const currentUser = g_user;
+ const requiredRoles = ['incident_manager', 'itil_admin'];
+
+ if (!currentUser.hasRole(requiredRoles.join(','))) {
+ alert('You do not have sufficient permissions to perform this action');
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Validate business rules
+ */
+ validateBusinessRules: function() {
+ const state = g_form.getValue('state');
+ const priority = g_form.getValue('priority');
+
+ // Business rule: High priority incidents must be in specific states
+ if (priority === '1' && !['1', '2'].includes(state)) {
+ alert('High priority incidents must be in New or In Progress state');
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Execute the conditional action
+ */
+ execute: function() {
+ if (!this.validateExecution()) {
+ return;
+ }
+
+ // Show loading indicator
+ const loadingMsg = g_form.addInfoMessage('Processing action...');
+
+ try {
+ // Perform the action
+ this.performAction();
+
+ // Clear loading message
+ g_form.hideFieldMsg(loadingMsg);
+ g_form.addInfoMessage('Action completed successfully');
+
+ } catch (error) {
+ g_form.hideFieldMsg(loadingMsg);
+ g_form.addErrorMessage('Error executing action: ' + error.message);
+ }
+ },
+
+ /**
+ * Perform the actual action
+ */
+ performAction: function() {
+ // Implementation specific to your business logic
+ const recordId = g_form.getUniqueValue();
+ const actionData = {
+ sys_id: recordId,
+ action_type: 'conditional_execution',
+ execution_context: this.getExecutionContext()
+ };
+
+ // Example: Make server call or update form
+ g_form.setValue('work_notes', 'Conditional action executed at ' + new Date());
+ g_form.save();
+ },
+
+ /**
+ * Get execution context
+ */
+ getExecutionContext: function() {
+ return {
+ user_id: g_user.userID,
+ timestamp: new Date().toISOString(),
+ form_state: g_form.serialize(),
+ browser_info: navigator.userAgent
+ };
+ }
+ };
+
+ // Execute the conditional action
+ ConditionalActionClient.execute();
+}
diff --git a/Client-Side Components/UI Actions/Advanced UI Action Patterns/interactive_form_controller.js b/Client-Side Components/UI Actions/Advanced UI Action Patterns/interactive_form_controller.js
new file mode 100644
index 0000000000..bc182fc07b
--- /dev/null
+++ b/Client-Side Components/UI Actions/Advanced UI Action Patterns/interactive_form_controller.js
@@ -0,0 +1,526 @@
+/**
+ * Interactive Form Controller
+ *
+ * Advanced UI Action pattern for creating interactive form experiences with
+ * real-time validation, dynamic field dependencies, and progressive disclosure.
+ *
+ * Features:
+ * - Real-time form validation and updates
+ * - Dynamic field dependencies
+ * - Progressive disclosure patterns
+ * - Smart defaults and auto-completion
+ * - Enhanced user experience
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+function initializeInteractiveForm() {
+ 'use strict';
+
+ /**
+ * Interactive Form Controller
+ */
+ const InteractiveFormController = {
+
+ // Configuration
+ config: {
+ validationDelay: 500,
+ autoSaveInterval: 30000,
+ dependencyUpdateDelay: 200,
+ progressiveDisclosureSteps: []
+ },
+
+ // Form state management
+ state: {
+ validationTimers: new Map(),
+ fieldDependencies: new Map(),
+ validationRules: new Map(),
+ formProgress: 0,
+ isAutoSaving: false,
+ lastSaveTime: null
+ },
+
+ /**
+ * Initialize interactive form
+ */
+ initialize: function() {
+ try {
+ this.setupFieldDependencies();
+ this.setupValidationRules();
+ this.setupProgressiveDisclosure();
+ this.setupAutoSave();
+ this.bindEventHandlers();
+
+ g_form.addInfoMessage('Interactive form mode enabled');
+ return true;
+ } catch (error) {
+ g_form.addErrorMessage('Failed to initialize interactive form: ' + error.message);
+ return false;
+ }
+ },
+
+ /**
+ * Setup field dependencies
+ */
+ setupFieldDependencies: function() {
+ const dependencies = {
+ // Category affects subcategory options
+ 'category': {
+ targets: ['subcategory', 'assignment_group'],
+ handler: this.handleCategoryChange.bind(this)
+ },
+
+ // Priority affects assignment and escalation
+ 'priority': {
+ targets: ['assignment_group', 'escalation'],
+ handler: this.handlePriorityChange.bind(this)
+ },
+
+ // Location affects configuration items
+ 'location': {
+ targets: ['cmdb_ci', 'affected_user'],
+ handler: this.handleLocationChange.bind(this)
+ },
+
+ // State affects available actions
+ 'state': {
+ targets: ['close_code', 'resolution_notes'],
+ handler: this.handleStateChange.bind(this)
+ }
+ };
+
+ // Register dependencies
+ Object.keys(dependencies).forEach(field => {
+ this.state.fieldDependencies.set(field, dependencies[field]);
+ g_form.getControl(field).onchange = () => {
+ this.processDependency(field);
+ };
+ });
+ },
+
+ /**
+ * Setup validation rules
+ */
+ setupValidationRules: function() {
+ const validationRules = {
+ 'short_description': {
+ required: true,
+ minLength: 10,
+ pattern: /^[A-Za-z0-9\s\-_.,!?]+$/,
+ customValidator: this.validateDescription.bind(this)
+ },
+
+ 'caller_id': {
+ required: true,
+ customValidator: this.validateCaller.bind(this)
+ },
+
+ 'priority': {
+ required: true,
+ customValidator: this.validatePriority.bind(this)
+ },
+
+ 'category': {
+ required: true,
+ dependsOn: ['caller_id'],
+ customValidator: this.validateCategory.bind(this)
+ }
+ };
+
+ // Register validation rules
+ Object.keys(validationRules).forEach(field => {
+ this.state.validationRules.set(field, validationRules[field]);
+ this.attachFieldValidator(field);
+ });
+ },
+
+ /**
+ * Attach validator to field
+ */
+ attachFieldValidator: function(fieldName) {
+ const field = g_form.getControl(fieldName);
+ if (field) {
+ field.onblur = () => this.validateField(fieldName);
+ field.oninput = () => this.scheduleValidation(fieldName);
+ }
+ },
+
+ /**
+ * Schedule field validation with debounce
+ */
+ scheduleValidation: function(fieldName) {
+ // Clear existing timer
+ if (this.state.validationTimers.has(fieldName)) {
+ clearTimeout(this.state.validationTimers.get(fieldName));
+ }
+
+ // Schedule new validation
+ const timer = setTimeout(() => {
+ this.validateField(fieldName);
+ this.state.validationTimers.delete(fieldName);
+ }, this.config.validationDelay);
+
+ this.state.validationTimers.set(fieldName, timer);
+ },
+
+ /**
+ * Validate individual field
+ */
+ validateField: function(fieldName) {
+ const rule = this.state.validationRules.get(fieldName);
+ if (!rule) return true;
+
+ const value = g_form.getValue(fieldName);
+ const isValid = this.executeValidationRule(fieldName, value, rule);
+
+ this.updateFieldValidationUI(fieldName, isValid);
+ this.updateFormProgress();
+
+ return isValid;
+ },
+
+ /**
+ * Execute validation rule
+ */
+ executeValidationRule: function(fieldName, value, rule) {
+ try {
+ // Required validation
+ if (rule.required && (!value || value.trim() === '')) {
+ this.showFieldError(fieldName, 'This field is required');
+ return false;
+ }
+
+ // Skip other validations if field is empty and not required
+ if (!value && !rule.required) return true;
+
+ // Minimum length validation
+ if (rule.minLength && value.length < rule.minLength) {
+ this.showFieldError(fieldName, `Minimum length is ${rule.minLength} characters`);
+ return false;
+ }
+
+ // Pattern validation
+ if (rule.pattern && !rule.pattern.test(value)) {
+ this.showFieldError(fieldName, 'Invalid format');
+ return false;
+ }
+
+ // Custom validation
+ if (rule.customValidator) {
+ const customResult = rule.customValidator(fieldName, value);
+ if (!customResult.isValid) {
+ this.showFieldError(fieldName, customResult.message);
+ return false;
+ }
+ }
+
+ // Clear any existing errors
+ this.clearFieldError(fieldName);
+ return true;
+
+ } catch (error) {
+ this.showFieldError(fieldName, 'Validation error: ' + error.message);
+ return false;
+ }
+ },
+
+ /**
+ * Custom validation: Description
+ */
+ validateDescription: function(fieldName, value) {
+ // Check for common words that indicate good description
+ const qualityWords = ['issue', 'problem', 'error', 'unable', 'cannot', 'when', 'how', 'what'];
+ const hasQualityWords = qualityWords.some(word => value.toLowerCase().includes(word));
+
+ if (!hasQualityWords) {
+ return {
+ isValid: false,
+ message: 'Please provide a more descriptive summary'
+ };
+ }
+
+ return { isValid: true };
+ },
+
+ /**
+ * Custom validation: Caller
+ */
+ validateCaller: function(fieldName, value) {
+ if (!value) return { isValid: false, message: 'Caller is required' };
+
+ // Additional validation could include checking if user exists, is active, etc.
+ return { isValid: true };
+ },
+
+ /**
+ * Custom validation: Priority
+ */
+ validatePriority: function(fieldName, value) {
+ const category = g_form.getValue('category');
+
+ // Business rule: Security incidents must be high priority
+ if (category === 'security' && !['1', '2'].includes(value)) {
+ return {
+ isValid: false,
+ message: 'Security incidents must be High or Critical priority'
+ };
+ }
+
+ return { isValid: true };
+ },
+
+ /**
+ * Custom validation: Category
+ */
+ validateCategory: function(fieldName, value) {
+ const callerId = g_form.getValue('caller_id');
+
+ if (callerId && value) {
+ // Could validate if caller is authorized for certain categories
+ return { isValid: true };
+ }
+
+ return { isValid: true };
+ },
+
+ /**
+ * Process field dependency
+ */
+ processDependency: function(sourceField) {
+ const dependency = this.state.fieldDependencies.get(sourceField);
+ if (!dependency) return;
+
+ // Debounce dependency processing
+ setTimeout(() => {
+ dependency.handler(sourceField);
+ }, this.config.dependencyUpdateDelay);
+ },
+
+ /**
+ * Handle category change
+ */
+ handleCategoryChange: function(sourceField) {
+ const category = g_form.getValue('category');
+
+ // Update subcategory options
+ this.updateSubcategoryOptions(category);
+
+ // Update assignment group based on category
+ this.updateAssignmentGroup(category);
+
+ // Auto-populate certain fields based on category
+ this.applyCategoryDefaults(category);
+ },
+
+ /**
+ * Handle priority change
+ */
+ handlePriorityChange: function(sourceField) {
+ const priority = g_form.getValue('priority');
+
+ // High priority items need immediate assignment
+ if (['1', '2'].includes(priority)) {
+ this.suggestImmediateAssignment();
+ }
+
+ // Update escalation settings
+ this.updateEscalationSettings(priority);
+ },
+
+ /**
+ * Handle location change
+ */
+ handleLocationChange: function(sourceField) {
+ const location = g_form.getValue('location');
+
+ // Filter CIs by location
+ this.filterConfigurationItems(location);
+
+ // Suggest affected users from location
+ this.suggestAffectedUsers(location);
+ },
+
+ /**
+ * Handle state change
+ */
+ handleStateChange: function(sourceField) {
+ const state = g_form.getValue('state');
+
+ // Show/hide resolution fields
+ this.toggleResolutionFields(state);
+
+ // Update available actions
+ this.updateAvailableActions(state);
+ },
+
+ /**
+ * Update form progress
+ */
+ updateFormProgress: function() {
+ const totalFields = this.state.validationRules.size;
+ let validFields = 0;
+
+ this.state.validationRules.forEach((rule, fieldName) => {
+ if (this.validateField(fieldName)) {
+ validFields++;
+ }
+ });
+
+ this.state.formProgress = Math.round((validFields / totalFields) * 100);
+ this.updateProgressIndicator();
+ },
+
+ /**
+ * Update progress indicator
+ */
+ updateProgressIndicator: function() {
+ // Create or update progress bar
+ let progressBar = document.getElementById('form-progress-bar');
+ if (!progressBar) {
+ progressBar = this.createProgressBar();
+ }
+
+ const progressFill = progressBar.querySelector('.progress-fill');
+ const progressText = progressBar.querySelector('.progress-text');
+
+ if (progressFill && progressText) {
+ progressFill.style.width = this.state.formProgress + '%';
+ progressText.textContent = `Form Completion: ${this.state.formProgress}%`;
+ }
+ },
+
+ /**
+ * Create progress bar
+ */
+ createProgressBar: function() {
+ const progressBar = document.createElement('div');
+ progressBar.id = 'form-progress-bar';
+ progressBar.className = 'form-progress-container';
+ progressBar.innerHTML = `
+ Form Completion: 0%
+
+ `;
+
+ // Insert at top of form
+ const formElement = document.querySelector('.form-container') || document.body;
+ formElement.insertBefore(progressBar, formElement.firstChild);
+
+ return progressBar;
+ },
+
+ /**
+ * Setup auto-save functionality
+ */
+ setupAutoSave: function() {
+ setInterval(() => {
+ if (!this.state.isAutoSaving && this.hasUnsavedChanges()) {
+ this.performAutoSave();
+ }
+ }, this.config.autoSaveInterval);
+ },
+
+ /**
+ * Check for unsaved changes
+ */
+ hasUnsavedChanges: function() {
+ // Implementation would check form dirty state
+ return g_form.isNewRecord() || g_form.hasFieldMessages();
+ },
+
+ /**
+ * Perform auto-save
+ */
+ performAutoSave: function() {
+ if (this.state.formProgress < 30) return; // Don't auto-save until form is reasonably complete
+
+ this.state.isAutoSaving = true;
+
+ // Show auto-save indicator
+ g_form.addInfoMessage('Auto-saving...', true);
+
+ // Perform save
+ g_form.save(() => {
+ this.state.isAutoSaving = false;
+ this.state.lastSaveTime = new Date();
+ g_form.addInfoMessage('Auto-saved at ' + this.state.lastSaveTime.toLocaleTimeString(), true);
+ });
+ },
+
+ /**
+ * Show field error
+ */
+ showFieldError: function(fieldName, message) {
+ g_form.showFieldMsg(fieldName, message, 'error');
+ },
+
+ /**
+ * Clear field error
+ */
+ clearFieldError: function(fieldName) {
+ g_form.hideFieldMsg(fieldName);
+ },
+
+ /**
+ * Update field validation UI
+ */
+ updateFieldValidationUI: function(fieldName, isValid) {
+ const field = g_form.getControl(fieldName);
+ if (field) {
+ if (isValid) {
+ field.classList.remove('field-error');
+ field.classList.add('field-valid');
+ } else {
+ field.classList.remove('field-valid');
+ field.classList.add('field-error');
+ }
+ }
+ },
+
+ /**
+ * Bind additional event handlers
+ */
+ bindEventHandlers: function() {
+ // Form submission handler
+ g_form.onSubmit(() => {
+ return this.validateAllFields();
+ });
+
+ // Before unload handler for unsaved changes
+ window.addEventListener('beforeunload', (e) => {
+ if (this.hasUnsavedChanges()) {
+ e.preventDefault();
+ e.returnValue = '';
+ }
+ });
+ },
+
+ /**
+ * Validate all fields
+ */
+ validateAllFields: function() {
+ let allValid = true;
+
+ this.state.validationRules.forEach((rule, fieldName) => {
+ if (!this.validateField(fieldName)) {
+ allValid = false;
+ }
+ });
+
+ if (!allValid) {
+ g_form.addErrorMessage('Please fix validation errors before submitting');
+ }
+
+ return allValid;
+ }
+ };
+
+ // Initialize the interactive form controller
+ InteractiveFormController.initialize();
+
+ // Make controller globally accessible
+ window.InteractiveFormController = InteractiveFormController;
+}
diff --git a/Client-Side Components/UI Actions/Advanced UI Action Patterns/workflow_integration_handler.js b/Client-Side Components/UI Actions/Advanced UI Action Patterns/workflow_integration_handler.js
new file mode 100644
index 0000000000..a96693faec
--- /dev/null
+++ b/Client-Side Components/UI Actions/Advanced UI Action Patterns/workflow_integration_handler.js
@@ -0,0 +1,662 @@
+/**
+ * Workflow Integration Handler
+ *
+ * Advanced UI Action pattern for seamless workflow integration with context
+ * preservation, parameter passing, and asynchronous monitoring capabilities.
+ *
+ * Features:
+ * - Seamless workflow triggering from UI actions
+ * - Context preservation and parameter passing
+ * - Asynchronous workflow monitoring
+ * - Status feedback and error handling
+ * - Dynamic workflow selection
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+function executeWorkflowIntegration() {
+ 'use strict';
+
+ /**
+ * Workflow Integration Handler
+ */
+ const WorkflowIntegrationHandler = {
+
+ // Configuration
+ config: {
+ pollInterval: 2000,
+ maxPollAttempts: 150, // 5 minutes at 2-second intervals
+ workflowTimeout: 300000, // 5 minutes
+ preserveContext: true
+ },
+
+ // Workflow state tracking
+ state: {
+ activeWorkflows: new Map(),
+ workflowHistory: [],
+ currentExecution: null,
+ isMonitoring: false
+ },
+
+ /**
+ * Initialize workflow integration
+ */
+ initialize: function() {
+ try {
+ this.setupWorkflowRegistry();
+ this.createWorkflowSelector();
+ this.setupMonitoringInterface();
+ return true;
+ } catch (error) {
+ this.handleError('Workflow integration initialization failed', error);
+ return false;
+ }
+ },
+
+ /**
+ * Setup workflow registry
+ */
+ setupWorkflowRegistry: function() {
+ const tableName = g_form.getTableName();
+
+ this.workflowRegistry = {
+ // Standard approval workflows
+ 'approval_workflow': {
+ name: 'Standard Approval Process',
+ description: 'Route record through standard approval chain',
+ requiredFields: ['short_description', 'requested_for'],
+ supportedTables: ['sc_req_item', 'change_request', 'incident'],
+ parameters: {
+ 'approval_type': 'normal',
+ 'skip_approvals': false,
+ 'due_date_offset': 2
+ }
+ },
+
+ // Emergency change workflow
+ 'emergency_change': {
+ name: 'Emergency Change Process',
+ description: 'Expedited approval for emergency changes',
+ requiredFields: ['short_description', 'justification', 'risk_impact_analysis'],
+ supportedTables: ['change_request'],
+ parameters: {
+ 'approval_type': 'emergency',
+ 'notification_groups': ['change_advisory_board', 'it_management'],
+ 'expedite': true
+ }
+ },
+
+ // Incident escalation workflow
+ 'incident_escalation': {
+ name: 'Incident Escalation Process',
+ description: 'Escalate incident through management chain',
+ requiredFields: ['short_description', 'escalation_reason'],
+ supportedTables: ['incident'],
+ parameters: {
+ 'escalation_level': 1,
+ 'notify_management': true,
+ 'create_task': true
+ }
+ },
+
+ // Asset provisioning workflow
+ 'asset_provisioning': {
+ name: 'Asset Provisioning Workflow',
+ description: 'Automated asset provisioning and configuration',
+ requiredFields: ['requested_for', 'asset_type', 'configuration'],
+ supportedTables: ['sc_req_item'],
+ parameters: {
+ 'auto_assign': true,
+ 'provision_immediately': false,
+ 'send_notifications': true
+ }
+ }
+ };
+ },
+
+ /**
+ * Create workflow selector dialog
+ */
+ createWorkflowSelector: function() {
+ const tableName = g_form.getTableName();
+ const availableWorkflows = this.getAvailableWorkflows(tableName);
+
+ if (availableWorkflows.length === 0) {
+ g_form.addErrorMessage('No workflows available for this record type');
+ return;
+ }
+
+ if (availableWorkflows.length === 1) {
+ // Auto-select if only one workflow available
+ this.startWorkflow(availableWorkflows[0].id);
+ } else {
+ // Show selection dialog
+ this.showWorkflowSelectionDialog(availableWorkflows);
+ }
+ },
+
+ /**
+ * Get available workflows for table
+ */
+ getAvailableWorkflows: function(tableName) {
+ const available = [];
+
+ Object.keys(this.workflowRegistry).forEach(workflowId => {
+ const workflow = this.workflowRegistry[workflowId];
+ if (workflow.supportedTables.includes(tableName)) {
+ available.push({
+ id: workflowId,
+ ...workflow
+ });
+ }
+ });
+
+ return available;
+ },
+
+ /**
+ * Show workflow selection dialog
+ */
+ showWorkflowSelectionDialog: function(workflows) {
+ let dialogHtml = '';
+ dialogHtml += '
Select Workflow to Execute
';
+ dialogHtml += '
';
+
+ workflows.forEach(workflow => {
+ dialogHtml += `
+
+
${workflow.name}
+
${workflow.description}
+
+ Required fields: ${workflow.requiredFields.join(', ')}
+
+
+ `;
+ });
+
+ dialogHtml += '
';
+ dialogHtml += '
';
+ dialogHtml += '
';
+
+ // Show dialog (simplified - would typically use GlideDialogWindow)
+ this.showDialog('Workflow Selection', dialogHtml);
+ },
+
+ /**
+ * Select workflow from dialog
+ */
+ selectWorkflow: function(workflowId) {
+ this.closeDialog();
+ this.startWorkflow(workflowId);
+ },
+
+ /**
+ * Start workflow execution
+ */
+ startWorkflow: function(workflowId) {
+ const workflow = this.workflowRegistry[workflowId];
+ if (!workflow) {
+ this.handleError('Unknown workflow', new Error('Workflow not found: ' + workflowId));
+ return;
+ }
+
+ try {
+ // Validate prerequisites
+ if (!this.validateWorkflowPrerequisites(workflow)) {
+ return;
+ }
+
+ // Collect workflow parameters
+ const parameters = this.collectWorkflowParameters(workflow);
+
+ // Execute workflow
+ this.executeWorkflow(workflowId, parameters);
+
+ } catch (error) {
+ this.handleError('Failed to start workflow', error);
+ }
+ },
+
+ /**
+ * Validate workflow prerequisites
+ */
+ validateWorkflowPrerequisites: function(workflow) {
+ // Check required fields
+ for (let field of workflow.requiredFields) {
+ const value = g_form.getValue(field);
+ if (!value || value.trim() === '') {
+ g_form.showFieldMsg(field, 'This field is required for the workflow', 'error');
+ g_form.flash(field, '#ff0000', 0);
+ return false;
+ }
+ }
+
+ // Check record state
+ if (g_form.isNewRecord()) {
+ g_form.addErrorMessage('Record must be saved before starting workflow');
+ return false;
+ }
+
+ // Check user permissions
+ if (!this.hasWorkflowPermissions(workflow)) {
+ g_form.addErrorMessage('You do not have permission to execute this workflow');
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Check workflow permissions
+ */
+ hasWorkflowPermissions: function(workflow) {
+ // Basic role check - would be more sophisticated in real implementation
+ return g_user.hasRole('workflow_admin') || g_user.hasRole('admin');
+ },
+
+ /**
+ * Collect workflow parameters
+ */
+ collectWorkflowParameters: function(workflow) {
+ const parameters = {
+ // Base parameters
+ record_id: g_form.getUniqueValue(),
+ table_name: g_form.getTableName(),
+ initiated_by: g_user.userID,
+ initiated_at: new Date().toISOString(),
+
+ // Workflow-specific parameters
+ ...workflow.parameters
+ };
+
+ // Add form context if enabled
+ if (this.config.preserveContext) {
+ parameters.form_context = this.captureFormContext();
+ }
+
+ // Add user-provided parameters
+ const userParams = this.getUserParameters(workflow);
+ Object.assign(parameters, userParams);
+
+ return parameters;
+ },
+
+ /**
+ * Capture current form context
+ */
+ captureFormContext: function() {
+ const context = {
+ form_values: {},
+ field_states: {},
+ user_info: {
+ user_id: g_user.userID,
+ user_name: g_user.userName,
+ roles: g_user.roles
+ },
+ timestamp: new Date().toISOString()
+ };
+
+ // Capture current field values
+ const fields = g_form.getFieldNames();
+ fields.forEach(field => {
+ context.form_values[field] = g_form.getValue(field);
+ context.field_states[field] = {
+ visible: g_form.isVisible(field),
+ mandatory: g_form.isMandatory(field),
+ readonly: g_form.isReadOnly(field)
+ };
+ });
+
+ return context;
+ },
+
+ /**
+ * Get user-provided parameters
+ */
+ getUserParameters: function(workflow) {
+ // This would typically show a parameter collection dialog
+ // For now, returning default parameters
+ return {
+ user_comments: g_form.getValue('work_notes') || '',
+ priority_override: false,
+ send_notifications: true
+ };
+ },
+
+ /**
+ * Execute workflow
+ */
+ executeWorkflow: function(workflowId, parameters) {
+ g_form.addInfoMessage('Starting workflow execution...');
+
+ // Create execution tracking
+ const executionId = this.generateExecutionId();
+ const execution = {
+ id: executionId,
+ workflow_id: workflowId,
+ parameters: parameters,
+ status: 'starting',
+ start_time: new Date(),
+ progress: 0,
+ steps_completed: 0,
+ total_steps: 0
+ };
+
+ this.state.activeWorkflows.set(executionId, execution);
+ this.state.currentExecution = executionId;
+
+ // Make server call to start workflow
+ this.callWorkflowServer(workflowId, parameters, executionId);
+
+ // Start monitoring
+ this.startWorkflowMonitoring(executionId);
+ },
+
+ /**
+ * Call server-side workflow execution
+ */
+ callWorkflowServer: function(workflowId, parameters, executionId) {
+ const ga = new GlideAjax('WorkflowIntegrationProcessor');
+ ga.addParam('sysparm_name', 'executeWorkflow');
+ ga.addParam('sysparm_workflow_id', workflowId);
+ ga.addParam('sysparm_parameters', JSON.stringify(parameters));
+ ga.addParam('sysparm_execution_id', executionId);
+
+ ga.getXMLAnswer((response) => {
+ try {
+ const result = JSON.parse(response);
+ this.handleWorkflowResponse(executionId, result);
+ } catch (error) {
+ this.handleWorkflowError(executionId, error);
+ }
+ });
+ },
+
+ /**
+ * Handle workflow response
+ */
+ handleWorkflowResponse: function(executionId, result) {
+ const execution = this.state.activeWorkflows.get(executionId);
+ if (!execution) return;
+
+ if (result.success) {
+ execution.status = 'running';
+ execution.workflow_context_id = result.workflow_context_id;
+ execution.total_steps = result.total_steps || 0;
+
+ g_form.addInfoMessage('Workflow started successfully');
+ this.updateWorkflowStatus(executionId);
+ } else {
+ this.handleWorkflowError(executionId, new Error(result.error || 'Unknown workflow error'));
+ }
+ },
+
+ /**
+ * Handle workflow error
+ */
+ handleWorkflowError: function(executionId, error) {
+ const execution = this.state.activeWorkflows.get(executionId);
+ if (execution) {
+ execution.status = 'error';
+ execution.error = error.message;
+ execution.end_time = new Date();
+ }
+
+ this.stopWorkflowMonitoring(executionId);
+ g_form.addErrorMessage('Workflow execution failed: ' + error.message);
+ },
+
+ /**
+ * Start workflow monitoring
+ */
+ startWorkflowMonitoring: function(executionId) {
+ if (this.state.isMonitoring) return;
+
+ this.state.isMonitoring = true;
+ this.showMonitoringInterface();
+
+ const monitor = () => {
+ if (!this.state.isMonitoring) return;
+
+ this.checkWorkflowStatus(executionId)
+ .then((status) => {
+ this.updateWorkflowStatus(executionId, status);
+
+ if (status.is_complete) {
+ this.completeWorkflowMonitoring(executionId, status);
+ } else {
+ setTimeout(monitor, this.config.pollInterval);
+ }
+ })
+ .catch((error) => {
+ this.handleWorkflowError(executionId, error);
+ });
+ };
+
+ // Start monitoring
+ setTimeout(monitor, this.config.pollInterval);
+ },
+
+ /**
+ * Check workflow status
+ */
+ checkWorkflowStatus: function(executionId) {
+ return new Promise((resolve, reject) => {
+ const execution = this.state.activeWorkflows.get(executionId);
+ if (!execution || !execution.workflow_context_id) {
+ reject(new Error('Invalid execution context'));
+ return;
+ }
+
+ const ga = new GlideAjax('WorkflowIntegrationProcessor');
+ ga.addParam('sysparm_name', 'checkWorkflowStatus');
+ ga.addParam('sysparm_workflow_context_id', execution.workflow_context_id);
+
+ ga.getXMLAnswer((response) => {
+ try {
+ const status = JSON.parse(response);
+ resolve(status);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+ },
+
+ /**
+ * Update workflow status
+ */
+ updateWorkflowStatus: function(executionId, status) {
+ const execution = this.state.activeWorkflows.get(executionId);
+ if (!execution) return;
+
+ if (status) {
+ execution.status = status.state || execution.status;
+ execution.progress = status.progress || 0;
+ execution.steps_completed = status.steps_completed || 0;
+ execution.current_step = status.current_step;
+ execution.last_update = new Date();
+ }
+
+ this.updateMonitoringDisplay(execution);
+ },
+
+ /**
+ * Complete workflow monitoring
+ */
+ completeWorkflowMonitoring: function(executionId, finalStatus) {
+ const execution = this.state.activeWorkflows.get(executionId);
+ if (execution) {
+ execution.status = finalStatus.state;
+ execution.end_time = new Date();
+ execution.result = finalStatus.result;
+
+ // Move to history
+ this.state.workflowHistory.push(execution);
+ this.state.activeWorkflows.delete(executionId);
+ }
+
+ this.stopWorkflowMonitoring(executionId);
+
+ // Show completion message
+ const duration = Math.round((execution.end_time - execution.start_time) / 1000);
+ g_form.addInfoMessage(`Workflow completed in ${duration} seconds`);
+
+ // Refresh form if needed
+ if (finalStatus.refresh_form) {
+ g_form.reload();
+ }
+ },
+
+ /**
+ * Stop workflow monitoring
+ */
+ stopWorkflowMonitoring: function(executionId) {
+ this.state.isMonitoring = false;
+ this.hideMonitoringInterface();
+ },
+
+ /**
+ * Setup monitoring interface
+ */
+ setupMonitoringInterface: function() {
+ // Create monitoring container
+ const monitoringContainer = document.createElement('div');
+ monitoringContainer.id = 'workflow-monitoring';
+ monitoringContainer.style.display = 'none';
+ monitoringContainer.innerHTML = `
+
+
+
+
+
Initializing...
+
Step 0 of 0
+
+
+ `;
+
+ document.body.appendChild(monitoringContainer);
+ },
+
+ /**
+ * Show monitoring interface
+ */
+ showMonitoringInterface: function() {
+ const container = document.getElementById('workflow-monitoring');
+ if (container) {
+ container.style.display = 'block';
+ }
+ },
+
+ /**
+ * Hide monitoring interface
+ */
+ hideMonitoringInterface: function() {
+ const container = document.getElementById('workflow-monitoring');
+ if (container) {
+ container.style.display = 'none';
+ }
+ },
+
+ /**
+ * Update monitoring display
+ */
+ updateMonitoringDisplay: function(execution) {
+ const progressBar = document.getElementById('workflow-progress-bar');
+ const progressText = document.getElementById('workflow-progress-text');
+ const currentStep = document.getElementById('workflow-current-step');
+ const stepCounter = document.getElementById('workflow-step-counter');
+
+ if (progressBar && progressText) {
+ progressBar.style.width = execution.progress + '%';
+ progressText.textContent = Math.round(execution.progress) + '%';
+ }
+
+ if (currentStep && execution.current_step) {
+ currentStep.textContent = execution.current_step;
+ }
+
+ if (stepCounter) {
+ stepCounter.textContent = `Step ${execution.steps_completed} of ${execution.total_steps}`;
+ }
+ },
+
+ /**
+ * Cancel workflow
+ */
+ cancelWorkflow: function() {
+ if (confirm('Are you sure you want to cancel the workflow execution?')) {
+ const executionId = this.state.currentExecution;
+ if (executionId) {
+ this.stopWorkflowMonitoring(executionId);
+ // Would also call server to cancel workflow
+ }
+ }
+ },
+
+ /**
+ * Generate unique execution ID
+ */
+ generateExecutionId: function() {
+ return 'wf_exec_' + new Date().getTime() + '_' + Math.random().toString(36).substr(2, 9);
+ },
+
+ /**
+ * Show dialog (simplified implementation)
+ */
+ showDialog: function(title, content) {
+ // Simplified dialog - would use GlideDialogWindow in real implementation
+ const dialog = document.createElement('div');
+ dialog.className = 'workflow-dialog';
+ dialog.innerHTML = `
+
+
+
${title}
+ ${content}
+
+
+ `;
+ document.body.appendChild(dialog);
+ },
+
+ /**
+ * Close dialog
+ */
+ closeDialog: function() {
+ const dialog = document.querySelector('.workflow-dialog');
+ if (dialog) {
+ dialog.remove();
+ }
+ },
+
+ /**
+ * Cancel workflow selection
+ */
+ cancelWorkflowSelection: function() {
+ this.closeDialog();
+ },
+
+ /**
+ * Handle errors
+ */
+ handleError: function(message, error) {
+ const errorMsg = `${message}: ${error.message || error}`;
+ g_form.addErrorMessage(errorMsg);
+ console.error('WorkflowIntegrationHandler:', errorMsg);
+ }
+ };
+
+ // Initialize workflow integration
+ WorkflowIntegrationHandler.initialize();
+
+ // Make handler globally accessible
+ window.WorkflowIntegrationHandler = WorkflowIntegrationHandler;
+}
diff --git a/Integration/Scripted REST APIs/Advanced API Patterns/README.md b/Integration/Scripted REST APIs/Advanced API Patterns/README.md
new file mode 100644
index 0000000000..d9ddc43881
--- /dev/null
+++ b/Integration/Scripted REST APIs/Advanced API Patterns/README.md
@@ -0,0 +1,84 @@
+# Advanced Scripted REST API Patterns
+
+This collection demonstrates enterprise-grade Scripted REST API patterns for ServiceNow, focusing on security, performance, and maintainability best practices.
+
+## 🎯 Features
+
+### 1. **API Gateway Pattern** (`api_gateway_pattern.js`)
+- Centralized request routing and transformation
+- Rate limiting and throttling
+- Request/response validation
+- API versioning support
+- Comprehensive logging and monitoring
+
+### 2. **Authentication & Authorization Framework** (`auth_framework.js`)
+- Multiple authentication strategies (OAuth2, JWT, API Keys)
+- Role-based access control (RBAC)
+- Resource-level permissions
+- Token validation and refresh
+- Security audit logging
+
+### 3. **Data Transformation Pipeline** (`data_transformation_pipeline.js`)
+- Flexible input/output data mapping
+- Schema validation and transformation
+- Data sanitization and normalization
+- Custom field processors
+- Batch processing capabilities
+
+### 4. **Error Handling & Resilience** (`error_handling_resilience.js`)
+- Comprehensive error response patterns
+- Circuit breaker implementation
+- Retry mechanisms with exponential backoff
+- Graceful degradation strategies
+- Health check endpoints
+
+### 5. **Performance Optimization** (`performance_optimization.js`)
+- Intelligent caching strategies
+- Database query optimization
+- Response compression and pagination
+- Asynchronous processing patterns
+- Resource pooling
+
+## 🚀 Key Benefits
+
+- **Security**: Multi-layered security with authentication, authorization, and validation
+- **Performance**: Optimized for high-throughput scenarios with caching and pagination
+- **Reliability**: Robust error handling with circuit breakers and retry logic
+- **Scalability**: Designed for enterprise-scale deployments
+- **Maintainability**: Clean, modular code with comprehensive documentation
+
+## 📋 Implementation Guidelines
+
+1. **Security First**: Always validate inputs and implement proper authentication
+2. **Performance**: Use caching and pagination for large datasets
+3. **Error Handling**: Provide meaningful error messages and proper HTTP status codes
+4. **Documentation**: Auto-generate OpenAPI/Swagger documentation
+5. **Testing**: Include comprehensive test suites for all endpoints
+
+## 🔧 Usage Requirements
+
+- ServiceNow Madrid or later
+- Proper REST API roles and permissions
+- Understanding of HTTP protocols and REST principles
+- Knowledge of ServiceNow scripting and GlideRecord APIs
+
+## 📖 Best Practices
+
+- Follow RESTful design principles
+- Use appropriate HTTP methods and status codes
+- Implement proper input validation and sanitization
+- Use structured logging for debugging and monitoring
+- Consider API versioning from the start
+- Implement rate limiting to prevent abuse
+
+## 🔒 Security Considerations
+
+- Always validate and sanitize input data
+- Implement proper authentication and authorization
+- Use HTTPS for all API communications
+- Log security events for audit purposes
+- Regularly review and update security configurations
+
+---
+
+*Part of the ServiceNow Code Snippets collection - Advanced Scripted REST API Patterns*
diff --git a/Integration/Scripted REST APIs/Advanced API Patterns/api_gateway_pattern.js b/Integration/Scripted REST APIs/Advanced API Patterns/api_gateway_pattern.js
new file mode 100644
index 0000000000..bf6fda28d5
--- /dev/null
+++ b/Integration/Scripted REST APIs/Advanced API Patterns/api_gateway_pattern.js
@@ -0,0 +1,651 @@
+/**
+ * API Gateway Pattern for ServiceNow Scripted REST APIs
+ *
+ * Advanced pattern implementing a centralized API gateway with routing,
+ * transformation, rate limiting, and comprehensive monitoring capabilities.
+ *
+ * Features:
+ * - Centralized request routing and transformation
+ * - Rate limiting and throttling
+ * - Request/response validation
+ * - API versioning support
+ * - Comprehensive logging and monitoring
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
+ 'use strict';
+
+ /**
+ * API Gateway Implementation
+ */
+ const APIGateway = {
+
+ // Configuration
+ config: {
+ enableRateLimiting: true,
+ enableLogging: true,
+ enableTransformation: true,
+ defaultApiVersion: 'v1',
+ maxRequestSize: 10485760, // 10MB
+ requestTimeout: 30000, // 30 seconds
+ rateLimitWindow: 3600000, // 1 hour
+ rateLimitMax: 1000 // requests per hour
+ },
+
+ // API version routing
+ versionRoutes: {
+ 'v1': {
+ 'incidents': 'IncidentAPIv1',
+ 'changes': 'ChangeAPIv1',
+ 'users': 'UserAPIv1',
+ 'catalog': 'CatalogAPIv1'
+ },
+ 'v2': {
+ 'incidents': 'IncidentAPIv2',
+ 'changes': 'ChangeAPIv2',
+ 'users': 'UserAPIv2',
+ 'catalog': 'CatalogAPIv2'
+ }
+ },
+
+ /**
+ * Main gateway processing function
+ */
+ process: function(request, response) {
+ try {
+ // Initialize request context
+ const context = this.initializeContext(request, response);
+
+ // Pre-processing validation
+ if (!this.validateRequest(context)) {
+ return;
+ }
+
+ // Rate limiting check
+ if (!this.checkRateLimit(context)) {
+ return;
+ }
+
+ // Route the request
+ this.routeRequest(context);
+
+ } catch (error) {
+ this.handleError(context || { response: response }, error, 'GATEWAY_ERROR');
+ }
+ },
+
+ /**
+ * Initialize request context
+ */
+ initializeContext: function(request, response) {
+ const context = {
+ request: request,
+ response: response,
+ startTime: new Date(),
+ requestId: this.generateRequestId(),
+ clientIP: this.getClientIP(request),
+ userAgent: request.getHeader('User-Agent') || 'unknown',
+ contentType: request.getHeader('Content-Type') || 'application/json',
+ apiVersion: this.extractApiVersion(request),
+ resource: this.extractResource(request),
+ method: request.method,
+ path: request.pathInfo,
+ queryParams: request.queryParams,
+ headers: this.extractHeaders(request),
+ body: null,
+ user: gs.getUserID(),
+ sessionId: gs.getSessionID()
+ };
+
+ // Parse request body if present
+ if (request.body && request.body.dataString) {
+ try {
+ context.body = JSON.parse(request.body.dataString);
+ } catch (e) {
+ context.body = request.body.dataString;
+ }
+ }
+
+ // Log request initiation
+ this.logRequest(context, 'REQUEST_INITIATED');
+
+ return context;
+ },
+
+ /**
+ * Validate incoming request
+ */
+ validateRequest: function(context) {
+ // Check request size
+ if (context.request.body && context.request.body.dataString) {
+ const requestSize = context.request.body.dataString.length;
+ if (requestSize > this.config.maxRequestSize) {
+ this.sendError(context, 413, 'REQUEST_TOO_LARGE',
+ 'Request body exceeds maximum size limit');
+ return false;
+ }
+ }
+
+ // Validate Content-Type for POST/PUT/PATCH
+ if (['POST', 'PUT', 'PATCH'].includes(context.method)) {
+ if (!context.contentType.includes('application/json')) {
+ this.sendError(context, 415, 'UNSUPPORTED_MEDIA_TYPE',
+ 'Content-Type must be application/json');
+ return false;
+ }
+ }
+
+ // Validate API version
+ if (!this.versionRoutes[context.apiVersion]) {
+ this.sendError(context, 400, 'INVALID_API_VERSION',
+ 'Unsupported API version: ' + context.apiVersion);
+ return false;
+ }
+
+ // Validate resource
+ if (!this.versionRoutes[context.apiVersion][context.resource]) {
+ this.sendError(context, 404, 'RESOURCE_NOT_FOUND',
+ 'Resource not found: ' + context.resource);
+ return false;
+ }
+
+ // Custom validation rules
+ return this.executeCustomValidation(context);
+ },
+
+ /**
+ * Execute custom validation rules
+ */
+ executeCustomValidation: function(context) {
+ // Example: Validate required headers
+ const requiredHeaders = ['Authorization'];
+ for (let header of requiredHeaders) {
+ if (!context.headers[header.toLowerCase()]) {
+ this.sendError(context, 401, 'MISSING_HEADER',
+ 'Required header missing: ' + header);
+ return false;
+ }
+ }
+
+ // Example: Validate JSON schema for POST/PUT
+ if (['POST', 'PUT'].includes(context.method) && context.body) {
+ if (!this.validateJsonSchema(context.resource, context.body)) {
+ this.sendError(context, 400, 'INVALID_SCHEMA',
+ 'Request body does not match expected schema');
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Check rate limiting
+ */
+ checkRateLimit: function(context) {
+ if (!this.config.enableRateLimiting) return true;
+
+ const rateLimitKey = this.getRateLimitKey(context);
+ const currentCount = this.getRateLimitCount(rateLimitKey);
+
+ if (currentCount >= this.config.rateLimitMax) {
+ // Add rate limit headers
+ context.response.setHeader('X-RateLimit-Limit', this.config.rateLimitMax.toString());
+ context.response.setHeader('X-RateLimit-Remaining', '0');
+ context.response.setHeader('X-RateLimit-Reset',
+ (Math.floor(Date.now() / 1000) + 3600).toString());
+
+ this.sendError(context, 429, 'RATE_LIMIT_EXCEEDED',
+ 'Rate limit exceeded. Try again later.');
+ return false;
+ }
+
+ // Increment rate limit counter
+ this.incrementRateLimitCount(rateLimitKey);
+
+ // Add rate limit headers
+ context.response.setHeader('X-RateLimit-Limit', this.config.rateLimitMax.toString());
+ context.response.setHeader('X-RateLimit-Remaining',
+ (this.config.rateLimitMax - currentCount - 1).toString());
+
+ return true;
+ },
+
+ /**
+ * Route request to appropriate handler
+ */
+ routeRequest: function(context) {
+ const handlerName = this.versionRoutes[context.apiVersion][context.resource];
+
+ try {
+ // Transform request if needed
+ if (this.config.enableTransformation) {
+ this.transformRequest(context);
+ }
+
+ // Get handler instance
+ const handler = this.getHandlerInstance(handlerName);
+
+ if (!handler) {
+ this.sendError(context, 500, 'HANDLER_NOT_FOUND',
+ 'Handler not available: ' + handlerName);
+ return;
+ }
+
+ // Execute handler
+ const result = this.executeHandler(handler, context);
+
+ // Transform response if needed
+ if (this.config.enableTransformation) {
+ this.transformResponse(context, result);
+ } else {
+ this.sendResponse(context, result);
+ }
+
+ } catch (error) {
+ this.handleError(context, error, 'ROUTING_ERROR');
+ }
+ },
+
+ /**
+ * Transform request data
+ */
+ transformRequest: function(context) {
+ // Example transformations based on API version
+ if (context.apiVersion === 'v1' && context.body) {
+ // V1 to internal format transformation
+ if (context.resource === 'incidents') {
+ this.transformIncidentRequest(context);
+ }
+ }
+
+ // Add common transformations
+ this.addCommonRequestFields(context);
+ },
+
+ /**
+ * Transform incident request for v1 compatibility
+ */
+ transformIncidentRequest: function(context) {
+ if (context.body.description) {
+ context.body.short_description = context.body.description;
+ delete context.body.description;
+ }
+
+ if (context.body.reporter) {
+ context.body.caller_id = context.body.reporter;
+ delete context.body.reporter;
+ }
+ },
+
+ /**
+ * Add common request fields
+ */
+ addCommonRequestFields: function(context) {
+ if (context.body && typeof context.body === 'object') {
+ context.body._gateway_metadata = {
+ request_id: context.requestId,
+ api_version: context.apiVersion,
+ client_ip: context.clientIP,
+ user_agent: context.userAgent,
+ timestamp: context.startTime.toISOString()
+ };
+ }
+ },
+
+ /**
+ * Get handler instance
+ */
+ getHandlerInstance: function(handlerName) {
+ try {
+ // In real implementation, this would instantiate the appropriate handler class
+ // For this example, we'll return a mock handler
+ return {
+ process: function(context) {
+ return {
+ success: true,
+ data: { message: 'Processed by ' + handlerName },
+ metadata: {
+ handler: handlerName,
+ processing_time: new Date() - context.startTime
+ }
+ };
+ }
+ };
+ } catch (error) {
+ gs.error('APIGateway: Failed to get handler instance: ' + error.message);
+ return null;
+ }
+ },
+
+ /**
+ * Execute handler with timeout
+ */
+ executeHandler: function(handler, context) {
+ const startTime = new Date();
+
+ try {
+ // Set timeout for handler execution
+ const timeoutPromise = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject(new Error('Handler execution timeout'));
+ }, this.config.requestTimeout);
+ });
+
+ // Execute handler
+ const result = handler.process(context);
+
+ // Log handler execution
+ this.logRequest(context, 'HANDLER_EXECUTED', {
+ handler: handler.constructor.name,
+ execution_time: new Date() - startTime
+ });
+
+ return result;
+
+ } catch (error) {
+ this.logRequest(context, 'HANDLER_ERROR', {
+ error: error.message,
+ execution_time: new Date() - startTime
+ });
+ throw error;
+ }
+ },
+
+ /**
+ * Transform response data
+ */
+ transformResponse: function(context, result) {
+ // Version-specific response transformations
+ if (context.apiVersion === 'v1') {
+ result = this.transformToV1Response(result);
+ }
+
+ // Add common response metadata
+ result._gateway_metadata = {
+ request_id: context.requestId,
+ api_version: context.apiVersion,
+ processing_time: new Date() - context.startTime,
+ timestamp: new Date().toISOString()
+ };
+
+ this.sendResponse(context, result);
+ },
+
+ /**
+ * Transform to v1 response format
+ */
+ transformToV1Response: function(result) {
+ // Example v1 compatibility transformations
+ if (result.data && Array.isArray(result.data)) {
+ result.items = result.data;
+ result.count = result.data.length;
+ delete result.data;
+ }
+
+ return result;
+ },
+
+ /**
+ * Send successful response
+ */
+ sendResponse: function(context, result) {
+ const processingTime = new Date() - context.startTime;
+
+ // Set response headers
+ context.response.setHeader('X-Request-ID', context.requestId);
+ context.response.setHeader('X-Processing-Time', processingTime.toString());
+ context.response.setHeader('X-API-Version', context.apiVersion);
+
+ // Set response body
+ context.response.setBody(result);
+ context.response.setStatus(result.status || 200);
+
+ // Log successful response
+ this.logRequest(context, 'RESPONSE_SENT', {
+ status: result.status || 200,
+ processing_time: processingTime
+ });
+ },
+
+ /**
+ * Send error response
+ */
+ sendError: function(context, statusCode, errorCode, message, details) {
+ const errorResponse = {
+ error: {
+ code: errorCode,
+ message: message,
+ details: details,
+ request_id: context.requestId,
+ timestamp: new Date().toISOString()
+ }
+ };
+
+ context.response.setStatus(statusCode);
+ context.response.setHeader('X-Request-ID', context.requestId);
+ context.response.setBody(errorResponse);
+
+ // Log error
+ this.logRequest(context, 'ERROR_RESPONSE', {
+ status: statusCode,
+ error_code: errorCode,
+ error_message: message
+ });
+ },
+
+ /**
+ * Handle unexpected errors
+ */
+ handleError: function(context, error, errorType) {
+ gs.error('APIGateway ' + errorType + ': ' + error.message);
+
+ this.sendError(context, 500, 'INTERNAL_ERROR',
+ 'An internal error occurred', {
+ type: errorType,
+ message: error.message
+ });
+ },
+
+ /**
+ * Extract API version from request
+ */
+ extractApiVersion: function(request) {
+ // Try to get version from path (e.g., /api/v1/incidents)
+ const pathParts = request.pathInfo.split('/');
+ for (let part of pathParts) {
+ if (part.match(/^v\d+$/)) {
+ return part;
+ }
+ }
+
+ // Try to get version from header
+ const versionHeader = request.getHeader('API-Version');
+ if (versionHeader) {
+ return versionHeader;
+ }
+
+ // Default version
+ return this.config.defaultApiVersion;
+ },
+
+ /**
+ * Extract resource from request path
+ */
+ extractResource: function(request) {
+ const pathParts = request.pathInfo.split('/').filter(part => part.length > 0);
+
+ // Find resource after version or use first path segment
+ let resourceIndex = 0;
+ for (let i = 0; i < pathParts.length; i++) {
+ if (pathParts[i].match(/^v\d+$/)) {
+ resourceIndex = i + 1;
+ break;
+ }
+ }
+
+ return pathParts[resourceIndex] || 'unknown';
+ },
+
+ /**
+ * Extract headers from request
+ */
+ extractHeaders: function(request) {
+ const headers = {};
+ const headerNames = ['Authorization', 'Content-Type', 'Accept', 'User-Agent', 'X-Forwarded-For'];
+
+ headerNames.forEach(name => {
+ const value = request.getHeader(name);
+ if (value) {
+ headers[name.toLowerCase()] = value;
+ }
+ });
+
+ return headers;
+ },
+
+ /**
+ * Get client IP address
+ */
+ getClientIP: function(request) {
+ return request.getHeader('X-Forwarded-For') ||
+ request.getHeader('X-Real-IP') ||
+ 'unknown';
+ },
+
+ /**
+ * Generate unique request ID
+ */
+ generateRequestId: function() {
+ return 'req_' + gs.generateGUID();
+ },
+
+ /**
+ * Get rate limit key for client
+ */
+ getRateLimitKey: function(context) {
+ return 'rate_limit_' + context.clientIP + '_' + context.user;
+ },
+
+ /**
+ * Get current rate limit count
+ */
+ getRateLimitCount: function(key) {
+ const gr = new GlideRecord('sys_properties');
+ gr.addQuery('name', key);
+ gr.query();
+
+ if (gr.next()) {
+ const data = JSON.parse(gr.getValue('value') || '{}');
+ const now = Date.now();
+
+ // Check if window has expired
+ if (now - data.window_start > this.config.rateLimitWindow) {
+ return 0; // Reset count
+ }
+
+ return data.count || 0;
+ }
+
+ return 0;
+ },
+
+ /**
+ * Increment rate limit count
+ */
+ incrementRateLimitCount: function(key) {
+ const gr = new GlideRecord('sys_properties');
+ gr.addQuery('name', key);
+ gr.query();
+
+ const now = Date.now();
+ let data = { count: 1, window_start: now };
+
+ if (gr.next()) {
+ const existing = JSON.parse(gr.getValue('value') || '{}');
+
+ // Check if window has expired
+ if (now - existing.window_start > this.config.rateLimitWindow) {
+ data = { count: 1, window_start: now };
+ } else {
+ data = {
+ count: (existing.count || 0) + 1,
+ window_start: existing.window_start
+ };
+ }
+
+ gr.setValue('value', JSON.stringify(data));
+ gr.update();
+ } else {
+ gr.initialize();
+ gr.setValue('name', key);
+ gr.setValue('value', JSON.stringify(data));
+ gr.insert();
+ }
+ },
+
+ /**
+ * Validate JSON schema
+ */
+ validateJsonSchema: function(resource, data) {
+ // Simplified schema validation - in real implementation would use proper JSON schema
+ const schemas = {
+ 'incidents': ['short_description', 'caller_id'],
+ 'changes': ['short_description', 'requested_by'],
+ 'users': ['user_name', 'email']
+ };
+
+ const requiredFields = schemas[resource];
+ if (!requiredFields) return true;
+
+ return requiredFields.every(field => data.hasOwnProperty(field));
+ },
+
+ /**
+ * Log request events
+ */
+ logRequest: function(context, event, details) {
+ if (!this.config.enableLogging) return;
+
+ const logEntry = {
+ request_id: context.requestId,
+ event: event,
+ timestamp: new Date().toISOString(),
+ method: context.method,
+ path: context.path,
+ api_version: context.apiVersion,
+ resource: context.resource,
+ client_ip: context.clientIP,
+ user: context.user,
+ details: details || {}
+ };
+
+ // Log to system log
+ gs.info('APIGateway: ' + JSON.stringify(logEntry));
+
+ // Could also log to custom table for analytics
+ this.logToCustomTable(logEntry);
+ },
+
+ /**
+ * Log to custom table for analytics
+ */
+ logToCustomTable: function(logEntry) {
+ try {
+ // Would create custom table for API analytics
+ // For now, just log to system
+ gs.debug('APIGateway Analytics: ' + JSON.stringify(logEntry));
+ } catch (error) {
+ gs.error('APIGateway: Failed to log analytics: ' + error.message);
+ }
+ }
+ };
+
+ // Process the request through the gateway
+ APIGateway.process(request, response);
+
+})(request, response);
diff --git a/Integration/Scripted REST APIs/Advanced API Patterns/auth_framework.js b/Integration/Scripted REST APIs/Advanced API Patterns/auth_framework.js
new file mode 100644
index 0000000000..7149367ff5
--- /dev/null
+++ b/Integration/Scripted REST APIs/Advanced API Patterns/auth_framework.js
@@ -0,0 +1,702 @@
+/**
+ * Authentication & Authorization Framework for ServiceNow Scripted REST APIs
+ *
+ * Comprehensive framework implementing multiple authentication strategies,
+ * role-based access control, and security audit logging.
+ *
+ * Features:
+ * - Multiple authentication strategies (OAuth2, JWT, API Keys)
+ * - Role-based access control (RBAC)
+ * - Resource-level permissions
+ * - Token validation and refresh
+ * - Security audit logging
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
+ 'use strict';
+
+ /**
+ * Authentication & Authorization Framework
+ */
+ const AuthFramework = {
+
+ // Configuration
+ config: {
+ enableJWT: true,
+ enableOAuth2: true,
+ enableAPIKey: true,
+ jwtSecret: gs.getProperty('api.jwt.secret', 'default-secret-change-me'),
+ jwtExpiration: 3600, // 1 hour
+ apiKeyExpiration: 86400, // 24 hours
+ maxLoginAttempts: 5,
+ lockoutDuration: 1800, // 30 minutes
+ enableAuditLogging: true,
+ requireHTTPS: true
+ },
+
+ // Supported authentication methods
+ authMethods: {
+ 'bearer': 'validateBearerToken',
+ 'basic': 'validateBasicAuth',
+ 'apikey': 'validateAPIKey',
+ 'oauth': 'validateOAuth2Token'
+ },
+
+ // Resource permissions matrix
+ permissions: {
+ 'incidents': {
+ 'read': ['incident_manager', 'itil', 'admin'],
+ 'write': ['incident_manager', 'admin'],
+ 'delete': ['admin']
+ },
+ 'changes': {
+ 'read': ['change_manager', 'itil', 'admin'],
+ 'write': ['change_manager', 'admin'],
+ 'delete': ['admin']
+ },
+ 'users': {
+ 'read': ['user_admin', 'admin'],
+ 'write': ['user_admin', 'admin'],
+ 'delete': ['admin']
+ },
+ 'catalog': {
+ 'read': ['catalog_admin', 'itil', 'admin'],
+ 'write': ['catalog_admin', 'admin'],
+ 'delete': ['admin']
+ }
+ },
+
+ /**
+ * Main authentication and authorization processor
+ */
+ process: function(request, response) {
+ try {
+ // Security checks
+ if (!this.performSecurityChecks(request, response)) {
+ return false;
+ }
+
+ // Extract authentication info
+ const authInfo = this.extractAuthInfo(request);
+ if (!authInfo) {
+ this.sendAuthError(response, 'MISSING_AUTH', 'Authentication required');
+ return false;
+ }
+
+ // Authenticate user
+ const authResult = this.authenticateUser(authInfo);
+ if (!authResult.success) {
+ this.sendAuthError(response, authResult.error, authResult.message);
+ return false;
+ }
+
+ // Check authorization
+ const authzResult = this.authorizeRequest(request, authResult.user);
+ if (!authzResult.success) {
+ this.sendAuthError(response, authzResult.error, authzResult.message, 403);
+ return false;
+ }
+
+ // Log successful authentication
+ this.logSecurityEvent('AUTH_SUCCESS', authResult.user, request);
+
+ // Add user context to request
+ request.user = authResult.user;
+ request.permissions = authzResult.permissions;
+
+ return true;
+
+ } catch (error) {
+ this.logSecurityEvent('AUTH_ERROR', null, request, { error: error.message });
+ this.sendAuthError(response, 'INTERNAL_ERROR', 'Authentication system error');
+ return false;
+ }
+ },
+
+ /**
+ * Perform initial security checks
+ */
+ performSecurityChecks: function(request, response) {
+ // Check HTTPS requirement
+ if (this.config.requireHTTPS && !this.isHTTPS(request)) {
+ this.sendAuthError(response, 'HTTPS_REQUIRED', 'HTTPS connection required');
+ return false;
+ }
+
+ // Check for suspicious patterns
+ if (this.detectSuspiciousActivity(request)) {
+ this.sendAuthError(response, 'SUSPICIOUS_ACTIVITY', 'Request blocked due to suspicious activity');
+ return false;
+ }
+
+ // Rate limiting for authentication attempts
+ if (!this.checkAuthRateLimit(request)) {
+ this.sendAuthError(response, 'RATE_LIMITED', 'Too many authentication attempts', 429);
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Extract authentication information from request
+ */
+ extractAuthInfo: function(request) {
+ const authHeader = request.getHeader('Authorization');
+ const apiKeyHeader = request.getHeader('X-API-Key');
+ const sessionToken = request.getHeader('X-Session-Token');
+
+ if (authHeader) {
+ const parts = authHeader.split(' ');
+ if (parts.length === 2) {
+ return {
+ method: parts[0].toLowerCase(),
+ credentials: parts[1]
+ };
+ }
+ }
+
+ if (apiKeyHeader) {
+ return {
+ method: 'apikey',
+ credentials: apiKeyHeader
+ };
+ }
+
+ if (sessionToken) {
+ return {
+ method: 'session',
+ credentials: sessionToken
+ };
+ }
+
+ return null;
+ },
+
+ /**
+ * Authenticate user based on method
+ */
+ authenticateUser: function(authInfo) {
+ const methodHandler = this.authMethods[authInfo.method];
+ if (!methodHandler || !this[methodHandler]) {
+ return {
+ success: false,
+ error: 'UNSUPPORTED_AUTH_METHOD',
+ message: 'Unsupported authentication method: ' + authInfo.method
+ };
+ }
+
+ try {
+ return this[methodHandler](authInfo.credentials);
+ } catch (error) {
+ return {
+ success: false,
+ error: 'AUTH_PROCESSING_ERROR',
+ message: 'Error processing authentication: ' + error.message
+ };
+ }
+ },
+
+ /**
+ * Validate Bearer Token (JWT)
+ */
+ validateBearerToken: function(token) {
+ if (!this.config.enableJWT) {
+ return {
+ success: false,
+ error: 'JWT_DISABLED',
+ message: 'JWT authentication is disabled'
+ };
+ }
+
+ try {
+ // Decode and validate JWT
+ const decoded = this.decodeJWT(token);
+ if (!decoded) {
+ return {
+ success: false,
+ error: 'INVALID_TOKEN',
+ message: 'Invalid or expired token'
+ };
+ }
+
+ // Check token expiration
+ if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
+ return {
+ success: false,
+ error: 'TOKEN_EXPIRED',
+ message: 'Token has expired'
+ };
+ }
+
+ // Get user information
+ const user = this.getUserInfo(decoded.sub || decoded.user_id);
+ if (!user) {
+ return {
+ success: false,
+ error: 'USER_NOT_FOUND',
+ message: 'User not found or inactive'
+ };
+ }
+
+ return {
+ success: true,
+ user: user,
+ tokenData: decoded
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: 'TOKEN_VALIDATION_ERROR',
+ message: 'Error validating token: ' + error.message
+ };
+ }
+ },
+
+ /**
+ * Validate Basic Authentication
+ */
+ validateBasicAuth: function(credentials) {
+ try {
+ // Decode base64 credentials
+ const decoded = GlideStringUtil.base64Decode(credentials);
+ const parts = decoded.split(':');
+
+ if (parts.length !== 2) {
+ return {
+ success: false,
+ error: 'INVALID_CREDENTIALS_FORMAT',
+ message: 'Invalid credentials format'
+ };
+ }
+
+ const username = parts[0];
+ const password = parts[1];
+
+ // Check account lockout
+ if (this.isAccountLocked(username)) {
+ return {
+ success: false,
+ error: 'ACCOUNT_LOCKED',
+ message: 'Account is temporarily locked due to failed login attempts'
+ };
+ }
+
+ // Validate credentials
+ const user = this.validateUserCredentials(username, password);
+ if (!user) {
+ this.recordFailedLogin(username);
+ return {
+ success: false,
+ error: 'INVALID_CREDENTIALS',
+ message: 'Invalid username or password'
+ };
+ }
+
+ // Reset failed login attempts on successful login
+ this.clearFailedLogins(username);
+
+ return {
+ success: true,
+ user: user
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: 'BASIC_AUTH_ERROR',
+ message: 'Error processing basic authentication: ' + error.message
+ };
+ }
+ },
+
+ /**
+ * Validate API Key
+ */
+ validateAPIKey: function(apiKey) {
+ if (!this.config.enableAPIKey) {
+ return {
+ success: false,
+ error: 'API_KEY_DISABLED',
+ message: 'API Key authentication is disabled'
+ };
+ }
+
+ try {
+ // Look up API key in database
+ const keyRecord = this.getAPIKeyRecord(apiKey);
+ if (!keyRecord) {
+ return {
+ success: false,
+ error: 'INVALID_API_KEY',
+ message: 'Invalid API key'
+ };
+ }
+
+ // Check if key is active
+ if (!keyRecord.active) {
+ return {
+ success: false,
+ error: 'API_KEY_INACTIVE',
+ message: 'API key is inactive'
+ };
+ }
+
+ // Check expiration
+ if (keyRecord.expires_on && new GlideDateTime(keyRecord.expires_on).before(new GlideDateTime())) {
+ return {
+ success: false,
+ error: 'API_KEY_EXPIRED',
+ message: 'API key has expired'
+ };
+ }
+
+ // Update last used timestamp
+ this.updateAPIKeyUsage(keyRecord.sys_id);
+
+ // Get associated user
+ const user = this.getUserInfo(keyRecord.user_id);
+ if (!user) {
+ return {
+ success: false,
+ error: 'USER_NOT_FOUND',
+ message: 'Associated user not found or inactive'
+ };
+ }
+
+ return {
+ success: true,
+ user: user,
+ apiKeyRecord: keyRecord
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: 'API_KEY_VALIDATION_ERROR',
+ message: 'Error validating API key: ' + error.message
+ };
+ }
+ },
+
+ /**
+ * Validate OAuth2 Token
+ */
+ validateOAuth2Token: function(token) {
+ if (!this.config.enableOAuth2) {
+ return {
+ success: false,
+ error: 'OAUTH2_DISABLED',
+ message: 'OAuth2 authentication is disabled'
+ };
+ }
+
+ try {
+ // Validate token with OAuth2 provider
+ const tokenInfo = this.validateOAuth2TokenWithProvider(token);
+ if (!tokenInfo) {
+ return {
+ success: false,
+ error: 'INVALID_OAUTH_TOKEN',
+ message: 'Invalid OAuth2 token'
+ };
+ }
+
+ // Get user information from token
+ const user = this.getUserInfo(tokenInfo.user_id);
+ if (!user) {
+ return {
+ success: false,
+ error: 'USER_NOT_FOUND',
+ message: 'User not found or inactive'
+ };
+ }
+
+ return {
+ success: true,
+ user: user,
+ tokenInfo: tokenInfo
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: 'OAUTH2_VALIDATION_ERROR',
+ message: 'Error validating OAuth2 token: ' + error.message
+ };
+ }
+ },
+
+ /**
+ * Authorize request based on user permissions
+ */
+ authorizeRequest: function(request, user) {
+ try {
+ const resource = this.extractResource(request.pathInfo);
+ const action = this.mapMethodToAction(request.method);
+
+ // Check if resource has permission requirements
+ if (!this.permissions[resource]) {
+ // No specific permissions defined - allow if authenticated
+ return {
+ success: true,
+ permissions: ['authenticated']
+ };
+ }
+
+ // Get required roles for the action
+ const requiredRoles = this.permissions[resource][action];
+ if (!requiredRoles || requiredRoles.length === 0) {
+ return {
+ success: false,
+ error: 'ACTION_NOT_ALLOWED',
+ message: `Action '${action}' not allowed on resource '${resource}'`
+ };
+ }
+
+ // Check if user has any of the required roles
+ const userRoles = this.getUserRoles(user.sys_id);
+ const hasPermission = requiredRoles.some(role => userRoles.includes(role));
+
+ if (!hasPermission) {
+ return {
+ success: false,
+ error: 'INSUFFICIENT_PERMISSIONS',
+ message: `Insufficient permissions for action '${action}' on resource '${resource}'`
+ };
+ }
+
+ // Additional resource-level checks
+ if (!this.checkResourceLevelPermissions(request, user, resource, action)) {
+ return {
+ success: false,
+ error: 'RESOURCE_ACCESS_DENIED',
+ message: 'Access denied to specific resource instance'
+ };
+ }
+
+ return {
+ success: true,
+ permissions: requiredRoles.filter(role => userRoles.includes(role))
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: 'AUTHORIZATION_ERROR',
+ message: 'Error during authorization: ' + error.message
+ };
+ }
+ },
+
+ /**
+ * Check resource-level permissions
+ */
+ checkResourceLevelPermissions: function(request, user, resource, action) {
+ // Extract resource ID from path if present
+ const pathParts = request.pathInfo.split('/');
+ const resourceId = pathParts[pathParts.length - 1];
+
+ // If no specific resource ID, allow (list operations)
+ if (!resourceId || resourceId === resource) {
+ return true;
+ }
+
+ // Check ACLs for specific record access
+ return this.checkRecordACL(user, resource, resourceId, action);
+ },
+
+ /**
+ * Check record-level ACL
+ */
+ checkRecordACL: function(user, tableName, recordId, action) {
+ try {
+ // Use ServiceNow's built-in security to check record access
+ const gr = new GlideRecord(tableName);
+ if (gr.get(recordId)) {
+ // Check if user can read the record
+ if (action === 'read') {
+ return gr.canRead();
+ } else if (action === 'write') {
+ return gr.canWrite();
+ } else if (action === 'delete') {
+ return gr.canDelete();
+ }
+ }
+ return false;
+ } catch (error) {
+ gs.error('AuthFramework: Error checking record ACL: ' + error.message);
+ return false;
+ }
+ },
+
+ /**
+ * Utility methods
+ */
+
+ isHTTPS: function(request) {
+ const proto = request.getHeader('X-Forwarded-Proto') ||
+ request.getHeader('X-Forwarded-Protocol') ||
+ 'http';
+ return proto.toLowerCase() === 'https';
+ },
+
+ detectSuspiciousActivity: function(request) {
+ // Implement suspicious activity detection logic
+ const userAgent = request.getHeader('User-Agent') || '';
+ const suspiciousPatterns = ['bot', 'crawler', 'scan', 'hack'];
+
+ return suspiciousPatterns.some(pattern =>
+ userAgent.toLowerCase().includes(pattern));
+ },
+
+ checkAuthRateLimit: function(request) {
+ // Implement rate limiting for authentication attempts
+ const clientIP = this.getClientIP(request);
+ const key = 'auth_rate_limit_' + clientIP;
+
+ // Simple rate limiting - would be more sophisticated in production
+ const attempts = parseInt(gs.getProperty(key, '0'));
+ if (attempts >= this.config.maxLoginAttempts) {
+ return false;
+ }
+
+ return true;
+ },
+
+ getClientIP: function(request) {
+ return request.getHeader('X-Forwarded-For') ||
+ request.getHeader('X-Real-IP') ||
+ 'unknown';
+ },
+
+ decodeJWT: function(token) {
+ // Simplified JWT decoding - would use proper JWT library in production
+ try {
+ const parts = token.split('.');
+ if (parts.length !== 3) return null;
+
+ const payload = GlideStringUtil.base64Decode(parts[1]);
+ return JSON.parse(payload);
+ } catch (error) {
+ return null;
+ }
+ },
+
+ getUserInfo: function(userId) {
+ const user = new GlideRecord('sys_user');
+ if (user.get(userId) && user.active) {
+ return {
+ sys_id: user.getUniqueValue(),
+ user_name: user.getValue('user_name'),
+ email: user.getValue('email'),
+ first_name: user.getValue('first_name'),
+ last_name: user.getValue('last_name'),
+ active: user.getValue('active') === 'true'
+ };
+ }
+ return null;
+ },
+
+ getUserRoles: function(userId) {
+ const roles = [];
+ const gr = new GlideRecord('sys_user_has_role');
+ gr.addQuery('user', userId);
+ gr.addQuery('role.active', true);
+ gr.query();
+
+ while (gr.next()) {
+ const role = gr.getDisplayValue('role');
+ if (role) roles.push(role);
+ }
+
+ return roles;
+ },
+
+ validateUserCredentials: function(username, password) {
+ // This would integrate with ServiceNow's authentication system
+ // For security reasons, this is a simplified example
+ const user = new GlideRecord('sys_user');
+ user.addQuery('user_name', username);
+ user.addQuery('active', true);
+ user.query();
+
+ if (user.next()) {
+ // In real implementation, would validate password hash
+ return this.getUserInfo(user.getUniqueValue());
+ }
+
+ return null;
+ },
+
+ extractResource: function(pathInfo) {
+ const parts = pathInfo.split('/').filter(p => p.length > 0);
+ // Assuming format: /api/v1/resource or /resource
+ return parts[parts.length - 1] || parts[parts.length - 2] || 'unknown';
+ },
+
+ mapMethodToAction: function(method) {
+ const mapping = {
+ 'GET': 'read',
+ 'POST': 'write',
+ 'PUT': 'write',
+ 'PATCH': 'write',
+ 'DELETE': 'delete'
+ };
+ return mapping[method.toUpperCase()] || 'read';
+ },
+
+ sendAuthError: function(response, errorCode, message, statusCode) {
+ statusCode = statusCode || 401;
+
+ const errorResponse = {
+ error: {
+ code: errorCode,
+ message: message,
+ timestamp: new Date().toISOString()
+ }
+ };
+
+ response.setStatus(statusCode);
+ response.setHeader('WWW-Authenticate', 'Bearer realm="ServiceNow API"');
+ response.setBody(errorResponse);
+ },
+
+ logSecurityEvent: function(eventType, user, request, details) {
+ if (!this.config.enableAuditLogging) return;
+
+ const logEntry = {
+ event_type: eventType,
+ timestamp: new Date().toISOString(),
+ user_id: user ? user.sys_id : null,
+ user_name: user ? user.user_name : null,
+ client_ip: this.getClientIP(request),
+ user_agent: request.getHeader('User-Agent'),
+ method: request.method,
+ path: request.pathInfo,
+ details: details || {}
+ };
+
+ // Log to security audit table
+ this.writeSecurityAuditLog(logEntry);
+ },
+
+ writeSecurityAuditLog: function(logEntry) {
+ try {
+ // Would write to custom security audit table
+ gs.info('SecurityAudit: ' + JSON.stringify(logEntry));
+ } catch (error) {
+ gs.error('AuthFramework: Failed to write security audit log: ' + error.message);
+ }
+ }
+ };
+
+ // Process authentication and authorization
+ return AuthFramework.process(request, response);
+
+})(request, response);
diff --git a/Integration/Scripted REST APIs/Advanced API Patterns/data_transformation_pipeline.js b/Integration/Scripted REST APIs/Advanced API Patterns/data_transformation_pipeline.js
new file mode 100644
index 0000000000..4f5273d32f
--- /dev/null
+++ b/Integration/Scripted REST APIs/Advanced API Patterns/data_transformation_pipeline.js
@@ -0,0 +1,710 @@
+/**
+ * Data Transformation Pipeline for ServiceNow Scripted REST APIs
+ *
+ * Flexible data transformation framework with input/output mapping,
+ * schema validation, data sanitization, and batch processing capabilities.
+ *
+ * Features:
+ * - Flexible input/output data mapping
+ * - Schema validation and transformation
+ * - Data sanitization and normalization
+ * - Custom field processors
+ * - Batch processing capabilities
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
+ 'use strict';
+
+ /**
+ * Data Transformation Pipeline
+ */
+ const DataTransformationPipeline = {
+
+ // Configuration
+ config: {
+ enableValidation: true,
+ enableSanitization: true,
+ enableTransformation: true,
+ maxBatchSize: 1000,
+ enableFieldMapping: true,
+ strictMode: false,
+ preserveUnknownFields: false
+ },
+
+ // Field mapping configurations
+ fieldMappings: {
+ 'v1_to_internal': {
+ 'description': 'short_description',
+ 'reporter': 'caller_id',
+ 'category_name': 'category',
+ 'priority_level': 'priority',
+ 'created_date': 'sys_created_on',
+ 'updated_date': 'sys_updated_on'
+ },
+ 'internal_to_v1': {
+ 'short_description': 'description',
+ 'caller_id': 'reporter',
+ 'category': 'category_name',
+ 'priority': 'priority_level',
+ 'sys_created_on': 'created_date',
+ 'sys_updated_on': 'updated_date'
+ }
+ },
+
+ // Data type transformers
+ typeTransformers: {
+ 'string': {
+ sanitize: function(value) {
+ if (typeof value !== 'string') return String(value || '');
+ return value.trim().replace(/[<>\"'&]/g, '');
+ },
+ validate: function(value, constraints) {
+ if (constraints.maxLength && value.length > constraints.maxLength) {
+ throw new Error(`String too long: ${value.length} > ${constraints.maxLength}`);
+ }
+ if (constraints.pattern && !constraints.pattern.test(value)) {
+ throw new Error(`String does not match pattern: ${constraints.pattern}`);
+ }
+ return true;
+ }
+ },
+ 'number': {
+ sanitize: function(value) {
+ const num = parseFloat(value);
+ return isNaN(num) ? 0 : num;
+ },
+ validate: function(value, constraints) {
+ if (constraints.min !== undefined && value < constraints.min) {
+ throw new Error(`Number too small: ${value} < ${constraints.min}`);
+ }
+ if (constraints.max !== undefined && value > constraints.max) {
+ throw new Error(`Number too large: ${value} > ${constraints.max}`);
+ }
+ return true;
+ }
+ },
+ 'datetime': {
+ sanitize: function(value) {
+ if (!value) return '';
+ const date = new GlideDateTime(value);
+ return date.isValid() ? date.getValue() : '';
+ },
+ validate: function(value, constraints) {
+ const date = new GlideDateTime(value);
+ if (!date.isValid()) {
+ throw new Error(`Invalid datetime: ${value}`);
+ }
+ return true;
+ }
+ },
+ 'email': {
+ sanitize: function(value) {
+ if (typeof value !== 'string') return '';
+ return value.trim().toLowerCase();
+ },
+ validate: function(value, constraints) {
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailPattern.test(value)) {
+ throw new Error(`Invalid email format: ${value}`);
+ }
+ return true;
+ }
+ }
+ },
+
+ // Schema definitions
+ schemas: {
+ 'incident': {
+ 'short_description': { type: 'string', required: true, maxLength: 160 },
+ 'description': { type: 'string', maxLength: 4000 },
+ 'caller_id': { type: 'reference', table: 'sys_user', required: true },
+ 'category': { type: 'string', required: true },
+ 'priority': { type: 'number', min: 1, max: 5 },
+ 'state': { type: 'number', min: 1, max: 8 },
+ 'assigned_to': { type: 'reference', table: 'sys_user' },
+ 'assignment_group': { type: 'reference', table: 'sys_user_group' }
+ },
+ 'change_request': {
+ 'short_description': { type: 'string', required: true, maxLength: 160 },
+ 'description': { type: 'string', maxLength: 4000 },
+ 'requested_by': { type: 'reference', table: 'sys_user', required: true },
+ 'category': { type: 'string', required: true },
+ 'priority': { type: 'number', min: 1, max: 5 },
+ 'risk': { type: 'number', min: 1, max: 4 },
+ 'impact': { type: 'number', min: 1, max: 3 },
+ 'start_date': { type: 'datetime' },
+ 'end_date': { type: 'datetime' }
+ },
+ 'user': {
+ 'user_name': { type: 'string', required: true, maxLength: 40 },
+ 'first_name': { type: 'string', required: true, maxLength: 40 },
+ 'last_name': { type: 'string', required: true, maxLength: 40 },
+ 'email': { type: 'email', required: true },
+ 'phone': { type: 'string', pattern: /^\+?[\d\s\-\(\)\.]+$/ },
+ 'department': { type: 'reference', table: 'cmn_department' }
+ }
+ },
+
+ /**
+ * Main transformation pipeline processor
+ */
+ process: function(request, response) {
+ try {
+ const context = this.initializeContext(request, response);
+
+ // Process request based on method
+ if (request.method === 'GET') {
+ return this.processRead(context);
+ } else if (['POST', 'PUT', 'PATCH'].includes(request.method)) {
+ return this.processWrite(context);
+ } else {
+ this.sendError(context, 405, 'METHOD_NOT_ALLOWED', 'Method not supported');
+ return;
+ }
+
+ } catch (error) {
+ this.handleError(context || { response: response }, error);
+ }
+ },
+
+ /**
+ * Initialize transformation context
+ */
+ initializeContext: function(request, response) {
+ return {
+ request: request,
+ response: response,
+ tableName: this.extractTableName(request.pathInfo),
+ operation: this.mapMethodToOperation(request.method),
+ apiVersion: this.extractApiVersion(request),
+ requestData: this.parseRequestData(request),
+ transformationRules: [],
+ validationErrors: [],
+ transformedData: null
+ };
+ },
+
+ /**
+ * Process read operations (GET)
+ */
+ processRead: function(context) {
+ // Get data from ServiceNow
+ const rawData = this.fetchData(context);
+
+ // Transform for output
+ const transformedData = this.transformForOutput(context, rawData);
+
+ // Send response
+ this.sendSuccessResponse(context, transformedData);
+ },
+
+ /**
+ * Process write operations (POST, PUT, PATCH)
+ */
+ processWrite: function(context) {
+ // Validate input data
+ if (!this.validateInput(context)) {
+ return;
+ }
+
+ // Transform input data
+ const transformedData = this.transformForInput(context);
+ if (!transformedData) {
+ return;
+ }
+
+ // Process batch if applicable
+ if (Array.isArray(transformedData)) {
+ return this.processBatch(context, transformedData);
+ }
+
+ // Process single record
+ const result = this.processRecord(context, transformedData);
+
+ // Transform output
+ const outputData = this.transformForOutput(context, result);
+
+ // Send response
+ this.sendSuccessResponse(context, outputData);
+ },
+
+ /**
+ * Validate input data
+ */
+ validateInput: function(context) {
+ if (!this.config.enableValidation) return true;
+
+ const schema = this.schemas[context.tableName];
+ if (!schema) {
+ if (this.config.strictMode) {
+ this.sendError(context, 400, 'SCHEMA_NOT_FOUND',
+ 'No schema defined for table: ' + context.tableName);
+ return false;
+ }
+ return true; // Allow if no schema in non-strict mode
+ }
+
+ try {
+ if (Array.isArray(context.requestData)) {
+ // Validate each item in batch
+ for (let i = 0; i < context.requestData.length; i++) {
+ this.validateRecord(context.requestData[i], schema, `[${i}]`);
+ }
+ } else {
+ // Validate single record
+ this.validateRecord(context.requestData, schema);
+ }
+
+ // Check for validation errors
+ if (context.validationErrors.length > 0) {
+ this.sendValidationError(context);
+ return false;
+ }
+
+ return true;
+
+ } catch (error) {
+ this.sendError(context, 400, 'VALIDATION_ERROR',
+ 'Validation failed: ' + error.message);
+ return false;
+ }
+ },
+
+ /**
+ * Validate individual record
+ */
+ validateRecord: function(record, schema, prefix) {
+ prefix = prefix || '';
+
+ // Check required fields
+ Object.keys(schema).forEach(fieldName => {
+ const fieldSchema = schema[fieldName];
+ const value = record[fieldName];
+
+ // Required field check
+ if (fieldSchema.required && (value === undefined || value === null || value === '')) {
+ this.addValidationError(prefix + fieldName, 'Field is required');
+ return;
+ }
+
+ // Skip validation if field is not present and not required
+ if (value === undefined || value === null) return;
+
+ // Type-specific validation
+ try {
+ const transformer = this.typeTransformers[fieldSchema.type];
+ if (transformer && transformer.validate) {
+ transformer.validate(value, fieldSchema);
+ }
+ } catch (error) {
+ this.addValidationError(prefix + fieldName, error.message);
+ }
+ });
+
+ // Check for unknown fields in strict mode
+ if (this.config.strictMode && !this.config.preserveUnknownFields) {
+ Object.keys(record).forEach(fieldName => {
+ if (!schema[fieldName] && !fieldName.startsWith('_')) {
+ this.addValidationError(prefix + fieldName, 'Unknown field');
+ }
+ });
+ }
+ },
+
+ /**
+ * Add validation error
+ */
+ addValidationError: function(field, message) {
+ this.validationErrors = this.validationErrors || [];
+ this.validationErrors.push({
+ field: field,
+ message: message
+ });
+ },
+
+ /**
+ * Transform data for input (API to ServiceNow)
+ */
+ transformForInput: function(context) {
+ try {
+ if (Array.isArray(context.requestData)) {
+ return context.requestData.map(item => this.transformRecord(item, context, 'input'));
+ } else {
+ return this.transformRecord(context.requestData, context, 'input');
+ }
+ } catch (error) {
+ this.sendError(context, 400, 'TRANSFORMATION_ERROR',
+ 'Input transformation failed: ' + error.message);
+ return null;
+ }
+ },
+
+ /**
+ * Transform data for output (ServiceNow to API)
+ */
+ transformForOutput: function(context, data) {
+ try {
+ if (Array.isArray(data)) {
+ return data.map(item => this.transformRecord(item, context, 'output'));
+ } else {
+ return this.transformRecord(data, context, 'output');
+ }
+ } catch (error) {
+ gs.error('DataTransformationPipeline: Output transformation error: ' + error.message);
+ return data; // Return original data if transformation fails
+ }
+ },
+
+ /**
+ * Transform individual record
+ */
+ transformRecord: function(record, context, direction) {
+ if (!record || typeof record !== 'object') return record;
+
+ let transformed = {};
+
+ // Apply field mappings
+ if (this.config.enableFieldMapping) {
+ transformed = this.applyFieldMapping(record, context, direction);
+ } else {
+ transformed = Object.assign({}, record);
+ }
+
+ // Apply sanitization
+ if (this.config.enableSanitization) {
+ transformed = this.sanitizeRecord(transformed, context);
+ }
+
+ // Apply custom transformations
+ transformed = this.applyCustomTransformations(transformed, context, direction);
+
+ return transformed;
+ },
+
+ /**
+ * Apply field mapping
+ */
+ applyFieldMapping: function(record, context, direction) {
+ const mappingKey = direction === 'input' ?
+ context.apiVersion + '_to_internal' :
+ 'internal_to_' + context.apiVersion;
+
+ const mapping = this.fieldMappings[mappingKey];
+ if (!mapping) return record;
+
+ const transformed = {};
+
+ // Apply mappings
+ Object.keys(record).forEach(sourceField => {
+ const targetField = mapping[sourceField] || sourceField;
+ transformed[targetField] = record[sourceField];
+ });
+
+ // Preserve unmapped fields if configured
+ if (this.config.preserveUnknownFields) {
+ Object.keys(record).forEach(field => {
+ if (!mapping[field] && !transformed[field]) {
+ transformed[field] = record[field];
+ }
+ });
+ }
+
+ return transformed;
+ },
+
+ /**
+ * Sanitize record data
+ */
+ sanitizeRecord: function(record, context) {
+ const schema = this.schemas[context.tableName];
+ if (!schema) return record;
+
+ const sanitized = {};
+
+ Object.keys(record).forEach(fieldName => {
+ const value = record[fieldName];
+ const fieldSchema = schema[fieldName];
+
+ if (fieldSchema && this.typeTransformers[fieldSchema.type]) {
+ const transformer = this.typeTransformers[fieldSchema.type];
+ sanitized[fieldName] = transformer.sanitize ?
+ transformer.sanitize(value) : value;
+ } else {
+ sanitized[fieldName] = value;
+ }
+ });
+
+ return sanitized;
+ },
+
+ /**
+ * Apply custom transformations
+ */
+ applyCustomTransformations: function(record, context, direction) {
+ // Example custom transformations
+
+ // Add audit fields for input
+ if (direction === 'input') {
+ if (context.operation === 'create') {
+ record.sys_created_by = gs.getUserID();
+ record.sys_created_on = new GlideDateTime().getValue();
+ }
+ record.sys_updated_by = gs.getUserID();
+ record.sys_updated_on = new GlideDateTime().getValue();
+ }
+
+ // Format display values for output
+ if (direction === 'output') {
+ // Convert reference fields to display values
+ this.addDisplayValues(record, context);
+
+ // Format dates
+ this.formatDates(record);
+
+ // Add computed fields
+ this.addComputedFields(record, context);
+ }
+
+ return record;
+ },
+
+ /**
+ * Add display values for reference fields
+ */
+ addDisplayValues: function(record, context) {
+ const schema = this.schemas[context.tableName];
+ if (!schema) return;
+
+ Object.keys(schema).forEach(fieldName => {
+ const fieldSchema = schema[fieldName];
+ if (fieldSchema.type === 'reference' && record[fieldName]) {
+ // Add display value
+ const displayValue = this.getDisplayValue(fieldSchema.table, record[fieldName]);
+ if (displayValue) {
+ record[fieldName + '_display'] = displayValue;
+ }
+ }
+ });
+ },
+
+ /**
+ * Get display value for reference field
+ */
+ getDisplayValue: function(tableName, sysId) {
+ try {
+ const gr = new GlideRecord(tableName);
+ if (gr.get(sysId)) {
+ return gr.getDisplayValue();
+ }
+ } catch (error) {
+ gs.debug('DataTransformationPipeline: Error getting display value: ' + error.message);
+ }
+ return null;
+ },
+
+ /**
+ * Format date fields
+ */
+ formatDates: function(record) {
+ Object.keys(record).forEach(fieldName => {
+ const value = record[fieldName];
+ if (typeof value === 'string' && this.isDateField(fieldName)) {
+ const date = new GlideDateTime(value);
+ if (date.isValid()) {
+ record[fieldName + '_formatted'] = date.getDisplayValue();
+ }
+ }
+ });
+ },
+
+ /**
+ * Check if field is a date field
+ */
+ isDateField: function(fieldName) {
+ const dateFields = ['sys_created_on', 'sys_updated_on', 'start_date', 'end_date', 'due_date'];
+ return dateFields.includes(fieldName) || fieldName.includes('date') || fieldName.includes('time');
+ },
+
+ /**
+ * Add computed fields
+ */
+ addComputedFields: function(record, context) {
+ // Example computed fields
+ if (context.tableName === 'incident') {
+ // Add age in days
+ if (record.sys_created_on) {
+ const created = new GlideDateTime(record.sys_created_on);
+ const now = new GlideDateTime();
+ const diffInDays = gs.dateDiff(created.getValue(), now.getValue(), true) / (1000 * 60 * 60 * 24);
+ record.age_days = Math.floor(diffInDays);
+ }
+
+ // Add urgency indicator
+ if (record.priority && record.impact) {
+ record.urgency_indicator = this.calculateUrgency(record.priority, record.impact);
+ }
+ }
+ },
+
+ /**
+ * Process batch operations
+ */
+ processBatch: function(context, batchData) {
+ if (batchData.length > this.config.maxBatchSize) {
+ this.sendError(context, 400, 'BATCH_TOO_LARGE',
+ `Batch size ${batchData.length} exceeds maximum ${this.config.maxBatchSize}`);
+ return;
+ }
+
+ const results = [];
+ const errors = [];
+
+ batchData.forEach((record, index) => {
+ try {
+ const result = this.processRecord(context, record);
+ results.push(result);
+ } catch (error) {
+ errors.push({
+ index: index,
+ error: error.message,
+ record: record
+ });
+ }
+ });
+
+ const response = {
+ success: true,
+ processed: results.length,
+ errors: errors.length,
+ results: results
+ };
+
+ if (errors.length > 0) {
+ response.errors_detail = errors;
+ }
+
+ this.sendSuccessResponse(context, response);
+ },
+
+ /**
+ * Process individual record
+ */
+ processRecord: function(context, record) {
+ // This would interact with ServiceNow tables
+ // For demo purposes, returning mock result
+ return {
+ sys_id: gs.generateGUID(),
+ operation: context.operation,
+ table: context.tableName,
+ ...record,
+ sys_updated_on: new GlideDateTime().getValue()
+ };
+ },
+
+ /**
+ * Utility methods
+ */
+
+ extractTableName: function(pathInfo) {
+ const parts = pathInfo.split('/').filter(p => p.length > 0);
+ return parts[parts.length - 1] || 'unknown';
+ },
+
+ mapMethodToOperation: function(method) {
+ const mapping = {
+ 'POST': 'create',
+ 'PUT': 'update',
+ 'PATCH': 'update',
+ 'GET': 'read',
+ 'DELETE': 'delete'
+ };
+ return mapping[method.toUpperCase()] || 'unknown';
+ },
+
+ extractApiVersion: function(request) {
+ // Extract from path or default to v1
+ const pathParts = request.pathInfo.split('/');
+ for (let part of pathParts) {
+ if (part.match(/^v\d+$/)) {
+ return part;
+ }
+ }
+ return 'v1';
+ },
+
+ parseRequestData: function(request) {
+ if (!request.body || !request.body.dataString) {
+ return {};
+ }
+
+ try {
+ return JSON.parse(request.body.dataString);
+ } catch (error) {
+ throw new Error('Invalid JSON in request body');
+ }
+ },
+
+ fetchData: function(context) {
+ // Mock data fetching - would query actual ServiceNow tables
+ return {
+ sys_id: gs.generateGUID(),
+ short_description: 'Sample incident',
+ state: '1',
+ priority: '3',
+ sys_created_on: new GlideDateTime().getValue()
+ };
+ },
+
+ calculateUrgency: function(priority, impact) {
+ // Simple urgency calculation
+ const p = parseInt(priority) || 3;
+ const i = parseInt(impact) || 3;
+ return (p + i) <= 4 ? 'high' : 'normal';
+ },
+
+ sendSuccessResponse: function(context, data) {
+ context.response.setStatus(200);
+ context.response.setBody(data);
+ },
+
+ sendError: function(context, statusCode, errorCode, message) {
+ const errorResponse = {
+ error: {
+ code: errorCode,
+ message: message,
+ timestamp: new Date().toISOString()
+ }
+ };
+
+ context.response.setStatus(statusCode);
+ context.response.setBody(errorResponse);
+ },
+
+ sendValidationError: function(context) {
+ const errorResponse = {
+ error: {
+ code: 'VALIDATION_FAILED',
+ message: 'Input validation failed',
+ validation_errors: context.validationErrors,
+ timestamp: new Date().toISOString()
+ }
+ };
+
+ context.response.setStatus(400);
+ context.response.setBody(errorResponse);
+ },
+
+ handleError: function(context, error) {
+ gs.error('DataTransformationPipeline: ' + error.message);
+ this.sendError(context, 500, 'INTERNAL_ERROR', 'Internal processing error');
+ }
+ };
+
+ // Process the request through the transformation pipeline
+ DataTransformationPipeline.process(request, response);
+
+})(request, response);
diff --git a/Integration/Scripted REST APIs/Advanced API Patterns/error_handling_resilience.js b/Integration/Scripted REST APIs/Advanced API Patterns/error_handling_resilience.js
new file mode 100644
index 0000000000..b7312b4729
--- /dev/null
+++ b/Integration/Scripted REST APIs/Advanced API Patterns/error_handling_resilience.js
@@ -0,0 +1,776 @@
+/**
+ * Error Handling & Resilience Patterns for ServiceNow Scripted REST APIs
+ *
+ * Comprehensive error handling framework with circuit breaker implementation,
+ * retry mechanisms, graceful degradation, and health check capabilities.
+ *
+ * Features:
+ * - Comprehensive error response patterns
+ * - Circuit breaker implementation
+ * - Retry mechanisms with exponential backoff
+ * - Graceful degradation strategies
+ * - Health check endpoints
+ *
+ * @author ServiceNow Developer Community
+ * @version 1.0.0
+ * @requires ServiceNow Madrid+
+ */
+
+(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
+ 'use strict';
+
+ /**
+ * Error Handling & Resilience Framework
+ */
+ const ResilienceFramework = {
+
+ // Configuration
+ config: {
+ circuitBreaker: {
+ enabled: true,
+ failureThreshold: 5,
+ recoveryTimeout: 60000, // 1 minute
+ monitoringWindow: 300000 // 5 minutes
+ },
+ retry: {
+ enabled: true,
+ maxAttempts: 3,
+ baseDelay: 1000,
+ maxDelay: 10000,
+ exponentialBase: 2,
+ jitterPercent: 10
+ },
+ healthCheck: {
+ enabled: true,
+ checkInterval: 30000, // 30 seconds
+ dependencies: ['database', 'external_api', 'cache']
+ },
+ gracefulDegradation: {
+ enabled: true,
+ fallbackResponses: true,
+ cacheOnFailure: true
+ },
+ monitoring: {
+ enabled: true,
+ logErrors: true,
+ trackMetrics: true
+ }
+ },
+
+ // Circuit breaker states
+ circuitBreakerStates: new Map(),
+
+ // Health check status
+ healthStatus: {
+ overall: 'healthy',
+ dependencies: new Map(),
+ lastCheck: null
+ },
+
+ // Error categories and handling strategies
+ errorCategories: {
+ 'VALIDATION_ERROR': {
+ retryable: false,
+ statusCode: 400,
+ logLevel: 'warn',
+ userMessage: 'Invalid input data provided'
+ },
+ 'AUTHENTICATION_ERROR': {
+ retryable: false,
+ statusCode: 401,
+ logLevel: 'warn',
+ userMessage: 'Authentication required'
+ },
+ 'AUTHORIZATION_ERROR': {
+ retryable: false,
+ statusCode: 403,
+ logLevel: 'warn',
+ userMessage: 'Access denied'
+ },
+ 'NOT_FOUND_ERROR': {
+ retryable: false,
+ statusCode: 404,
+ logLevel: 'info',
+ userMessage: 'Resource not found'
+ },
+ 'RATE_LIMIT_ERROR': {
+ retryable: true,
+ statusCode: 429,
+ logLevel: 'warn',
+ userMessage: 'Rate limit exceeded'
+ },
+ 'DATABASE_ERROR': {
+ retryable: true,
+ statusCode: 503,
+ logLevel: 'error',
+ userMessage: 'Database temporarily unavailable'
+ },
+ 'EXTERNAL_API_ERROR': {
+ retryable: true,
+ statusCode: 502,
+ logLevel: 'error',
+ userMessage: 'External service unavailable'
+ },
+ 'TIMEOUT_ERROR': {
+ retryable: true,
+ statusCode: 504,
+ logLevel: 'error',
+ userMessage: 'Request timeout'
+ },
+ 'INTERNAL_ERROR': {
+ retryable: false,
+ statusCode: 500,
+ logLevel: 'error',
+ userMessage: 'Internal server error'
+ }
+ },
+
+ /**
+ * Main resilience processor
+ */
+ process: function(request, response) {
+ try {
+ // Initialize request context
+ const context = this.initializeContext(request, response);
+
+ // Check if this is a health check request
+ if (this.isHealthCheckRequest(request)) {
+ return this.handleHealthCheck(context);
+ }
+
+ // Execute with resilience patterns
+ this.executeWithResilience(context);
+
+ } catch (error) {
+ this.handleUnexpectedError(response, error);
+ }
+ },
+
+ /**
+ * Initialize request context
+ */
+ initializeContext: function(request, response) {
+ return {
+ request: request,
+ response: response,
+ requestId: this.generateRequestId(),
+ startTime: new Date(),
+ operation: this.extractOperation(request),
+ attempts: 0,
+ errors: [],
+ circuitBreakerKey: this.getCircuitBreakerKey(request)
+ };
+ },
+
+ /**
+ * Execute request with resilience patterns
+ */
+ executeWithResilience: function(context) {
+ // Check circuit breaker
+ if (!this.checkCircuitBreaker(context)) {
+ return;
+ }
+
+ // Execute with retry logic
+ this.executeWithRetry(context);
+ },
+
+ /**
+ * Execute with retry mechanism
+ */
+ executeWithRetry: function(context) {
+ const executeAttempt = () => {
+ context.attempts++;
+
+ try {
+ // Execute the actual business logic
+ const result = this.executeBusinessLogic(context);
+
+ // Success - reset circuit breaker
+ this.recordSuccess(context);
+
+ // Send successful response
+ this.sendSuccessResponse(context, result);
+
+ } catch (error) {
+ // Record failure
+ this.recordFailure(context, error);
+
+ // Determine if we should retry
+ if (this.shouldRetry(context, error)) {
+ const delay = this.calculateRetryDelay(context.attempts);
+
+ gs.info(`Retrying request ${context.requestId} in ${delay}ms (attempt ${context.attempts})`);
+
+ // Schedule retry
+ setTimeout(() => {
+ executeAttempt();
+ }, delay);
+ } else {
+ // No more retries - handle final error
+ this.handleFinalError(context, error);
+ }
+ }
+ };
+
+ // Start first attempt
+ executeAttempt();
+ },
+
+ /**
+ * Check circuit breaker status
+ */
+ checkCircuitBreaker: function(context) {
+ if (!this.config.circuitBreaker.enabled) return true;
+
+ const key = context.circuitBreakerKey;
+ const state = this.circuitBreakerStates.get(key) || {
+ state: 'closed',
+ failures: 0,
+ lastFailure: null,
+ nextAttempt: null
+ };
+
+ const now = Date.now();
+
+ switch (state.state) {
+ case 'closed':
+ // Normal operation
+ return true;
+
+ case 'open':
+ // Circuit is open - check if we can try again
+ if (now >= state.nextAttempt) {
+ // Move to half-open state
+ state.state = 'half-open';
+ this.circuitBreakerStates.set(key, state);
+ return true;
+ } else {
+ // Still in open state
+ this.sendCircuitBreakerError(context);
+ return false;
+ }
+
+ case 'half-open':
+ // Allow one request to test if service is recovered
+ return true;
+
+ default:
+ return true;
+ }
+ },
+
+ /**
+ * Record successful operation
+ */
+ recordSuccess: function(context) {
+ if (!this.config.circuitBreaker.enabled) return;
+
+ const key = context.circuitBreakerKey;
+ const state = this.circuitBreakerStates.get(key);
+
+ if (state) {
+ if (state.state === 'half-open') {
+ // Reset circuit breaker on successful half-open attempt
+ state.state = 'closed';
+ state.failures = 0;
+ state.lastFailure = null;
+ state.nextAttempt = null;
+ this.circuitBreakerStates.set(key, state);
+ }
+ }
+ },
+
+ /**
+ * Record failed operation
+ */
+ recordFailure: function(context, error) {
+ context.errors.push({
+ attempt: context.attempts,
+ error: error.message,
+ timestamp: new Date(),
+ category: this.categorizeError(error)
+ });
+
+ // Update circuit breaker
+ this.updateCircuitBreaker(context, error);
+
+ // Log error
+ this.logError(context, error);
+ },
+
+ /**
+ * Update circuit breaker state
+ */
+ updateCircuitBreaker: function(context, error) {
+ if (!this.config.circuitBreaker.enabled) return;
+
+ const errorCategory = this.categorizeError(error);
+
+ // Only count certain types of errors towards circuit breaker
+ if (!this.shouldCountForCircuitBreaker(errorCategory)) return;
+
+ const key = context.circuitBreakerKey;
+ const state = this.circuitBreakerStates.get(key) || {
+ state: 'closed',
+ failures: 0,
+ lastFailure: null,
+ nextAttempt: null
+ };
+
+ state.failures++;
+ state.lastFailure = Date.now();
+
+ // Check if we should open the circuit
+ if (state.failures >= this.config.circuitBreaker.failureThreshold) {
+ state.state = 'open';
+ state.nextAttempt = Date.now() + this.config.circuitBreaker.recoveryTimeout;
+
+ gs.warn(`Circuit breaker opened for ${key} after ${state.failures} failures`);
+ }
+
+ this.circuitBreakerStates.set(key, state);
+ },
+
+ /**
+ * Determine if error should be retried
+ */
+ shouldRetry: function(context, error) {
+ if (!this.config.retry.enabled) return false;
+
+ // Check max attempts
+ if (context.attempts >= this.config.retry.maxAttempts) return false;
+
+ // Check if error type is retryable
+ const errorCategory = this.categorizeError(error);
+ const categoryConfig = this.errorCategories[errorCategory];
+
+ return categoryConfig ? categoryConfig.retryable : false;
+ },
+
+ /**
+ * Calculate retry delay with exponential backoff and jitter
+ */
+ calculateRetryDelay: function(attempt) {
+ const baseDelay = this.config.retry.baseDelay;
+ const exponentialDelay = baseDelay * Math.pow(this.config.retry.exponentialBase, attempt - 1);
+ const cappedDelay = Math.min(exponentialDelay, this.config.retry.maxDelay);
+
+ // Add jitter to avoid thundering herd
+ const jitterRange = cappedDelay * (this.config.retry.jitterPercent / 100);
+ const jitter = (Math.random() * 2 - 1) * jitterRange;
+
+ return Math.max(100, cappedDelay + jitter); // Minimum 100ms delay
+ },
+
+ /**
+ * Execute business logic (placeholder)
+ */
+ executeBusinessLogic: function(context) {
+ // This would contain the actual API business logic
+ // For demo purposes, we'll simulate different scenarios
+
+ const operation = context.operation;
+ const random = Math.random();
+
+ // Simulate different failure scenarios for testing
+ if (random < 0.1) {
+ throw new Error('DATABASE_ERROR: Connection timeout');
+ } else if (random < 0.15) {
+ throw new Error('EXTERNAL_API_ERROR: Service unavailable');
+ } else if (random < 0.2) {
+ throw new Error('TIMEOUT_ERROR: Request timeout');
+ }
+
+ // Simulate successful operation
+ return {
+ success: true,
+ operation: operation,
+ requestId: context.requestId,
+ timestamp: new Date().toISOString(),
+ data: { message: 'Operation completed successfully' }
+ };
+ },
+
+ /**
+ * Handle final error (no more retries)
+ */
+ handleFinalError: function(context, error) {
+ const errorCategory = this.categorizeError(error);
+
+ // Try graceful degradation
+ if (this.config.gracefulDegradation.enabled) {
+ const fallbackResult = this.tryGracefulDegradation(context, error);
+ if (fallbackResult) {
+ this.sendDegradedResponse(context, fallbackResult);
+ return;
+ }
+ }
+
+ // Send error response
+ this.sendErrorResponse(context, error, errorCategory);
+ },
+
+ /**
+ * Try graceful degradation
+ */
+ tryGracefulDegradation: function(context, error) {
+ const operation = context.operation;
+
+ // Example degradation strategies
+ switch (operation) {
+ case 'get_user_profile':
+ // Return cached profile or basic info
+ return this.getCachedUserProfile(context);
+
+ case 'search_incidents':
+ // Return recent incidents from cache
+ return this.getCachedIncidents(context);
+
+ case 'get_catalog_items':
+ // Return popular items from cache
+ return this.getCachedCatalogItems(context);
+
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Handle health check requests
+ */
+ handleHealthCheck: function(context) {
+ if (this.config.healthCheck.enabled) {
+ const healthStatus = this.performHealthCheck();
+
+ const statusCode = healthStatus.overall === 'healthy' ? 200 : 503;
+ context.response.setStatus(statusCode);
+ context.response.setBody(healthStatus);
+ } else {
+ context.response.setStatus(200);
+ context.response.setBody({ status: 'ok', timestamp: new Date().toISOString() });
+ }
+ },
+
+ /**
+ * Perform comprehensive health check
+ */
+ performHealthCheck: function() {
+ const now = new Date();
+ const checks = {};
+ let overallHealthy = true;
+
+ // Check each dependency
+ this.config.healthCheck.dependencies.forEach(dependency => {
+ try {
+ const status = this.checkDependencyHealth(dependency);
+ checks[dependency] = status;
+
+ if (!status.healthy) {
+ overallHealthy = false;
+ }
+ } catch (error) {
+ checks[dependency] = {
+ healthy: false,
+ error: error.message,
+ timestamp: now.toISOString()
+ };
+ overallHealthy = false;
+ }
+ });
+
+ // Update health status
+ this.healthStatus = {
+ overall: overallHealthy ? 'healthy' : 'unhealthy',
+ dependencies: checks,
+ lastCheck: now.toISOString(),
+ uptime: this.getUptime(),
+ version: '1.0.0'
+ };
+
+ return this.healthStatus;
+ },
+
+ /**
+ * Check individual dependency health
+ */
+ checkDependencyHealth: function(dependency) {
+ const checkStart = Date.now();
+
+ try {
+ switch (dependency) {
+ case 'database':
+ return this.checkDatabaseHealth(checkStart);
+ case 'external_api':
+ return this.checkExternalAPIHealth(checkStart);
+ case 'cache':
+ return this.checkCacheHealth(checkStart);
+ default:
+ return { healthy: true, message: 'Unknown dependency' };
+ }
+ } catch (error) {
+ return {
+ healthy: false,
+ error: error.message,
+ responseTime: Date.now() - checkStart
+ };
+ }
+ },
+
+ /**
+ * Check database health
+ */
+ checkDatabaseHealth: function(startTime) {
+ try {
+ // Perform a simple database query
+ const gr = new GlideRecord('sys_properties');
+ gr.addQuery('name', 'instance.name');
+ gr.setLimit(1);
+ gr.query();
+
+ const responseTime = Date.now() - startTime;
+
+ return {
+ healthy: true,
+ responseTime: responseTime,
+ message: 'Database connection successful'
+ };
+ } catch (error) {
+ return {
+ healthy: false,
+ error: error.message,
+ responseTime: Date.now() - startTime
+ };
+ }
+ },
+
+ /**
+ * Check external API health
+ */
+ checkExternalAPIHealth: function(startTime) {
+ // Simplified external API check
+ const responseTime = Date.now() - startTime;
+
+ // In real implementation, would make actual external API call
+ return {
+ healthy: true,
+ responseTime: responseTime,
+ message: 'External API accessible'
+ };
+ },
+
+ /**
+ * Check cache health
+ */
+ checkCacheHealth: function(startTime) {
+ const responseTime = Date.now() - startTime;
+
+ // In real implementation, would test cache operations
+ return {
+ healthy: true,
+ responseTime: responseTime,
+ message: 'Cache operational'
+ };
+ },
+
+ /**
+ * Utility methods
+ */
+
+ categorizeError: function(error) {
+ const message = error.message || error.toString();
+
+ // Match error patterns to categories
+ if (message.includes('VALIDATION_ERROR') || message.includes('Invalid')) {
+ return 'VALIDATION_ERROR';
+ } else if (message.includes('AUTH') || message.includes('Unauthorized')) {
+ return 'AUTHENTICATION_ERROR';
+ } else if (message.includes('FORBIDDEN') || message.includes('Access denied')) {
+ return 'AUTHORIZATION_ERROR';
+ } else if (message.includes('NOT_FOUND') || message.includes('not found')) {
+ return 'NOT_FOUND_ERROR';
+ } else if (message.includes('RATE_LIMIT') || message.includes('Too many')) {
+ return 'RATE_LIMIT_ERROR';
+ } else if (message.includes('DATABASE_ERROR') || message.includes('Connection')) {
+ return 'DATABASE_ERROR';
+ } else if (message.includes('EXTERNAL_API_ERROR') || message.includes('Service unavailable')) {
+ return 'EXTERNAL_API_ERROR';
+ } else if (message.includes('TIMEOUT_ERROR') || message.includes('timeout')) {
+ return 'TIMEOUT_ERROR';
+ } else {
+ return 'INTERNAL_ERROR';
+ }
+ },
+
+ shouldCountForCircuitBreaker: function(errorCategory) {
+ // Only count service-level errors, not client errors
+ const serviceErrors = ['DATABASE_ERROR', 'EXTERNAL_API_ERROR', 'TIMEOUT_ERROR', 'INTERNAL_ERROR'];
+ return serviceErrors.includes(errorCategory);
+ },
+
+ isHealthCheckRequest: function(request) {
+ return request.pathInfo.includes('/health') || request.pathInfo.includes('/status');
+ },
+
+ extractOperation: function(request) {
+ // Extract operation from path
+ const pathParts = request.pathInfo.split('/').filter(p => p.length > 0);
+ return pathParts[pathParts.length - 1] || 'unknown';
+ },
+
+ getCircuitBreakerKey: function(request) {
+ // Create a key for circuit breaker based on operation
+ const operation = this.extractOperation(request);
+ return `circuit_breaker_${operation}`;
+ },
+
+ generateRequestId: function() {
+ return 'req_' + gs.generateGUID();
+ },
+
+ getUptime: function() {
+ // Simplified uptime calculation
+ const startTime = gs.getProperty('system.started', new Date().getTime());
+ return Date.now() - parseInt(startTime);
+ },
+
+ // Response methods
+
+ sendSuccessResponse: function(context, result) {
+ context.response.setStatus(200);
+ context.response.setHeader('X-Request-ID', context.requestId);
+ context.response.setBody(result);
+
+ this.logSuccess(context);
+ },
+
+ sendErrorResponse: function(context, error, errorCategory) {
+ const categoryConfig = this.errorCategories[errorCategory] || this.errorCategories['INTERNAL_ERROR'];
+
+ const errorResponse = {
+ error: {
+ code: errorCategory,
+ message: categoryConfig.userMessage,
+ requestId: context.requestId,
+ timestamp: new Date().toISOString(),
+ attempts: context.attempts
+ }
+ };
+
+ // Add retry information if applicable
+ if (categoryConfig.retryable && context.attempts >= this.config.retry.maxAttempts) {
+ errorResponse.error.retryAfter = this.calculateRetryDelay(1);
+ }
+
+ context.response.setStatus(categoryConfig.statusCode);
+ context.response.setHeader('X-Request-ID', context.requestId);
+ context.response.setBody(errorResponse);
+ },
+
+ sendCircuitBreakerError: function(context) {
+ const errorResponse = {
+ error: {
+ code: 'SERVICE_UNAVAILABLE',
+ message: 'Service temporarily unavailable due to circuit breaker',
+ requestId: context.requestId,
+ timestamp: new Date().toISOString()
+ }
+ };
+
+ context.response.setStatus(503);
+ context.response.setHeader('X-Request-ID', context.requestId);
+ context.response.setBody(errorResponse);
+ },
+
+ sendDegradedResponse: function(context, fallbackResult) {
+ fallbackResult.degraded = true;
+ fallbackResult.requestId = context.requestId;
+
+ context.response.setStatus(200);
+ context.response.setHeader('X-Request-ID', context.requestId);
+ context.response.setHeader('X-Service-Degraded', 'true');
+ context.response.setBody(fallbackResult);
+ },
+
+ handleUnexpectedError: function(response, error) {
+ gs.error('ResilienceFramework: Unexpected error: ' + error.message);
+
+ const errorResponse = {
+ error: {
+ code: 'UNEXPECTED_ERROR',
+ message: 'An unexpected error occurred',
+ timestamp: new Date().toISOString()
+ }
+ };
+
+ response.setStatus(500);
+ response.setBody(errorResponse);
+ },
+
+ // Logging methods
+
+ logError: function(context, error) {
+ if (!this.config.monitoring.logErrors) return;
+
+ const logEntry = {
+ requestId: context.requestId,
+ operation: context.operation,
+ attempt: context.attempts,
+ error: error.message,
+ category: this.categorizeError(error),
+ timestamp: new Date().toISOString()
+ };
+
+ gs.error('ResilienceFramework: ' + JSON.stringify(logEntry));
+ },
+
+ logSuccess: function(context) {
+ if (!this.config.monitoring.enabled) return;
+
+ const duration = Date.now() - context.startTime.getTime();
+
+ const logEntry = {
+ requestId: context.requestId,
+ operation: context.operation,
+ attempts: context.attempts,
+ duration: duration,
+ timestamp: new Date().toISOString()
+ };
+
+ gs.info('ResilienceFramework: Success - ' + JSON.stringify(logEntry));
+ },
+
+ // Graceful degradation helpers (mock implementations)
+
+ getCachedUserProfile: function(context) {
+ return {
+ success: true,
+ data: { name: 'Cached User', email: 'user@example.com' },
+ source: 'cache',
+ timestamp: new Date().toISOString()
+ };
+ },
+
+ getCachedIncidents: function(context) {
+ return {
+ success: true,
+ data: [{ number: 'INC0000001', description: 'Cached incident' }],
+ source: 'cache',
+ timestamp: new Date().toISOString()
+ };
+ },
+
+ getCachedCatalogItems: function(context) {
+ return {
+ success: true,
+ data: [{ name: 'Popular Item', category: 'Hardware' }],
+ source: 'cache',
+ timestamp: new Date().toISOString()
+ };
+ }
+ };
+
+ // Process the request through the resilience framework
+ ResilienceFramework.process(request, response);
+
+})(request, response);