UNPKG

@itwin/reality-data-client

Version:

HTTP Client for the iTwin Platform Reality Management APIs

806 lines (715 loc) 28.3 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 RealityDataClient */ import { type AccessToken, BentleyError } from "@itwin/core-bentley"; import type { AuthorizationClient, CartographicRange, RealityDataAccess, } from "@itwin/core-common"; import axios, { type AxiosResponse } from "axios"; import { ITwinRealityData } from "./RealityData"; import { getRequestConfig } from "./RequestOptions"; import { Project } from "./Projects"; import { Angle } from "./helper/Angle"; /** Options for initializing Reality Data Client * @beta */ export interface RealityDataClientOptions { /** The authorization client to use to get access token to Context Share API (authority: https://ims.bentley.com ) * When define it will ignore accessToken from API parameters and will get an access token from this client. */ authorizationClient?: AuthorizationClient; /** API Version. v1 by default */ version?: ApiVersion; /** API Url. Used to select environment. Defaults to "https://api.bentley.com/reality-management" */ baseUrl?: string; } /** Available Reality Management API Versions */ export enum ApiVersion { v1, } /** Criteria used to query for reality data associated with an iTwin context. * @see getRealityDatas * @beta */ export interface RealityDataQueryCriteria { /** If supplied, only reality data overlapping this range will be included. */ extent?: CartographicRange; /** If true, return all properties for every reality data found in query. * If false or undefined, return a minimal representation containing id, displayName and type, along with a url to get full reality data details. */ getFullRepresentation?: boolean; /** If supplied, queries a maximum number of first results Found. Max 500. If not supplied, the query should return the first 100 RealityData found.*/ top?: number; /** Continuation token to get current query's next results.*/ continuationToken?: string; /** Parameter that orders reality data in ascending or descending order. Default is ascending (asc). Can be used on any simple text, date or number property. Example : size desc */ orderBy?: string; /** Searches the given text (case insensitive) in reality data's text properties, such as in Group, DisplayName, Description, RootDocument, Acquirer, Tags. */ search?: string; /** Queries for reality data of specified types.*/ types?: string[]; /** Queries for reality data in which the acquisition is in given date range.*/ acquisitionDates?: DateRange; /** Queries for reality data where the creation date is in given date range.*/ createdDateTime?: DateRange; /** Queries for reality data where the modification date is in given date range.*/ modifiedDateTime?: DateRange; /** Queries for reality data where the last accessed date is in given date range.*/ lastAccessedDateTime?: DateRange; /** Queries for reality data owned by a specific user.*/ ownerId?: string; /** Queries for reality data stored in a specific data center.*/ dataCenter?: string; /** Queries for reality data with exact matching tag.*/ tag?: string; } /** Date range*/ export interface DateRange { startDateTime: Date; endDateTime: Date; } /** * Response object containing RealityData and continuation token */ export interface RealityDataResponse { realityDatas: ITwinRealityData[]; continuationToken?: string; } /** * Client wrapper to Reality Management API. * An instance of this class is used to extract reality data from the Reality Management API. * Most important methods enable to obtain a specific reality data, fetch all reality data associated with an iTwin and * all reality data of an iTwin within a provided spatial extent. * This class also implements extraction of the Azure blob address. * @beta */ export class RealityDataAccessClient implements RealityDataAccess { public readonly baseUrl: string = "https://api.bentley.com/reality-management/reality-data"; public readonly apiVersion: ApiVersion = ApiVersion.v1; public readonly authorizationClient: AuthorizationClient | undefined = undefined; /** * Creates an instance of RealityDataAccessClient. */ public constructor(realityDataClientOptions?: RealityDataClientOptions) { // runtime config if (realityDataClientOptions) { if (realityDataClientOptions.version) this.apiVersion = realityDataClientOptions.version; if (realityDataClientOptions.baseUrl) this.baseUrl = this.setBaseUrl(realityDataClientOptions.baseUrl); if (realityDataClientOptions.authorizationClient) this.authorizationClient = realityDataClientOptions.authorizationClient; } } /** * Ensures the reality data client points to Reality Management API, as many users hardcode the url to the deprecated Reality Data API. * @param baseUrl base url given by users of this client * @returns base url to Reality Management API */ private setBaseUrl(baseUrl: string): string { const url = new URL(baseUrl); switch (url.host) { case "dev-api.bentley.com": return "https://dev-api.bentley.com/reality-management/reality-data"; case "qa-api.bentley.com": return "https://qa-api.bentley.com/reality-management/reality-data"; case "api.bentley.com": return "https://api.bentley.com/reality-management/reality-data"; default: throw new Error("invalid host"); } } /** * Try to use authorizationClient in RealityDataClientOptions to get the access token * otherwise, will return the input token * This is a workaround to support different authorization client for the reality data client and iTwin-core. */ private async resolveAccessToken(accessToken: AccessToken): Promise<string> { return this.authorizationClient ? this.authorizationClient.getAccessToken() : accessToken; } /** * This method returns the URL to obtain the Reality Data details. * Technically it should never be required as the RealityData object returned should have all the information to obtain the * data. * @param iTwinId the iTwin identifier * @param realityDataId realityData identifier * @returns string containing the URL to reality data for indicated tile. * @beta */ public async getRealityDataUrl( iTwinId: string | undefined, realityDataId: string, ): Promise<string> { if (iTwinId) { return `${this.baseUrl}/${realityDataId}?iTwinId=${iTwinId}`; } return `${this.baseUrl}/${realityDataId}`; } /** * Gets reality data with all of its properties * @param accessToken The client request context. * @param iTwinId id of associated iTwin (or project) * @param realityDataId realityData identifier * @returns The requested reality data. * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 404 when the specified reality data is not found * @throws [[BentleyError]] with code 422 when the request is invalid * @beta */ public async getRealityData( accessToken: AccessToken, iTwinId: string | undefined, realityDataId: string, ): Promise<ITwinRealityData> { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = `${await this.getRealityDataUrl(iTwinId, realityDataId)}`; try { const realityDataResponse = await axios.get( url, getRequestConfig(accessTokenResolved, "GET", url, this.apiVersion), ); // Axios throws on 4XX and 5XX; we make sure the response here is 200 if (realityDataResponse.status !== 200) throw new BentleyError( 422, iTwinId ? `Could not fetch reality data: ${realityDataId} with iTwinId ${iTwinId}` : `Could not fetch reality data: ${realityDataId}`, ); const realityData = new ITwinRealityData( this, realityDataResponse.data.realityData, iTwinId, ); return realityData; } catch (error) { return this.handleError(error); } } /** * Gets all reality data associated with the iTwin. * @param accessToken The client request context. * @param iTwinId id of associated iTwin * @param criteria Criteria by which to query. * @returns an array of RealityData that are associated to the iTwin. * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 422 when the request is invalid * @beta */ public async getRealityDatas( accessToken: AccessToken, iTwinId: string | undefined, criteria: RealityDataQueryCriteria | undefined, ): Promise<RealityDataResponse> { try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = new URL(this.baseUrl); if (iTwinId) url.searchParams.append("iTwinId", iTwinId); if (criteria) { if (criteria.continuationToken) { url.searchParams.append( "continuationToken", criteria.continuationToken, ); } if (criteria.top) { const top = criteria.top; if (top > 500) { throw new BentleyError( 422, "Maximum value for top parameter is 500.", ); } url.searchParams.append("$top", top.toString()); } if (criteria.extent) { const iModelRange = criteria.extent.getLongitudeLatitudeBoundingBox(); const extent = `${Angle.radiansToDegrees(iModelRange.low.x)},${Angle.radiansToDegrees(iModelRange.low.y)},${Angle.radiansToDegrees(iModelRange.high.x)},${Angle.radiansToDegrees(iModelRange.high.y)}`; url.searchParams.append("extent", extent); } if (criteria.orderBy) { url.searchParams.append("$orderBy", criteria.orderBy); } if (criteria.search) { url.searchParams.append("$search", criteria.search); } if (criteria.types) { url.searchParams.append("types", criteria.types.join(",")); } if (criteria.acquisitionDates) { const startDateTime = this.formatIsoString( criteria.acquisitionDates.startDateTime, ); const endDateTime = this.formatIsoString( criteria.acquisitionDates.endDateTime, ); url.searchParams.append( "acquisitionDateTime", `${startDateTime}/${endDateTime}`, ); } if (criteria.createdDateTime) { const startDateTime = this.formatIsoString( criteria.createdDateTime.startDateTime, ); const endDateTime = this.formatIsoString( criteria.createdDateTime.endDateTime, ); url.searchParams.append( "createdDateTime", `${startDateTime}/${endDateTime}`, ); } if (criteria.modifiedDateTime) { const startDateTime = this.formatIsoString( criteria.modifiedDateTime.startDateTime, ); const endDateTime = this.formatIsoString( criteria.modifiedDateTime.endDateTime, ); url.searchParams.append( "modifiedDateTime", `${startDateTime}/${endDateTime}`, ); } if (criteria.lastAccessedDateTime) { const startDateTime = this.formatIsoString( criteria.lastAccessedDateTime.startDateTime, ); const endDateTime = this.formatIsoString( criteria.lastAccessedDateTime.endDateTime, ); url.searchParams.append( "lastAccessedDateTime", `${startDateTime}/${endDateTime}`, ); } if (criteria.ownerId) { url.searchParams.append("ownerId", criteria.ownerId); } if (criteria.dataCenter) { url.searchParams.append("dataCenter", criteria.dataCenter); } if (criteria.tag) { url.searchParams.append("tag", criteria.tag); } } const response = await axios.get( url.href, getRequestConfig( accessTokenResolved, "GET", url.href, this.apiVersion, criteria?.getFullRepresentation === true ? true : false, ), ); // Axios throws on 4XX and 5XX; we make sure the response here is 200 if (response.status !== 200) throw new BentleyError( 422, iTwinId ? `Could not fetch reality data with iTwinId ${iTwinId}` : "Could not fetch reality data", ); const realityDatasResponseBody = response.data; const realityDataResponse: RealityDataResponse = { realityDatas: [], continuationToken: this.extractContinuationToken( response.data._links?.next?.href, ), }; realityDatasResponseBody.realityData.forEach((realityData: any) => { realityDataResponse.realityDatas.push( new ITwinRealityData(this, realityData, iTwinId), ); }); return realityDataResponse; } catch (error) { return this.handleError(error); } } /** * trims milliseconds from date.toISOString() method to conform to for date parameters in the API. * See https://developer.bentley.com/apis/reality-management/operations/get-all-reality-data/#request-parameters * @param date date to format * @returns dateTime string in format YYYY-MM-DDTHH:mm:ssZ e.g. 2021-08-01T00:00:00Z */ private formatIsoString(date: Date): string { return `${date.toISOString().slice(0, -5)}Z`; } private extractContinuationToken( url: string | undefined, ): string | undefined { if (url) { // API returns some case sensitive parameters e.g. "ContinuationToken". Therefore first, set parameters to lowercase const searchParams = new URLSearchParams(url); const newParams = new URLSearchParams(); for (const [name, value] of searchParams) { newParams.append(name.toLowerCase(), value); } // Then get continuation token value in case insensitive manner. const token = newParams.get("continuationtoken"); return token ? token : undefined; } return undefined; } /** * Retrieves the list of Projects associated to the specified realityData. * @deprecated in 1.0.1, getRealityDataProjects is deprecated and no longer used as Projects API is deprecated. Use getRealityDatasITwins method. * @param accessToken The client request context. * @param realityDataId realityData identifier * @returns an array of Projects that are associated to the realityData. * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @beta */ public async getRealityDataProjects( accessToken: AccessToken, realityDataId: string, ): Promise<Project[]> { try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = `${this.baseUrl}/${realityDataId}/itwins`; const options = getRequestConfig( accessTokenResolved, "GET", url, this.apiVersion, ); // execute query const response = await axios.get(url, options); const projectsResponseBody = response.data; const projectsResponse: Project[] = []; // make up projects details link manually const projectsBaseUrl = this.baseUrl.replace( "/reality-management/reality-data", "/projects", ); projectsResponseBody.iTwins.forEach((itwinValue: any) => { // build Project object with _links.self.href structure const href = new URL(`${projectsBaseUrl}/${itwinValue}`); const self = { href, }; const newProject = new Project({ id: itwinValue, // eslint-disable-next-line @typescript-eslint/naming-convention _links: { self, }, }); projectsResponse.push(newProject); }); return projectsResponse; } catch (error) { return this.handleError(error); } } /** * Retrieves the list of iTwins associated to the specified realityData. * @param accessToken The client request context. * @param realityDataId realityData identifier * @returns an array of iTwin identifiers that are associated to the realityData. * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @beta */ public async getRealityDataITwins( accessToken: AccessToken, realityDataId: string, ): Promise<string[]> { try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = `${this.baseUrl}/${realityDataId}/itwins`; const options = getRequestConfig( accessTokenResolved, "GET", url, this.apiVersion, ); // execute query const response = await axios.get(url, options); const iTwinsResponseBody = response.data; const iTwinsResponse: string[] = []; iTwinsResponseBody.iTwins.forEach((itwinValue: any) => { iTwinsResponse.push(itwinValue); }); return iTwinsResponse; } catch (error) { return this.handleError(error); } } /** * Creates a RealityData * @param accessToken The client request context. * @param iTwinId id of associated iTwin * @param iTwinRealityData the realityData to create * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 403 when user does not have required permissions to create a reality data * @throws [[BentleyError]] with code 422 when the request is invalid * @beta */ public async createRealityData( accessToken: AccessToken, iTwinId: string | undefined, iTwinRealityData: ITwinRealityData, ): Promise<ITwinRealityData> { try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = this.baseUrl; const options = getRequestConfig( accessTokenResolved, "POST", url, this.apiVersion, ); // creation payload const realityDataToCreate = { displayName: iTwinRealityData.displayName, classification: iTwinRealityData.classification, type: iTwinRealityData.type, iTwinId, dataset: iTwinRealityData.dataset, group: iTwinRealityData.group, description: iTwinRealityData.description, rootDocument: iTwinRealityData.rootDocument, tags: iTwinRealityData.tags, acquisition: iTwinRealityData.acquisition, authoring: iTwinRealityData.authoring, extent: iTwinRealityData.extent, crs: iTwinRealityData.crs, attribution: iTwinRealityData.attribution, termsOfUse: iTwinRealityData.termsOfUse, }; const response = await axios.post(url, realityDataToCreate, options); iTwinRealityData = new ITwinRealityData( this, response.data.realityData, iTwinId, ); } catch (error) { return this.handleError(error); } return iTwinRealityData; } /** * Modifies an existing RealityData * @param accessToken The client request context. * @param iTwinId id of associated iTwin * @param iTwinRealityData the realityData to modify * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 404 when the specified reality data was not found * @throws [[BentleyError]] with code 422 when the request is invalid * @beta */ public async modifyRealityData( accessToken: AccessToken, iTwinId: string | undefined, iTwinRealityData: ITwinRealityData, ): Promise<ITwinRealityData> { try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = new URL(`${this.baseUrl}/${iTwinRealityData.id}`); const options = getRequestConfig( accessTokenResolved, "PATCH", url.href, this.apiVersion, ); // payload const realityDataToModify = { id: iTwinRealityData.id, displayName: iTwinRealityData.displayName, classification: iTwinRealityData.classification, type: iTwinRealityData.type, iTwinId, dataset: iTwinRealityData.dataset, group: iTwinRealityData.group, description: iTwinRealityData.description, rootDocument: iTwinRealityData.rootDocument, tags: iTwinRealityData.tags, acquisition: iTwinRealityData.acquisition, authoring: iTwinRealityData.authoring, extent: iTwinRealityData.extent, crs: iTwinRealityData.crs, attribution: iTwinRealityData.attribution, termsOfUse: iTwinRealityData.termsOfUse, }; const response = await axios.patch( url.href, realityDataToModify, options, ); iTwinRealityData = new ITwinRealityData( this, response.data.realityData, iTwinId, ); } catch (error) { return this.handleError(error); } return iTwinRealityData; } /** * Deletes a RealityData * @param accessToken The client request context. * @param realityDataId the realityData to delete * @returns true if successful (204 response), false if not * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 404 when the specified reality data was not found * @throws [[BentleyError]] with code 422 when the request is invalid * @beta */ public async deleteRealityData( accessToken: AccessToken, realityDataId: string, ): Promise<boolean> { let response: AxiosResponse; try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = `${this.baseUrl}/${realityDataId}`; const options = getRequestConfig( accessTokenResolved, "POST", url, this.apiVersion, ); response = await axios.delete(url, options); } catch (error) { return this.handleError(error); } if (response.status === 204) return true; else return false; } /** * Associates a RealityData to an iTwin * @param accessToken The client request context. * @param iTwinId id of iTwin to associate the realityData to. * @param realityDataId id of the RealityData. * @returns true if successful (200 response) or false if not * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 404 when the specified reality data or iTwin was not found * @throws [[BentleyError]] with code 422 when the request is invalid * @beta */ public async associateRealityData( accessToken: AccessToken, iTwinId: string, realityDataId: string, ): Promise<boolean> { let response: AxiosResponse; try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = `${this.baseUrl}/${realityDataId}/iTwins/${iTwinId}`; const options = getRequestConfig( accessTokenResolved, "POST", url, this.apiVersion, ); response = await axios.post(url, undefined, options); } catch (error) { return this.handleError(error); } if (response.status === 200) return true; else return false; } /** * Dissociates a RealityData from an iTwin * @param accessToken The client request context. * @param iTwinId id of iTwin to dissociate the realityData from. * @param realityDataId id of the RealityData. * @returns true if successful (204 response) or false if not * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 404 when the association between the reality data and iTwin was not found * @throws [[BentleyError]] with code 422 when the request is invalid * @beta */ public async dissociateRealityData( accessToken: AccessToken, iTwinId: string, realityDataId: string, ): Promise<boolean> { let response: AxiosResponse; try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = `${this.baseUrl}/${realityDataId}/iTwins/${iTwinId}`; const options = getRequestConfig( accessTokenResolved, "DELETE", url, this.apiVersion, ); response = await axios.delete(url, options); } catch (error) { return this.handleError(error); } if (response.status === 204) return true; else return false; } /** * Moves a RealityData to a different iTwin * @param accessToken The client request context. * @param realityDataId The id of the RealityData to move. * @param iTwinId The id of the iTwin to move the RealityData to. * @returns true if successful (204 response) or false if not * @throws [[BentleyError]] with code 401 when the request lacks valid authentication credentials * @throws [[BentleyError]] with code 404 when the specified reality data or iTwin was not found * @throws [[BentleyError]] with code 422 when the request is invalid * @throws [[BentleyError]] with code 409 when the reality data is already associated with the specified iTwin * @beta */ public async moveRealityData( accessToken: AccessToken, realityDataId: string, iTwinId: string, ): Promise<boolean> { let response: AxiosResponse; try { const accessTokenResolved = await this.resolveAccessToken(accessToken); const url = `${this.baseUrl}/${realityDataId}/move`; const options = getRequestConfig( accessTokenResolved, "PATCH", url, this.apiVersion, ); const payload = { iTwinId }; response = await axios.patch(url, payload, options); } catch (error) { return this.handleError(error); } if (response.status === 204) return true; else return false; } /** * Handle errors thrown. * Handled errors can be of AxiosError type or BentleyError. * @beta */ private handleError(error: any): any { // Default error let status = 422; let message = "Unknown error. Please ensure that the request is valid."; if (axios.isAxiosError(error) && error.response) { const axiosResponse = error.response; status = axiosResponse.status; message = axiosResponse.data?.error?.message; } else { const bentleyError = error as BentleyError; if (bentleyError !== undefined) { status = bentleyError.errorNumber; message = bentleyError.message; } } return Promise.reject(new BentleyError(status, message)); } }