@rschili/melodi-cli
Version:
iModel utility
1,302 lines (1,288 loc) • 169 kB
JavaScript
#!/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._