mcpField
S
script-includes

script-includes

github.com/happy-technologies-llc開発4143 回インストール2 日前に更新
リポジトリ

script-includes について

中級から上級の ServiceNow 開発者向けに、再利用・テスト可能なサーバー側 Script Include の設計を解説するスキルです。クラス構造、GlideAjax、継承、スコープ、設計パターン、ATF テストを扱います。

主な機能

  1. 01クラスベースの Script Include 設計
  2. 02GlideAjax によるクライアント・サーバー通信
  3. 03クラスの拡張と継承
  4. 04スコープとグローバルアクセス制御
  5. 05Singleton、Factory、Strategy パターン
  6. 06ATF によるユニットテスト

活用シーン

  1. 01ビジネスルールやワークフロー間でロジックを共有
  2. 02クライアントスクリプトにサーバーメソッドを公開
  3. 03プラットフォームクラスや独自クラスを拡張
  4. 04Script Include とビジネスルールの使い分け

MCP 対応

ServiceNow で Script Include レコードを作成する際に、SN-Create-Record などの MCP ツールを使う手順を示します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/happy-technologies-llc/happy-platform-skills --skill script-includes

インストール後、Claude Code で/script-includesを入力して読み込まれたか確認します。

詳細

スクリプトインクルードの開発

概要

スクリプトインクルードは、ServiceNow における再利用可能なサーバーサイドコードの基盤です。このスキルでは、以下を扱います。

  • クラスベースのスクリプトインクルードアーキテクチャ
  • クライアント呼び出し可能なスクリプトインクルード(GlideAjax パターン)
  • 既存クラスの拡張と継承
  • プロトタイプパターンとオブジェクト指向設計
  • ユーティリティライブラリとヘルパー関数
  • ビジネスルールとスクリプトインクルードの使い分け
  • スコープとグローバルの考慮事項
  • デザインパターン(シングルトン、ファクトリー、ストラテジー)
  • ATF を使用したスクリプトインクルードのユニットテスト

スクリプトインクルードを使用する場合:

  • 複数のビジネスルール、ワークフロー、または UI アクションで必要な再利用可能なロジック
  • ユニットテストが必要な複雑なビジネスロジック
  • クライアントとサーバー間の通信(GlideAjax)
  • 標準機能の拡張またはオーバーライド
  • サービスレイヤーと API の作成

対象ユーザー: 保守性、テスト容易性、再利用性に優れたコードを開発する中級から上級の ServiceNow 開発者です。

前提条件

  • ロール: admin、scriptinclude_create、またはスコープ付きアプリの開発者
  • 知識:
    • JavaScript の基礎(オブジェクト、プロトタイプ、クロージャ)
    • ServiceNow スクリプトの基礎(GlideRecord、gs)
    • OOP の概念に関する基本的な理解
  • アクセス: sys_script_include テーブル
  • 関連スキル:
    • admin/script-sync - ローカル開発ワークフロー
    • admin/update-set-management - 変更の追跡
    • admin/application-scope - スコープ付き開発

テーブルアーキテクチャ

sys_script_include スキーマ
Tool: SN-Get-Table-Schema
Parameters:
  table_name: sys_script_include

主なフィールド:

フィールド型用途
name文字列クラス名(クラス定義と一致する必要があります)
api_name文字列完全な API 名(scope.ClassName)
scriptスクリプトJavaScript クラス定義
client_callableブール値GlideAjax 経由で公開
activeブール値有効化/無効化
access選択肢public、package_private、private
sys_scope参照アプリケーションスコープ
description文字列ドキュメント

手順

フェーズ 1:クラスベースの基本的なスクリプトインクルード
ステップ 1.1:基本的なスクリプトインクルードを作成する

MCP を使用:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "IncidentUtils"
    api_name: "global.IncidentUtils"
    description: "Utility methods for incident management"
    active: true
    client_callable: false
    access: "public"
    script: |
      var IncidentUtils = Class.create();
      IncidentUtils.prototype = {
          initialize: function() {
              // Constructor - called when new instance created
          },

          /**
           * Calculate incident priority based on impact and urgency
           * @param {GlideRecord} incidentGR - Incident GlideRecord
           * @returns {Number} Calculated priority (1-5)
           */
          calculatePriority: function(incidentGR) {
              var impact = parseInt(incidentGR.impact);
              var urgency = parseInt(incidentGR.urgency);

              // Priority matrix: Lower number = higher priority
              var priorityMatrix = {
                  '1-1': 1, '1-2': 2, '1-3': 3,
                  '2-1': 2, '2-2': 3, '2-3': 4,
                  '3-1': 3, '3-2': 4, '3-3': 5
              };

              var key = impact + '-' + urgency;
              return priorityMatrix[key] || 4;
          },

          /**
           * Check if incident is a P1 (Critical)
           * @param {GlideRecord} incidentGR - Incident GlideRecord
           * @returns {Boolean} True if P1
           */
          isCritical: function(incidentGR) {
              return incidentGR.priority == 1;
          },

          /**
           * Get assignment group based on category
           * @param {String} category - Incident category
           * @returns {String} sys_id of assignment group
           */
          getAssignmentGroup: function(category) {
              var groupMapping = {
                  'network': 'Network Support',
                  'hardware': 'Hardware Support',
                  'software': 'Software Support',
                  'database': 'Database Team'
              };

              var groupName = groupMapping[category] || 'Service Desk';

              var gr = new GlideRecord('sys_user_group');
              gr.addQuery('name', groupName);
              gr.query();

              if (gr.next()) {
                  return gr.sys_id.toString();
              }
              return '';
          },

          type: 'IncidentUtils'
      };
ステップ 1.2:クラス構造を理解する

Class.create() パターン:

var ClassName = Class.create();  // Create class constructor
ClassName.prototype = {          // Define prototype (methods)

    initialize: function(param1, param2) {
        // Constructor - runs when 'new ClassName(p1, p2)' called
        this.param1 = param1;
        this.param2 = param2;
    },

    methodOne: function() {
        // Instance method - has access to 'this'
        return this.param1;
    },

    methodTwo: function(arg) {
        // Another instance method
        return arg + this.param2;
    },

    type: 'ClassName'  // Required: Must match class name
};

ビジネスルールまたはその他のスクリプトでの使用:

// Create instance
var utils = new IncidentUtils();

// Call methods
var priority = utils.calculatePriority(current);
var isCritical = utils.isCritical(current);
var groupId = utils.getAssignmentGroup(current.category);
ステップ 1.3:コンストラクターのパラメーター

コンストラクターを含むスクリプトインクルード:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "IncidentHandler"
    description: "Handles incident operations with configurable options"
    active: true
    client_callable: false
    script: |
      var IncidentHandler = Class.create();
      IncidentHandler.prototype = {
          /**
           * Initialize handler with incident record
           * @param {GlideRecord} incidentGR - The incident to handle
           * @param {Object} options - Configuration options
           */
          initialize: function(incidentGR, options) {
              this.incident = incidentGR;
              this.options = options || {};
              this.log = new GSLog('com.company.incident', 'IncidentHandler');

              // Default options
              this.autoAssign = this.options.autoAssign !== false;
              this.sendNotifications = this.options.sendNotifications !== false;
          },

          /**
           * Process the incident based on its state
           * @returns {Boolean} Success status
           */
          process: function() {
              if (!this.incident || !this.incident.isValidRecord()) {
                  this.log.error('Invalid incident record');
                  return false;
              }

              if (this.autoAssign && this._needsAssignment()) {
                  this._assignToGroup();
              }

              if (this.sendNotifications) {
                  this._notifyStakeholders();
              }

              return true;
          },

          // Private method (convention: prefix with underscore)
          _needsAssignment: function() {
              return this.incident.assignment_group.nil();
          },

          _assignToGroup: function() {
              var utils = new IncidentUtils();
              var groupId = utils.getAssignmentGroup(this.incident.category);
              if (groupId) {
                  this.incident.assignment_group = groupId;
              }
          },

          _notifyStakeholders: function() {
              // Notification logic
              gs.eventQueue('incident.processed', this.incident);
          },

          type: 'IncidentHandler'
      };

使用方法:

// With options
var handler = new IncidentHandler(current, {
    autoAssign: true,
    sendNotifications: false
});
handler.process();

// Default options
var handler2 = new IncidentHandler(current);
handler2.process();
フェーズ 2:クライアント呼び出し可能なスクリプトインクルード(GlideAjax)
ステップ 2.1:クライアント呼び出し可能なスクリプトインクルードを作成する

クライアント呼び出し可能なスクリプトインクルードを使用すると、クライアントスクリプトとサーバー間で通信できます。

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "IncidentAjax"
    description: "AJAX methods for incident client scripts"
    active: true
    client_callable: true
    access: "public"
    script: |
      var IncidentAjax = Class.create();
      IncidentAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

          /**
           * Get incident details for client display
           * Called from client: new GlideAjax('IncidentAjax').addParam('sysparm_name', 'getIncidentDetails')
           */
          getIncidentDetails: function() {
              var incidentId = this.getParameter('sysparm_incident_id');
              var result = {};

              var gr = new GlideRecord('incident');
              if (gr.get(incidentId)) {
                  result.number = gr.number.toString();
                  result.short_description = gr.short_description.toString();
                  result.priority = gr.priority.toString();
                  result.priority_display = gr.priority.getDisplayValue();
                  result.state = gr.state.toString();
                  result.state_display = gr.state.getDisplayValue();
                  result.assigned_to = gr.assigned_to.getDisplayValue();
                  result.assignment_group = gr.assignment_group.getDisplayValue();
              }

              return JSON.stringify(result);
          },

          /**
           * Validate if user can close incident
           * Security check performed server-side
           */
          canCloseIncident: function() {
              var incidentId = this.getParameter('sysparm_incident_id');

              var gr = new GlideRecord('incident');
              if (!gr.get(incidentId)) {
                  return 'false';
              }

              // Check if current user can close
              if (!gr.canWrite()) {
                  return 'false';
              }

              // Check incident state allows closure
              if (gr.state == 6 || gr.state == 7) {  // Already closed/cancelled
                  return 'false';
              }

              // Check if required fields are filled
              if (gr.resolution_code.nil() || gr.close_notes.nil()) {
                  return 'false';
              }

              return 'true';
          },

          /**
           * Get related incidents count
           * Used for UI display without full query
           */
          getRelatedIncidentsCount: function() {
              var callerId = this.getParameter('sysparm_caller_id');
              var excludeId = this.getParameter('sysparm_exclude_id');

              var ga = new GlideAggregate('incident');
              ga.addQuery('caller_id', callerId);
              ga.addQuery('active', true);
              if (excludeId) {
                  ga.addQuery('sys_id', '!=', excludeId);
              }
              ga.addAggregate('COUNT');
              ga.query();

              if (ga.next()) {
                  return ga.getAggregate('COUNT');
              }
              return '0';
          },

          /**
           * Get assignment groups for category (for dropdown)
           */
          getAssignmentGroups: function() {
              var category = this.getParameter('sysparm_category');
              var groups = [];

              // Get groups based on category (simplified example)
              var gr = new GlideRecord('sys_user_group');
              gr.addQuery('active', true);
              gr.addQuery('type', '!=', '');  // Has type defined
              gr.orderBy('name');
              gr.setLimit(50);
              gr.query();

              while (gr.next()) {
                  groups.push({
                      sys_id: gr.sys_id.toString(),
                      name: gr.name.toString()
                  });
              }

              return JSON.stringify(groups);
          },

          /**
           * SECURITY: Define which methods are callable
           * Methods NOT listed here cannot be called from client
           */
          isPublic: function() {
              return true;  // All methods in this class are public
          },

          type: 'IncidentAjax'
      });
ステップ 2.2:GlideAjax を呼び出すクライアントスクリプト

クライアントスクリプト(同期 — 本番環境では避けてください):

// NOT RECOMMENDED - blocks UI
function getIncidentSync(incidentId) {
    var ga = new GlideAjax('IncidentAjax');
    ga.addParam('sysparm_name', 'getIncidentDetails');
    ga.addParam('sysparm_incident_id', incidentId);
    ga.getXMLWait();  // SYNCHRONOUS - blocks browser

    var answer = ga.getAnswer();
    return JSON.parse(answer);
}

クライアントスクリプト(非同期 — 推奨):

// RECOMMENDED - non-blocking
function getIncidentAsync(incidentId, callback) {
    var ga = new GlideAjax('IncidentAjax');
    ga.addParam('sysparm_name', 'getIncidentDetails');
    ga.addParam('sysparm_incident_id', incidentId);
    ga.getXMLAnswer(function(answer) {
        var result = JSON.parse(answer);
        callback(result);
    });
}

// Usage in client script
getIncidentAsync(g_form.getValue('sys_id'), function(incident) {
    console.log('Incident: ' + incident.number);
    g_form.setValue('work_notes', 'Related: ' + incident.number);
});

エラー処理を含むクライアントスクリプト:

function getIncidentWithErrorHandling(incidentId) {
    var ga = new GlideAjax('IncidentAjax');
    ga.addParam('sysparm_name', 'getIncidentDetails');
    ga.addParam('sysparm_incident_id', incidentId);

    ga.getXML(function(response) {
        // Check for errors
        var answer = response.responseXML.documentElement.getAttribute('answer');

        if (!answer) {
            g_form.addErrorMessage('Error retrieving incident details');
            return;
        }

        try {
            var result = JSON.parse(answer);
            // Process result
            console.log('Got incident: ', result);
        } catch (e) {
            g_form.addErrorMessage('Error parsing response: ' + e.message);
        }
    });
}
ステップ 2.3:クライアント呼び出し可能なメソッドのセキュリティ保護

メソッドレベルのセキュリティ:

var SecureIncidentAjax = Class.create();
SecureIncidentAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    /**
     * Public method - callable by any user
     */
    getPublicData: function() {
        return JSON.stringify({ status: 'ok' });
    },

    /**
     * Restricted method - requires specific role
     */
    getAdminData: function() {
        // Check role before processing
        if (!gs.hasRole('admin')) {
            return JSON.stringify({ error: 'Access denied' });
        }

        // Process admin-only request
        return JSON.stringify({ adminData: 'sensitive info' });
    },

    /**
     * Method requiring record-level access
     */
    updateIncident: function() {
        var incidentId = this.getParameter('sysparm_incident_id');
        var newValue = this.getParameter('sysparm_value');

        var gr = new GlideRecord('incident');
        if (!gr.get(incidentId)) {
            return JSON.stringify({ error: 'Not found' });
        }

        // Check ACL - does user have write access?
        if (!gr.canWrite()) {
            return JSON.stringify({ error: 'Write access denied' });
        }

        // Safe to update
        gr.short_description = newValue;
        gr.update();

        return JSON.stringify({ success: true });
    },

    /**
     * Control which methods are exposed
     */
    isPublic: function() {
        var methodName = this.getParameter('sysparm_name');
        var publicMethods = ['getPublicData', 'getAdminData', 'updateIncident'];
        return publicMethods.indexOf(methodName) !== -1;
    },

    type: 'SecureIncidentAjax'
});
フェーズ 3:既存クラスの拡張
ステップ 3.1:標準スクリプトインクルードを拡張する

AbstractAjaxProcessor の拡張(最も一般的):

var MyAjaxProcessor = Class.create();
MyAjaxProcessor.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    // Your methods here
    type: 'MyAjaxProcessor'
});

サーバーサイドでの GlideAjax の拡張:

var EnhancedIncidentUtils = Class.create();
EnhancedIncidentUtils.prototype = Object.extendsObject(IncidentUtils, {

    initialize: function(incidentGR) {
        // Call parent constructor
        IncidentUtils.prototype.initialize.call(this);
        this.incident = incidentGR;
    },

    /**
     * Override parent method with enhanced logic
     */
    calculatePriority: function(incidentGR) {
        var gr = incidentGR || this.incident;

        // Call parent implementation first
        var basePriority = IncidentUtils.prototype.calculatePriority.call(this, gr);

        // Enhance with additional logic
        if (this._isVIPCaller(gr)) {
            basePriority = Math.max(1, basePriority - 1);  // Increase priority
        }

        return basePriority;
    },

    /**
     * New method specific to enhanced class
     */
    _isVIPCaller: function(incidentGR) {
        var caller = incidentGR.caller_id;
        if (caller.nil()) {
            return false;
        }
        return caller.vip == true;
    },

    /**
     * Get full incident analysis
     */
    getAnalysis: function() {
        return {
            priority: this.calculatePriority(),
            isCritical: this.isCritical(this.incident),
            isVIP: this._isVIPCaller(this.incident)
        };
    },

    type: 'EnhancedIncidentUtils'
});
ステップ 3.2:プラットフォームクラスを拡張する

RESTMessageV2 の拡張:

var EnhancedRESTMessage = Class.create();
EnhancedRESTMessage.prototype = Object.extendsObject(sn_ws.RESTMessageV2, {

    initialize: function(messageName, methodName) {
        sn_ws.RESTMessageV2.prototype.initialize.call(this, messageName, methodName);
        this.log = new GSLog('com.company.rest', 'EnhancedRESTMessage');
    },

    /**
     * Execute with automatic retry on failure
     */
    executeWithRetry: function(maxRetries) {
        var retries = maxRetries || 3;
        var lastError = null;

        for (var i = 0; i < retries; i++) {
            try {
                var response = this.execute();
                if (response.getStatusCode() >= 200 && response.getStatusCode() < 300) {
                    return response;
                }
                lastError = 'HTTP ' + response.getStatusCode();
            } catch (e) {
                lastError = e.message;
                this.log.warn('Retry ' + (i + 1) + ' failed: ' + lastError);
            }

            // Wait before retry (exponential backoff)
            if (i < retries - 1) {
                gs.sleep(1000 * Math.pow(2, i));
            }
        }

        throw new Error('All retries failed: ' + lastError);
    },

    /**
     * Execute and parse JSON response
     */
    executeAndParseJSON: function() {
        var response = this.execute();
        var body = response.getBody();

        try {
            return JSON.parse(body);
        } catch (e) {
            this.log.error('Failed to parse JSON: ' + body);
            throw new Error('Invalid JSON response');
        }
    },

    type: 'EnhancedRESTMessage'
});
ステップ 3.3:カスタム階層用の基底クラスを作成する

基底クラス:

var BaseServiceHandler = Class.create();
BaseServiceHandler.prototype = {

    initialize: function(tableName) {
        this.tableName = tableName;
        this.log = new GSLog('com.company.handlers', this.type);
    },

    /**
     * Get record by sys_id
     * @param {String} sysId - Record sys_id
     * @returns {GlideRecord} Record or null
     */
    getRecord: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            return gr;
        }
        return null;
    },

    /**
     * Abstract method - must be overridden
     */
    process: function(record) {
        throw new Error('process() must be implemented by subclass');
    },

    /**
     * Abstract method - must be overridden
     */
    validate: function(record) {
        throw new Error('validate() must be implemented by subclass');
    },

    /**
     * Common logging method
     */
    logAction: function(action, recordId) {
        this.log.info(action + ' on ' + this.tableName + ': ' + recordId);
    },

    type: 'BaseServiceHandler'
};

具象実装:

var IncidentServiceHandler = Class.create();
IncidentServiceHandler.prototype = Object.extendsObject(BaseServiceHandler, {

    initialize: function() {
        // Call parent constructor with table name
        BaseServiceHandler.prototype.initialize.call(this, 'incident');
    },

    /**
     * Override abstract method
     */
    process: function(record) {
        if (!this.validate(record)) {
            return false;
        }

        // Incident-specific processing
        this._assignToGroup(record);
        this._calculatePriority(record);
        this.logAction('Processed incident', record.sys_id);

        return true;
    },

    /**
     * Override abstract method
     */
    validate: function(record) {
        if (record.short_description.nil()) {
            this.log.error('Validation failed: missing short description');
            return false;
        }
        return true;
    },

    _assignToGroup: function(record) {
        // Implementation
    },

    _calculatePriority: function(record) {
        // Implementation
    },

    type: 'IncidentServiceHandler'
});
フェーズ 4:デザインパターン
ステップ 4.1:シングルトンパターン

アプリケーション全体で共有するインスタンスが 1 つだけ必要な場合に使用します。

var ConfigurationManager = Class.create();
ConfigurationManager.prototype = {

    initialize: function() {
        this._loadConfiguration();
    },

    _loadConfiguration: function() {
        this.config = {};

        var gr = new GlideRecord('sys_properties');
        gr.addQuery('name', 'STARTSWITH', 'com.company.');
        gr.query();

        while (gr.next()) {
            var key = gr.name.toString().replace('com.company.', '');
            this.config[key] = gr.value.toString();
        }
    },

    get: function(key) {
        return this.config[key];
    },

    set: function(key, value) {
        this.config[key] = value;

        // Persist to database
        var gr = new GlideRecord('sys_properties');
        gr.addQuery('name', 'com.company.' + key);
        gr.query();

        if (gr.next()) {
            gr.value = value;
            gr.update();
        } else {
            gr.initialize();
            gr.name = 'com.company.' + key;
            gr.value = value;
            gr.insert();
        }
    },

    type: 'ConfigurationManager'
};

/**
 * Singleton accessor
 * Usage: var config = ConfigurationManager.getInstance();
 */
ConfigurationManager.getInstance = function() {
    if (!ConfigurationManager._instance) {
        ConfigurationManager._instance = new ConfigurationManager();
    }
    return ConfigurationManager._instance;
};

// Clear singleton (for testing or refresh)
ConfigurationManager.clearInstance = function() {
    ConfigurationManager._instance = null;
};

使用方法:

// Get singleton instance
var config = ConfigurationManager.getInstance();

// Use configuration
var apiKey = config.get('api_key');
var timeout = config.get('timeout') || '30000';

// Update configuration
config.set('last_sync', gs.nowDateTime());
ステップ 4.2:ファクトリーパターン

具体的なクラスを指定せずにオブジェクトを作成する必要がある場合に使用します。

var NotificationFactory = Class.create();
NotificationFactory.prototype = {

    initialize: function() {
        // Register notification types
        this.types = {
            'email': EmailNotification,
            'sms': SMSNotification,
            'slack': SlackNotification,
            'teams': TeamsNotification
        };
    },

    /**
     * Create notification handler based on type
     * @param {String} type - Notification type
     * @param {Object} options - Configuration options
     * @returns {Object} Notification handler
     */
    create: function(type, options) {
        var NotificationClass = this.types[type.toLowerCase()];

        if (!NotificationClass) {
            throw new Error('Unknown notification type: ' + type);
        }

        return new NotificationClass(options);
    },

    /**
     * Register custom notification type
     */
    register: function(type, handlerClass) {
        this.types[type.toLowerCase()] = handlerClass;
    },

    /**
     * Get available types
     */
    getAvailableTypes: function() {
        return Object.keys(this.types);
    },

    type: 'NotificationFactory'
};

// Base notification class
var BaseNotification = Class.create();
BaseNotification.prototype = {
    initialize: function(options) {
        this.options = options || {};
    },
    send: function(recipient, message) {
        throw new Error('send() must be implemented');
    },
    type: 'BaseNotification'
};

// Email implementation
var EmailNotification = Class.create();
EmailNotification.prototype = Object.extendsObject(BaseNotification, {
    send: function(recipient, message) {
        var email = new GlideEmailOutbound();
        email.setSubject(this.options.subject || 'Notification');
        email.setFrom(this.options.from || '[email protected]');
        email.addAddress(recipient);
        email.setBody(message);
        email.send();
        return true;
    },
    type: 'EmailNotification'
});

// SMS implementation
var SMSNotification = Class.create();
SMSNotification.prototype = Object.extendsObject(BaseNotification, {
    send: function(recipient, message) {
        // SMS API integration
        var rest = new sn_ws.RESTMessageV2('SMS_API', 'send');
        rest.setStringParameterNoEscape('to', recipient);
        rest.setStringParameterNoEscape('message', message);
        var response = rest.execute();
        return response.getStatusCode() == 200;
    },
    type: 'SMSNotification'
});

使用方法:

var factory = new NotificationFactory();

// Create email notification
var emailHandler = factory.create('email', {
    subject: 'Important Alert',
    from: '[email protected]'
});
emailHandler.send('[email protected]', 'Your incident was updated.');

// Create SMS notification
var smsHandler = factory.create('sms', {});
smsHandler.send('+1234567890', 'P1 Alert: Check your email.');

// Use multiple notification types
var types = ['email', 'slack'];
types.forEach(function(type) {
    var handler = factory.create(type, {});
    handler.send(recipient, message);
});
ステップ 4.3:ストラテジーパターン

アルゴリズムを切り替えて使用する必要がある場合に使用します。

var IncidentAssigner = Class.create();
IncidentAssigner.prototype = {

    initialize: function() {
        this.strategies = {
            'round_robin': new RoundRobinStrategy(),
            'least_busy': new LeastBusyStrategy(),
            'skills_based': new SkillsBasedStrategy(),
            'random': new RandomStrategy()
        };
        this.currentStrategy = 'round_robin';
    },

    /**
     * Set assignment strategy
     */
    setStrategy: function(strategyName) {
        if (!this.strategies[strategyName]) {
            throw new Error('Unknown strategy: ' + strategyName);
        }
        this.currentStrategy = strategyName;
    },

    /**
     * Assign incident using current strategy
     */
    assign: function(incidentGR) {
        var strategy = this.strategies[this.currentStrategy];
        var assignee = strategy.findAssignee(incidentGR);

        if (assignee) {
            incidentGR.assigned_to = assignee;
            return true;
        }
        return false;
    },

    type: 'IncidentAssigner'
};

// Strategy interface
var AssignmentStrategy = Class.create();
AssignmentStrategy.prototype = {
    findAssignee: function(incidentGR) {
        throw new Error('findAssignee() must be implemented');
    },
    type: 'AssignmentStrategy'
};

// Round Robin implementation
var RoundRobinStrategy = Class.create();
RoundRobinStrategy.prototype = Object.extendsObject(AssignmentStrategy, {
    findAssignee: function(incidentGR) {
        var groupId = incidentGR.assignment_group.toString();
        if (!groupId) return null;

        // Get last assigned user for this group
        var lastAssigned = gs.getProperty('round_robin.' + groupId, '');

        // Get all active members
        var members = [];
        var gm = new GlideRecord('sys_user_grmember');
        gm.addQuery('group', groupId);
        gm.addQuery('user.active', true);
        gm.query();

        while (gm.next()) {
            members.push(gm.user.toString());
        }

        if (members.length === 0) return null;

        // Find next user in rotation
        var lastIndex = members.indexOf(lastAssigned);
        var nextIndex = (lastIndex + 1) % members.length;
        var nextUser = members[nextIndex];

        // Save for next rotation
        gs.setProperty('round_robin.' + groupId, nextUser);

        return nextUser;
    },
    type: 'RoundRobinStrategy'
});

// Least Busy implementation
var LeastBusyStrategy = Class.create();
LeastBusyStrategy.prototype = Object.extendsObject(AssignmentStrategy, {
    findAssignee: function(incidentGR) {
        var groupId = incidentGR.assignment_group.toString();
        if (!groupId) return null;

        var leastBusy = null;
        var lowestCount = Infinity;

        var gm = new GlideRecord('sys_user_grmember');
        gm.addQuery('group', groupId);
        gm.addQuery('user.active', true);
        gm.query();

        while (gm.next()) {
            var userId = gm.user.toString();
            var count = this._getOpenIncidentCount(userId);

            if (count < lowestCount) {
                lowestCount = count;
                leastBusy = userId;
            }
        }

        return leastBusy;
    },

    _getOpenIncidentCount: function(userId) {
        var ga = new GlideAggregate('incident');
        ga.addQuery('assigned_to', userId);
        ga.addQuery('active', true);
        ga.addAggregate('COUNT');
        ga.query();

        if (ga.next()) {
            return parseInt(ga.getAggregate('COUNT'));
        }
        return 0;
    },
    type: 'LeastBusyStrategy'
});

使用方法:

var assigner = new IncidentAssigner();

// Configure strategy based on system property
var strategy = gs.getProperty('incident.assignment.strategy', 'round_robin');
assigner.setStrategy(strategy);

// Assign incident
if (assigner.assign(current)) {
    gs.info('Assigned to: ' + current.assigned_to.getDisplayValue());
} else {
    gs.warn('No assignee found');
}
フェーズ 5:スコープ付きスクリプトインクルードとグローバルスクリプトインクルード
ステップ 5.1:グローバルスクリプトインクルード

特徴:

  • すべてのスコープから利用可能
  • 名前空間のプレフィックスが不要
  • すべてのプラットフォーム API にアクセス可能
  • 名前の競合が発生するリスクがある
Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "GlobalUtils"
    api_name: "global.GlobalUtils"
    sys_scope: "global"
    access: "public"
    script: |
      var GlobalUtils = Class.create();
      GlobalUtils.prototype = {
          initialize: function() {},

          formatDate: function(gdt) {
              // Available to all scopes
              return gdt.getDisplayValue();
          },

          type: 'GlobalUtils'
      };
Step 5.2:スコープ付きスクリプトインクルード

特徴:

  • アプリケーションスコープ内に分離されています
  • スコープ名が接頭辞として付きます(例:x_company_app.ClassName)
  • access フィールドによってアクセスが制御されます
  • より安全な名前空間管理が可能です
Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "ScopedUtils"
    api_name: "x_company_app.ScopedUtils"
    sys_scope: "[scoped_app_sys_id]"
    access: "public"
    script: |
      var ScopedUtils = Class.create();
      ScopedUtils.prototype = {
          initialize: function() {},

          /**
           * This method is accessible from other scopes
           * because access is 'public'
           */
          publicMethod: function() {
              return 'Accessible anywhere';
          },

          type: 'ScopedUtils'
      };
Step 5.3:アクセス修飾子
アクセスレベルスコープアクセス使用例
publicすべてのスコープ共有ユーティリティ、API
package_private同じスコープのみ内部ヘルパー
private同じスコープ、継承なし保護されたユーティリティ

グローバルからスコープ付きスクリプトインクルードを呼び出す場合:

// From global scope, need full API name
var utils = new x_company_app.ScopedUtils();
var result = utils.publicMethod();

スコープ付きからグローバルのスクリプトインクルードを呼び出す場合:

// From scoped app, global is accessible
var globalUtils = new GlobalUtils();
var result = globalUtils.formatDate(new GlideDateTime());
フェーズ 6:スクリプトインクルードとビジネスルールの使い分け
判断基準
シナリオスクリプトインクルードを使用ビジネスルールを使用
再利用可能なロジック(複数箇所)はいいいえ
単一テーブル、単一トリガーいいえはい
クライアントとサーバー間の通信はい(GlideAjax)いいえ
単体テストが必要はい可能ですが、より困難です
複雑なアルゴリズム/計算はいスクリプトインクルードに切り出します
単純なフィールド検証いいえはい
ワークフロー/Flow アクティビティはい該当なし
スケジュール済みジョブのロジックスクリプトインクルードに切り出します該当なし
テーブル間の操作はい可能
避けるべきアンチパターン

悪い例:ビジネスルールでロジックを重複させる

// Business Rule 1 on Incident
if (current.priority == 1 && current.assignment_group.nil()) {
    current.assignment_group = getDefaultGroup('incident');
}

// Business Rule 2 on Change - DUPLICATE LOGIC!
if (current.priority == 1 && current.assignment_group.nil()) {
    current.assignment_group = getDefaultGroup('change_request');
}

良い例:スクリプトインクルードに切り出す

// Script Include: AssignmentUtils
var AssignmentUtils = Class.create();
AssignmentUtils.prototype = {
    initialize: function() {},

    ensureAssignment: function(record, tableName) {
        if (record.priority == 1 && record.assignment_group.nil()) {
            record.assignment_group = this.getDefaultGroup(tableName);
        }
    },

    getDefaultGroup: function(tableName) {
        // Lookup logic
    },

    type: 'AssignmentUtils'
};

// Business Rule - Simple delegation
var utils = new AssignmentUtils();
utils.ensureAssignment(current, 'incident');
フェーズ 7:スクリプトインクルードの単体テスト
Step 7.1:スクリプトインクルード用の ATF テストを作成する
Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_test
  data:
    name: "Test IncidentUtils - calculatePriority"
    description: "Unit tests for IncidentUtils.calculatePriority method"
    active: true
Step 7.2:テストステップを作成する

テストステップ 1:P1 の計算をテストする

Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_step
  data:
    test: [test_sys_id]
    order: 100
    step_config: "atf_test_step_config:run_server_side_script"
    description: "Test P1 priority calculation"
    inputs:
      script: |
        (function(outputs, steps, params, stepResult, assertEqual) {
            // Arrange
            var incident = new GlideRecord('incident');
            incident.newRecord();
            incident.impact = 1;
            incident.urgency = 1;

            // Act
            var utils = new IncidentUtils();
            var priority = utils.calculatePriority(incident);

            // Assert
            assertEqual({
                name: 'Priority should be 1 for high impact + high urgency',
                shouldbe: 1,
                value: priority
            });

        })(outputs, steps, params, stepResult, assertEqual);

テストステップ 2:P3 の計算をテストする

Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_step
  data:
    test: [test_sys_id]
    order: 200
    step_config: "atf_test_step_config:run_server_side_script"
    description: "Test P3 priority calculation"
    inputs:
      script: |
        (function(outputs, steps, params, stepResult, assertEqual) {
            // Arrange
            var incident = new GlideRecord('incident');
            incident.newRecord();
            incident.impact = 2;
            incident.urgency = 2;

            // Act
            var utils = new IncidentUtils();
            var priority = utils.calculatePriority(incident);

            // Assert
            assertEqual({
                name: 'Priority should be 3 for medium impact + medium urgency',
                shouldbe: 3,
                value: priority
            });

        })(outputs, steps, params, stepResult, assertEqual);
Step 7.3:テスト用モックパターン

モック可能なスクリプトインクルード:

var IncidentService = Class.create();
IncidentService.prototype = {

    initialize: function(dependencies) {
        // Dependency injection for testability
        this.notificationService = dependencies && dependencies.notificationService
            ? dependencies.notificationService
            : new NotificationService();

        this.assignmentUtils = dependencies && dependencies.assignmentUtils
            ? dependencies.assignmentUtils
            : new AssignmentUtils();
    },

    processNewIncident: function(incidentGR) {
        // Assign
        this.assignmentUtils.autoAssign(incidentGR);

        // Notify
        if (incidentGR.priority == 1) {
            this.notificationService.notifyOnCall(incidentGR);
        }

        return true;
    },

    type: 'IncidentService'
};

モックを使った ATF テスト:

(function(outputs, steps, params, stepResult, assertEqual) {

    // Create mock notification service
    var mockNotificationService = {
        notifyCalled: false,
        lastIncident: null,
        notifyOnCall: function(incidentGR) {
            this.notifyCalled = true;
            this.lastIncident = incidentGR;
        }
    };

    // Create mock assignment utils
    var mockAssignmentUtils = {
        autoAssign: function(incidentGR) {
            incidentGR.assigned_to = 'mock_user_id';
        }
    };

    // Inject mocks
    var service = new IncidentService({
        notificationService: mockNotificationService,
        assignmentUtils: mockAssignmentUtils
    });

    // Arrange
    var incident = new GlideRecord('incident');
    incident.newRecord();
    incident.priority = 1;
    incident.short_description = 'Test incident';

    // Act
    service.processNewIncident(incident);

    // Assert
    assertEqual({
        name: 'Notification should be called for P1',
        shouldbe: true,
        value: mockNotificationService.notifyCalled
    });

    assertEqual({
        name: 'Incident should be assigned',
        shouldbe: 'mock_user_id',
        value: incident.assigned_to.toString()
    });

})(outputs, steps, params, stepResult, assertEqual);

ツールの使用方法の概要

MCP ツール
ツール目的
SN-Create-Record新しいスクリプトインクルードを作成する
SN-Update-Record既存のスクリプトインクルードを更新する
SN-Query-Tableスクリプトインクルードを検索する
SN-Get-Recordスクリプトインクルードの詳細を取得する
SN-Get-Recordローカル開発用にスクリプトと最新状態のメタデータを取得する
SN-Update-Recordリモートの最新状態を確認後、レビュー済みスクリプトをプッシュする
SN-Execute-Background-Scriptスクリプトインクルードをテストする
SN-Get-Table-Schemasys_script_include のフィールドを調べる

Happy Platform MCP 5.1 には、自動監視や同期を行うツールはありません。ローカル開発では、SN-Get-Record で script、sys_updated_on、sys_mod_count を取得し、ローカルで編集してから、再度取得して競合を検出してください。その後、レビュー済みの script フィールドのみを SN-Update-Record でプッシュします。各呼び出しでは、対象の instance を明示してください。

REST API
エンドポイントメソッド目的
/api/now/table/sys_script_includePOSTスクリプトインクルードを作成する
/api/now/table/sys_script_include/{id}PATCHスクリプトインクルードを更新する
/api/now/table/sys_script_includeGETスクリプトインクルードを検索する

ベストプラクティス

コードの整理
  • ファイルごとに 1 クラス - バージョン管理がすっきりします
  • 意味のある名前 - Utils2 ではなく CustomerOrderProcessor
  • 一貫した命名規則 - クラスには PascalCase、メソッドには camelCase を使用します
  • プライベートメソッド - アンダースコアを接頭辞として付けます:_internalMethod
  • JSDoc コメント - すべての公開メソッドを文書化します
パフォーマンス
  • 遅延読み込み - 必要になるまで依存関係を読み込まないでください
  • キャッシュ - コストの高い検索(GlideRecord クエリ)をキャッシュします
  • グローバル状態を避ける - インスタンス変数またはパラメーターを使用します
  • GlideRecord クエリを最小限に抑える - 可能な場合はまとめて実行します
セキュリティ
  • 入力を検証する - GlideAjax でクライアントデータを決して信用しないでください
  • 権限を確認する - 操作の前に canRead()、canWrite() を使用します
  • 公開範囲を制限する - 必要な場合にのみメソッドを client_callable にします
  • isPublic() を実装する - 許可するメソッドをホワイトリストに登録します
テスト
  • まずテストを書く - TDD によって設計が改善されます
  • 依存性注入を使用する - モックが容易になります
  • エッジケースをテストする - null 値、空文字列、境界条件
  • テストを高速に保つ - コストの高い処理はモックします

トラブルシューティング

スクリプトインクルードが見つからない

症状: TypeError: ClassName is not a constructor 原因: スクリプトインクルードが有効でないか、名前が一致していません 解決策:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_include
  query: name=ClassName
  fields: name,api_name,active,sys_scope.name

次の点を確認してください:

  1. active が true である
  2. name がクラス定義と完全に一致している
  3. 正しいスコープから呼び出している
GlideAjax が空の値を返す

症状: getAnswer() が空または undefined を返します 原因: メソッドが値を返していないか、client_callable ではありません 解決策:

  1. client_callable が true であることを確認します
  2. メソッドが値(文字列)を返すことを確認します
  3. isPublic() に対象のメソッドが含まれていることを確認します
  4. サーバーエラーがないか、システムログを確認します
クラスを拡張できない

症状: 親クラスのメソッドを使用できません 原因: 拡張構文が正しくありません 解決策:

// WRONG
ChildClass.prototype = ParentClass.prototype;

// CORRECT
ChildClass.prototype = Object.extendsObject(ParentClass, {
    // child methods
    type: 'ChildClass'
});
スコープ付きクラスにアクセスできない

症状: スコープ付きスクリプトインクルードをインスタンス化できません 原因: アクセス修飾子またはスコープに問題があります 解決策:

  1. access フィールドを確認します(スコープをまたぐ場合は 'public' にする必要があります)
  2. 完全な API 名を使用します:new x_scope.ClassName()
  3. 呼び出し元のコードが許可されたスコープ内にあることを確認します

例

例 1:完全な CRUD サービス
var CRUDService = Class.create();
CRUDService.prototype = {

    initialize: function(tableName) {
        this.tableName = tableName;
        this.log = new GSLog('com.company.crud', 'CRUDService');
    },

    create: function(data) {
        var gr = new GlideRecord(this.tableName);
        gr.initialize();

        for (var field in data) {
            if (data.hasOwnProperty(field) && gr.isValidField(field)) {
                gr.setValue(field, data[field]);
            }
        }

        var sysId = gr.insert();
        this.log.info('Created ' + this.tableName + ': ' + sysId);
        return sysId;
    },

    read: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            return this._toObject(gr);
        }
        return null;
    },

    update: function(sysId, data) {
        var gr = new GlideRecord(this.tableName);
        if (!gr.get(sysId)) {
            return false;
        }

        for (var field in data) {
            if (data.hasOwnProperty(field) && gr.isValidField(field)) {
                gr.setValue(field, data[field]);
            }
        }

        gr.update();
        this.log.info('Updated ' + this.tableName + ': ' + sysId);
        return true;
    },

    delete: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            gr.deleteRecord();
            this.log.info('Deleted ' + this.tableName + ': ' + sysId);
            return true;
        }
        return false;
    },

    findBy: function(query) {
        var results = [];
        var gr = new GlideRecord(this.tableName);
        gr.addEncodedQuery(query);
        gr.query();

        while (gr.next()) {
            results.push(this._toObject(gr));
        }

        return results;
    },

    _toObject: function(gr) {
        var obj = {};
        var fields = new GlideRecordUtil().getFields(gr);

        for (var i = 0; i < fields.size(); i++) {
            var field = fields.get(i);
            obj[field] = gr.getValue(field);
        }

        return obj;
    },

    type: 'CRUDService'
};
例 2:完全な GlideAjax サービス
var FormHelperAjax = Class.create();
FormHelperAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    getUserDetails: function() {
        var userId = this.getParameter('sysparm_user_id');
        var result = { success: false };

        var gr = new GlideRecord('sys_user');
        if (gr.get(userId)) {
            result.success = true;
            result.data = {
                sys_id: gr.sys_id.toString(),
                name: gr.name.toString(),
                email: gr.email.toString(),
                department: gr.department.getDisplayValue(),
                manager: gr.manager.getDisplayValue(),
                phone: gr.phone.toString()
            };
        } else {
            result.error = 'User not found';
        }

        return JSON.stringify(result);
    },

    getCIDetails: function() {
        var ciId = this.getParameter('sysparm_ci_id');
        var result = { success: false };

        var gr = new GlideRecord('cmdb_ci');
        if (gr.get(ciId)) {
            result.success = true;
            result.data = {
                sys_id: gr.sys_id.toString(),
                name: gr.name.toString(),
                sys_class_name: gr.sys_class_name.getDisplayValue(),
                operational_status: gr.operational_status.getDisplayValue(),
                support_group: gr.support_group.getDisplayValue()
            };
        } else {
            result.error = 'CI not found';
        }

        return JSON.stringify(result);
    },

    validateEmail: function() {
        var email = this.getParameter('sysparm_email');
        var result = { valid: false };

        // Basic email regex
        var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        result.valid = emailPattern.test(email);

        if (result.valid) {
            // Check if email exists in system
            var gr = new GlideRecord('sys_user');
            gr.addQuery('email', email);
            gr.query();
            result.exists = gr.hasNext();
        }

        return JSON.stringify(result);
    },

    isPublic: function() {
        return true;
    },

    type: 'FormHelperAjax'
});

関連スキル

  • admin/script-sync - ローカル開発とバージョン管理
  • admin/update-set-management - スクリプトの変更を追跡
  • admin/application-scope - スコープ付きアプリケーションの開発
  • admin/script-execution - スクリプトをテスト
  • catalog/variable-management - カタログでスクリプトインクルードを使用

参考資料

コメント