@uipath/uipath-typescript
Version:
UiPath TypeScript SDK
1,572 lines (1,542 loc) • 224 kB
JavaScript
'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