UNPKG

@rschili/melodi-cli

Version:
1,302 lines (1,288 loc) 169 kB
#!/usr/bin/env node // src/buildInfo.ts var __BUILD_DATE__ = "2026-03-18T18:11:14Z"; // package.json var package_default = { name: "@rschili/melodi-cli", version: "1.7.0", description: "iModel utility", main: "dist/index.mjs", type: "module", engines: { node: ">=22.14.0" }, scripts: { typecheck: "tsc --noEmit", build: "node esbuild.config.mjs", test: "vitest", start: "node dist/index.mjs", lint: "eslint 'src/**/*.ts' 'test/**/*.ts' --fix", prebuild: `echo "export const __BUILD_DATE__ = '$(date -u +%Y-%m-%dT%H:%M:%SZ)';" > src/buildInfo.ts && npm run lint && npm run typecheck` }, bin: { melodi: "dist/index.mjs" }, publishConfig: { access: "public" }, files: [ "dist", "LICENSE", "README.md", "CHANGELOG.md" ], repository: { type: "git", url: "git+https://github.com/rschili/melodi-cli.git" }, keywords: [ "itwin", "imodel", "bentley", "ecdb", "bim" ], author: "Robert Schili", license: "MIT", bugs: { url: "https://github.com/rschili/melodi-cli/issues" }, homepage: "https://github.com/rschili/melodi-cli#readme", devDependencies: { "@eslint/js": "^10.0.1", "@types/node": "^25.5.0", "@vitest/coverage-v8": "^4.1.0", esbuild: "^0.27.4", "esbuild-node-externals": "^1.20.1", eslint: "^10.0.3", globals: "^17.4.0", typescript: "^5.9.3", "typescript-eslint": "^8.57.1", vitest: "^4.1.0" }, dependencies: { "@clack/prompts": "^1.1.0", "@itwin/core-backend": "^5.7.2", "@itwin/core-bentley": "^5.7.2", "@itwin/core-common": "^5.7.2", "@itwin/ecschema-metadata": "^5.7.2", "@itwin/imodels-access-backend": "^6.0.2", "@itwin/imodels-access-common": "^6.0.2", "@itwin/imodels-client-authoring": "^6.0.2", "@itwin/itwins-client": "^2.5.2", "@itwin/node-cli-authorization": "^3.0.2", "@itwin/object-storage-azure": "^3.0.4", "@itwin/object-storage-core": "^3.0.4", "@itwin/object-storage-google": "^3.0.4", "@modelcontextprotocol/sdk": "^1.27.1", "@types/semver": "^7.7.1", axios: "^1.13.6", chalk: "^5.6.2", emphasize: "^7.0.0", globby: "^16.1.1", "gradient-string": "^3.0.0", "module-alias": "^2.3.4", semver: "^7.7.4", "simple-update-notifier": "^2.0.0", table: "^6.9.0", zod: "^4.3.6" }, overrides: { inversify: "7.5.2", "reflect-metadata": "^0.2.2" } }; // src/Diagnostics.ts import updateNotifier from "simple-update-notifier"; var applicationVersion = package_default.version; var applicationBuildDate = new Date(__BUILD_DATE__).toLocaleString(); async function checkUpdates() { await updateNotifier({ pkg: package_default }); } // src/index.ts import gradient from "gradient-string"; // src/ConsoleHelper.ts import { log } from "@clack/prompts"; import chalk from "chalk"; var formatPath = chalk.blueBright.underline; var formatError = chalk.redBright.bold; var formatWarning = chalk.yellowBright; var formatSuccess = chalk.greenBright.bold; var resetChar = "\x1B[0m"; function printError(error, printAsWarning = false) { const formatter = printAsWarning ? formatWarning : formatError; const label = printAsWarning ? "Warning" : "Error"; if (error instanceof Error) { console.error(formatter(`${label}: ${error.message}`)); } else { console.error(formatter(`${label}: ${String(error)}`)); } } function logError(error) { if (error instanceof Error) { log.error(error.message); } else { log.error(`Error: ${String(error)}`); } } function generateColorizerMap(values) { const colorizerMap = /* @__PURE__ */ new Map(); const colors = [ chalk.redBright, chalk.greenBright, chalk.blueBright, chalk.yellowBright, chalk.cyanBright, chalk.magentaBright, chalk.whiteBright ]; const uniqueValues = Array.from(new Set(values)); uniqueValues.forEach((value, index) => { const color = colors[index % colors.length]; colorizerMap.set(value, color); }); return colorizerMap; } var msInSecond = 1e3; var msInMinute = msInSecond * 60; var msInHour = msInMinute * 60; var msInDay = msInHour * 24; var msInYear = msInDay * 365.25; function timeSpanToString(span) { if (span > msInYear * 100 || span <= 0) { return void 0; } if (span < msInMinute) { const seconds = Math.floor(span / msInSecond); return `${seconds} second${seconds !== 1 ? "s" : ""}`; } else if (span < msInHour) { const minutes = Math.floor(span / msInMinute); return `${minutes} minute${minutes !== 1 ? "s" : ""}`; } else if (span < msInDay) { const hours = Math.floor(span / msInHour); return `${hours} hour${hours !== 1 ? "s" : ""}`; } else if (span < msInYear) { const days = Math.floor(span / msInDay); return `${days} day${days !== 1 ? "s" : ""}`; } else { const years = Math.floor(span / msInYear); return `${years} year${years !== 1 ? "s" : ""}`; } } // src/UserConfig.ts import * as fs from "fs"; import path from "path"; import { z } from "zod/v4"; var LogLevel = /* @__PURE__ */ ((LogLevel3) => { LogLevel3[LogLevel3["Trace"] = 0] = "Trace"; LogLevel3[LogLevel3["Info"] = 1] = "Info"; LogLevel3[LogLevel3["Warning"] = 2] = "Warning"; LogLevel3[LogLevel3["Error"] = 3] = "Error"; LogLevel3[LogLevel3["None"] = 4] = "None"; return LogLevel3; })(LogLevel || {}); var UserConfigSchema = z.object({ melodiVersion: z.string(), logging: z.enum(LogLevel).optional() }); var UserConfigFileName = "config.json"; async function readUserConfig(userConfigDir) { try { const userConfigPath = path.join(userConfigDir, UserConfigFileName); if (fs.existsSync(userConfigPath)) { const data = await fs.promises.readFile(userConfigPath, "utf-8"); const json = JSON.parse(data); return await UserConfigSchema.parseAsync(json); } } catch (err) { console.error(formatError("Failed to read user config. Using default config.")); printError(err); } return { melodiVersion: applicationVersion, logging: 4 /* None */ }; } async function saveUserConfig(cfg, userConfigDir) { const userConfigPath = path.join(userConfigDir, UserConfigFileName); if (!fs.lstatSync(userConfigDir).isDirectory()) { throw new Error(`The user config directory is not a valid directory: ${userConfigDir}`); } try { fs.accessSync(userConfigDir, fs.constants.R_OK | fs.constants.W_OK); } catch { throw new Error(`The user config directory is not accessible: ${userConfigDir}. Please check permissions.`); } cfg.melodiVersion = applicationVersion; const data = JSON.stringify(cfg, void 0, 2); await fs.promises.writeFile(userConfigPath, data, "utf-8"); } // src/index.ts import chalk12 from "chalk"; // src/SystemFolders.ts import os from "os"; import path2 from "path"; import fs2 from "fs"; var MELODI_CONFIG_ENV = "MELODI_CONFIG"; var MELODI_CACHE_ENV = "MELODI_CACHE"; var MELODI_ROOT_ENV = "MELODI_ROOT"; var appName = "melodi"; function getConfigDir() { if (process.env[MELODI_CONFIG_ENV]) { return process.env[MELODI_CONFIG_ENV]; } const home = os.homedir(); switch (process.platform) { case "win32": return path2.join(process.env.LOCALAPPDATA || path2.join(home, "AppData", "Local"), appName, "config"); case "darwin": case "linux": default: return path2.join(process.env.XDG_CONFIG_HOME || path2.join(home, ".config"), appName); } } function getCacheDir() { if (process.env[MELODI_CACHE_ENV]) { return process.env[MELODI_CACHE_ENV]; } const home = os.homedir(); switch (process.platform) { case "win32": return path2.join(process.env.LOCALAPPDATA || path2.join(home, "AppData", "Local"), appName, "cache"); case "darwin": return path2.join(process.env.XDG_CACHE_HOME || path2.join(home, "Library", "Caches"), appName); case "linux": default: return path2.join(process.env.XDG_CACHE_HOME || path2.join(home, ".cache"), appName); } } function getRootDir() { if (process.env[MELODI_ROOT_ENV]) { return process.env[MELODI_ROOT_ENV]; } const home = os.homedir(); if (process.platform === "win32") { const docs = path2.join(process.env.USERPROFILE || home, "Documents"); return path2.join(docs, appName); } if (process.platform === "darwin") { const docs = path2.join(home, "Documents"); return path2.join(docs, appName); } if (process.platform === "linux") { const userDirsFile = path2.join(home, ".config", "user-dirs.dirs"); let docs = null; if (fs2.existsSync(userDirsFile)) { const content = fs2.readFileSync(userDirsFile, "utf8"); const match = content.match(/XDG_DOCUMENTS_DIR="?([^"\n]+)"?/); if (match) { docs = match[1].replace("$HOME", home); } } if (!docs) { docs = path2.join(home, "Documents"); } return path2.join(docs, appName); } return path2.join(home, appName); } // src/index.ts import { promises as fs8 } from "fs"; // src/Context.ts import * as fs3 from "fs"; import path3 from "path"; import { z as z2 } from "zod/v4"; import { globby } from "globby"; import { SQLiteDb } from "@itwin/core-backend"; import { DbResult, OpenMode } from "@itwin/core-bentley"; import { SemVer } from "semver"; // src/EnvironmentManager.ts import { select } from "@clack/prompts"; import { IModelHost } from "@itwin/core-backend"; import { IModelsClient } from "@itwin/imodels-client-authoring"; // node_modules/@itwin/itwins-client/lib/esm/BaseBentleyAPIClient.js function isValidError(error) { if (typeof error !== "object" || error === null) { return false; } const obj = error; return typeof obj.code === "string" && typeof obj.message === "string"; } function isErrorResponse(data) { if (typeof data !== "object" || data === null) { return false; } const obj = data; return "error" in obj && isValidError(obj.error); } var BaseBentleyAPIClient = class { /** * The max redirects for iTwins API endpoints. * The max redirects can be customized via the constructor parameter or automatically * modified based on the IMJS_MAX_REDIRECTS environment variable. * * @readonly */ _maxRedirects = 5; /** * Creates a new BaseClient instance for API operations * @param maxRedirects - Optional custom max redirects, defaults to 5 * * @example * ```typescript * // Use default max redirects * const client = new BaseClient(); * * // Use custom max redirects * const client = new BaseClient(10); * ``` */ constructor(maxRedirects) { if (maxRedirects !== void 0) { this._maxRedirects = maxRedirects; } else { this._maxRedirects = globalThis.IMJS_MAX_REDIRECTS ?? 5; } } /** * Sends a generic API request with type safety and response validation. * Handles authentication, error responses, and data extraction automatically. * Error responses follow APIM standards for consistent error handling. * * @param accessToken - The client access token for authentication * @param method - The HTTP method type (GET, POST, DELETE, etc.) * @param url - The complete URL of the request endpoint * @param data - Optional payload data for the request body * @param headers - Optional additional request headers * @returns Promise that resolves to the parsed API response with type safety */ async sendGenericAPIRequest(accessToken, method, url, data, headers, allowRedirects = false) { try { const requestOptions = this.createRequestOptions(accessToken, method, url, data, headers); const response = await fetch(requestOptions.url, { method: requestOptions.method, headers: requestOptions.headers, body: requestOptions.body, redirect: "manual" }); if (response.type === "opaqueredirect") { if (!allowRedirects) { return { status: 403, error: { code: "RedirectsNotAllowed", message: "Redirects are not allowed for this request." } }; } return await this.followRedirectWithFetchFollow(requestOptions); } if (response.status === 302) { if (!allowRedirects) { return { status: 403, error: { code: "RedirectsNotAllowed", message: "Redirects are not allowed for this request." } }; } return await this.followRedirect(response, accessToken, method, data, headers); } return await this.processResponse(response); } catch { return this.createInternalServerError(); } } /** * Follows redirects using the fetch default 'follow' behavior. * Used for environments where manual redirect returns opaque responses. * * @param requestOptions - The original request options * @returns Promise that resolves to the final API response */ async followRedirectWithFetchFollow(requestOptions) { try { const response = await fetch(requestOptions.url, { method: requestOptions.method, headers: requestOptions.headers, body: requestOptions.body, redirect: "follow" }); if (response.redirected) { try { this.validateRedirectUrlSecurity(response.url); } catch (error) { return { status: 502, error: { code: "InvalidRedirectUrl", message: error instanceof Error ? error.message : "Invalid redirect URL" } }; } } return await this.processResponse(response); } catch { return this.createInternalServerError(); } } /** * Handles 302 redirect responses by validating and following the redirect. * * @param response - The 302 redirect response * @param accessToken - The client access token * @param method - The HTTP method * @param data - Optional request payload * @param headers - Optional request headers (will be forwarded to redirect) * @param redirectCount - Current redirect depth * @returns Promise that resolves to the final API response */ async followRedirect(response, accessToken, method, data, headers, redirectCount = 0) { const verificationResult = this.checkRedirectValidity(response, redirectCount); if (verificationResult.error) { return verificationResult.error; } const redirectUrl = verificationResult.redirectUrl; try { const requestOptions = this.createRequestOptions(accessToken, method, redirectUrl, data, headers); const redirectResponse = await fetch(requestOptions.url, { method: requestOptions.method, headers: requestOptions.headers, body: requestOptions.body, redirect: "manual" }); if (redirectResponse.status === 302) { return await this.followRedirect(redirectResponse, accessToken, method, data, headers, redirectCount + 1); } return await this.processResponse(redirectResponse); } catch { return this.createInternalServerError(); } } /** * Processes a non-redirect HTTP response. * * @param response - The HTTP response to process * @returns Promise that resolves to a typed API response */ async processResponse(response) { const responseData = response.status !== 204 ? await response.json() : void 0; if (!response.ok) { if (isErrorResponse(responseData)) { return { status: response.status, error: responseData.error }; } throw new Error("An error occurred while processing the request"); } return { status: response.status, data: responseData === void 0 || responseData === "" ? void 0 : responseData }; } /** * Creates a generic internal server error response. * * @returns A 500 error response for internal exceptions */ createInternalServerError() { return { status: 500, error: { code: "InternalServerError", message: "An internal exception happened while calling iTwins Service" } }; } /** * Verifies that a redirect response is valid and safe to follow. * Performs three critical validations: * 1. Checks redirect count to prevent infinite loops * 2. Ensures Location header is present * 3. Validates redirect URL for security * * @param response - The 302 redirect response to verify * @param redirectCount - Current redirect depth * @returns Verification result with either error or validated redirect URL */ checkRedirectValidity(response, redirectCount) { if (redirectCount >= this._maxRedirects) { return { error: { status: 508, error: { code: "TooManyRedirects", message: `Maximum redirect limit (${this._maxRedirects}) exceeded. Possible redirect loop detected.` } }, redirectUrl: "" }; } const redirectUrl = response.headers.get("location"); if (!redirectUrl) { return { error: { status: 502, error: { code: "InvalidRedirect", message: "302 redirect response missing Location header" } }, redirectUrl: "" }; } try { this.validateRedirectUrlSecurity(redirectUrl); } catch (error) { return { error: { status: 502, error: { code: "InvalidRedirectUrl", message: error instanceof Error ? error.message : "Invalid redirect URL" } }, redirectUrl: "" }; } return { redirectUrl }; } /** * Validates that a redirect URL is secure and targets a trusted APIM Bentley domain. * * This method enforces security requirements for following HTTP redirects: * - URL must use HTTPS protocol (not HTTP) * - Domain must be a Bentley-owned domain (*api.bentley.com) * * @param url - The redirect URL to validate * @returns True if the URL is valid and safe to follow * @throws Error if the URL is invalid, uses HTTP, or targets an untrusted domain * * @remarks * This validation is critical for security when following 302 redirects in federated * architecture scenarios. It prevents redirect attacks that could leak authentication * credentials to malicious domains. * * @example * ```typescript * // Valid URLs * this.validateRedirectUrl("https://api.bentley.com/resource"); * // Invalid URLs (will throw) * this.validateRedirectUrl("https://evil-tuna.com/phishing/"); // Non-Bentley domain * this.validateRedirectUrl("https://bentley.com.evil.com/fake"); // Domain spoofing attempt * ``` */ validateRedirectUrlSecurity(url) { let parsedUrl; try { parsedUrl = new URL(url); } catch { throw new Error(`Invalid redirect URL: malformed URL "${url}"`); } if (parsedUrl.protocol !== "https:") { throw new Error(`Invalid redirect URL: HTTPS required, but URL uses "${parsedUrl.protocol}" protocol. URL: ${url}`); } const hostname = parsedUrl.hostname.toLowerCase(); const allowedDomains = [ "api.bentley.com" ]; const isBentleyDomain = allowedDomains.some((domain) => hostname === domain || hostname.endsWith(`-${domain}`)); if (!isBentleyDomain) { throw new Error(`Invalid redirect URL: domain "${hostname}" is not a trusted Bentley domain. Only api.bentley.com and its subdomains are allowed.`); } return true; } /** * Creates request configuration options with authentication headers. * Validates required parameters and sets up proper content type for JSON requests. * * @param accessTokenString - The client access token string for authorization * @param method - The HTTP method type (GET, POST, DELETE, etc.) * @param url - The complete URL of the request endpoint * @param data - Optional payload data to be JSON stringified for the request body * @param headers - Optional additional request headers to include * @returns RequestConfig object with method, URL, body, and headers configured * @throws Will throw an error if access token or URL are missing/invalid */ createRequestOptions(accessTokenString, method, url, data, headers = {}) { if (!accessTokenString) { throw new Error("Access token is required"); } if (!url) { throw new Error("URL is required"); } let body; if (!(data instanceof Blob)) { body = JSON.stringify(data); } else { body = data; } return { method, url, body, headers: { ...headers, authorization: accessTokenString, "content-type": headers.contentType || headers["content-type"] ? headers.contentType || headers["content-type"] : "application/json" } }; } /** * 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.getQueryStringArg( * ITwinsAccess.ITWINS_QUERY_PARAM_MAPPING, * { * search: "Building A", * top: 10, * subClass: "Asset" * } * ); * // Returns: "$search=Building%20A&$top=10&subClass=Asset" * ``` */ getQueryStringArg(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( * { search: "Building A", top: 10 }, * { search: "$search", top: "$top" } * ); * // Returns: ["$search=Building%20A", "$top=10"] * ``` */ buildQueryParams(queryArg, mapping) { const params = []; for (const [paramKey, mappedValue] of Object.entries(mapping)) { if (mappedValue === "") continue; const queryArgValue = queryArg[paramKey]; if (queryArgValue !== void 0 && queryArgValue !== null) { const stringValue = String(queryArgValue); params.push(`${mappedValue}=${encodeURIComponent(stringValue)}`); } } return params; } }; // node_modules/@itwin/itwins-client/lib/esm/BaseITwinsApiClient.js var BaseITwinsApiClient = class extends BaseBentleyAPIClient { /** * Maps the properties of {@link ITwinsQueryArg} to their corresponding query parameter names. * * @remarks * This mapping is used to translate internal property names to the expected parameter names * when constructing iTwins queries. Properties mapped to empty strings are excluded from * the query string as they should be sent as headers instead. * * The mapping includes both OData query parameters (prefixed with $) and iTwins-specific * parameters for filtering and pagination. * * @readonly */ static iTwinsQueryParamMapping = { subClass: "subClass", type: "type", status: "status", search: "$search", displayName: "displayName", // eslint-disable-next-line id-denylist number: "number", top: "$top", skip: "$skip", parentId: "parentId", iTwinAccountId: "iTwinAccountId", includeInactive: "includeInactive", resultMode: "", queryScope: "" }; /** * Maps the properties some of the {@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 iTwins queries. Properties mapped to empty strings are excluded from * the query string as they should be sent as headers instead. * * The mapping includes both OData query parameters (prefixed with $) and iTwins-specific * parameters for filtering and pagination. * * @readonly */ // eslint-disable-next-line @typescript-eslint/naming-convention static ODataParamMapping = { top: "$top", skip: "$skip", search: "$search" }; /** * Maps the properties some of the {@link ODataQueryParams} and all of the {@link ITwinsQueryArg} to their corresponding query parameter names. * * @remarks * This mapping is used to translate internal property names to the expected parameter names * when constructing iTwins queries. Properties mapped to empty strings are excluded from * the query string as they should be sent as headers instead. * * The mapping includes both OData query parameters (prefixed with $) and iTwins-specific * parameters for filtering and pagination. * * @readonly */ // eslint-disable-next-line @typescript-eslint/naming-convention static ITwinsGetQueryParamMapping = { subClass: "subClass", type: "type", status: "status", search: "$search", displayName: "displayName", // eslint-disable-next-line id-denylist number: "number", top: "$top", skip: "$skip", parentId: "parentId", iTwinAccountId: "iTwinAccountId", includeInactive: "includeInactive", resultMode: "", queryScope: "", filter: "$filter", orderby: "$orderby", select: "$select" }; /** * Maps the properties of class and subclass to their corresponding query parameter names. * * @remarks * This mapping is used to translate internal property names to the expected parameter names * when constructing repository queries. * * @readonly */ static repositoryParamMapping = { class: "class", subClass: "subClass" }; /** * The base URL for iTwins API endpoints. * The URL can be customized via the constructor parameter or automatically * modified based on the IMJS_URL_PREFIX environment variable for different * deployment environments. * * @readonly */ _baseUrl = "https://api.bentley.com/itwins"; /** * Creates a new BaseClient instance for iTwins 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://dev-api.bentley.com/itwins"); * ``` */ constructor(url, maxRedirects) { super(maxRedirects); if (url !== void 0) { this._baseUrl = url; } else { const urlPrefix = globalThis.IMJS_URL_PREFIX; if (urlPrefix) { const baseUrl = new URL(this._baseUrl); baseUrl.hostname = `${urlPrefix}${baseUrl.hostname}`; this._baseUrl = baseUrl.href; } } } }; // node_modules/@itwin/itwins-client/lib/esm/iTwinsClient.js var ITwinsClient = class _ITwinsClient extends BaseITwinsApiClient { constructor(url, maxRedirects) { super(url, maxRedirects); } /** Get a list of iTwin exports for the current user * @param accessToken The client access token string * @returns Promise that resolves with an array of export operations */ async getExports(accessToken) { const url = `${this._baseUrl}/exports`; return this.sendGenericAPIRequest(accessToken, "GET", url); } /** Get details of a specific iTwin export operation * @param accessToken The client access token string * @param id The id of the export operation to retrieve * @returns Promise that resolves with the export operation details */ async getExport(accessToken, id) { const url = `${this._baseUrl}/exports/${id}`; return this.sendGenericAPIRequest(accessToken, "GET", url); } /** * Create a new iTwin export * @param accessToken The client access token string * @param args Export query arguments including scope, filters, and output format * @returns Export response with operation details */ async createExport(accessToken, args) { const url = `${this._baseUrl}/exports`; return this.sendGenericAPIRequest(accessToken, "POST", url, args, void 0); } /** Get favorites iTwins accessible to the user * @param accessToken The client access token string * @param arg Optional query arguments, for paging, searching, and filtering * @returns Array of iTwins, may be empty, if no favorites * @example * ```typescript * // Returns MultiITwinMinimalResponse * const minimal = await client.getFavoritesITwins(token, { resultMode: "minimal" }); * * // Returns MultiITwinRepresentationResponse * const detailed = await client.getFavoritesITwins(token, { resultMode: "representation" }); * * // Defaults to minimal when no resultMode specified * const defaultResult = await client.getFavoritesITwins(token); * ``` */ async getFavoritesITwins(accessToken, arg) { const headers = this.getHeaders(arg); const url = `${this._baseUrl}/favorites/?${this.getQueryStringArg(_ITwinsClient.iTwinsQueryParamMapping, arg ?? {})}`; return this.sendGenericAPIRequest(accessToken, "GET", url, void 0, headers); } /** Add the specified iTwin to the user's favorites list * @param accessToken The client access token string * @param iTwinId The id of the iTwin to add to favorites * @returns Promise that resolves when the iTwin is successfully added to favorites */ async addITwinToFavorites(accessToken, iTwinId) { const url = `${this._baseUrl}/favorites/${iTwinId}`; return this.sendGenericAPIRequest(accessToken, "POST", url); } /** Remove the specified iTwin from the user's favorites list * @param accessToken The client access token string * @param iTwinId The id of the iTwin to remove from favorites * @returns Promise that resolves when the iTwin is successfully removed from favorites */ async removeITwinFromFavorites(accessToken, iTwinId) { const url = `${this._baseUrl}/favorites/${iTwinId}`; return this.sendGenericAPIRequest(accessToken, "DELETE", url); } /** Upload an image to the specified iTwin * @param accessToken The client access token string * @param iTwinId The id of the iTwin to upload the image to * @param imageBlob The image file as a Blob (must be PNG or JPEG) * @param contentType The content type of the image ("image/png" | "image/jpeg") * @returns Promise that resolves with the uploaded image details including URLs for small and large versions */ async uploadITwinImage(accessToken, iTwinId, imageBlob, contentType) { const url = `${this._baseUrl}/${iTwinId}/image`; return this.sendGenericAPIRequest(accessToken, "PUT", url, imageBlob, { contentType }); } /** Get the image associated with the specified iTwin * @param accessToken The client access token string * @param iTwinId The id of the iTwin to retrieve the image from * @returns Promise that resolves with the image details including URLs for small and large versions */ async getITwinImage(accessToken, iTwinId) { const url = `${this._baseUrl}/${iTwinId}/image`; return this.sendGenericAPIRequest(accessToken, "GET", url); } /** Delete the image associated with the specified iTwin * @param accessToken The client access token string * @param iTwinId The id of the iTwin to delete the image from * @returns Promise that resolves when the image is successfully deleted */ async deleteITwinImage(accessToken, iTwinId) { const url = `${this._baseUrl}/${iTwinId}/image`; return this.sendGenericAPIRequest(accessToken, "DELETE", url); } /** Add the specified iTwin to the user's recently used list * @param accessToken The client access token string * @param iTwinId The id of the iTwin to add to the recently used list * @returns Promise that resolves when the iTwin is successfully added to the recently used list */ async addITwinToMyRecents(accessToken, iTwinId) { const url = `${this._baseUrl}/recents/${iTwinId}`; return this.sendGenericAPIRequest(accessToken, "POST", url); } /** Get recently used iTwins for the current user * * Retrieves a list of recently used iTwins for the calling user. A user can only have 25 recently used iTwins. * They are returned in order with the most recently used iTwin first in the list. * * iTwins with status=Inactive are not returned by default. This improves query performance and reduces clutter * in user interfaces by filtering out unused iTwins. You should still provide a way for users to see their * Inactive iTwins if they request them. In the API, you can do this by setting the status parameter or by * using the includeInactive parameter. * * @param accessToken The client access token string * @param arg Optional query arguments, for paging, searching, and filtering (including status and includeInactive) * @returns Promise that resolves with an array of recently used iTwins (maximum 25), ordered by most recent first * @example * ```typescript * // Returns MultiITwinMinimalResponse * const minimal = await client.getRecentUsedITwins(token, { resultMode: "minimal" }); * * // Returns MultiITwinRepresentationResponse * const detailed = await client.getRecentUsedITwins(token, { resultMode: "representation" }); * * // Defaults to minimal when no resultMode specified * const defaultResult = await client.getRecentUsedITwins(token); * ``` */ async getRecentUsedITwins(accessToken, arg) { const headers = this.getHeaders(arg); let url = `${this._baseUrl}/recents`; const query = this.getQueryStringArg(_ITwinsClient.iTwinsQueryParamMapping, arg ?? {}); if (query !== "") url += `?${query}`; return this.sendGenericAPIRequest(accessToken, "GET", url, void 0, headers); } /** Create a new iTwin Repository * @param accessToken The client access token string * @param iTwinId The id of the iTwin * @param repository The Repository data to be created * @returns Promise that resolves with the created repository details * @beta */ async createRepository(accessToken, iTwinId, repository) { const url = `${this._baseUrl}/${iTwinId}/repositories`; return this.sendGenericAPIRequest(accessToken, "POST", url, repository); } /** Delete the specified iTwin Repository * @param accessToken The client access token string * @param iTwinId The id of the iTwin * @param repositoryId The id of the Repository to delete * @returns Promise that resolves when the repository is successfully deleted */ async deleteRepository(accessToken, iTwinId, repositoryId) { const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}`; return this.sendGenericAPIRequest(accessToken, "DELETE", url); } /** Get repositories accessible to user with optional filtering * @param accessToken The client access token string * @param iTwinId The id of the iTwin * @param arg Optional query arguments for class and subClass filtering. If subClass is specified, class is also required. * @returns Promise that resolves with an array of repositories, may be empty */ async getRepositories(accessToken, iTwinId, arg) { const url = `${this._baseUrl}/${iTwinId}/repositories/?${this.getQueryStringArg(_ITwinsClient.repositoryParamMapping, arg)}`; return this.sendGenericAPIRequest(accessToken, "GET", url); } /** Get a specific repository by ID * @param accessToken The client access token string * @param iTwinId The id of the iTwin * @param repositoryId The id of the Repository * @returns Promise that resolves with the repository details * @beta */ async getRepository(accessToken, iTwinId, repositoryId) { const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}`; return this.sendGenericAPIRequest(accessToken, "GET", url); } /** Update the specified iTwin Repository * @param accessToken The client access token string * @param iTwinId The id of the iTwin * @param repositoryId The id of the Repository * @param repository Updated repository data (excluding id, class, and subClass) * @returns Promise that resolves with the updated repository * @beta */ async updateRepository(accessToken, iTwinId, repositoryId, repository) { const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}`; return this.sendGenericAPIRequest(accessToken, "PATCH", url, repository); } /** * Create a repository resource for a repository of class GeographicInformationSystem * @param accessToken - The client access token string for authorization * @param iTwinId - The id of the iTwin that contains the repository * @param repositoryId - The id of the GeographicInformationSystem repository to add the resource to * @param repositoryResource - The repository resource to create with required id and displayName properties * @returns Promise that resolves with the created repository resource details * * @beta */ async createRepositoryResource(accessToken, iTwinId, repositoryId, repositoryResource) { const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}/resources`; return this.sendGenericAPIRequest(accessToken, "POST", url, repositoryResource); } /** * Delete a repository resource * @param accessToken - The client access token string for authorization * @param iTwinId - The id of the iTwin that contains the repository * @param repositoryId - The id of the GeographicInformationSystem repository to add the resource to * @param resourceId - The id repository resource to delete * @returns Promise that resolves when the iTwin is successfully deleted * * @beta */ async deleteRepositoryResource(accessToken, iTwinId, repositoryId, resourceId) { const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}/resources/${resourceId}`; return this.sendGenericAPIRequest(accessToken, "DELETE", url); } /** * Get a specific repository resource by ID * * Automatically follows 302 redirects to federated repository endpoints when the repository * uses a federated architecture. Authentication headers are forwarded transparently. * * @param accessToken - The client access token string for authorization * @param iTwinId - The id of the iTwin that contains the repository * @param repositoryId - The id of the repository containing the resource * @param resourceId - The unique id of the repository resource to retrieve * @param resultMode - Optional result mode controlling the level of detail returned (minimal or representation) * @returns Promise that resolves with the repository resource details in the requested format * @example * ```typescript * // Returns GetRepositoryResourceMinimalResponse * const minimal = await client.getRepositoryResource(token, "iTwinId", "repoId", "resourceId", "minimal"); * * // Returns GetRepositoryResourceRepresentationResponse * const detailed = await client.getRepositoryResource(token, "iTwinId", "repoId", "resourceId", "representation"); * * // Defaults to minimal when no resultMode specified * const defaultResult = await client.getRepositoryResource(token, "iTwinId", "repoId", "resourceId"); * ``` * @beta */ async getRepositoryResource(accessToken, iTwinId, repositoryId, resourceId, resultMode) { const headers = this.getResultModeHeaders(resultMode); const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}/resources/${resourceId}`; return this.sendGenericAPIRequest(accessToken, "GET", url, void 0, headers, true); } /** * Get multiple repository resources with optional filtering and pagination * * Automatically follows 302 redirects to federated repository endpoints when the repository * uses a federated architecture. Authentication headers are forwarded transparently. * * @param accessToken - The client access token string for authorization * @param iTwinId - The id of the iTwin that contains the repository * @param repositoryId - The id of the repository containing the resources * @param args - Optional query parameters for search, pagination (skip, top) * @param resultMode - Optional result mode controlling the level of detail returned (minimal or representation) * @returns Promise that resolves with an array of repository resources in the requested format * @example * ```typescript * // Returns GetMultiRepositoryResourceMinimalResponse * const minimal = await client.getRepositoryResources(token, "iTwinId", "repoId", undefined, "minimal"); * * // Returns GetMultiRepositoryResourceRepresentationResponse * const detailed = await client.getRepositoryResources(token, "iTwinId", "repoId", { search: "test" }, "representation"); * * // Defaults to minimal when no resultMode specified * const defaultResult = await client.getRepositoryResources(token, "iTwinId", "repoId"); * ``` * @beta */ async getRepositoryResources(accessToken, iTwinId, repositoryId, args, resultMode) { const headers = this.getResultModeHeaders(resultMode); const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}/resources?${this.getQueryStringArg(_ITwinsClient.ODataParamMapping, args)}`; return this.sendGenericAPIRequest(accessToken, "GET", url, void 0, headers, true); } /** * Get a list of resources from a repository using a capability URI * * This method enables direct calls to federated repository endpoints using URIs from * repository capabilities. * * @param accessToken - The client access token string for authorization * @param uri - The capability URI from repository.capabilities.resources.uri * @param args - Optional OData query parameters for filtering and pagination * @param resultMode - Optional result mode controlling the level of detail returned (minimal or representation) * @returns Promise that resolves with the list of repository resources in the requested format * @example * ```typescript * // Get repository with capabilities * const repo = await client.getRepository(token, iTwinId, repositoryId); * * // Extract capability URI * const resourcesUri = repo.data?.repository.capabilities?.resources?.uri; * * if (resourcesUri) { * // Returns GetMultiRepositoryResourceMinimalResponse * const minimal = await client.getRepositoryResourcesByUri(token, resourcesUri, undefined, "minimal"); * * // Returns GetMultiRepositoryResourceRepresentationResponse * const detailed = await client.getRepositoryResourcesByUri(token, resourcesUri, undefined, "representation"); * * // Defaults to minimal when no resultMode specified * const defaultResult = await client.getRepositoryResourcesByUri(token, resourcesUri); * } * ``` * @beta */ async getRepositoryResourcesByUri(accessToken, uri, args, resultMode) { const headers = this.getResultModeHeaders(resultMode); const urlWithQuery = args ? `${uri}?${this.getQueryStringArg(_ITwinsClient.ODataParamMapping, args)}` : uri; return this.sendGenericAPIRequest(accessToken, "GET", urlWithQuery, void 0, headers, true); } /** * Get a specific resource from a repository using a capability URI * * This method enables direct calls to federated repository endpoints using URIs from * repository capabilities. * * @param accessToken - The client access token string for authorization * @param uri - The capability URI from repository.capabilities.resources.uri for a specific resource * @param resultMode - Optional result mode controlling the level of detail returned (minimal or representation) * @returns Promise that resolves with the repository resource details in the requested format * @example * ```typescript * // Get repository with capabilities * const repo = await client.getRepository(token, iTwinId, repositoryId); * * // Construct resource URI (typically from a previous query or known resource ID) * const baseUri = repo.data?.repository.capabilities?.resources?.uri; * const resourceUri = `${baseUri}/resourceId`; * * if (resourceUri) { * // Returns GetRepositoryResourceMinimalResponse * const minimal = await client.getRepositoryResourceByUri(token, resourceUri, "minimal"); * * // Returns GetRepositoryResourceRepresentationResponse * const detailed = await client.getRepositoryResourceByUri(token, resourceUri, "representation"); * * // Defaults to minimal when no resultMode specified * const defaultResult = await client.getRepositoryResourceByUri(token, resourceUri); * } * ``` * @beta */ async getRepositoryResourceByUri(accessToken, uri, resultMode) { const headers = this.getResultModeHeaders(resultMode); return this.sendGenericAPIRequest(accessToken, "GET", uri, void 0, headers, true); } /** * Retrieves graphics metadata for a specific repository resource using ID-based parameters. * * * Returns graphics content URIs and authentication information needed to access visualization data * for a resource. The response includes content type, access URI, optional authentication credentials, * and CesiumJS provider configuration when applicable. This method supports redirect-based routing to * federated graphics services. * * For federated architecture support, consider using getResourceGraphicsByUri with the URI * from resource.capabilities.graphics.uri instead. * * @param accessToken - The client access token string for authorization * @param iTwinId - The iTwin identifier * @param repositoryId - The repository identifier * @param resourceId - The resource identifier * @returns Promise that resolves with graphics metadata including content type, URI, and authentication * @example * ```typescript * // Get graphics for a specific resource * const graphics = await client.getResourceGraphics( * token, * 'itwin-id', * 'imodels', * 'imodel-resource-id' * ); * * if (graphics.data) { * graphics.data.graphics.forEach(graphic => { * console.log('Content type:', graphic.type); * console.log('Graphics URI:', graphic.uri); * * // Handle authentication if present * if (graphic.authentication) { * switch (graphic.authentication.type) { * case 'Header': * case 'QueryParameter': * console.log('Auth key:', graphic.authentication.key); * break; * case 'Basic': * console.log('Username:', graphic.authentication.username); * break; * } * } * }); * } * ``` * @beta */ async getResourceGraphics(accessToken, iTwinId, repositoryId, resourceId) { const url = `${this._baseUrl}/${iTwinId}/repositories/${repositoryId}/resources/${resourceId}/graphics`; return this.sendGenericAPIRequest(accessToken, "GET", url, void 0, void 0, true); } /** * Get graphics metadata for a repository resource using a capability URI * * This method enables direct calls to federated graphics endpoints using URIs from * resource capabilities. Instead of constructing URLs from iTwinId, repositoryId, and * resourceId, it accepts the URI directly from capabilities.graphics.uri. * * Note: This method requires that the resource supports graphics capabilities and that * the access token has appropriate permissions for the target graphics service. * * @param accessToken - The client access token string for authorization * @param uri - The capability URI from resource.capabilities.graphics.uri * @returns Promise that resolves with the graphics metadata including authentication and provider information * @example * ```typescript * // Get resource with graphics capability * const resource = await client.getRepositoryResource(token, iTwinId, repositoryId, resourceId); * * // Extract graphics capability URI * const graphicsUri = resource.data?.resource.capabilities?.graphics?.uri; * * if (graphicsUri) { * const graphics = await client.getResourceGraphicsByUri(token, graphicsUri); * * if (graphics.data) { * console.log('Graphics content type:', graphics.data.graphics.contentType); * console.log('Graphics URI:', graphics.data.graphics.uri); * } * } * ``` * @beta */ async getResourceGraphicsByUri(accessToken, uri) { return this.sendGenericAPIRequest(accessToken, "GET", uri, void 0, void 0, true); } /** Get a specific iTwin by ID * @param accessToken The client access token string * @param iTwinId The id of the iTwin * @param resultMode (Optional) iTwin result mode: minimal or representation * @returns Promise that resolves with the iTwin details * @example * ```typescript * // Returns ITwinMinimalResponse * const minimal = await client.getITwin(token, "id", "minimal"); * * // Returns ITwinRepresentationResponse * const detailed = await client.getITwin(token, "id", "representation"); * * // Defaults to minimal when no resultMode specified * const defaultResult = await client.getITwin(token, "id"); * ``` */ async getITwin(accessToken, iTwinId, resultMode) { const headers = this.getResultModeHeaders(resultMode); const url = `${this._