@itwin/access-control-client
Version:
Access control client for the iTwin platform
223 lines • 8.71 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module AccessControlClient
*/
import { hasProperty } from "../types/typeUtils";
/**
* Type guard to validate if an object is a valid Error structure
* @param error - Unknown object to validate
* @returns True if the object is a valid Error type
*/
function isValidError(error) {
if (typeof error !== "object" || error === null) {
return false;
}
const obj = error;
return typeof obj.code === "string" && typeof obj.message === "string";
}
/**
* Type guard to validate if response data contains an error
* @param data - Unknown response data to validate
* @returns True if the data contains a valid Error object
*/
function isErrorResponse(data) {
if (typeof data !== "object" || data === null) {
return false;
}
const obj = data;
return "error" in obj && isValidError(obj.error);
}
/**
* Base client class providing common functionality for Access Control API requests.
* Handles authentication, request configuration, and query string building, and error validation.
*/
export class BaseClient {
_baseUrl = "https://api.bentley.com/accesscontrol/itwins";
/**
* Creates a new BaseClient instance for Access Control API operations
* @param url - Optional custom base URL, defaults to production iTwins API URL
*
* @example
* ```typescript
* // Use default production URL
* const client = new BaseClient();
*
* // Use custom URL for development/testing
* const client = new ITwinsAccessClient("https://api.bentley.com/accesscontrol/itwins");
* ```
*/
constructor(url) {
if (url !== undefined) {
this._baseUrl = url;
}
else {
const urlPrefix = process.env.IMJS_URL_PREFIX;
if (urlPrefix) {
const baseUrl = new URL(this._baseUrl);
baseUrl.hostname = urlPrefix + baseUrl.hostname;
this._baseUrl = baseUrl.href;
}
}
}
/**
* Maps the some properties of {@link ODataQueryParams} to their corresponding query parameter names.
*
* @remarks
* This mapping is used to translate internal property names to the expected parameter names
* when constructing requests. Properties mapped to empty strings are excluded from
* the query string as they should be sent as headers instead.
*
* The mapping includes OData query parameters (prefixed with $) for pagination.
*
* @readonly
*/
static paginationParamMapping = {
top: "$top",
skip: "$skip",
};
/**
* Sends a basic API request to the specified URL with the given method and data.
* @param accessToken The client access token
* @param method The HTTP method of the request (GET, POST, DELETE, etc)
* @param url The URL of the request
* @param data Optional request body data
* @param property Optional property name to extract from response data
* @param additionalHeaders Optional additional headers to include in the request
* @returns Promise resolving to a BentleyAPIResponse containing the response data or error
*/
async sendGenericAPIRequest(accessToken, method, url, data, property, additionalHeaders) {
const requestOptions = this.getRequestOptions(accessToken, method, url, data, additionalHeaders);
try {
const response = await fetch(requestOptions.url, {
method: requestOptions.method,
headers: requestOptions.headers,
body: requestOptions.body,
});
// Convert Headers object to plain object for compatibility
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
const responseData = response.status !== 204 ? await response.json() : undefined;
if (!response.ok) {
if (isErrorResponse(responseData)) {
return {
status: response.status,
error: responseData.error,
headers,
};
}
throw new Error("An error occurred while processing the request");
}
return {
status: response.status,
data: (responseData === "" || responseData === undefined
? undefined
: property && hasProperty(responseData, property)
? responseData[property]
: responseData),
headers,
};
}
catch {
return {
status: 500,
error: {
code: "InternalServerError",
message: "An internal exception happened while calling iTwins Service",
},
headers: {},
};
}
}
/**
* Build the request configuration including method, headers, and body
* @param accessTokenString The client access token
* @param method The HTTP method of the request
* @param url The URL of the request
* @param data Optional request body data
* @param additionalHeaders Optional additional headers to include in the request
* @returns RequestConfig object with method, url, body, and headers
*/
getRequestOptions(accessTokenString, method, url, data, additionalHeaders) {
if (!accessTokenString) {
throw new Error("Access token is required");
}
if (!url) {
throw new Error("URL is required");
}
const body = JSON.stringify(data);
return {
method,
url,
body,
headers: {
authorization: accessTokenString,
"content-type": "application/json",
accept: "application/vnd.bentley.itwin-platform.v2+json",
...additionalHeaders,
},
};
}
/**
* Builds a query string to be appended to a URL from query arguments
* @param parameterMapping - Parameter mapping configuration that maps object properties to query parameter names
* @param queryArg - Object containing queryable properties for filtering
* @returns Query string with parameters applied, ready to append to a URL
*
* @example
* ```typescript
* const queryString = this.getQueryString(
* BaseClient.paginationParamMapping,
* {
* top: 10,
* skip: 5,
* }
* );
* // Returns: "$top=10&$skip=5"
* ```
*/
getQueryString(parameterMapping, queryArg) {
if (!queryArg)
return "";
const params = this.buildQueryParams(queryArg, parameterMapping);
return params.join("&");
}
/**
* Helper method to build query parameter array from mapping.
* Uses exhaustive parameter mapping to ensure type safety and prevent missing parameters.
* Automatically handles URL encoding and filters out excluded parameters.
*
* @param queryArg - Object containing queryable properties
* @param mapping - Parameter mapping configuration that maps object properties to query parameter names
* @returns Array of formatted query parameter strings ready for URL construction
*
* @example
* ```typescript
* const params = this.buildQueryParams(
* { top: 10, skip: 5 },
* { top: "$top", skip: "$skip" }
* );
* // Returns: ["$top=10", "$skip=5"]
* ```
*/
buildQueryParams(queryArg, mapping) {
const params = [];
// Type assertion constrains paramKey to actual property names and mappedValue to the specific strings from the mapping
// Narrows from set of all strings to only valid keys/values
for (const [paramKey, mappedValue] of Object.entries(mapping)) {
if (mappedValue === "")
continue;
const queryArgValue = queryArg[paramKey];
if (queryArgValue !== undefined && queryArgValue !== null) {
const stringValue = String(queryArgValue);
params.push(`${mappedValue}=${encodeURIComponent(stringValue)}`);
}
}
return params;
}
}
//# sourceMappingURL=BaseClient.js.map