Skip to main content
Glama
marco-looy
by marco-looy
client-v1.js18.1 kB
import { BaseApiClient } from '../base-api-client.js'; /** * Traditional DX API (V1) Client * * Implements Pega's Traditional DX API (V1) with transformation to V2-like response structure * for consistency across the MCP server. * * Key V1 Characteristics: * - Base URL: /api/v1/ * - Flat JSON response structures * - No eTag support (no optimistic locking) * - Different error response formats * - No UI metadata separation * - Limited to basic CRUD operations * * V1-Exclusive Features: * - GET /cases - Get all cases for authenticated user * - PUT /cases/{ID} - Direct case update * - GET /assignments - Get all assignments * - PUT /casetypes/{ID}/refresh - Refresh case type metadata * * @extends BaseApiClient */ export class PegaV1Client extends BaseApiClient { /** * Get API version identifier * @returns {string} 'v1' */ getApiVersion() { return 'v1'; } /** * Get base URL for V1 API * @returns {string} Base URL ending with /api/v1 */ getApiBaseUrl() { return `${this.config.pega.baseUrl}/prweb/api/v1`; } /** * Handle V1-specific error responses * * V1 Error Format: * { * "pxObjClass": "Pega-API-CaseManagement", * "errors": [ * { * "ID": "Pega_API_019", * "message": "Insufficient privilege", * "pxObjClass": "Pega-API-Error" * } * ] * } * * @param {Response} response - HTTP response object * @returns {Promise<Object>} Structured error response */ async handleErrorResponse(response) { let errorData; try { errorData = await response.json(); } catch (e) { // Can't read body twice - use status text as fallback errorData = { message: response.statusText || 'Unknown error' }; } const errorResponse = { success: false, error: { status: response.status, statusText: response.statusText, apiVersion: 'v1' } }; // V1 uses an errors array if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) { const primaryError = errorData.errors[0]; errorResponse.error.type = this.mapV1ErrorType(response.status, primaryError.ID); errorResponse.error.message = this.mapV1ErrorMessage(response.status); errorResponse.error.details = primaryError.message || 'Unknown error'; errorResponse.error.errorCode = primaryError.ID; errorResponse.error.errorClass = errorData.pxObjClass; errorResponse.error.errors = errorData.errors; } else { // Fallback for non-standard error format errorResponse.error.type = 'HTTP_ERROR'; errorResponse.error.message = `HTTP ${response.status} error`; errorResponse.error.details = errorData.message || response.statusText; } return errorResponse; } /** * Map V1 error code to error type * @private */ mapV1ErrorType(statusCode, errorCode) { // Common error code mappings const errorCodeMap = { 'Pega_API_001': 'BAD_REQUEST', 'Pega_API_002': 'BAD_REQUEST', 'Pega_API_003': 'NOT_FOUND', 'Pega_API_019': 'FORBIDDEN', 'Pega_API_020': 'NOT_FOUND', 'Pega_API_023': 'NOT_FOUND' }; if (errorCode && errorCodeMap[errorCode]) { return errorCodeMap[errorCode]; } // Fallback to status code mapping const statusMap = { 400: 'BAD_REQUEST', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 409: 'CONFLICT', 500: 'INTERNAL_SERVER_ERROR' }; return statusMap[statusCode] || 'HTTP_ERROR'; } /** * Map V1 status code to user-friendly message * @private */ mapV1ErrorMessage(statusCode) { const messageMap = { 400: 'Invalid request parameters', 401: 'Authentication failed', 403: 'Access denied - insufficient privileges', 404: 'Resource not found', 409: 'Conflict error', 500: 'Internal server error' }; return messageMap[statusCode] || `HTTP ${statusCode} error`; } /** * Transform V1 case response to V2-like structure * * Extracts system properties and content to match V2's separated structure * * @param {Object} v1Case - V1 case response * @returns {Object} V2-like case response */ transformCaseResponse(v1Case) { // System properties that go into caseInfo const systemProps = ['ID', 'status', 'urgency', 'stage', 'caseTypeID', 'parentCaseID', 'createTime', 'lastUpdateTime', 'createdBy', 'lastUpdatedBy', 'name']; const caseInfo = {}; const content = {}; // Separate system properties from content for (const [key, value] of Object.entries(v1Case)) { if (systemProps.includes(key)) { caseInfo[key] = value; // Convert urgency string to number if present if (key === 'urgency' && typeof value === 'string') { caseInfo[key] = parseInt(value) || 0; } } else if (!this.isSystemProperty(key)) { // Only include non-system properties in content content[key] = value; } } // Add content to caseInfo caseInfo.content = content; // Add assignments and actions if present if (v1Case.assignments) { caseInfo.assignments = v1Case.assignments; } if (v1Case.actions) { caseInfo.availableActions = v1Case.actions; } return { data: { caseInfo }, eTag: null, // V1 doesn't support eTags uiResources: null // V1 doesn't separate UI resources }; } /** * Transform V1 assignment response to V2-like structure * * @param {Object} v1Assignment - V1 assignment response * @returns {Object} V2-like assignment response */ transformAssignmentResponse(v1Assignment) { const assignmentInfo = { ID: v1Assignment.ID, name: v1Assignment.name, type: v1Assignment.type, caseID: v1Assignment.caseID, instructions: v1Assignment.instructions }; // Add actions if present if (v1Assignment.actions) { assignmentInfo.actions = v1Assignment.actions; } return { data: { assignmentInfo, caseInfo: v1Assignment.case ? { ID: v1Assignment.case.ID, status: v1Assignment.case.status } : null }, eTag: null, uiResources: null }; } // ======================================== // CASES ENDPOINTS // ======================================== /** * Get all cases created by authenticated user * V1 EXCLUSIVE - Not available in V2 (use Data Views instead) * * Note: Number of cases returned is controlled by pyMaxRecords DSS (default 500) * * @returns {Promise<Object>} Response with cases array * @throws {Error} If user lacks pxGetCases privilege * * @example * const result = await client.getAllCases(); * console.log(result.data.cases.length); */ async getAllCases() { const url = `${this.getApiBaseUrl()}/cases`; const response = await this.makeRequest(url, { method: 'GET', headers: { 'x-origin-channel': 'Web' } }); if (!response.success) { return response; } // Transform response for consistency return { success: true, data: { cases: response.data.cases.map(c => ({ ID: c.ID, parentCaseID: c.parentCaseID, caseTypeID: c.caseTypeID, name: c.name, stage: c.stage, status: c.status, urgency: parseInt(c.urgency) || 0, createTime: c.createTime, createdBy: c.createdBy, lastUpdateTime: c.lastUpdateTime, lastUpdatedBy: c.lastUpdatedBy })) }, metadata: { count: response.data.cases.length, maxRecords: 500, // From pyMaxRecords DSS apiVersion: 'v1' } }; } /** * Create a new case * * @param {Object} options - Case creation options * @param {string} options.caseTypeID - Case type ID (required) * @param {string} [options.processID='pyStartCase'] - Process ID (V1 specific, default: pyStartCase) * @param {Object} [options.content={}] - Case content/data * @param {string} [options.parentCaseID] - Parent case ID for child cases * @returns {Promise<Object>} Response with created case ID and next assignment ID * * @example * const result = await client.createCase({ * caseTypeID: 'MyCo-PAC-Work-ExpenseReport', * content: { * EmployeeName: 'John Doe', * ExpenseAmount: '500.00' * } * }); */ async createCase(options = {}) { const { caseTypeID, processID = 'pyStartCase', content = {}, parentCaseID } = options; if (!caseTypeID) { return { success: false, error: { type: 'BAD_REQUEST', message: 'caseTypeID is required', details: 'caseTypeID parameter must be provided' } }; } const url = `${this.getApiBaseUrl()}/cases`; const requestBody = { caseTypeID, processID, content }; if (parentCaseID) { requestBody.parentCaseID = parentCaseID; } const response = await this.makeRequest(url, { method: 'POST', headers: { 'x-origin-channel': 'Web' }, body: JSON.stringify(requestBody) }); if (!response.success) { return response; } // Transform to V2-like structure return { success: true, data: { caseInfo: { ID: response.data.ID, nextAssignmentID: response.data.nextAssignmentID } }, metadata: { apiVersion: 'v1' } }; } /** * Get case by ID * * @param {string} caseID - Case ID * @param {Object} options - Optional parameters (unused in V1, kept for compatibility) * @returns {Promise<Object>} Case details in V2-like structure * * @example * const result = await client.getCase('MYCO-PAC-WORK E-26'); */ async getCase(caseID, options = {}) { const encodedID = this.encodeParam(caseID); const url = `${this.getApiBaseUrl()}/cases/${encodedID}`; const response = await this.makeRequest(url, { method: 'GET', headers: { 'x-origin-channel': 'Web' } }); if (!response.success) { return response; } // Transform to V2-like structure const transformed = this.transformCaseResponse(response.data); return { success: true, ...transformed, eTag: response.eTag || null // Preserve eTag from response header }; } /** * Update case * V1 EXCLUSIVE - V2 uses case actions instead * * Performs case-wide local action or stage-wide local action on the case. * If actionID is not specified, pyUpdateCaseDetails is performed by default. * If eTag is not provided, automatically fetches latest eTag from case. * * @param {string} caseID - Case ID * @param {Object} options - Update options * @param {Object} options.content - Updated content properties * @param {string} [options.actionID] - Optional action ID (defaults to pyUpdateCaseDetails) * @param {string} [options.eTag] - Optional eTag for optimistic locking. If not provided, automatically fetches latest eTag. * @param {Array} [options.pageInstructions] - Optional page-related operations * @param {Array} [options.attachments] - Optional attachments to add * @returns {Promise<Object>} Success response (204 No Content) * * @example * // Simple update (eTag auto-fetched) * const result = await client.updateCase('MYCO-PAC-WORK E-26', { * content: { * ExpenseAmount: '600.00', * Status: 'Approved' * } * }); * * @example * // Update with manual eTag * const result = await client.updateCase('MYCO-PAC-WORK E-26', { * content: { ExpenseAmount: '750.00' }, * eTag: '20250116T120000.000 GMT' * }); * * @example * // Update with specific action * const result = await client.updateCase('MYCO-PAC-WORK E-26', { * content: { Status: 'Approved' }, * actionID: 'ApproveCase' * }); */ async updateCase(caseID, options = {}) { const { content = {}, actionID, eTag, pageInstructions = [], attachments = [] } = options; // Auto-fetch eTag if not provided (V1 API requires eTag for updates) let finalETag = eTag; let autoFetchedETag = false; if (!finalETag) { console.log(`Auto-fetching latest eTag for case ${caseID}...`); const caseResponse = await this.getCase(caseID); if (!caseResponse.success) { return { success: false, error: { type: 'AUTO_FETCH_FAILED', message: 'Failed to auto-fetch eTag', details: caseResponse.error?.message || 'Could not retrieve case to obtain eTag', originalError: caseResponse.error } }; } finalETag = caseResponse.eTag; autoFetchedETag = true; if (!finalETag) { return { success: false, error: { type: 'ETAG_MISSING', message: 'eTag required for case update', details: 'Auto-fetch succeeded but no eTag was returned from getCase. This may indicate a server issue.' } }; } console.log(`Successfully auto-fetched eTag: ${finalETag}`); } const encodedID = this.encodeParam(caseID); let url = `${this.getApiBaseUrl()}/cases/${encodedID}`; // Add actionID as query parameter if provided if (actionID) { url += `?actionID=${encodeURIComponent(actionID)}`; } // Build request body const requestBody = { content }; // Add pageInstructions if provided if (pageInstructions.length > 0) { requestBody.pageInstructions = pageInstructions; } // Add attachments if provided if (attachments.length > 0) { requestBody.attachments = attachments; } // Build headers with required eTag const headers = { 'x-origin-channel': 'Web', 'if-match': finalETag } const response = await this.makeRequest(url, { method: 'PUT', headers, body: JSON.stringify(requestBody) }); if (!response.success) { return response; } // PUT /cases/{ID} returns 204 No Content // Response data might be empty, so handle that case if (response.status === 204 || !response.data) { return { success: true, data: { message: 'Case updated successfully', caseID: caseID }, eTag: response.eTag || null, // New eTag from response header metadata: { statusCode: 204, apiVersion: 'v1', autoFetchedETag } }; } // If response has data, transform it const transformed = this.transformCaseResponse(response.data); return { success: true, ...transformed, eTag: response.eTag || null, // New eTag from response header metadata: { apiVersion: 'v1', autoFetchedETag } }; } // ======================================== // CASE TYPES ENDPOINTS // ======================================== /** * Get list of case types that the user can create * * @returns {Promise<Object>} API response with case types list */ async getCaseTypes() { const url = `${this.getApiBaseUrl()}/casetypes`; return await this.makeRequest(url, { method: 'GET', headers: { 'x-origin-channel': 'Web' } }); } // ======================================== // DATA ENDPOINTS // ======================================== /** * Get list of available data objects * Note: V1 may not have this endpoint - providing stub for tool compatibility * * @param {Object} options - Optional parameters * @param {string} options.type - Data object type filter ("data" or "case") * @returns {Promise<Object>} API response with data objects list */ async getDataObjects(options = {}) { // V1 may not support this endpoint - return error for now // TODO: Check if V1 has equivalent endpoint return { success: false, error: { type: 'NOT_SUPPORTED', message: 'getDataObjects is not supported in V1 API', details: 'This feature is only available in Constellation DX API (V2). V1 API does not provide data object introspection.' } }; } /** * Get data view metadata by data view ID * Note: V1 may not have this endpoint - providing stub for tool compatibility * * @param {string} dataViewID - ID of the data view to retrieve metadata for * @returns {Promise<Object>} API response with data view metadata */ async getDataViewMetadata(dataViewID) { // V1 may not support this endpoint - return error for now // TODO: Check if V1 has equivalent endpoint return { success: false, error: { type: 'NOT_SUPPORTED', message: 'getDataViewMetadata is not supported in V1 API', details: 'This feature is only available in Constellation DX API (V2). V1 API does not provide data view metadata introspection.' } }; } // Additional V1 methods will be added as Stage 5 progresses // This provides a solid foundation for Stage 4 completion /** * Test connectivity with V1-specific ping * @override * @returns {Promise<Object>} Ping test results with V1 API info */ async ping() { const result = await super.ping(); // Add V1-specific notes if (result.success) { result.data.v1Notes = { features: [ 'GET /cases - List all cases', 'PUT /cases/{ID} - Direct case update', 'No eTag support', 'Flat response structure' ], limitations: [ 'No participants support', 'No followers support', 'No tags support', 'No stage navigation', 'Maximum 500 cases per GET /cases' ] }; } return result; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/marco-looy/pega-dx-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server