import deepAssign from "deep-assign" ; import { once } from "lodash" ; import { AccessToken , GetMetaDataFunction , HttpStatus , Logger } from "@itwin/core-bentley" ; import { Client , DefaultRequestOptionsProvider } from "../request/Client" ; import { request , RequestGlobalOptions , RequestOptions , RequestQueryOptions , RequestTimeoutOptions , Response , ResponseError } from "../itwin-client/Request" ; import { ChunkedQueryContext } from "./ChunkedQueryContext" ; import { ECJsonTypeMap , WsgInstance } from "./ECJsonTypeMap" ; import { WsgClientLoggerCategory } from "./WsgLoggerCategory" ; const loggerCategory = WsgClientLoggerCategory . Client ; internal export class AuthenticationError extends ResponseError { } internal export enum WSStatus { Success = 0 , WSERROR_BASE = 0x18000 , Unknown = WSERROR_BASE + 1 , LoginFailed = WSERROR_BASE + 2 , SslRequired = WSERROR_BASE + 3 , NotEnoughRights = WSERROR_BASE + 4 , RepositoryNotFound = WSERROR_BASE + 5 , SchemaNotFound = WSERROR_BASE + 6 , ClassNotFound = WSERROR_BASE + 7 , PropertyNotFound = WSERROR_BASE + 8 , InstanceNotFound = WSERROR_BASE + 9 , FileNotFound = WSERROR_BASE + 10 , NotSupported = WSERROR_BASE + 11 , NoServerLicense = WSERROR_BASE + 12 , NoClientLicense = WSERROR_BASE + 13 , TooManyBadLoginAttempts = WSERROR_BASE + 14 , LoginRequired = WSERROR_BASE + 15 } internal export class WsgError extends ResponseError { public constructor ( errorNumber , message ? , getMetaData ? ) { super ( errorNumber , message , getMetaData ) ; } param response returns public static override parse ( response , log = true ) { const responseError = ResponseError . parse ( response , false ) ; const wsgError = new WsgError ( WSStatus . Unknown ) ; deepAssign ( wsgError , responseError ) ; if ( wsgError . _data ) { if ( typeof wsgError . _data === "object" ) { if ( wsgError . _data . errorMessage || wsgError . _data . errorId ) { wsgError . message = wsgError . _data . errorMessage || wsgError . message ; wsgError . name = wsgError . _data . errorId || wsgError . name ; wsgError . description = wsgError . _data . errorDescription || wsgError . description ; wsgError . errorNumber = WsgError . getWSStatusId ( wsgError . name ? wsgError . name : "" ) ; if ( log ) wsgError . log ( ) ; return wsgError ; } } else { if ( wsgError . status === 302 && wsgError . _data . indexOf ( "ims.bentley.com" ) >= 0 ) { const authenticationError = new AuthenticationError ( WSStatus . LoginRequired ) ; deepAssign ( authenticationError , responseError ) ; authenticationError . name = "Authentication Error" ; authenticationError . message = "Authentication Error - Check if the accessToken is valid" ; return authenticationError ; } } } if ( log ) responseError . log ( ) ; return responseError ; } param error param response public static override shouldRetry ( error , response ) { if ( response === undefined || response === null ) { return super . shouldRetry ( error , response ) ; } if ( super . parseHttpStatus ( response . statusType ) === HttpStatus . Success ) { return false ; } const parsedError = WsgError . parse ( { response } , false ) ; if ( ! ( parsedError instanceof WsgError ) ) { return super . shouldRetry ( error , response ) ; } const errorCodesToRetry = [ WSStatus . LoginFailed , WSStatus . SslRequired , WSStatus . NotEnoughRights , WSStatus . RepositoryNotFound , WSStatus . SchemaNotFound , WSStatus . ClassNotFound , WSStatus . PropertyNotFound , WSStatus . InstanceNotFound , WSStatus . FileNotFound , WSStatus . NotSupported , WSStatus . NoServerLicense , WSStatus . NoClientLicense , WSStatus . TooManyBadLoginAttempts , HttpStatus . ServerError , HttpStatus . ClientError , WSStatus . Unknown , ] ; const errorStatus = WsgError . getErrorStatus ( parsedError . name !== undefined ? WsgError . getWSStatusId ( parsedError . name ) : WSStatus . Unknown , response . statusType ) ; return errorCodesToRetry . includes ( errorStatus ) ; } param error param response public static getErrorStatus ( errorId , httpStatusType ) { if ( WSStatus . Unknown !== errorId ) { return errorId ; } if ( super . parseHttpStatus ( httpStatusType ) === HttpStatus . ServerError ) { return HttpStatus . ServerError ; } if ( super . parseHttpStatus ( httpStatusType ) === HttpStatus . ClientError ) { return HttpStatus . ClientError ; } return WSStatus . Unknown ; } param error public static getWSStatusId ( error ) { switch ( error ) { case "LoginFailed" : return WSStatus . LoginFailed ; case "SslRequired" : return WSStatus . SslRequired ; case "NotEnoughRights" : return WSStatus . NotEnoughRights ; case "DatasourceNotFound" : return WSStatus . RepositoryNotFound ; case "RepositoryNotFound" : return WSStatus . RepositoryNotFound ; case "SchemaNotFound" : return WSStatus . SchemaNotFound ; case "ClassNotFound" : return WSStatus . ClassNotFound ; case "PropertyNotFound" : return WSStatus . PropertyNotFound ; case "LinkTypeNotFound" : return WSStatus . ClassNotFound ; case "ObjectNotFound" : return WSStatus . InstanceNotFound ; case "InstanceNotFound" : return WSStatus . InstanceNotFound ; case "FileNotFound" : return WSStatus . FileNotFound ; case "NotSupported" : return WSStatus . NotSupported ; case "NoServerLicense" : return WSStatus . NoServerLicense ; case "NoClientLicense" : return WSStatus . NoClientLicense ; case "TooManyBadLoginAttempts" : return WSStatus . TooManyBadLoginAttempts ; default : return WSStatus . Unknown ; } } /**
     * Logs this error
     */ public override log ( ) { Logger . logError ( loggerCategory , this . logMessage ( ) , ( ) => this . getMetaData ( ) ) ; } } internal export class DefaultWsgRequestOptionsProvider extends DefaultRequestOptionsProvider { /**
     * Creates an instance of DefaultWsgRequestOptionsProvider and sets up the default options.
     */ constructor ( ) { super ( ) ; this . _defaultOptions . errorCallback = WsgError . parse ; this . _defaultOptions . retryCallback = WsgError . shouldRetry ; } } internal export interface WsgRequestOptions { ResponseContent ? : "FullInstance" | "Empty" | "InstanceId" ; RefreshInstances ? : boolean ; CustomOptions ? : any ; } internal export interface HttpRequestOptions { headers ? : any ; timeout ? : RequestTimeoutOptions ; } internal export abstract class WsgClient extends Client { private static _defaultWsgRequestOptionsProvider : DefaultWsgRequestOptionsProvider ; protected override _url ? : string ; param apiVersion protected constructor ( public apiVersion ) { super ( ) ; this . apiVersion = apiVersion ; } note param options returns protected override async setupOptionDefaults ( options ) { if ( ! WsgClient . _defaultWsgRequestOptionsProvider ) WsgClient . _defaultWsgRequestOptionsProvider = new DefaultWsgRequestOptionsProvider ( ) ; return WsgClient . _defaultWsgRequestOptionsProvider . assignOptions ( options ) ; } param excludeApiVersion returns public override async getUrl ( excludeApiVersion ? ) { return this . _getUrlHelper ( excludeApiVersion ) ; } private _getUrlHelper = once ( async ( excludeApiVersion ? ) => { const url = await super . getUrl ( ) ; this . _url = url ; if ( ! excludeApiVersion ) { this . _url = `${ this . _url }/${ this . apiVersion }` ; } return this . _url ; } ) ; /** used by clients to delete strongly typed instances through the standard WSG REST API */ protected async deleteInstance < T extends WsgInstance > ( accessToken , relativeUrlPath , instance ? , requestOptions ? , httpRequestOptions ? ) { const url = await this . getUrl ( ) + relativeUrlPath ; const untypedInstance = instance ? ECJsonTypeMap . toJson < T > ( "wsg" , instance ) : undefined ; const options = { method : "DELETE" , headers : { authorization : accessToken } , body : { instance : untypedInstance } } ; if ( requestOptions ) { options . body . requestOptions = requestOptions ; } this . applyUserConfiguredHttpRequestOptions ( options , httpRequestOptions ) ; await this . setupOptionDefaults ( options ) ; await request ( url , options ) ; } param typedConstructor param relativeUrlPath param instance param requestOptions param httpRequestOptions returns protected async postInstance < T extends WsgInstance > ( accessToken , typedConstructor , relativeUrlPath , instance , requestOptions ? , httpRequestOptions ? ) { const url = await this . getUrl ( ) + relativeUrlPath ; Logger . logInfo ( loggerCategory , "Sending POST request" , ( ) => ( { url } ) ) ; const untypedInstance = ECJsonTypeMap . toJson < T > ( "wsg" , instance ) ; const options = { method : "POST" , headers : { authorization : accessToken } , body : { instance : untypedInstance } } ; if ( requestOptions ) { options . body . requestOptions = requestOptions ; } this . applyUserConfiguredHttpRequestOptions ( options , httpRequestOptions ) ; await this . setupOptionDefaults ( options ) ; const res = await request ( url , options ) ; if ( ! res . body || ! res . body . changedInstance || ! res . body . changedInstance . instanceAfterChange ) { throw new Error ( `POST to URL ${ url } executed successfully, but did not return the expected result.` ) ; } const ecJsonInstance = res . body . changedInstance . instanceAfterChange ; const typedInstance = ECJsonTypeMap . fromJson < T > ( typedConstructor , "wsg" , ecJsonInstance ) ; if ( ! typedInstance ) { throw new Error ( `POST to URL ${ url } executed successfully, but could not convert response to a strongly typed instance.` ) ; } Logger . logTrace ( loggerCategory , "Successful POST request" , ( ) => ( { url } ) ) ; return typedInstance ; } param typedConstructor param relativeUrlPath param instances param requestOptions param httpRequestOptions returns protected async postInstances < T extends WsgInstance > ( accessToken , typedConstructor , relativeUrlPath , instances , requestOptions ? , httpRequestOptions ? ) { const url = await this . getUrl ( ) + relativeUrlPath ; Logger . logInfo ( loggerCategory , "Sending POST request" , ( ) => ( { url } ) ) ; const untypedInstances = instances . map ( ( value ) => ECJsonTypeMap . toJson < T > ( "wsg" , value ) ) ; const options = { method : "POST" , headers : { authorization : accessToken } , body : { instances : untypedInstances } } ; if ( requestOptions ) { options . body . requestOptions = requestOptions ; } this . applyUserConfiguredHttpRequestOptions ( options , httpRequestOptions ) ; await this . setupOptionDefaults ( options ) ; const res = await request ( url , options ) ; if ( ! res . body || ! res . body . changedInstances ) { throw new Error ( `POST to URL ${ url } executed successfully, but did not return the expected result.` ) ; } const changedInstances = ( res . body . changedInstances as any [ ] ) . map < T > ( ( value ) => { const untypedInstance = value . instanceAfterChange ; if ( ! untypedInstance ) { throw new Error ( `POST to URL ${ url } executed successfully, but could not convert response to a strongly typed instance.` ) ; } const typedInstance = ECJsonTypeMap . fromJson < T > ( typedConstructor , "wsg" , untypedInstance ) ; if ( ! typedInstance ) { throw new Error ( `POST to URL ${ url } executed successfully, but could not convert response to a strongly typed instance.` ) ; } return typedInstance ; } ) ; Logger . logTrace ( loggerCategory , "Successful POST request" , ( ) => ( { url } ) ) ; return changedInstances ; } param typedConstructor param relativeUrlPath param queryOptions param httpRequestOptions returns protected async getInstances < T extends WsgInstance > ( accessToken , typedConstructor , relativeUrlPath , queryOptions ? , httpRequestOptions ? ) { const url = await this . getUrl ( ) + relativeUrlPath ; Logger . logInfo ( loggerCategory , "Sending GET request" , ( ) => ( { url } ) ) ; const chunkedQueryContext = queryOptions ? ChunkedQueryContext . create ( queryOptions ) : undefined ; const typedInstances = new Array < T > ( ) ; do { const chunk = await this . getInstancesChunk ( accessToken , url , chunkedQueryContext , typedConstructor , queryOptions , httpRequestOptions ) ; typedInstances . push ( ... chunk ) ; } while ( chunkedQueryContext && ! chunkedQueryContext . isQueryFinished ) ; Logger . logTrace ( loggerCategory , "Successful GET request" , ( ) => ( { url } ) ) ; return typedInstances ; } param url param chunkedQueryContext param typedConstructor param queryOptions param httpRequestOptions returns protected async getInstancesChunk < T extends WsgInstance > ( accessToken , url , chunkedQueryContext , typedConstructor , queryOptions ? , httpRequestOptions ? ) { const resultInstances = new Array < T > ( ) ; if ( chunkedQueryContext ) chunkedQueryContext . handleIteration ( queryOptions ! ) ; const options = { method : "GET" , qs : queryOptions , accept : "application/json" } ; options . headers = { authorization : accessToken } ; this . applyUserConfiguredHttpRequestOptions ( options , httpRequestOptions ) ; if ( chunkedQueryContext && chunkedQueryContext . skipToken && chunkedQueryContext . skipToken . length > 0 ) options . headers . skiptoken = chunkedQueryContext . skipToken ; await this . setupOptionDefaults ( options ) ; const res = await request ( url , options ) ; if ( ! res . body || ! res . body . hasOwnProperty ( "instances" ) ) { throw new Error ( `Query to URL ${ url } executed successfully, but did NOT return any instances.` ) ; } for ( const ecJsonInstance of res . body . instances ) { const typedInstance = ECJsonTypeMap . fromJson < T > ( typedConstructor , "wsg" , ecJsonInstance ) ; if ( typedInstance ) { resultInstances . push ( typedInstance ) ; } } if ( chunkedQueryContext ) chunkedQueryContext . skipToken = res . header . skiptoken ; return resultInstances ; } /** used by clients to send delete requests */ protected override async delete ( accessToken , relativeUrlPath , httpRequestOptions ? ) { const url = await this . getUrl ( ) + relativeUrlPath ; Logger . logInfo ( loggerCategory , "Sending DELETE request" , ( ) => ( { url } ) ) ; const options = { method : "DELETE" , headers : { authorization : accessToken } } ; this . applyUserConfiguredHttpRequestOptions ( options , httpRequestOptions ) ; await this . setupOptionDefaults ( options ) ; await request ( url , options ) ; Logger . logTrace ( loggerCategory , "Successful DELETE request" , ( ) => ( { url } ) ) ; } private getQueryRequestBody ( queryOptions ) { const addPart = ( query , key , value ) => { if ( query !== "" ) query += "&" ; query += `${ key }=${ value }` ; return query ; } ; let result = "" ; if ( queryOptions . $filter ) { result = addPart ( result , "$filter" , queryOptions . $filter ) ; } if ( queryOptions . $orderby ) { result = addPart ( result , "$orderby" , queryOptions . $orderby ) ; } if ( queryOptions . $select ) { result = addPart ( result , "$select" , queryOptions . $select ) ; } if ( queryOptions . $skip ) { result = addPart ( result , "$skip" , queryOptions . $skip . toString ( 10 ) ) ; } if ( queryOptions . $top ) { result = addPart ( result , "$top" , queryOptions . $top . toString ( 10 ) ) ; } return result ; } param typedConstructor param relativeUrlPath param queryOptions param httpRequestOptions returns protected async postQuery < T extends WsgInstance > ( accessToken , typedConstructor , relativeUrlPath , queryOptions , httpRequestOptions ? ) { const url = `${ await this . getUrl ( ) }${ relativeUrlPath }$query` ; Logger . logInfo ( loggerCategory , "Sending POST request" , ( ) => ( { url } ) ) ; const options = { method : "POST" , headers : { authorization : accessToken } , body : this . getQueryRequestBody ( queryOptions ) } ; this . applyUserConfiguredHttpRequestOptions ( options , httpRequestOptions ) ; await this . setupOptionDefaults ( options ) ; const res = await request ( url , options ) ; if ( ! res . body || ! res . body . hasOwnProperty ( "instances" ) ) { throw new Error ( `Query to URL ${ url } executed successfully, but did NOT return any instances.` ) ; } const typedInstances = new Array < T > ( ) ; for ( const ecJsonInstance of res . body . instances ) { const typedInstance = ECJsonTypeMap . fromJson < T > ( typedConstructor , "wsg" , ecJsonInstance ) ; if ( typedInstance ) { typedInstances . push ( typedInstance ) ; } } Logger . logTrace ( loggerCategory , "Successful POST request" , ( ) => ( { url } ) ) ; return typedInstances ; } /** Configures request options based on user defined values in HttpRequestOptions */ protected applyUserConfiguredHttpRequestOptions ( requestOptions , userDefinedRequestOptions ? ) { if ( ! userDefinedRequestOptions ) return ; if ( userDefinedRequestOptions . headers ) { requestOptions . headers = { ... requestOptions . headers , ... userDefinedRequestOptions . headers } ; } if ( userDefinedRequestOptions . timeout ) { this . applyUserConfiguredTimeout ( requestOptions , userDefinedRequestOptions . timeout ) ; } } /** Sets the request timeout based on user defined values */ private applyUserConfiguredTimeout ( requestOptions , userDefinedTimeout ) { requestOptions . timeout = { ... requestOptions . timeout } ; if ( userDefinedTimeout . response ) requestOptions . timeout . response = userDefinedTimeout . response ; if ( userDefinedTimeout . deadline ) requestOptions . timeout . deadline = userDefinedTimeout . deadline ; else if ( userDefinedTimeout . response ) { const defaultNetworkOverheadBuffer = ( RequestGlobalOptions . timeout . deadline as number ) - ( RequestGlobalOptions . timeout . response as number ) ; requestOptions . timeout . deadline = userDefinedTimeout . response + defaultNetworkOverheadBuffer ; } } }