packageDocumentation module import deepAssign from "deep-assign" ; import { AccessToken , GuidString , Id64String , IModelHubStatus , Logger } from "@itwin/core-bentley" ; import { ResponseError } from "../itwin-client/Request" ; import { ECJsonTypeMap , WsgInstance } from "../wsg/ECJsonTypeMap" ; import { WsgQuery } from "../wsg/WsgQuery" ; import { WsgRequestOptions } from "../wsg/WsgClient" ; import { IModelHubClientLoggerCategory } from "../IModelHubClientLoggerCategories" ; import { IModelBaseHandler } from "./BaseHandler" ; import { AggregateResponseError , ArgumentCheck , IModelHubClientError , IModelHubError } from "./Errors" ; const loggerCategory = IModelHubClientLoggerCategory . IModelHub ; internal export enum CodeState { /** Code with this state is not persisted in iModelHub. Code that is updated to 'Available' state is deleted from iModelHub. */ Available = 0 , /** Code is reserved by the [[Briefcase]], no one else is allowed to change its state. */ Reserved = 1 , /** Code is used in a [[ChangeSet]] committed to iModelHub. */ Used = 2 , /** Retired Code can not be reserved or used. It can only be deleted and then reserved again */ Retired = 3 } internal export class CodeBase extends WsgInstance { /** Code specification Id. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.CodeSpecId" ) public codeSpecId ? : Id64String ; /** Code scope. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.CodeScope" ) public codeScope ? : string ; /** Code state. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.State" ) public state ? : CodeState ; /** Date the Code was created. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.CreatedDate" ) public createdDate ? : string ; /** Id of the Briefcase that owns the Code. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.BriefcaseId" ) public briefcaseId ? : number ; /** If set to true in a request, it will only check whether code state can be modified, but will not actually change it. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.QueryOnly" ) public queryOnly ? : boolean ; } internal @ ECJsonTypeMap . classToJson ( "wsg" , "iModelScope.Code" , { schemaPropertyName : "schemaName" , classPropertyName : "className" } ) export class HubCode extends CodeBase { /** The unique string that can be used as a name value in iModel. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.Value" ) public value ? : string ; } internal @ ECJsonTypeMap . classToJson ( "wsg" , "iModelScope.MultiCode" , { schemaPropertyName : "schemaName" , classPropertyName : "className" } ) export class MultiCode extends CodeBase { @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.Values" ) public values ? : string [ ] ; } param str returns function encodeForCodeId ( str ) { return encodeURIComponent ( str . replace ( /-/g , "_2D_" ) ) . replace ( /~/g , "~7E" ) . replace ( /\*/g , "~2A" ) . replace ( /%/g , "~" ) ; } param code returns function getCodeInstanceId ( code ) { if ( ! code || ! code . codeSpecId || ! code . codeScope || ! code . value ) return undefined ; return `'${ code . codeSpecId }-${ encodeForCodeId ( code . codeScope ) }-${ encodeForCodeId ( code . value ) }'` ; } internal export interface CodeUpdateOptions { /** Return [Code]($common)s that could not be acquired. Conflicting Codes will be set to [[ConflictingCodesError.conflictingCodes]]. If unlimitedReporting is enabled and CodesPerRequest value is high, some conflicting Codes could be missed.  */ deniedCodes ? : boolean ; /** Attempt to get all failed [Code]($common)s, ignoring iModelHub limits. Server responses might fail when trying to return large number of conflicting Codes. */ unlimitedReporting ? : boolean ; /** Number of [Code]($common)s per single request. Multiple requests will be sent if there are more Codes. If an error happens on a subsequent request, previous successful updates will not be reverted. */ codesPerRequest ? : number ; /** Don't fail request on a conflict. If conflict occurs, [Code]($common)s that didn't have conflicts will be updated and any remaining subsequent requests will still be sent. */ continueOnConflict ? : boolean ; } internal export class DefaultCodeUpdateOptionsProvider { protected _defaultOptions : CodeUpdateOptions ; /**  Creates an instance of DefaultRequestOptionsProvider and sets up the default options. */ constructor ( ) { this . _defaultOptions = { codesPerRequest : 2000 } ; } param options public async assignOptions ( options ) { const clonedOptions = { ... options } ; deepAssign ( options , this . _defaultOptions ) ; deepAssign ( options , clonedOptions ) ; } } internal export class ConflictingCodesError extends IModelHubError { /** Codes that couldn't be updated due to other users owning them or setting them to [[CodeState.Retired]]. */ public conflictingCodes ? : HubCode [ ] ; param error returns internal public static fromError ( error ) { if ( error . errorNumber !== IModelHubStatus . CodeReservedByAnotherBriefcase && error . errorNumber !== IModelHubStatus . ConflictsAggregate ) { return undefined ; } const result = new ConflictingCodesError ( error . errorNumber ) ; deepAssign ( result , error ) ; result . addCodes ( error ) ; return result ; } param error internal public addCodes ( error ) { if ( ! error . data || ! error . data . ConflictingCodes ) { return ; } if ( ! this . conflictingCodes ) { this . conflictingCodes = [ ] ; } for ( const value of ( error . data . ConflictingCodes as any [ ] ) ) { const instance = { className : "Code" , schemaName : "iModelScope" , properties : value } ; const code = ECJsonTypeMap . fromJson < HubCode > ( HubCode , "wsg" , instance ) ; if ( code ) { this . conflictingCodes . push ( code ) ; } } } } internal export class CodeQuery extends WsgQuery { private _isMultiCodeQuery = true ; internal public static defaultPageSize : number = 10000 ; /** Constructor that sets default page size. */ constructor ( ) { super ( ) ; this . pageSize ( CodeQuery . defaultPageSize ) ; } internal public get isMultiCodeQuery ( ) { return this . _isMultiCodeQuery ; } param briefcaseId returns throws public byBriefcaseId ( briefcaseId ) { ArgumentCheck . validBriefcaseId ( "briefcaseId" , briefcaseId ) ; this . addFilter ( `BriefcaseId+eq+${ briefcaseId }` ) ; return this ; } param codeSpecId returns throws public byCodeSpecId ( codeSpecId ) { ArgumentCheck . defined ( "codeSpecId" , codeSpecId ) ; this . addFilter ( `CodeSpecId+eq+'${ codeSpecId }'` ) ; return this ; } param codeScope returns throws public byCodeScope ( codeScope ) { ArgumentCheck . defined ( "codeScope" , codeScope ) ; this . addFilter ( `CodeScope+eq+'${ codeScope }'` ) ; return this ; } param codes returns throws public byCodes ( codes ) { ArgumentCheck . nonEmptyArray ( "codes" , codes ) ; this . _isMultiCodeQuery = false ; if ( codes . length < 1 ) { throw IModelHubClientError . invalidArgument ( "codes" ) ; } let filter = "$id+in+[" ; let index = 0 ; for ( const code of codes ) { const id = getCodeInstanceId ( code ) ; ArgumentCheck . valid ( `codes[${ index }]` , id ) ; if ( 0 !== index ++ ) filter += "," ; filter += id ; } filter += "]" ; this . addFilter ( filter ) ; return this ; } param briefcaseId returns throws public unavailableCodes ( briefcaseId ) { ArgumentCheck . validBriefcaseId ( "briefcaseId" , briefcaseId ) ; const filter = `BriefcaseId+ne+${ briefcaseId }` ; this . addFilter ( filter ) ; return this ; } } internal export enum CodeSequenceType { /** Return largest already used value. */ LargestUsed = 0 , /** Return next available value in the sequence. */ NextAvailable = 1 } internal @ ECJsonTypeMap . classToJson ( "wsg" , "iModelScope.CodeSequence" , { schemaPropertyName : "schemaName" , classPropertyName : "className" } ) export class CodeSequence extends WsgInstance { /** Code specification Id (hexadecimal ("0XA") or decimal ("10") string)). */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.CodeSpecId" ) public codeSpecId ? : Id64String ; /** Code scope. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.CodeScope" ) public codeScope ? : string ; /** Pattern describing the sequence. # characters will be replaced with the index. Only a single group of # characters is allowed in the pattern. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.ValuePattern" ) public valuePattern ? : string ; /** Suggested index value returned from the query. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.Value" ) public value ? : string ; /** Starting index of the sequence. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.StartIndex" ) public startIndex ? : number ; /** Index difference between two consecutive members in this sequence. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.IncrementBy" ) public incrementBy ? : number ; /** Type of the sequence results. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.Type" ) public type ? : CodeSequenceType ; } internal export class CodeSequenceHandler { private _handler : IModelBaseHandler ; param handler internal constructor ( handler ) { this . _handler = handler ; } param iModelId private getRelativeUrl ( iModelId ) { return `/Repositories/iModel--${ iModelId }/iModelScope/CodeSequence/` ; } param iModelId param sequence returns throws public async get ( accessToken , iModelId , sequence ) { Logger . logInfo ( loggerCategory , "Querying code sequence for iModel" , ( ) => ( { iModelId } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; const result = await this . _handler . postInstance < CodeSequence > ( accessToken , CodeSequence , this . getRelativeUrl ( iModelId ) , sequence ) ; Logger . logTrace ( loggerCategory , "Queried code sequence for iModel" , ( ) => ( { iModelId } ) ) ; return result . value ! ; } } internal export class CodeHandler { private _handler : IModelBaseHandler ; private static _defaultUpdateOptionsProvider : DefaultCodeUpdateOptionsProvider ; param handler internal constructor ( handler ) { this . _handler = handler ; } /** Get handler for querying [[CodeSequence]]s. */ public get sequences ( ) : CodeSequenceHandler { return new CodeSequenceHandler ( this . _handler ) ; } param iModelId param codeId private getRelativeUrl ( iModelId , multiCode = true , codeId ? ) { return `/Repositories/iModel--${ iModelId }/iModelScope/${ multiCode ? "MultiCode" : "Code" }/${ codeId || "" }` ; } /** Convert Codes to MultiCodes. */ private static convertCodesToMultiCodes ( codes ) { const map = new Map < string , MultiCode > ( ) ; for ( const code of codes ) { const id = `${ code . codeScope }-${ code . codeSpecId }-${ code . state }` ; if ( map . has ( id ) ) { map . get ( id ) ! . values ! . push ( code . value ! ) ; } else { const multiCode = new MultiCode ( ) ; multiCode . changeState = "new" ; multiCode . briefcaseId = code . briefcaseId ; multiCode . codeScope = code . codeScope ; multiCode . codeSpecId = code . codeSpecId ; multiCode . state = code . state ; multiCode . values = [ code . value ! ] ; map . set ( id , multiCode ) ; } } return Array . from ( map . values ( ) ) ; } /** Convert MultiCodes to Codes. */ private static convertMultiCodesToCodes ( multiCodes ) { const result = [ ] ; for ( const multiCode of multiCodes ) { for ( const value of multiCode . values ! ) { const code = new HubCode ( ) ; code . briefcaseId = multiCode . briefcaseId ; code . codeScope = multiCode . codeScope ; code . codeSpecId = multiCode . codeSpecId ; code . state = multiCode . state ; code . value = value ; result . push ( code ) ; } } return result ; } param options returns private async setupOptionDefaults ( options ) { if ( ! CodeHandler . _defaultUpdateOptionsProvider ) CodeHandler . _defaultUpdateOptionsProvider = new DefaultCodeUpdateOptionsProvider ( ) ; return CodeHandler . _defaultUpdateOptionsProvider . assignOptions ( options ) ; } /** Send partial request for code updates */ private async updateInternal ( accessToken , iModelId , codes , updateOptions ? ) { let requestOptions ; if ( updateOptions ) { requestOptions = { } ; requestOptions . CustomOptions = { } ; if ( updateOptions . deniedCodes === false ) { requestOptions . CustomOptions . DetailedError_Codes = "false" ; } if ( updateOptions . unlimitedReporting ) { requestOptions . CustomOptions . DetailedError_MaximumInstances = "-1" ; } if ( updateOptions . continueOnConflict ) { requestOptions . CustomOptions . ConflictStrategy = "Continue" ; } if ( Object . getOwnPropertyNames ( requestOptions . CustomOptions ) . length === 0 ) requestOptions = undefined ; } const result = await this . _handler . postInstances < MultiCode > ( accessToken , MultiCode , `/Repositories/iModel--${ iModelId }/$changeset` , CodeHandler . convertCodesToMultiCodes ( codes ) , requestOptions ) ; return CodeHandler . convertMultiCodesToCodes ( result ) ; } param iModelId param codes param updateOptions returns throws throws throws throws throws throws public async update ( accessToken , iModelId , codes , updateOptions ? ) { Logger . logInfo ( loggerCategory , "Requesting codes for iModel" , ( ) => ( { iModelId } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; ArgumentCheck . nonEmptyArray ( "codes" , codes ) ; updateOptions = updateOptions || { } ; await this . setupOptionDefaults ( updateOptions ) ; const result = [ ] ; let conflictError ; const aggregateError = new AggregateResponseError ( ) ; for ( let i = 0 ; i < codes . length ; i += updateOptions . codesPerRequest ! ) { const chunk = codes . slice ( i , i + updateOptions . codesPerRequest ! ) ; try { result . push ( ... await this . updateInternal ( accessToken , iModelId , chunk , updateOptions ) ) ; } catch ( error ) { if ( error instanceof ResponseError ) { if ( updateOptions && updateOptions . deniedCodes && error instanceof IModelHubError && ( error . errorNumber === IModelHubStatus . CodeReservedByAnotherBriefcase || error . errorNumber === IModelHubStatus . ConflictsAggregate ) ) { if ( conflictError ) { conflictError . addCodes ( error ) ; } else { conflictError = ConflictingCodesError . fromError ( error ) ; } if ( ! updateOptions . continueOnConflict ) { throw conflictError ; } } else { aggregateError . errors . push ( error ) ; } } } } if ( conflictError ) { throw conflictError ; } if ( aggregateError . errors . length > 0 ) { throw aggregateError . errors . length > 1 ? aggregateError : aggregateError . errors [ 0 ] ; } Logger . logTrace ( loggerCategory , `Requested ${ codes . length } codes for iModel` , ( ) => ( { iModelId } ) ) ; return result ; } param iModelId param query returns throws public async get ( accessToken , iModelId , query = new CodeQuery ( ) ) { Logger . logInfo ( loggerCategory , "Querying codes for iModel" , ( ) => ( { iModelId } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; let codes ; if ( query . isMultiCodeQuery ) { const multiCodes = await this . _handler . getInstances < MultiCode > ( accessToken , MultiCode , this . getRelativeUrl ( iModelId ) , query . getQueryOptions ( ) ) ; codes = CodeHandler . convertMultiCodesToCodes ( multiCodes ) ; } else { codes = await this . _handler . postQuery < HubCode > ( accessToken , HubCode , this . getRelativeUrl ( iModelId , false ) , query . getQueryOptions ( ) ) ; } Logger . logTrace ( loggerCategory , `Queried ${ codes . length } codes for iModel` , ( ) => ( { iModelId } ) ) ; return codes ; } param iModelId param briefcaseId throws throws throws public async deleteAll ( accessToken , iModelId , briefcaseId ) { Logger . logInfo ( loggerCategory , "Deleting all codes from briefcase" , ( ) => ( { briefcaseId , iModelId } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; ArgumentCheck . validBriefcaseId ( "briefcaseId" , briefcaseId ) ; await this . _handler . delete ( accessToken , this . getRelativeUrl ( iModelId , false , `DiscardReservedCodes-${ briefcaseId }` ) ) ; Logger . logTrace ( loggerCategory , "Deleted all codes from briefcase" , ( ) => ( { briefcaseId , iModelId } ) ) ; } }