UNPKG

@itwin/access-control-client

Version:

Access control client for the iTwin platform

223 lines 8.71 kB
/*--------------------------------------------------------------------------------------------- * 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