UNPKG

@uipath/uipath-typescript

Version:
1,572 lines (1,542 loc) 224 kB
'use strict'; var zod = require('zod'); var sdkLogs = require('@opentelemetry/sdk-logs'); var mimeTypes = require('mime-types'); var axios = require('axios'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var mimeTypes__namespace = /*#__PURE__*/_interopNamespaceDefault(mimeTypes); zod.z.object({ baseUrl: zod.z.string().url().default('https://cloud.uipath.com'), orgName: zod.z.string().min(1), tenantName: zod.z.string().min(1), secret: zod.z.string().optional(), clientId: zod.z.string().optional(), redirectUri: zod.z.string().url().optional(), scope: zod.z.string().optional() }); class UiPathConfig { constructor(options) { this.baseUrl = options.baseUrl; this.orgName = options.orgName; this.tenantName = options.tenantName; this.secret = options.secret; this.clientId = options.clientId; this.redirectUri = options.redirectUri; this.scope = options.scope; } } /** * ExecutionContext manages the state and context of API operations. * It provides a way to share context across service calls and maintain * execution state throughout the lifecycle of operations. */ class ExecutionContext { constructor() { this.context = new Map(); this.headers = {}; } /** * Set a context value that will be available throughout the execution */ set(key, value) { this.context.set(key, value); } /** * Get a previously set context value */ get(key) { return this.context.get(key); } /** * Set custom headers that will be included in all API requests */ setHeaders(headers) { this.headers = { ...this.headers, ...headers }; } /** * Get all custom headers */ getHeaders() { return { ...this.headers }; } /** * Clear all context and headers */ clear() { this.context.clear(); this.headers = {}; } /** * Create a request spec for an API call */ createRequestSpec(spec = {}) { return { ...spec, headers: { ...this.getHeaders(), ...spec.headers } }; } } /** * Base error class for all UiPath SDK errors * Pure TypeScript class with clean interface */ class UiPathError { constructor(type, params) { this.type = type; this.message = params.message; this.statusCode = params.statusCode; this.requestId = params.requestId; this.timestamp = new Date(); // Capture stack trace for debugging this.stack = (new Error()).stack; } /** * Returns a clean JSON representation of the error */ toJSON() { return { type: this.type, message: this.message, statusCode: this.statusCode, requestId: this.requestId, timestamp: this.timestamp }; } /** * Returns detailed debug information including stack trace */ getDebugInfo() { return { ...this.toJSON(), stack: this.stack }; } } /** * HTTP status code constants for error handling */ const HttpStatus = { // Client errors (4xx) BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, TOO_MANY_REQUESTS: 429, // Server errors (5xx) INTERNAL_SERVER_ERROR: 500, NOT_IMPLEMENTED: 501, BAD_GATEWAY: 502, SERVICE_UNAVAILABLE: 503, GATEWAY_TIMEOUT: 504 }; /** * Error type constants for consistent error identification */ const ErrorType = { AUTHENTICATION: 'AuthenticationError', AUTHORIZATION: 'AuthorizationError', VALIDATION: 'ValidationError', NOT_FOUND: 'NotFoundError', RATE_LIMIT: 'RateLimitError', SERVER: 'ServerError', NETWORK: 'NetworkError' }; /** * HTTP header constants for error handling */ const HttpHeaders = { X_REQUEST_ID: 'x-request-id' }; /** * Standard error message constants */ const ErrorMessages = { // Authentication errors AUTHENTICATION_FAILED: 'Authentication failed', // Authorization errors ACCESS_DENIED: 'Access denied', // Validation errors VALIDATION_FAILED: 'Validation failed', // Not found errors RESOURCE_NOT_FOUND: 'Resource not found', // Rate limit errors RATE_LIMIT_EXCEEDED: 'Rate limit exceeded', // Server errors INTERNAL_SERVER_ERROR: 'Internal Server error occurred', // Network errors NETWORK_ERROR: 'Network error occurred', REQUEST_TIMEOUT: 'Request timed out', REQUEST_ABORTED: 'Request was aborted', }; /** * Error name constants for network error identification */ const ErrorNames = { ABORT_ERROR: 'AbortError'}; /** * Error thrown when authentication fails (401 errors) * Common scenarios: * - Invalid credentials * - Expired token * - Missing authentication */ class AuthenticationError extends UiPathError { constructor(params = {}) { super(ErrorType.AUTHENTICATION, { message: params.message || ErrorMessages.AUTHENTICATION_FAILED, statusCode: params.statusCode ?? HttpStatus.UNAUTHORIZED, requestId: params.requestId }); } } /** * Error thrown when authorization fails (403 errors) * Common scenarios: * - Insufficient permissions * - Access denied to resource * - Invalid scope */ class AuthorizationError extends UiPathError { constructor(params = {}) { super(ErrorType.AUTHORIZATION, { message: params.message || ErrorMessages.ACCESS_DENIED, statusCode: params.statusCode ?? HttpStatus.FORBIDDEN, requestId: params.requestId }); } } /** * Error thrown when validation fails (400 errors or client-side validation) * Common scenarios: * - Invalid input parameters * - Missing required fields * - Invalid data format */ class ValidationError extends UiPathError { constructor(params = {}) { super(ErrorType.VALIDATION, { message: params.message || ErrorMessages.VALIDATION_FAILED, statusCode: params.statusCode ?? HttpStatus.BAD_REQUEST, requestId: params.requestId }); } } /** * Error thrown when a resource is not found (404 errors) * Common scenarios: * - Resource doesn't exist * - Invalid ID provided * - Resource deleted */ class NotFoundError extends UiPathError { constructor(params = {}) { super(ErrorType.NOT_FOUND, { message: params.message || ErrorMessages.RESOURCE_NOT_FOUND, statusCode: params.statusCode ?? HttpStatus.NOT_FOUND, requestId: params.requestId }); } } /** * Error thrown when rate limit is exceeded (429 errors) * Common scenarios: * - Too many requests in a time window * - API throttling */ class RateLimitError extends UiPathError { constructor(params = {}) { super(ErrorType.RATE_LIMIT, { message: params.message || ErrorMessages.RATE_LIMIT_EXCEEDED, statusCode: params.statusCode ?? HttpStatus.TOO_MANY_REQUESTS, requestId: params.requestId }); } } /** * Error thrown when server encounters an error (5xx errors) * Common scenarios: * - Internal server error * - Service unavailable * - Gateway timeout */ class ServerError extends UiPathError { constructor(params = {}) { super(ErrorType.SERVER, { message: params.message || ErrorMessages.INTERNAL_SERVER_ERROR, statusCode: params.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR, requestId: params.requestId }); } /** * Checks if this is a temporary error that might succeed on retry */ get isRetryable() { return this.statusCode === HttpStatus.BAD_GATEWAY || this.statusCode === HttpStatus.SERVICE_UNAVAILABLE || this.statusCode === HttpStatus.GATEWAY_TIMEOUT; } } /** * Error thrown when network/connection issues occur * Common scenarios: * - Connection timeout * - DNS resolution failure * - Network unreachable * - Request aborted */ class NetworkError extends UiPathError { constructor(params = {}) { super(ErrorType.NETWORK, { message: params.message || ErrorMessages.NETWORK_ERROR, statusCode: params.statusCode, // Network errors typically don't have HTTP status codes requestId: params.requestId }); } } /** * Type guard to check if an error is a UiPathError */ function isUiPathError(error) { return error instanceof UiPathError; } /** * Type guard to check if an error is an AuthenticationError */ function isAuthenticationError(error) { return error instanceof AuthenticationError; } /** * Type guard to check if an error is an AuthorizationError */ function isAuthorizationError(error) { return error instanceof AuthorizationError; } /** * Type guard to check if an error is a ValidationError */ function isValidationError(error) { return error instanceof ValidationError; } /** * Type guard to check if an error is a NotFoundError */ function isNotFoundError(error) { return error instanceof NotFoundError; } /** * Type guard to check if an error is a RateLimitError */ function isRateLimitError(error) { return error instanceof RateLimitError; } /** * Type guard to check if an error is a ServerError */ function isServerError(error) { return error instanceof ServerError; } /** * Type guard to check if an error is a NetworkError */ function isNetworkError(error) { return error instanceof NetworkError; } /** * Helper to get error details in a safe way */ function getErrorDetails(error) { if (isUiPathError(error)) { return { message: error.message, statusCode: error.statusCode }; } if (error instanceof Error) { return { message: error.message }; } return { message: String(error) }; } /** * Type guards for error response types */ function isOrchestratorError(error) { return typeof error === 'object' && error !== null && 'message' in error && 'errorCode' in error && typeof error.message === 'string' && typeof error.errorCode === 'number'; } function isEntityError(error) { return typeof error === 'object' && error !== null && 'error' in error && typeof error.error === 'string'; } function isPimsError(error) { return typeof error === 'object' && error !== null && 'type' in error && 'title' in error && 'status' in error && typeof error.type === 'string' && typeof error.title === 'string' && typeof error.status === 'number'; } /** * Parser for Orchestrator/Task error format */ class OrchestratorErrorParser { canParse(errorBody) { return isOrchestratorError(errorBody); } parse(errorBody, response) { const error = errorBody; return { message: error.message, code: response?.status?.toString(), details: { errorCode: error.errorCode, traceId: error.traceId, originalResponse: error }, requestId: error.traceId }; } } /** * Parser for Entity (Data Fabric) error format */ class EntityErrorParser { canParse(errorBody) { return isEntityError(errorBody); } parse(errorBody, response) { const error = errorBody; return { message: error.error, code: response?.status?.toString(), details: { error: error.error, traceId: error.traceId, originalResponse: error }, requestId: error.traceId }; } } /** * Parser for PIMS error format */ class PimsErrorParser { canParse(errorBody) { return isPimsError(errorBody); } parse(errorBody, response) { const error = errorBody; let message = error.title; // If there are validation errors, append them to the message for better visibility if (error.errors && Object.keys(error.errors).length > 0) { const errorMessages = Object.entries(error.errors) .map(([field, messages]) => `${field}: ${messages.join(', ')}`) .join('; '); message += `. Validation errors: ${errorMessages}`; } return { message, code: response?.status?.toString(), details: { type: error.type, title: error.title, status: error.status, errors: error.errors, traceId: error.traceId, originalResponse: error }, requestId: error.traceId }; } } /** * Fallback parser for unrecognized formats */ class GenericErrorParser { canParse(_errorBody) { return true; // Always can parse as last resort } parse(errorBody, response) { // For unknown error formats, just pass through the raw error with fallback message const message = response?.statusText || 'An error occurred'; return { message, code: response?.status?.toString(), details: { originalResponse: errorBody }, }; } } /** * Main error response parser using Chain of Responsibility pattern * * This parser standardizes error responses from different UiPath services into a * consistent format, regardless of the original error structure. * * Supported formats: * 1. Orchestrator/Task: { message, errorCode, traceId } * 2. Entity (Data Fabric): { error, traceId } * 3. PIMS/Maestro: { type, title, status, errors?, traceId? } * 4. Generic: Fallback for any other format * * @example * const parser = new ErrorResponseParser(); * const errorInfo = await parser.parse(response); * // errorInfo will have consistent structure regardless of service */ class ErrorResponseParser { constructor() { this.strategies = [ new OrchestratorErrorParser(), new EntityErrorParser(), new PimsErrorParser(), new GenericErrorParser() // Must be last ]; } /** * Parses error response body into standardized format * @param response - The HTTP response object * @returns Standardized error information */ async parse(response) { try { const errorBody = await response.json(); // Find the first strategy that can parse this error format const strategy = this.strategies.find(s => s.canParse(errorBody)); // GenericErrorParser always returns true, so this will never be null return strategy.parse(errorBody, response); } catch (error) { // Handle non-JSON responses const responseText = await response.text().catch(() => ''); return { message: response.statusText, code: response.status.toString(), details: { parseError: 'Failed to parse error response as JSON', responseText }, requestId: response.headers.get(HttpHeaders.X_REQUEST_ID) || undefined }; } } } // Export singleton instance const errorResponseParser = new ErrorResponseParser(); /** * Factory for creating typed errors based on HTTP status codes * Follows the Factory pattern for clean error instantiation */ class ErrorFactory { /** * Creates appropriate error instance based on HTTP status code */ static createFromHttpStatus(statusCode, errorInfo) { const { message, requestId, details } = errorInfo; // Map status codes to error types switch (statusCode) { case HttpStatus.BAD_REQUEST: return new ValidationError({ message, statusCode, requestId }); case HttpStatus.UNAUTHORIZED: return new AuthenticationError({ message, statusCode, requestId }); case HttpStatus.FORBIDDEN: return new AuthorizationError({ message, statusCode, requestId }); case HttpStatus.NOT_FOUND: return new NotFoundError({ message, statusCode, requestId }); case HttpStatus.TOO_MANY_REQUESTS: return new RateLimitError({ message, statusCode, requestId }); default: // For 5xx errors or any other status code if (statusCode >= HttpStatus.INTERNAL_SERVER_ERROR) { return new ServerError({ message, statusCode, requestId }); } // For unknown client errors, treat as validation error return new ValidationError({ message: `${message} (HTTP ${statusCode})`, statusCode, requestId }); } } /** * Creates a NetworkError from a fetch/network error */ static createNetworkError(error) { let message = ErrorMessages.NETWORK_ERROR; if (error instanceof Error) { if (error.name === ErrorNames.ABORT_ERROR) { message = ErrorMessages.REQUEST_ABORTED; } else if (error.message.includes('timeout')) { message = ErrorMessages.REQUEST_TIMEOUT; } else { message = error.message; } } return new NetworkError({ message }); } } const FOLDER_KEY = 'X-UIPATH-FolderKey'; const FOLDER_ID = 'X-UIPATH-OrganizationUnitId'; /** * Content type constants for HTTP requests/responses */ const CONTENT_TYPES = { JSON: 'application/json', XML: 'application/xml', OCTET_STREAM: 'application/octet-stream' }; class ApiClient { constructor(config, executionContext, tokenManager, clientConfig = {}) { this.defaultHeaders = {}; this.config = config; this.executionContext = executionContext; this.clientConfig = clientConfig; this.tokenManager = tokenManager; } setDefaultHeaders(headers) { this.defaultHeaders = { ...this.defaultHeaders, ...headers }; } /** * Checks if the current token needs refresh and refreshes it if necessary * @returns The valid token * @throws Error if token refresh fails */ async ensureValidToken() { // Try to get token info from context const tokenInfo = this.executionContext.get('tokenInfo'); if (!tokenInfo) { throw new AuthenticationError({ message: 'No authentication token available. Make sure to initialize the SDK first.' }); } // For secret-based tokens, they never expire if (tokenInfo.type === 'secret') { return tokenInfo.token; } // If token is not expired, return it if (!this.tokenManager.isTokenExpired(tokenInfo)) { return tokenInfo.token; } try { const newToken = await this.tokenManager.refreshAccessToken(); return newToken.access_token; } catch (error) { throw new AuthenticationError({ message: `Token refresh failed: ${error.message}. Please re-authenticate.`, statusCode: HttpStatus.UNAUTHORIZED }); } } async getDefaultHeaders() { // Get headers from execution context first const contextHeaders = this.executionContext.getHeaders(); // If Authorization header is already set in context, use that if (contextHeaders['Authorization']) { return { ...contextHeaders, 'Content-Type': CONTENT_TYPES.JSON, ...this.defaultHeaders, ...this.clientConfig.headers }; } const token = await this.ensureValidToken(); return { ...contextHeaders, 'Authorization': `Bearer ${token}`, 'Content-Type': CONTENT_TYPES.JSON, ...this.defaultHeaders, ...this.clientConfig.headers }; } async request(method, path, options = {}) { // Ensure path starts with a forward slash const normalizedPath = path.startsWith('/') ? path.substring(1) : path; // Construct URL with org and tenant names const url = new URL(`${this.config.orgName}/${this.config.tenantName}/${normalizedPath}`, this.config.baseUrl).toString(); const headers = { ...await this.getDefaultHeaders(), ...options.headers }; // Convert params to URLSearchParams const searchParams = new URLSearchParams(); if (options.params) { Object.entries(options.params).forEach(([key, value]) => { searchParams.append(key, value.toString()); }); } const fullUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url; try { const response = await fetch(fullUrl, { method, headers, body: options.body ? JSON.stringify(options.body) : undefined, signal: options.signal }); if (!response.ok) { const errorInfo = await errorResponseParser.parse(response); throw ErrorFactory.createFromHttpStatus(response.status, errorInfo); } if (response.status === 204) { return undefined; } // Check if we're expecting XML const acceptHeader = headers['Accept'] || headers['accept']; if (acceptHeader === CONTENT_TYPES.XML) { const text = await response.text(); return text; } return response.json(); } catch (error) { // If it's already one of our errors, re-throw it if (error.type && error.type.includes('Error')) { throw error; } // Otherwise, it's likely a network error throw ErrorFactory.createNetworkError(error); } } async get(path, options = {}) { return this.request('GET', path, options); } async post(path, data, options = {}) { return this.request('POST', path, { ...options, body: data }); } async put(path, data, options = {}) { return this.request('PUT', path, { ...options, body: data }); } async patch(path, data, options = {}) { return this.request('PATCH', path, { ...options, body: data }); } async delete(path, options = {}) { return this.request('DELETE', path, options); } } /** * Pagination types supported by the SDK */ var PaginationType; (function (PaginationType) { PaginationType["OFFSET"] = "offset"; PaginationType["TOKEN"] = "token"; })(PaginationType || (PaginationType = {})); /** * Collection of utility functions for working with objects */ /** * Filters out undefined values from an object * @param obj The source object * @returns A new object without undefined values * * @example * ```typescript * // Object with undefined values * const options = { * name: 'test', * count: 5, * prefix: undefined, * suffix: null * }; * const result = filterUndefined(options); * // result = { name: 'test', count: 5, suffix: null } * ``` */ function filterUndefined(obj) { const result = {}; for (const [key, value] of Object.entries(obj)) { if (value !== undefined) { result[key] = value; } } return result; } /** * Helper function for OData responses that ALWAYS return 200 with array indication * Empty array = success, Non-empty array = error * Used for task assignment APIs that don't use HTTP status codes for errors */ function processODataArrayResponse(oDataResponse, successData) { // Empty array = success if (oDataResponse.value.length === 0) { return { success: true, data: successData }; } // Non-empty array = error details return { success: false, data: oDataResponse.value }; } /** * Utility functions for platform detection */ /** * Checks if code is running in a browser environment */ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; /** * Base64 encoding/decoding */ /** * Encodes a string to base64 * @param str - The string to encode * @returns Base64 encoded string */ function encodeBase64(str) { // TextEncoder for UTF-8 encoding (works in both browser and Node.js) const encoder = new TextEncoder(); const data = encoder.encode(str); // Convert Uint8Array to base64 if (isBrowser) { // Browser environment // Convert Uint8Array to binary string then to base64 const binaryString = Array.from(data, byte => String.fromCharCode(byte)).join(''); return btoa(binaryString); } else { // Node.js environment return Buffer.from(data).toString('base64'); } } /** * Decodes a base64 string * @param base64 - The base64 string to decode * @returns Decoded string */ function decodeBase64(base64) { let bytes; if (isBrowser) { // Browser environment const binaryString = atob(base64); bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } } else { // Node.js environment bytes = new Uint8Array(Buffer.from(base64, 'base64')); } // TextDecoder for UTF-8 decoding (works in both browser and Node.js) const decoder = new TextDecoder(); return decoder.decode(bytes); } /** * PaginationManager handles the conversion between uniform cursor-based pagination * and the specific pagination type for each service */ class PaginationManager { /** * Create a pagination cursor for subsequent page requests */ static createCursor({ pageInfo, type }) { if (!pageInfo.hasMore) { return undefined; } const cursorData = { type, pageSize: pageInfo.pageSize, }; switch (type) { case PaginationType.OFFSET: if (pageInfo.currentPage) { cursorData.pageNumber = pageInfo.currentPage + 1; } break; case PaginationType.TOKEN: if (pageInfo.continuationToken) { cursorData.continuationToken = pageInfo.continuationToken; } else { return undefined; // No continuation token, can't continue } break; } return { value: encodeBase64(JSON.stringify(cursorData)) }; } /** * Create a paginated response with navigation cursors */ static createPaginatedResponse({ pageInfo, type }, items) { const nextCursor = PaginationManager.createCursor({ pageInfo, type }); // Create previous page cursor if applicable let previousCursor = undefined; if (pageInfo.currentPage && pageInfo.currentPage > 1) { const prevCursorData = { type, pageNumber: pageInfo.currentPage - 1, pageSize: pageInfo.pageSize, }; previousCursor = { value: encodeBase64(JSON.stringify(prevCursorData)) }; } // Calculate total pages if we have totalCount and pageSize let totalPages = undefined; if (pageInfo.totalCount !== undefined && pageInfo.pageSize) { totalPages = Math.ceil(pageInfo.totalCount / pageInfo.pageSize); } // Determine if this pagination type supports page jumping const supportsPageJump = type === PaginationType.OFFSET; // Create the result object with all fields, then filter out undefined values const result = filterUndefined({ items, totalCount: pageInfo.totalCount, hasNextPage: pageInfo.hasMore, nextCursor: nextCursor, previousCursor: previousCursor, currentPage: pageInfo.currentPage, totalPages, supportsPageJump }); return result; } } /** * Creates headers object from key-value pairs * @param headersObj - Object containing header key-value pairs * @returns Headers object with all values converted to strings * * @example * ```typescript * // Single header * const headers = createHeaders({ 'X-UIPATH-FolderKey': '1234567890' }); * * // Multiple headers * const headers = createHeaders({ * 'X-UIPATH-FolderKey': '1234567890', * 'X-UIPATH-OrganizationUnitId': 123, * 'Accept': 'application/json' * }); * * // Using constants * import { FOLDER_KEY, FOLDER_ID } from '../constants/headers'; * const headers = createHeaders({ * [FOLDER_KEY]: 'abc-123', * [FOLDER_ID]: 456 * }); * * // Empty headers * const headers = createHeaders(); * ``` */ function createHeaders(headersObj) { const headers = {}; for (const [key, value] of Object.entries(headersObj)) { if (value !== undefined && value !== null) { headers[key] = value.toString(); } } return headers; } /** * Common constants used across the SDK */ /** * Prefix used for OData query parameters */ const ODATA_PREFIX = '$'; /** * OData pagination constants */ const ODATA_PAGINATION = { /** Default field name for items in a paginated OData response */ ITEMS_FIELD: 'value', /** Default field name for total count in a paginated OData response */ TOTAL_COUNT_FIELD: '@odata.count' }; /** * Entity pagination constants for Data Fabric entities */ const ENTITY_PAGINATION = { /** Field name for items in entity response */ ITEMS_FIELD: 'value', /** Field name for total count in entity response */ TOTAL_COUNT_FIELD: 'totalRecordCount' }; /** * Bucket pagination constants for token-based pagination */ const BUCKET_PAGINATION = { /** Field name for items in bucket file metadata response */ ITEMS_FIELD: 'items', /** Field name for continuation token in bucket file metadata response */ CONTINUATION_TOKEN_FIELD: 'continuationToken' }; /** * Process Instance pagination constants for token-based pagination */ const PROCESS_INSTANCE_PAGINATION = { /** Field name for items in process instance response */ ITEMS_FIELD: 'instances', /** Field name for continuation token in process instance response */ CONTINUATION_TOKEN_FIELD: 'nextPage' }; /** * OData OFFSET pagination parameter names (ODATA-style) */ const ODATA_OFFSET_PARAMS = { /** OData page size parameter name */ PAGE_SIZE_PARAM: '$top', /** OData offset parameter name */ OFFSET_PARAM: '$skip', /** OData count parameter name */ COUNT_PARAM: '$count' }; /** * Entity OFFSET pagination parameter names (limit/start style) */ const ENTITY_OFFSET_PARAMS = { /** Entity page size parameter name */ PAGE_SIZE_PARAM: 'limit', /** Entity offset parameter name */ OFFSET_PARAM: 'start', /** Entity count parameter (not used) */ COUNT_PARAM: undefined }; /** * Bucket TOKEN pagination parameter names */ const BUCKET_TOKEN_PARAMS = { /** Bucket page size parameter name */ PAGE_SIZE_PARAM: 'takeHint', /** Bucket token parameter name */ TOKEN_PARAM: 'continuationToken' }; /** * Process Instance TOKEN pagination parameter names */ const PROCESS_INSTANCE_TOKEN_PARAMS = { /** Process instance page size parameter name */ PAGE_SIZE_PARAM: 'pageSize', /** Process instance token parameter name */ TOKEN_PARAM: 'nextPage' }; /** * Transforms data by mapping fields according to the provided field mapping * @param data The source data to transform * @param fieldMapping Object mapping source field names to target field names * @returns Transformed data with mapped field names * * @example * ```typescript * // Single object transformation * const data = { id: '123', userName: 'john' }; * const mapping = { id: 'userId', userName: 'name' }; * const result = transformData(data, mapping); * // result = { userId: '123', name: 'john' } * * // Array transformation * const dataArray = [ * { id: '123', userName: 'john' }, * { id: '456', userName: 'jane' } * ]; * const result = transformData(dataArray, mapping); * // result = [ * // { userId: '123', name: 'john' }, * // { userId: '456', name: 'jane' } * // ] * ``` */ function transformData(data, fieldMapping) { // Handle array of objects if (Array.isArray(data)) { return data.map(item => transformData(item, fieldMapping)); } // Handle single object const result = { ...data }; for (const [sourceField, targetField] of Object.entries(fieldMapping)) { if (sourceField in result) { const value = result[sourceField]; delete result[sourceField]; result[targetField] = value; } } return result; } /** * Converts a string from PascalCase to camelCase * @param str The PascalCase string to convert * @returns The camelCase version of the string * * @example * ```typescript * pascalToCamelCase('HelloWorld'); // 'helloWorld' * pascalToCamelCase('TaskAssignmentCriteria'); // 'taskAssignmentCriteria' * ``` */ function pascalToCamelCase(str) { if (!str) return str; return str.charAt(0).toLowerCase() + str.slice(1); } /** * Converts a string from camelCase to PascalCase * @param str The camelCase string to convert * @returns The PascalCase version of the string * * @example * ```typescript * camelToPascalCase('helloWorld'); // 'HelloWorld' * camelToPascalCase('taskAssignmentCriteria'); // 'TaskAssignmentCriteria' * ``` */ function camelToPascalCase(str) { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1); } /** * Generic function to transform object keys using a provided case conversion function * @param data The object to transform * @param convertCase The function to convert each key * @returns A new object with transformed keys */ function transformCaseKeys(data, convertCase) { // Handle array of objects if (Array.isArray(data)) { return data.map(item => { // If the array element is a primitive (string, number, etc.), return it as is if (item === null || typeof item !== 'object' || item instanceof String) { return item; } // Only recursively transform if it's actually an object return transformCaseKeys(item, convertCase); }); } const result = {}; for (const [key, value] of Object.entries(data)) { const transformedKey = convertCase(key); // Recursively transform nested objects and arrays if (value !== null && typeof value === 'object') { result[transformedKey] = transformCaseKeys(value, convertCase); } else { result[transformedKey] = value; } } return result; } /** * Transforms an object's keys from PascalCase to camelCase * @param data The object with PascalCase keys * @returns A new object with all keys converted to camelCase * * @example * ```typescript * // Simple object * pascalToCamelCaseKeys({ Id: "123", TaskName: "Invoice" }); * // Result: { id: "123", taskName: "Invoice" } * * // Nested object * pascalToCamelCaseKeys({ * TaskId: "456", * TaskDetails: { AssignedUser: "John", Priority: "High" } * }); * // Result: { * // taskId: "456", * // taskDetails: { assignedUser: "John", priority: "High" } * // } * * // Array of objects * pascalToCamelCaseKeys([ * { Id: "1", IsComplete: false }, * { Id: "2", IsComplete: true } * ]); * // Result: [ * // { id: "1", isComplete: false }, * // { id: "2", isComplete: true } * // ] * ``` */ function pascalToCamelCaseKeys(data) { return transformCaseKeys(data, pascalToCamelCase); } /** * Transforms an object's keys from camelCase to PascalCase * @param data The object with camelCase keys * @returns A new object with all keys converted to PascalCase * * @example * ```typescript * // Simple object * camelToPascalCaseKeys({ userId: "789", isActive: true }); * // Result: { UserId: "789", IsActive: true } * * // Nested object * camelToPascalCaseKeys({ * taskId: "ABC123", * submissionData: { customerName: "XYZ Corp" } * }); * // Result: { * // TaskId: "ABC123", * // SubmissionData: { CustomerName: "XYZ Corp" } * // } * * // Array of objects * camelToPascalCaseKeys([ * { userId: "u1", roleType: "admin" }, * { userId: "u2", roleType: "user" } * ]); * // Result: [ * // { UserId: "u1", RoleType: "admin" }, * // { UserId: "u2", RoleType: "user" } * // ] * ``` */ function camelToPascalCaseKeys(data) { return transformCaseKeys(data, camelToPascalCase); } /** * Maps a field value in an object using a provided mapping object. * Returns a new object with the mapped field value. * * @param obj The object to map * @param field The field name to map * @param valueMap The mapping object (from input value to output value) * @returns A new object with the mapped field value * * @example * const statusMap = { 0: 'Unassigned', 1: 'Pending', 2: 'Completed' }; * const task = { status: 1, id: 123 }; * const mapped = mapFieldValue(task, 'status', statusMap); * // mapped = { status: 'Pending', id: 123 } */ function _mapFieldValue(obj, field, valueMap) { const lookupKey = String(obj[field]); return { ...obj, [field]: lookupKey in valueMap ? valueMap[lookupKey] : obj[field], }; } /** * General API response transformer with optional field value mapping. * * @param data - The API response data to transform * @param options - Optional mapping options: * - field: The field name to map (optional) * - valueMap: The mapping object for the field (optional) * - transform: A function to further transform the data (optional) * @returns The transformed data, with field value mapped if specified * * @example * // Just transform * const result = applyDataTransforms(data); * * // Map a field value, then transform * const result = applyDataTransforms(data, { field: 'status', valueMap: StatusMap }); * * // Map a field value, then apply a custom transform * const result = applyDataTransforms(data, { field: 'status', valueMap: StatusMap, transform: customTransform }); */ function applyDataTransforms(data, options) { let result = data; if (options?.field && options?.valueMap) { result = _mapFieldValue(result, options.field, options.valueMap); } if (options?.transform) { result = options.transform(result); } return result; } /** * Adds a prefix to specified keys in an object, returning a new object. * Only the provided keys are prefixed; all others are left unchanged. * * @param obj The source object * @param prefix The prefix to add (e.g., '$') * @param keys The keys to prefix (e.g., ['expand', 'filter']) * @returns A new object with specified keys prefixed * * @example * addPrefixToKeys({ expand: 'a', foo: 1 }, '$', ['expand']) // { $expand: 'a', foo: 1 } */ function addPrefixToKeys(obj, prefix, keys) { const result = {}; for (const [key, value] of Object.entries(obj)) { if (keys.includes(key)) { result[`${prefix}${key}`] = value; } else { result[key] = value; } } return result; } /** * Creates a new map with the keys and values reversed * @param map The original map to reverse * @returns A new map with keys and values swapped * * @example * ```typescript * const original = { key1: 'value1', key2: 'value2' }; * const reversed = reverseMap(original); * // reversed = { value1: 'key1', value2: 'key2' } * ``` */ function reverseMap(map) { return Object.entries(map).reduce((acc, [key, value]) => { acc[value] = key; return acc; }, {}); } /** * Transforms an array-based dictionary with separate keys and values arrays * into a standard JavaScript object/record * * @param dictionary Object containing keys and values arrays * @returns A standard record object with direct key-value mapping * * @example * ```typescript * const arrayDict = { * keys: ['Content-Type', 'x-ms-blob-type'], * values: ['application/json', 'BlockBlob'] * }; * const record = arrayDictionaryToRecord(arrayDict); * // result = { * // 'Content-Type': 'application/json', * // 'x-ms-blob-type': 'BlockBlob' * // } * ``` */ function arrayDictionaryToRecord(dictionary) { if (!dictionary || !dictionary.keys || !dictionary.values) { return {}; } if (dictionary.keys.length !== dictionary.values.length) { console.warn('Keys and values arrays have different lengths'); } const record = {}; const length = Math.min(dictionary.keys.length, dictionary.values.length); for (let i = 0; i < length; i++) { record[dictionary.keys[i]] = dictionary.values[i]; } return record; } /** * Constants used throughout the pagination system */ /** Maximum number of items that can be requested in a single page */ const MAX_PAGE_SIZE = 1000; /** Default page size when jumpToPage is used without specifying pageSize */ const DEFAULT_PAGE_SIZE = 50; /** Default field name for items in a paginated response */ const DEFAULT_ITEMS_FIELD = 'value'; /** Default field name for total count in a paginated response */ const DEFAULT_TOTAL_COUNT_FIELD = '@odata.count'; /** * Limits the page size to the maximum allowed value * @param pageSize - Requested page size * @returns Limited page size value */ function getLimitedPageSize(pageSize) { if (pageSize === undefined || pageSize === null) { return DEFAULT_PAGE_SIZE; } return Math.max(1, Math.min(pageSize, MAX_PAGE_SIZE)); } /** * Helper functions for pagination that can be used across services */ class PaginationHelpers { /** * Checks if any pagination parameters are provided * * @param options - The options object to check * @returns True if any pagination parameter is defined, false otherwise */ static hasPaginationParameters(options = {}) { const { cursor, pageSize, jumpToPage } = options; return cursor !== undefined || pageSize !== undefined || jumpToPage !== undefined; } /** * Parse a pagination cursor string into cursor data */ static parseCursor(cursorString) { try { const cursorData = JSON.parse(decodeBase64(cursorString)); return cursorData; } catch (error) { throw new Error('Invalid pagination cursor'); } } /** * Validates cursor format and structure * * @param paginationOptions - The pagination options containing the cursor * @param paginationType - Optional pagination type to validate against */ static validateCursor(paginationOptions, paginationType) { if (paginationOptions.cursor !== undefined) { if (!paginationOptions.cursor || typeof paginationOptions.cursor.value !== 'string' || !paginationOptions.cursor.value) { throw new Error('cursor must contain a valid cursor string'); } try { // Try to parse the cursor to validate it const cursorData = PaginationHelpers.parseCursor(paginationOptions.cursor.value); // If type is provided, validate cursor contains expected type information if (paginationType) { if (!cursorData.type) { throw new Error('Invalid cursor: missing pagination type'); } // Check pagination type compatibility if (cursorData.type !== paginationType) { throw new Error(`Pagination type mismatch: cursor is for ${cursorData.type} but service uses ${paginationType}`); } } } catch (error) { if (error instanceof Error) { // If it's already our error with specific message, pass it through if (error.message.startsWith('Invalid cursor') || error.message.startsWith('Pagination type mismatch')) { throw error; } } throw new Error('Invalid pagination cursor format'); } } } /** * Comprehensive validation for pagination options * * @param options - The pagination options to validate * @param paginationType - The pagination type these options will be used with * @returns Processed pagination parameters ready for use */ static validatePaginationOptions(options, paginationType) { // Validate pageSize if (options.pageSize !== undefined && options.pageSize <= 0) { throw new Error('pageSize must be a positive number'); } // Validate jumpToPage if (options.jumpToPage !== undefined && options.jumpToPage <= 0) { throw new Error('jumpToPage must be a positive number'); } // Validate cursor PaginationHelpers.validateCursor(options, paginationType); // Validate service compatibility if (options.jumpToPage !== undefined && paginationType === PaginationType.TOKEN) { throw new Error('jumpToPage is not supported for token-based pagination. Use cursor-based navigation instead.'); } // Get processed parameters return PaginationHelpers.getRequestParameters(options); } /** * Convert a unified pagination options to service-specific parameters */ static getRequestParameters(options) { // Handle jumpToPage if (options.jumpToPage !== undefined) { const jumpToPageOptions = { pageSize: options.pageSize, pageNumber: options.jumpToPage }; return filterUndefined(jumpToPageOptions); } // If no cursor is provided, it's a first page request if (!options.cursor) { const firstPageOptions = { pageSize: options.pageSize, pageNumber: 1 }; return filterUndefined(firstPageOptions); } // Parse the cursor try { const cursorData = PaginationHelpers.parseCursor(options.cursor.value); const cursorBasedOptions = { pageSize: cursorData.pageSize || options.pageSize, pageNumber: cursorData.pageNumber, continuationToken: cursorData.continuationToken, type: cursorData.type, }; return filterUndefined(cursorBasedOptions); } catch (error) { throw new Error('Invalid pagination cursor'); } } /** * Helper method for paginated resource retrieval * * @param params - Parameters for pagination * @returns Promise resolving to a paginated result */ static async getAllPaginated(params) { const { serviceAccess, getEndpoint, folderId, paginationParams, additionalParams, transformFn, options = {} } = params; const endpoint = getEndpoint(folderId); const headers = folderId ? createHeaders({ [FOLDER_ID]: folderId }) : {}; const paginatedResponse = await serviceAccess.requestWithPagination('GET', endpoint, paginationParams, { headers, params: additionalParams, pagination: { paginationType: options.paginationType || PaginationType.OFFSET, itemsField: options.itemsField || DEFAULT_ITEMS_FIELD, totalCountField: options.totalCountField || DEFAULT_TOTAL_COUNT_FIELD, continuationTokenField: options.continuationTokenField, paginationParams: options.paginationParams } }); // Transform items only if a transform function is provided const transformedItems = transformFn ? paginatedResponse.items.map(transformFn) : paginatedResponse.items; return { ...paginatedResponse, items: transformedItems }; } /** * Helper method for non-paginated resource retrieval * * @param params - Parameters for non-paginated resource retrieval * @returns Promise resolving to an object with data and totalCount */ static async getAllNonPaginated(params) { const { serviceAccess, getAllEndpoint, getByFolderEndpoint, folderId, additionalParams, transformFn, options = {} } = params; // Set default field names const itemsField = options.itemsField || DEFAULT_ITEMS_FIELD; const totalCountField = options.totalCountField || DEFAULT_TOTAL_COUNT_FIELD; // Determine e