mcpField
S
scripted-rest-apis

scripted-rest-apis

github.com/happy-technologies-llc开发4143 次安装2 天前更新
代码仓库

关于 scripted-rest-apis

面向 ServiceNow 开发者、集成专员和管理员的技能,讲解如何构建自定义 Scripted REST API,涵盖端点设计、请求与响应、安全、版本控制和测试。

主要功能

  1. 01自定义 REST 端点设计
  2. 02请求与响应格式处理
  3. 03身份验证与访问控制
  4. 04查询参数和路径参数处理
  5. 05错误处理与 API 版本控制
  6. 06API Explorer 与 curl 测试

适用场景

  1. 01向外部系统提供专用端点
  2. 02在 API 操作中实现自定义业务逻辑
  3. 03聚合多个表的数据
  4. 04定义带访问控制的版本化 API

MCP 相关

指南通过 MCP 工具设置 ServiceNow 应用上下文,并创建、查询和更新 API 定义及操作。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/happy-technologies-llc/happy-platform-skills --skill scripted-rest-apis

安装后在 Claude Code 中输入/scripted-rest-apis确认已加载。

详细介绍

脚本化 REST API 开发

概述

本技能提供了在 ServiceNow 中开发自定义脚本化 REST API 的全面指南。脚本化 REST API 可用于:

  • 公开自定义端点 - 创建超出标准 Table API 范围的专用 API
  • 控制数据转换 - 调整请求和响应格式,以满足集成需求
  • 实现业务逻辑 - 在单次 API 调用中执行复杂操作
  • 保护集成 - 应用细粒度的身份验证和授权
  • 支持外部系统 - 为第三方系统集成提供接口

适用场景:

  • 标准 Table API 无法满足需求
  • 需要在 API 层实现自定义业务逻辑
  • 需要特定的请求和响应格式
  • 外部系统需要专用端点
  • 需要汇总多个表中的数据

适用对象: 构建自定义 ServiceNow 集成的开发人员、集成专家和管理员。

先决条件

  • 角色: admin、web_service_admin 或作用域应用开发人员
  • 权限: 创建/修改 sys_ws_definition、sys_ws_operation
  • 知识: JavaScript、REST 原则、HTTP 方法、JSON
  • 环境: 开发实例(切勿直接在生产环境中开发 API)
  • 相关技能:
    • admin/update-set-management - 捕获 API 定义
    • admin/application-scope - 作用域应用 API 开发
    • security/acl-management - API 安全控制

表架构

了解底层表对于以编程方式创建 API 至关重要。

核心表
sys_ws_definition (API Definition)
    |
    +-- sys_ws_operation (Resources/Endpoints)
            |
            +-- Request Headers (configured in operation)
            +-- Query Parameters (configured in operation)
            +-- Path Parameters (defined in relative_path)
            +-- Script (handles request/response)
主要关系
表用途关键字段
sys_ws_definitionAPI 容器name、namespace、base_path、active
sys_ws_operation单个端点name、http_method、relative_path、script
sys_script_include可复用逻辑name、script、api_name
sys_ws_api_header自定义标头name、api_id、direction
API URL 结构
https://<instance>.service-now.com/api/<namespace>/<api_name>/<version>/<resource>/<path_param>

Example:
https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/12345
    |                                |     |            |       |           |
    Instance                     Namespace  API Name  Version Resource  Path Param

操作步骤

阶段 1:API 设计

创建 API 之前,请仔细规划其结构。

步骤 1.1:定义 API 需求

记录 API 规范:

API Name: Customer Integration API
Namespace: x_company (scoped app) or now (global)
Version: v1
Base Path: /api/x_company/customer_api

Resources:
- GET /customers - List all customers (with pagination)
- GET /customers/{id} - Get specific customer
- POST /customers - Create new customer
- PUT /customers/{id} - Update customer
- DELETE /customers/{id} - Delete customer
- POST /customers/{id}/orders - Create order for customer
步骤 1.2:规划身份验证
身份验证类型使用场景配置
Basic Auth服务帐户默认启用
OAuth 2.0第三方应用配置 OAuth 提供程序
API Key简单集成自定义标头验证
Session基于浏览器基于 Cookie 的身份验证
Mutual TLS高安全性证书验证
阶段 2:创建 API 定义
步骤 2.1:设置上下文(作用域应用)

如果在作用域应用中创建:

使用 MCP:

Tool: SN-Set-Current-Application
Parameters:
  app_sys_id: [your_app_sys_id]
Tool: SN-Set-Update-Set
Parameters:
  update_set_sys_id: [your_update_set_sys_id]
步骤 2.2:创建 API 定义

使用 MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_definition
  data:
    name: Customer Integration API
    short_description: API for customer management and integration
    namespace: x_company
    doc_link: https://docs.company.com/api/customers
    active: true
    enforce_acl: true
    is_versioned: true
    baseline_version: v1
    published: false

关键字段说明:

字段用途建议
namespaceURL 路径段,通常为应用作用域使用应用作用域(x_company)
enforce_acl检查底层表上的 ACL设为 true 以确保安全
is_versioned启用 API 版本控制始终设为 true
baseline_version未指定版本时使用的默认版本从 v1 开始
published使 API 可供发现开发期间设为 false

保存 sys_id: api_definition_sys_id

步骤 2.3:验证 API 创建情况
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_definition
  query: name=Customer Integration API
  fields: sys_id,name,namespace,active,service_address

service_address 字段显示完整的 API URL。

阶段 3:创建资源(操作)
步骤 3.1:GET 集合资源(列表)

创建一个用于分页列出客户的端点。

使用 MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: List Customers
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /customers
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Get query parameters for pagination and filtering
        var limit = parseInt(request.queryParams.limit) || 20;
        var offset = parseInt(request.queryParams.offset) || 0;
        var active = request.queryParams.active;
        var search = request.queryParams.q;

        // Validate limit
        if (limit > 100) limit = 100;
        if (limit < 1) limit = 20;

        // Build query
        var gr = new GlideRecord('customer');

        if (active !== undefined) {
          gr.addQuery('active', active === 'true');
        }

        if (search) {
          var qc = gr.addQuery('name', 'CONTAINS', search);
          qc.addOrCondition('email', 'CONTAINS', search);
          qc.addOrCondition('account_number', 'CONTAINS', search);
        }

        // Get total count before pagination
        var countGR = new GlideAggregate('customer');
        countGR.addAggregate('COUNT');
        if (active !== undefined) {
          countGR.addQuery('active', active === 'true');
        }
        countGR.query();
        var totalCount = 0;
        if (countGR.next()) {
          totalCount = parseInt(countGR.getAggregate('COUNT'));
        }

        // Apply pagination
        gr.orderBy('name');
        gr.chooseWindow(offset, offset + limit);
        gr.query();

        // Build response
        var customers = [];
        while (gr.next()) {
          customers.push({
            sys_id: gr.getUniqueValue(),
            name: gr.getValue('name'),
            email: gr.getValue('email'),
            account_number: gr.getValue('account_number'),
            active: gr.getValue('active') === 'true',
            created: gr.getValue('sys_created_on'),
            updated: gr.getValue('sys_updated_on')
          });
        }

        // Set response
        response.setStatus(200);
        response.setBody({
          result: customers,
          meta: {
            total: totalCount,
            limit: limit,
            offset: offset,
            has_more: (offset + limit) < totalCount
          }
        });

      })(request, response);
    short_description: Retrieve list of customers with pagination
    requires_acl_authorization: true
    requires_authentication: true
    requires_snc_internal_role: false

保存 sys_id: list_customers_operation_id

步骤 3.2:GET 单个资源

创建一个用于检索指定客户的端点。

使用 MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Get Customer
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Get path parameter
        var customerId = request.pathParams.id;

        // Validate input
        if (!customerId) {
          response.setStatus(400);
          response.setBody({
            error: {
              message: 'Customer ID is required',
              code: 'MISSING_PARAMETER'
            }
          });
          return;
        }

        // Query customer
        var gr = new GlideRecord('customer');

        // Support both sys_id and account_number lookup
        if (customerId.length === 32) {
          gr.get(customerId);
        } else {
          gr.addQuery('account_number', customerId);
          gr.query();
          gr.next();
        }

        if (!gr.isValidRecord()) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND',
              detail: 'No customer exists with ID: ' + customerId
            }
          });
          return;
        }

        // Check ACL (optional if enforce_acl is true on definition)
        if (!gr.canRead()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Access denied',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Build detailed response
        var customer = {
          sys_id: gr.getUniqueValue(),
          name: gr.getValue('name'),
          email: gr.getValue('email'),
          phone: gr.getValue('phone'),
          account_number: gr.getValue('account_number'),
          address: {
            street: gr.getValue('street'),
            city: gr.getValue('city'),
            state: gr.getValue('state'),
            zip: gr.getValue('zip'),
            country: gr.getValue('country')
          },
          contacts: [],
          active: gr.getValue('active') === 'true',
          created: gr.getValue('sys_created_on'),
          updated: gr.getValue('sys_updated_on'),
          created_by: gr.getDisplayValue('sys_created_by'),
          updated_by: gr.getDisplayValue('sys_updated_by')
        };

        // Get related contacts
        var contacts = new GlideRecord('customer_contact');
        contacts.addQuery('customer', gr.getUniqueValue());
        contacts.query();
        while (contacts.next()) {
          customer.contacts.push({
            sys_id: contacts.getUniqueValue(),
            name: contacts.getValue('name'),
            email: contacts.getValue('email'),
            phone: contacts.getValue('phone'),
            primary: contacts.getValue('primary') === 'true'
          });
        }

        response.setStatus(200);
        response.setBody({ result: customer });

      })(request, response);
    short_description: Retrieve a specific customer by ID
    requires_acl_authorization: true
    requires_authentication: true
步骤 3.3:POST 资源(创建)

创建一个用于创建新客户的端点。

使用 MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Create Customer
    web_service_definition: [api_definition_sys_id]
    http_method: POST
    relative_path: /customers
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Parse request body
        var body = request.body.data;

        // Validate required fields
        var errors = [];
        if (!body.name) errors.push('name is required');
        if (!body.email) errors.push('email is required');

        // Validate email format
        if (body.email && !body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
          errors.push('email format is invalid');
        }

        if (errors.length > 0) {
          response.setStatus(400);
          response.setBody({
            error: {
              message: 'Validation failed',
              code: 'VALIDATION_ERROR',
              details: errors
            }
          });
          return;
        }

        // Check for duplicate email
        var existing = new GlideRecord('customer');
        existing.addQuery('email', body.email);
        existing.query();
        if (existing.next()) {
          response.setStatus(409);
          response.setBody({
            error: {
              message: 'Customer with this email already exists',
              code: 'DUPLICATE_ERROR',
              existing_id: existing.getUniqueValue()
            }
          });
          return;
        }

        // Create customer
        var gr = new GlideRecord('customer');
        gr.initialize();

        // Required fields
        gr.setValue('name', body.name);
        gr.setValue('email', body.email);

        // Optional fields
        if (body.phone) gr.setValue('phone', body.phone);
        if (body.account_number) gr.setValue('account_number', body.account_number);
        if (body.address) {
          if (body.address.street) gr.setValue('street', body.address.street);
          if (body.address.city) gr.setValue('city', body.address.city);
          if (body.address.state) gr.setValue('state', body.address.state);
          if (body.address.zip) gr.setValue('zip', body.address.zip);
          if (body.address.country) gr.setValue('country', body.address.country);
        }

        gr.setValue('active', true);

        // Check create permission
        if (!gr.canCreate()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to create customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        var sysId = gr.insert();

        if (sysId) {
          // Set Location header for created resource
          response.setHeader('Location', request.url + '/' + sysId);
          response.setStatus(201);
          response.setBody({
            result: {
              sys_id: sysId,
              message: 'Customer created successfully'
            }
          });
        } else {
          response.setStatus(500);
          response.setBody({
            error: {
              message: 'Failed to create customer',
              code: 'INTERNAL_ERROR'
            }
          });
        }

      })(request, response);
    short_description: Create a new customer
    requires_acl_authorization: true
    requires_authentication: true
第 3.4 步:PUT 资源(更新)

创建一个用于更新现有客户的端点。

使用 MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Update Customer
    web_service_definition: [api_definition_sys_id]
    http_method: PUT
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        var customerId = request.pathParams.id;
        var body = request.body.data;

        // Find customer
        var gr = new GlideRecord('customer');
        if (!gr.get(customerId)) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND'
            }
          });
          return;
        }

        // Check update permission
        if (!gr.canWrite()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to update customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Validate email if being changed
        if (body.email && body.email !== gr.getValue('email')) {
          if (!body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
            response.setStatus(400);
            response.setBody({
              error: {
                message: 'Invalid email format',
                code: 'VALIDATION_ERROR'
              }
            });
            return;
          }

          // Check for duplicate
          var existing = new GlideRecord('customer');
          existing.addQuery('email', body.email);
          existing.addQuery('sys_id', '!=', customerId);
          existing.query();
          if (existing.next()) {
            response.setStatus(409);
            response.setBody({
              error: {
                message: 'Another customer with this email exists',
                code: 'DUPLICATE_ERROR'
              }
            });
            return;
          }
        }

        // Update fields
        var updatedFields = [];

        if (body.name !== undefined) {
          gr.setValue('name', body.name);
          updatedFields.push('name');
        }
        if (body.email !== undefined) {
          gr.setValue('email', body.email);
          updatedFields.push('email');
        }
        if (body.phone !== undefined) {
          gr.setValue('phone', body.phone);
          updatedFields.push('phone');
        }
        if (body.active !== undefined) {
          gr.setValue('active', body.active);
          updatedFields.push('active');
        }
        if (body.address) {
          if (body.address.street !== undefined) gr.setValue('street', body.address.street);
          if (body.address.city !== undefined) gr.setValue('city', body.address.city);
          if (body.address.state !== undefined) gr.setValue('state', body.address.state);
          if (body.address.zip !== undefined) gr.setValue('zip', body.address.zip);
          if (body.address.country !== undefined) gr.setValue('country', body.address.country);
          updatedFields.push('address');
        }

        gr.update();

        response.setStatus(200);
        response.setBody({
          result: {
            sys_id: customerId,
            message: 'Customer updated successfully',
            updated_fields: updatedFields
          }
        });

      })(request, response);
    short_description: Update an existing customer
    requires_acl_authorization: true
    requires_authentication: true
第 3.5 步:DELETE 资源

创建一个用于删除客户的端点。

使用 MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Delete Customer
    web_service_definition: [api_definition_sys_id]
    http_method: DELETE
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        var customerId = request.pathParams.id;

        // Find customer
        var gr = new GlideRecord('customer');
        if (!gr.get(customerId)) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND'
            }
          });
          return;
        }

        // Check delete permission
        if (!gr.canDelete()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to delete customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Soft delete vs hard delete decision
        // Option 1: Soft delete (preferred)
        gr.setValue('active', false);
        gr.update();

        // Option 2: Hard delete (use with caution)
        // gr.deleteRecord();

        response.setStatus(200);
        response.setBody({
          result: {
            sys_id: customerId,
            message: 'Customer deactivated successfully'
          }
        });

        // Alternative: Return 204 No Content for true DELETE
        // response.setStatus(204);

      })(request, response);
    short_description: Delete (deactivate) a customer
    requires_acl_authorization: true
    requires_authentication: true
第 4 阶段:身份验证与安全
第 4.1 步:配置 API 级身份验证

API 定义控制身份验证要求:

Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    requires_authentication: true
    enforce_acl: true
    acl_failure_result: UNAUTHORIZED
第 4.2 步:实现 API 密钥身份验证

对于简单集成,实现 API 密钥验证:

创建 API 密钥脚本包含项:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: CustomerAPIAuth
    script: |
      var CustomerAPIAuth = Class.create();
      CustomerAPIAuth.prototype = {
        initialize: function() {
          this.API_KEY_HEADER = 'X-API-Key';
          this.API_KEY_TABLE = 'x_company_api_keys';
        },

        validateApiKey: function(request) {
          var apiKey = request.getHeader(this.API_KEY_HEADER);

          if (!apiKey) {
            return {
              valid: false,
              error: 'API key required',
              code: 'MISSING_API_KEY'
            };
          }

          // Look up API key
          var keyRecord = new GlideRecord(this.API_KEY_TABLE);
          keyRecord.addQuery('key_value', apiKey);
          keyRecord.addQuery('active', true);
          keyRecord.addQuery('expires', '>', new GlideDateTime());
          keyRecord.query();

          if (!keyRecord.next()) {
            return {
              valid: false,
              error: 'Invalid or expired API key',
              code: 'INVALID_API_KEY'
            };
          }

          // Update last used timestamp
          keyRecord.setValue('last_used', new GlideDateTime());
          keyRecord.update();

          return {
            valid: true,
            client_id: keyRecord.getValue('client_id'),
            permissions: keyRecord.getValue('permissions').split(','),
            rate_limit: parseInt(keyRecord.getValue('rate_limit')) || 1000
          };
        },

        type: 'CustomerAPIAuth'
      };
    api_name: CustomerAPIAuth
    active: true
    access: public

在 API 操作中使用:

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  // Validate API key
  var auth = new CustomerAPIAuth();
  var authResult = auth.validateApiKey(request);

  if (!authResult.valid) {
    response.setStatus(401);
    response.setBody({
      error: {
        message: authResult.error,
        code: authResult.code
      }
    });
    return;
  }

  // Check permission
  if (authResult.permissions.indexOf('read:customers') === -1) {
    response.setStatus(403);
    response.setBody({
      error: {
        message: 'Insufficient permissions',
        code: 'FORBIDDEN',
        required: 'read:customers'
      }
    });
    return;
  }

  // Continue with request processing...

})(request, response);
第 4.3 步:实现速率限制

添加速率限制以保护 API:

创建速率限制器脚本包含项:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: APIRateLimiter
    script: |
      var APIRateLimiter = Class.create();
      APIRateLimiter.prototype = {
        initialize: function(clientId, rateLimit) {
          this.clientId = clientId;
          this.rateLimit = rateLimit || 100;  // requests per minute
          this.CACHE_PREFIX = 'api_rate_';
        },

        isAllowed: function() {
          var cacheKey = this.CACHE_PREFIX + this.clientId;
          var cache = new GlideSysCache('APIRateLimit');

          // Get current count
          var countStr = cache.get(cacheKey);
          var count = countStr ? parseInt(countStr) : 0;

          if (count >= this.rateLimit) {
            return {
              allowed: false,
              remaining: 0,
              reset_in: 60
            };
          }

          // Increment count with 60 second TTL
          cache.put(cacheKey, String(count + 1), 60);

          return {
            allowed: true,
            remaining: this.rateLimit - count - 1,
            limit: this.rateLimit
          };
        },

        type: 'APIRateLimiter'
      };
    api_name: APIRateLimiter
    active: true

在 API 中使用速率限制:

// After authentication
var rateLimiter = new APIRateLimiter(authResult.client_id, authResult.rate_limit);
var rateResult = rateLimiter.isAllowed();

response.setHeader('X-RateLimit-Limit', rateResult.limit || authResult.rate_limit);
response.setHeader('X-RateLimit-Remaining', rateResult.remaining);

if (!rateResult.allowed) {
  response.setStatus(429);
  response.setHeader('Retry-After', rateResult.reset_in);
  response.setBody({
    error: {
      message: 'Rate limit exceeded',
      code: 'RATE_LIMITED',
      retry_after: rateResult.reset_in
    }
  });
  return;
}
第 5 阶段:查询参数和路径参数
第 5.1 步:常见查询参数模式
// Pagination
var limit = Math.min(parseInt(request.queryParams.limit) || 20, 100);
var offset = parseInt(request.queryParams.offset) || 0;
var page = parseInt(request.queryParams.page) || 1;
offset = (page - 1) * limit;  // If using page instead of offset

// Sorting
var sortField = request.queryParams.sort || 'name';
var sortOrder = request.queryParams.order || 'asc';
var validSortFields = ['name', 'email', 'created', 'updated'];
if (validSortFields.indexOf(sortField) === -1) {
  sortField = 'name';
}
if (sortOrder === 'desc') {
  gr.orderByDesc(sortField);
} else {
  gr.orderBy(sortField);
}

// Filtering
var filters = {
  active: request.queryParams.active,
  category: request.queryParams.category,
  created_after: request.queryParams.created_after,
  created_before: request.queryParams.created_before
};

if (filters.active !== undefined) {
  gr.addQuery('active', filters.active === 'true');
}
if (filters.category) {
  gr.addQuery('category', filters.category);
}
if (filters.created_after) {
  gr.addQuery('sys_created_on', '>=', filters.created_after);
}
if (filters.created_before) {
  gr.addQuery('sys_created_on', '<=', filters.created_before);
}

// Field selection
var fields = request.queryParams.fields;
if (fields) {
  var fieldList = fields.split(',');
  // Validate fields before use
  var validFields = ['sys_id', 'name', 'email', 'phone', 'active'];
  fieldList = fieldList.filter(function(f) {
    return validFields.indexOf(f.trim()) !== -1;
  });
}

// Search/Text filter
var searchQuery = request.queryParams.q || request.queryParams.search;
if (searchQuery) {
  var qc = gr.addQuery('name', 'CONTAINS', searchQuery);
  qc.addOrCondition('description', 'CONTAINS', searchQuery);
}
第 5.2 步:路径参数模式
// Single path parameter
// URL: /customers/{id}
var customerId = request.pathParams.id;

// Multiple path parameters
// URL: /customers/{customer_id}/orders/{order_id}
var customerId = request.pathParams.customer_id;
var orderId = request.pathParams.order_id;

// Validate path parameters
if (!customerId || customerId.length < 1) {
  response.setStatus(400);
  response.setBody({
    error: {
      message: 'Invalid customer ID',
      code: 'INVALID_PARAMETER'
    }
  });
  return;
}

// Support multiple ID formats
function resolveCustomerId(id) {
  // sys_id (32 char hex)
  if (/^[a-f0-9]{32}$/i.test(id)) {
    return { field: 'sys_id', value: id };
  }
  // Account number (numeric or alphanumeric)
  if (/^[A-Z]{2}\d{6}$/.test(id)) {
    return { field: 'account_number', value: id };
  }
  // Email
  if (id.indexOf('@') !== -1) {
    return { field: 'email', value: id };
  }
  return null;
}

var idInfo = resolveCustomerId(customerId);
if (!idInfo) {
  response.setStatus(400);
  response.setBody({
    error: {
      message: 'Unrecognized ID format',
      code: 'INVALID_ID_FORMAT',
      supported_formats: ['sys_id', 'account_number (XX000000)', 'email']
    }
  });
  return;
}
第 6 阶段:错误处理和状态码
第 6.1 步:标准错误响应格式
// Error response helper
function sendError(response, status, message, code, details) {
  response.setStatus(status);
  var error = {
    error: {
      message: message,
      code: code,
      timestamp: new GlideDateTime().getValue()
    }
  };
  if (details) {
    error.error.details = details;
  }
  response.setBody(error);
}

// Usage examples
sendError(response, 400, 'Invalid request body', 'BAD_REQUEST');
sendError(response, 401, 'Authentication required', 'UNAUTHORIZED');
sendError(response, 403, 'Permission denied', 'FORBIDDEN');
sendError(response, 404, 'Resource not found', 'NOT_FOUND');
sendError(response, 409, 'Conflict with existing resource', 'CONFLICT');
sendError(response, 422, 'Validation failed', 'VALIDATION_ERROR', errors);
sendError(response, 429, 'Rate limit exceeded', 'RATE_LIMITED');
sendError(response, 500, 'Internal server error', 'INTERNAL_ERROR');
sendError(response, 503, 'Service unavailable', 'SERVICE_UNAVAILABLE');
第 6.2 步:全面的错误处理
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  try {
    // Wrap entire request processing in try-catch

    // Validate content type for POST/PUT
    if (['POST', 'PUT', 'PATCH'].indexOf(request.getMethod()) !== -1) {
      var contentType = request.getHeader('Content-Type');
      if (!contentType || contentType.indexOf('application/json') === -1) {
        response.setStatus(415);
        response.setBody({
          error: {
            message: 'Content-Type must be application/json',
            code: 'UNSUPPORTED_MEDIA_TYPE'
          }
        });
        return;
      }
    }

    // Parse body with error handling
    var body;
    try {
      body = request.body.data;
      if (!body && request.body.dataString) {
        body = JSON.parse(request.body.dataString);
      }
    } catch (parseError) {
      response.setStatus(400);
      response.setBody({
        error: {
          message: 'Invalid JSON in request body',
          code: 'PARSE_ERROR',
          detail: parseError.message
        }
      });
      return;
    }

    // Main processing logic here...

  } catch (e) {
    // Log error for debugging
    gs.error('[CustomerAPI] Unhandled error: ' + e.message + '\nStack: ' + e.stack);

    response.setStatus(500);
    response.setBody({
      error: {
        message: 'An unexpected error occurred',
        code: 'INTERNAL_ERROR',
        reference: gs.generateGUID()  // For support reference
      }
    });
  }

})(request, response);
第 6.3 步:HTTP 状态码参考
状态含义使用场景
200成功GET、PUT、PATCH、DELETE 请求成功
201已创建POST 请求成功创建新资源
204无内容DELETE 请求成功且无响应正文
400错误请求语法无效、缺少必填字段
401未经授权缺少身份验证或身份验证无效
403禁止访问已通过身份验证,但未获授权
404未找到资源不存在
405方法不允许端点不支持该 HTTP 方法
409冲突资源重复、状态冲突
415不支持的媒体类型Content-Type 标头错误
422无法处理的实体验证错误
429请求过多超出速率限制
500内部服务器错误意外的服务器错误
503服务不可用临时维护或过载
第 7 阶段:API 版本控制
第 7.1 步:版本配置

更新 API 定义以支持版本控制:

Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    is_versioned: true
    baseline_version: v1
    deprecated_versions: v1-beta
第 7.2 步:特定版本的操作

创建特定版本的端点:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: List Customers v2
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /v2/customers
    operation_script: |
      // V2 version with enhanced response format
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // V2 includes additional fields and different structure
        var customers = [];
        var gr = new GlideRecord('customer');
        gr.query();

        while (gr.next()) {
          customers.push({
            id: gr.getUniqueValue(),
            attributes: {
              name: gr.getValue('name'),
              email: gr.getValue('email'),
              phone: gr.getValue('phone')
            },
            relationships: {
              contacts: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/contacts',
              orders: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/orders'
            },
            meta: {
              created: gr.getValue('sys_created_on'),
              updated: gr.getValue('sys_updated_on')
            }
          });
        }

        response.setStatus(200);
        response.setBody({
          data: customers,
          links: {
            self: request.url,
            next: null,
            prev: null
          }
        });

      })(request, response);
第 7.3 步:弃用标头
// Add deprecation headers for old versions
if (request.url.indexOf('/v1/') !== -1) {
  response.setHeader('Deprecation', 'true');
  response.setHeader('Sunset', 'Sat, 01 Jul 2027 00:00:00 GMT');
  response.setHeader('Link', '</api/x_company/customer_api/v2/customers>; rel="successor-version"');
}
第 8 阶段:文档和测试
第 8.1 步:集成 API Explorer

ServiceNow 会自动生成 API Explorer 文档。对其进行完善:

Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    doc_link: https://docs.company.com/api/customers
    short_description: |
      Customer management API for external integrations.

      Authentication: Basic Auth or API Key (X-API-Key header)
      Rate Limit: 100 requests per minute

      Base URL: https://instance.service-now.com/api/x_company/customer_api/v1
第 8.2 步:使用 curl 测试

GET 请求:

# List customers
curl -X GET \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers?limit=10&offset=0" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)"

# Get specific customer
curl -X GET \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
  -H "Accept: application/json" \
  -H "X-API-Key: your-api-key-here"

POST 请求:

curl -X POST \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)" \
  -d '{
    "name": "Acme Corporation",
    "email": "[email protected]",
    "phone": "555-1234",
    "address": {
      "street": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "zip": "62701"
    }
  }'

PUT 请求:

curl -X PUT \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)" \
  -d '{
    "name": "Acme Corporation Updated",
    "phone": "555-5678"
  }'

DELETE 请求:

curl -X DELETE \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)"
第 8.3 步:使用 Postman 测试

Postman 集合结构:

{
  "info": {
    "name": "Customer API",
    "description": "ServiceNow Customer Integration API"
  },
  "variable": [
    { "key": "base_url", "value": "https://dev12345.service-now.com" },
    { "key": "api_path", "value": "/api/x_company/customer_api/v1" }
  ],
  "auth": {
    "type": "basic",
    "basic": [
      { "key": "username", "value": "{{username}}" },
      { "key": "password", "value": "{{password}}" }
    ]
  },
  "item": [
    {
      "name": "List Customers",
      "request": {
        "method": "GET",
        "url": "{{base_url}}{{api_path}}/customers?limit=10"
      }
    },
    {
      "name": "Create Customer",
      "request": {
        "method": "POST",
        "url": "{{base_url}}{{api_path}}/customers",
        "body": {
          "mode": "raw",
          "raw": "{ \"name\": \"Test Customer\", \"email\": \"[email protected]\" }"
        }
      }
    }
  ]
}

Postman 测试:

// Test response status
pm.test("Status code is 200", function () {
  pm.response.to.have.status(200);
});

// Test response structure
pm.test("Response has result array", function () {
  var jsonData = pm.response.json();
  pm.expect(jsonData).to.have.property('result');
  pm.expect(jsonData.result).to.be.an('array');
});

// Test pagination
pm.test("Response has meta with pagination", function () {
  var jsonData = pm.response.json();
  pm.expect(jsonData.meta).to.have.property('total');
  pm.expect(jsonData.meta).to.have.property('limit');
  pm.expect(jsonData.meta).to.have.property('offset');
});

// Save sys_id for subsequent requests
pm.test("Save created customer ID", function () {
  var jsonData = pm.response.json();
  if (jsonData.result && jsonData.result.sys_id) {
    pm.environment.set("customer_id", jsonData.result.sys_id);
  }
});
第 9 阶段:发布和激活
第 9.1 步:发布前检查清单

发布前,请确认:

Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_operation
  query: web_service_definition=[api_definition_sys_id]
  fields: name,http_method,relative_path,requires_authentication

检查清单:

  • 所有操作均已启用身份验证
  • 所有脚本均已实现错误处理
  • 已配置速率限制
  • 已完成输入验证
  • 已启用 ACL 强制执行
  • 已更新文档
  • 测试用例均已通过
  • 已捕获更新集
第 9.2 步:发布 API
Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    published: true
    active: true
第 9.3 步:验证发布
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_definition
  query: sys_id=[api_definition_sys_id]
  fields: name,active,published,service_address

现在可以通过 service_address URL 访问该 API。

工具使用摘要

操作MCP 工具用途
创建 APISN-Create-Record (sys_ws_definition)创建 API 容器
创建资源SN-Create-Record (sys_ws_operation)添加端点
更新 APISN-Update-Record修改设置
查询 APISN-Query-Table列出/验证 API
架构发现SN-Get-Table-Schema了解表结构
测试脚本SN-Execute-Background-Script测试 API 逻辑

最佳实践

  • 先设计: 实施前规划 API 结构
  • 从一开始就进行版本控制: 始终启用版本控制(is_versioned: true)
  • 保持响应一致: 使用标准错误/成功格式
  • 验证所有内容: 处理前验证输入
  • 默认安全: 为所有端点启用身份验证
  • 完善文档: 使用 short_description 和 doc_link
  • 充分测试: 发布前使用 curl/Postman
  • 监控使用情况: 记录 API 调用以便调试和收集指标
  • 限制速率: 防止滥用
  • 使用脚本包含项: 集中管理可复用逻辑

故障排除

API 返回 404

症状: 未找到 API 端点 原因:

  • API 未发布/未启用
  • URL 路径不正确
  • 命名空间错误 解决方案:
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_definition
  query: name=[your_api_name]
  fields: active,published,service_address,namespace
401 未授权

症状: 身份验证失败 原因:

  • 凭据无效
  • 用户没有 REST API 访问权限
  • 缺少 web_service_admin 角色 解决方案:
  1. 首先使用 Table API 验证凭据
  2. 检查用户是否拥有 rest_service 角色
  3. 如果使用自定义身份验证,请验证 API 密钥
403 禁止访问

症状: 身份验证后访问被拒绝 原因:

  • ACL 阻止访问
  • 缺少表权限
  • 范围应用限制 解决方案:
  1. 检查底层表上的 ACL
  2. 确认用户角色符合 ACL 要求
  3. 检查 API 上的 enforce_acl 设置
500 内部服务器错误

症状: 服务器错误,但没有详细信息 原因:

  • 操作中脚本出错
  • GlideRecord 查询无效
  • 脚本中存在空引用 解决方案:
  1. 在 System Logs 中检查脚本错误
  2. 在操作脚本中添加 try-catch
  3. 先在后台脚本中测试脚本
响应正文为空

症状: 状态为 200,但没有正文 原因:

  • 缺少 response.setBody()
  • 脚本提前终止
  • 在返回前设置正文 解决方案:
  1. 确认已调用 response.setBody()
  2. 检查所有代码路径是否都设置了响应
  3. 确保没有未设置响应就提前返回的情况
脚本未执行

症状: API 返回了响应,但脚本未运行 原因:

  • 脚本语法错误
  • 操作未关联到 API
  • HTTP 方法错误 解决方案:
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_operation
  query: web_service_definition=[api_sys_id]
  fields: name,http_method,active,operation_script

示例

示例 1:最简 CRUD API

包含基本 CRUD 的完整最简 API:

# 1. Create API Definition
Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_definition
  data:
    name: Simple Task API
    namespace: x_company
    short_description: Simple task management API
    active: true
    is_versioned: true
    baseline_version: v1

# 2. Create List Operation
Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: List Tasks
    web_service_definition: [api_sys_id]
    http_method: GET
    relative_path: /tasks
    operation_script: |
      (function process(request, response) {
        var gr = new GlideRecord('x_company_task');
        gr.setLimit(100);
        gr.query();
        var tasks = [];
        while (gr.next()) {
          tasks.push({
            sys_id: gr.getUniqueValue(),
            name: gr.getValue('name'),
            status: gr.getValue('status')
          });
        }
        response.setBody({ result: tasks });
      })(request, response);

# 3. Create Get Operation
Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Get Task
    web_service_definition: [api_sys_id]
    http_method: GET
    relative_path: /tasks/{id}
    operation_script: |
      (function process(request, response) {
        var gr = new GlideRecord('x_company_task');
        if (gr.get(request.pathParams.id)) {
          response.setBody({
            result: {
              sys_id: gr.getUniqueValue(),
              name: gr.getValue('name'),
              description: gr.getValue('description'),
              status: gr.getValue('status')
            }
          });
        } else {
          response.setStatus(404);
          response.setBody({ error: { message: 'Task not found' }});
        }
      })(request, response);
示例 2:带聚合的搜索 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  var searchTerm = request.queryParams.q;
  var category = request.queryParams.category;
  var includeStats = request.queryParams.stats === 'true';

  // Search incidents
  var gr = new GlideRecord('incident');

  if (searchTerm) {
    var qc = gr.addQuery('short_description', 'CONTAINS', searchTerm);
    qc.addOrCondition('description', 'CONTAINS', searchTerm);
    qc.addOrCondition('number', searchTerm);
  }

  if (category) {
    gr.addQuery('category', category);
  }

  gr.addQuery('active', true);
  gr.setLimit(50);
  gr.orderByDesc('sys_created_on');
  gr.query();

  var results = [];
  while (gr.next()) {
    results.push({
      number: gr.getValue('number'),
      short_description: gr.getValue('short_description'),
      priority: gr.getDisplayValue('priority'),
      state: gr.getDisplayValue('state'),
      created: gr.getValue('sys_created_on')
    });
  }

  var responseBody = {
    query: searchTerm,
    count: results.length,
    results: results
  };

  // Add statistics if requested
  if (includeStats) {
    var stats = {};

    // Count by priority
    var priorityAgg = new GlideAggregate('incident');
    priorityAgg.addQuery('active', true);
    priorityAgg.addAggregate('COUNT');
    priorityAgg.groupBy('priority');
    priorityAgg.query();

    stats.by_priority = {};
    while (priorityAgg.next()) {
      var priority = priorityAgg.getDisplayValue('priority');
      stats.by_priority[priority] = parseInt(priorityAgg.getAggregate('COUNT'));
    }

    responseBody.stats = stats;
  }

  response.setStatus(200);
  response.setBody(responseBody);

})(request, response);
示例 3:批量操作 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  var body = request.body.data;

  // Validate bulk request
  if (!body.operations || !Array.isArray(body.operations)) {
    response.setStatus(400);
    response.setBody({
      error: {
        message: 'operations array required',
        code: 'VALIDATION_ERROR'
      }
    });
    return;
  }

  if (body.operations.length > 100) {
    response.setStatus(400);
    response.setBody({
      error: {
        message: 'Maximum 100 operations per request',
        code: 'LIMIT_EXCEEDED'
      }
    });
    return;
  }

  var results = [];

  body.operations.forEach(function(op, index) {
    var result = { index: index, operation: op.action };

    try {
      switch (op.action) {
        case 'create':
          var gr = new GlideRecord('customer');
          gr.initialize();
          for (var field in op.data) {
            gr.setValue(field, op.data[field]);
          }
          result.sys_id = gr.insert();
          result.success = !!result.sys_id;
          break;

        case 'update':
          var grUpdate = new GlideRecord('customer');
          if (grUpdate.get(op.sys_id)) {
            for (var updateField in op.data) {
              grUpdate.setValue(updateField, op.data[updateField]);
            }
            grUpdate.update();
            result.success = true;
          } else {
            result.success = false;
            result.error = 'Record not found';
          }
          break;

        case 'delete':
          var grDelete = new GlideRecord('customer');
          if (grDelete.get(op.sys_id)) {
            grDelete.setValue('active', false);
            grDelete.update();
            result.success = true;
          } else {
            result.success = false;
            result.error = 'Record not found';
          }
          break;

        default:
          result.success = false;
          result.error = 'Unknown action: ' + op.action;
      }
    } catch (e) {
      result.success = false;
      result.error = e.message;
    }

    results.push(result);
  });

  var successCount = results.filter(function(r) { return r.success; }).length;

  response.setStatus(successCount === results.length ? 200 : 207);  // 207 = Multi-Status
  response.setBody({
    summary: {
      total: results.length,
      succeeded: successCount,
      failed: results.length - successCount
    },
    results: results
  });

})(request, response);

相关技能

  • admin/update-set-management - 在更新集中捕获 API 定义
  • admin/application-scope - 在范围应用中创建 API
  • security/acl-management - 配置 API 安全性
  • admin/script-execution - 测试 API 脚本
  • catalog/request-fulfillment - 由 API 驱动的履行流程

参考资料

评论

支持 Markdown