packageDocumentation module import { AccessToken , GuidString , Logger } from "@itwin/core-bentley" ; import { DownloadFailed , FileHandler , SasUrlExpired } from "../itwin-client/FileHandler" ; import { ProgressCallback , ProgressInfo , RequestQueryOptions } from "../itwin-client/Request" ; import { ECJsonTypeMap , WsgInstance } from "../wsg/ECJsonTypeMap" ; import { ChunkedQueryContext } from "../wsg/ChunkedQueryContext" ; import { IModelHubClientLoggerCategory } from "../IModelHubClientLoggerCategories" ; import { IModelBaseHandler } from "./BaseHandler" ; import { ArgumentCheck , IModelHubClientError } from "./Errors" ; import { addSelectApplicationData , addSelectFileAccessKey , StringIdQuery } from "./HubQuery" ; const loggerCategory = IModelHubClientLoggerCategory . IModelHub ; public export enum ChangesType { /** [[ChangeSet]] contains regular file changes (e.g. changes to elements or models). */ Regular = 0 , /** [[ChangeSet]] only contains schema changes. */ Schema = 1 << 0 , /** [[ChangeSet]] contains definition changes. */ Definition = 1 << 1 , /** [[ChangeSet]] contains spatial data changes. */ SpatialData = 1 << 2 , /** [[ChangeSet]] contains sheets and drawings changes. */ SheetsAndDrawings = 1 << 3 , /** [[ChangeSet]] contains views and model changes. */ ViewsAndModels = 1 << 4 , /** [[ChangeSet]] contains changes of global properties. */ GlobalProperties = 1 << 5 } internal @ ECJsonTypeMap . classToJson ( "wsg" , "iModelScope.ChangeSet" , { schemaPropertyName : "schemaName" , classPropertyName : "className" } ) export class ChangeSet extends WsgInstance { /** Id of this ChangeSet. It has to be set during the push. It's a hash value based on the contents of ChangeSet file and its parentId. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.Id" ) public id ? : string ; /** Filename of the ChangeSet. It has to be set during the push. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.FileName" ) public fileName ? : string ; /** Description of this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.Description" ) public description ? : string ; /** Size of this ChangeSet file. It has to be set during the push. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.FileSize" ) public fileSize ? : string ; /** Index of this ChangeSet (increasing, but not necessarily sequential). */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.Index" ) public index ? : string ; /** Id of this ChangeSet's parent ChangeSet. It has to be set during the push. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.ParentId" ) public parentId ? : string ; internal deprecated @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.SeedFileId" ) public seedFileId ? : GuidString ; /** Id of the [[Briefcase]] that pushed this ChangeSet. It has to be set during the push. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.BriefcaseId" ) public briefcaseId ? : number ; /** Id of the user that pushed this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.UserCreated" ) public userCreated ? : string ; /** Date when this ChangeSet was pushed to iModelHub. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.PushDate" ) public pushDate ? : string ; /** Shows what kind of changes are contained in this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.ContainingChanges" ) public changesType ? : ChangesType ; internal @ ECJsonTypeMap . propertyToJson ( "wsg" , "properties.IsUploaded" ) public isUploaded ? : boolean ; internal @ ECJsonTypeMap . propertyToJson ( "wsg" , "relationshipInstances[FileAccessKey].relatedInstance[AccessKey].properties.DownloadUrl" ) public downloadUrl ? : string ; internal @ ECJsonTypeMap . propertyToJson ( "wsg" , "relationshipInstances[FileAccessKey].relatedInstance[AccessKey].properties.UploadUrl" ) public uploadUrl ? : string ; /** Id of the application that created this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "relationshipInstances[CreatedByApplication].relatedInstance[Application].properties.Id" ) public applicationId ? : string ; /** Name of the application that created this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "relationshipInstances[CreatedByApplication].relatedInstance[Application].properties.Name" ) public applicationName ? : string ; /** Id of the bridge that created this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "relationshipInstances[HasBridgeProperties](direction:forward).relatedInstance[BridgeProperties].properties.JobId" ) public bridgeJobId ? : string ; /** User identifiers of users who made changes contained in this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "relationshipInstances[HasBridgeProperties](direction:forward).relatedInstance[BridgeProperties].properties.Users" ) public bridgeUsers ? : string [ ] ; /** File names which have been modified with this ChangeSet. */ @ ECJsonTypeMap . propertyToJson ( "wsg" , "relationshipInstances[HasBridgeProperties](direction:forward).relatedInstance[BridgeProperties].properties.ChangedFiles" ) public bridgeChangedFiles ? : string [ ] ; /** Path to the download ChangeSet file on disk. */ public pathname ? : string ; private _fileSizeNumber ? : number ; /** Parsed number of this ChangeSet file size. */ public get fileSizeNumber ( ) : number { if ( this . _fileSizeNumber ) return this . _fileSizeNumber ; this . _fileSizeNumber = parseInt ( this . fileSize ! , 10 ) ; return this . _fileSizeNumber ; } } internal export class ChangeSetQuery extends StringIdQuery { /**
     * Default page size which is used when querying ChangeSets
     */ public static defaultPageSize : number = 1000 ; /** Constructor that sets default page size. */ constructor ( ) { super ( ) ; this . pageSize ( ChangeSetQuery . defaultPageSize ) ; } returns public clone ( ) { const changeSetQuery = new ChangeSetQuery ( ) ; changeSetQuery . _query = { ... this . getQueryOptions ( ) } ; changeSetQuery . _byId = this . getId ( ) ; return changeSetQuery ; } returns public selectDownloadUrl ( ) { addSelectFileAccessKey ( this . _query ) ; return this ; } returns public selectApplicationData ( ) { addSelectApplicationData ( this . _query ) ; return this ; } internal protected override checkValue ( id ) { ArgumentCheck . validChangeSetId ( "id" , id ) ; } param id returns public fromId ( id ) { ArgumentCheck . validChangeSetId ( "id" , id ) ; this . _query . $filter = `FollowingChangeSet-backward-ChangeSet.Id+eq+'${ id }'` ; return this ; } returns public latest ( ) { this . _query . $orderby = "Index+desc" ; return this ; } param firstChangeSetId param secondChangeSetId returns public betweenChangeSets ( firstChangeSetId , secondChangeSetId ? ) { ArgumentCheck . validChangeSetId ( "firstChangeSetId" , firstChangeSetId ) ; if ( secondChangeSetId ) ArgumentCheck . validChangeSetId ( "secondChangeSetId" , secondChangeSetId ) ; let query ; if ( ! secondChangeSetId ) { query = `CumulativeChangeSet-backward-ChangeSet.Id+eq+'${ firstChangeSetId }'` ; } else { query = `(CumulativeChangeSet-backward-ChangeSet.Id+eq+'${ firstChangeSetId }'` ; query += `+and+FollowingChangeSet-backward-ChangeSet.Id+eq+'${ secondChangeSetId }')` ; query += `+or+(CumulativeChangeSet-backward-ChangeSet.Id+eq+'${ secondChangeSetId }'` ; query += `+and+FollowingChangeSet-backward-ChangeSet.Id+eq+'${ firstChangeSetId }')` ; } this . _query . $filter = query ; return this ; } param versionId returns public getVersionChangeSets ( versionId ) { ArgumentCheck . validGuid ( "versionId" , versionId ) ; this . _query . $filter = `CumulativeChangeSet-backward-Version.Id+eq+'${ versionId }'` ; return this ; } param versionId returns public afterVersion ( versionId ) { ArgumentCheck . validGuid ( "versionId" , versionId ) ; this . _query . $filter = `FollowingChangeSet-backward-Version.Id+eq+'${ versionId }'` ; return this ; } param sourceVersionId param destinationVersionId returns public betweenVersions ( sourceVersionId , destinationVersionId ) { ArgumentCheck . validGuid ( "sourceVersionId" , sourceVersionId ) ; ArgumentCheck . validGuid ( "destinationVersionId" , destinationVersionId ) ; let query ; query = `(FollowingChangeSet-backward-Version.Id+eq+'${ sourceVersionId }'` ; query += `+and+CumulativeChangeSet-backward-Version.Id+eq+'${ destinationVersionId }')` ; query += `+or+(FollowingChangeSet-backward-Version.Id+eq+'${ destinationVersionId }'` ; query += `+and+CumulativeChangeSet-backward-Version.Id+eq+'${ sourceVersionId }')` ; this . _query . $filter = query ; return this ; } param versionId param changeSetId returns public betweenVersionAndChangeSet ( versionId , changeSetId ) { ArgumentCheck . validGuid ( "versionId" , versionId ) ; ArgumentCheck . validChangeSetId ( "changeSetId" , changeSetId ) ; let query ; query = `(CumulativeChangeSet-backward-Version.Id+eq+'${ versionId }'+and+FollowingChangeSet-backward-ChangeSet.Id+eq+'${ changeSetId }')` ; query += `+or+` ; query += `(FollowingChangeSet-backward-Version.Id+eq+'${ versionId }'+and+CumulativeChangeSet-backward-ChangeSet.Id+eq+'${ changeSetId }')` ; this . _query . $filter = query ; return this ; } param seedFileId returns internal public bySeedFileId ( seedFileId ) { ArgumentCheck . validGuid ( "seedFileId" , seedFileId ) ; this . addFilter ( `SeedFileId+eq+'${ seedFileId }'` ) ; return this ; } /** Select all bridge properties. */ public selectBridgeProperties ( ) { if ( ! this . _query . $select ) this . _query . $select = "*" ; return this . addSelect ( `HasBridgeProperties-forward-BridgeProperties.*` ) ; } } /** Queue for limiting number of promises executed in parallel. */ class ParallelQueue { private _queue : Array < ( ) => Promise < void > > = [ ] ; private _parallelDownloads = 10 ; /** Add a promise to the queue. */ public push ( downloadFunc ) { this . _queue . push ( downloadFunc ) ; } /** Wait for all promises in the queue to finish. */ public async waitAll ( ) { let i = 0 ; const promises = new Array < Promise < number > > ( ) ; const indexes = new Array < number > ( ) ; const completed = new Array < number > ( ) ; while ( this . _queue . length > 0 || promises . length > 0 ) { while ( this . _queue . length > 0 && promises . length < this . _parallelDownloads ) { const currentIndex = i ++ ; promises . push ( this . _queue [ 0 ] ( ) . then ( ( ) => completed . push ( currentIndex ) ) ) ; indexes . push ( currentIndex ) ; this . _queue . shift ( ) ; } await Promise . race ( promises ) ; while ( completed . length > 0 ) { const completedIndex = completed . shift ( ) ! ; const index = indexes . findIndex ( ( value ) => value === completedIndex ) ; if ( index !== undefined ) { promises . splice ( index , 1 ) ; indexes . splice ( index , 1 ) ; } } } } } /** Object for tracking changeSets download progress. */ class DownloadProgress { private _totalSize : number ; private _downloadedSize : number ; constructor ( totalSize ) { this . _downloadedSize = 0 ; this . _totalSize = totalSize ; } /** Gets total changeSets size to download. */ public get totalSize ( ) : number { return this . _totalSize ; } /** Gets currently downloaded changeSets size. */ public get downloadedSize ( ) : number { return this . _downloadedSize ; } /** Sets currently downloaded changeSets size. */ public set downloadedSize ( value ) { this . _downloadedSize = value ; } } internal export class ChangeSetHandler { private _handler : IModelBaseHandler ; private _fileHandler ? : FileHandler ; param handler param fileHandler internal constructor ( handler , fileHandler ? ) { this . _handler = handler ; this . _fileHandler = fileHandler ; } param iModelId param changeSetId internal private getRelativeUrl ( iModelId , changeSetId ? ) { return `/Repositories/iModel--${ iModelId }/iModelScope/ChangeSet/${ changeSetId || "" }` ; } param iModelId param query returns throws throws public async get ( accessToken , iModelId , query = new ChangeSetQuery ( ) ) { ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; Logger . logTrace ( loggerCategory , `Querying ChangeSets` , ( ) => ( { iModelId } ) ) ; const id = query . getId ( ) ; const changeSets = await this . _handler . getInstances < ChangeSet > ( accessToken , ChangeSet , this . getRelativeUrl ( iModelId , id ) , query . getQueryOptions ( ) ) ; return changeSets ; } param iModelId param url param chunkedQueryContext param queryOptions returns throws throws private async getChunk ( accessToken , iModelId , url , chunkedQueryContext , queryOptions ) { Logger . logTrace ( loggerCategory , `Querying ChangeSets chunk` , ( ) => ( { iModelId } ) ) ; const changeSets = await this . _handler . getInstancesChunk < ChangeSet > ( accessToken , url , chunkedQueryContext , ChangeSet , queryOptions ) ; return changeSets ; } param iModelId param query param path throws param progressCallback throws internal public async download ( accessToken , iModelId , query , path , progressCallback ? ) { ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; ArgumentCheck . defined ( "path" , path ) ; if ( typeof window !== "undefined" ) throw IModelHubClientError . browser ( ) ; if ( ! this . _fileHandler ) throw IModelHubClientError . fileHandler ( ) ; const changeSetsToDownloadQuery = query . clone ( ) ; changeSetsToDownloadQuery . select ( "FileSize" ) ; const changeSetsToDownload = await this . get ( accessToken , iModelId , changeSetsToDownloadQuery ) ; if ( changeSetsToDownload . length === 0 ) return new Array < ChangeSet > ( ) ; Logger . logInfo ( loggerCategory , `Downloading ${ changeSetsToDownload . length } changesets` ) ; let totalSize = 0 ; changeSetsToDownload . forEach ( ( value ) => totalSize += value . fileSizeNumber ) ; const downloadProgress = new DownloadProgress ( totalSize ) ; query . selectDownloadUrl ( ) ; const url = await this . _handler . getUrl ( ) + this . getRelativeUrl ( iModelId , query . getId ( ) ) ; const queryOptions = query . getQueryOptions ( ) ; const chunkedQueryContext = queryOptions ? ChunkedQueryContext . create ( queryOptions ) : undefined ; const changeSets = new Array < ChangeSet > ( ) ; do { const changeSetsChunk = await this . getChunk ( accessToken , iModelId , url , chunkedQueryContext , queryOptions ) ; await this . downloadChunk ( accessToken , iModelId , changeSetsChunk , path , downloadProgress , progressCallback ) ; changeSets . push ( ... changeSetsChunk ) ; } while ( chunkedQueryContext && ! chunkedQueryContext . isQueryFinished ) ; Logger . logInfo ( loggerCategory , `Downloaded ${ changeSets . length } changesets` ) ; return changeSets ; } param iModelId param changeSets param path param downloadProgress param progressCallback throws private async downloadChunk ( accessToken , iModelId , changeSets , path , downloadProgress , progressCallback ? ) { changeSets . forEach ( ( changeSet ) => { if ( ! changeSet . downloadUrl ) throw IModelHubClientError . missingDownloadUrl ( "changeSets" ) ; } ) ; Logger . logInfo ( loggerCategory , `Downloading ${ changeSets . length } changesets chunk` ) ; const sortedChangeSets = Array . from ( changeSets ) . sort ( ( a , b ) => a . fileSizeNumber < b . fileSizeNumber ? - 1 : a . fileSizeNumber === b . fileSizeNumber ? 0 : 1 ) ; const queue = new ParallelQueue ( ) ; const fileHandler = this . _fileHandler ! ; sortedChangeSets . forEach ( ( changeSet ) => queue . push ( async ( ) => { const downloadPath = fileHandler . join ( path , changeSet . fileName ! ) ; let previouslyDownloaded = 0 ; const callback = ( progress ) => { downloadProgress . downloadedSize += ( progress . loaded - previouslyDownloaded ) ; previouslyDownloaded = progress . loaded ; if ( progressCallback ) progressCallback ( { loaded : downloadProgress . downloadedSize , total : downloadProgress . totalSize , percent : downloadProgress . downloadedSize / downloadProgress . totalSize } ) ; } ; if ( this . wasChangeSetDownloaded ( downloadPath , changeSet . fileSizeNumber , changeSet ) ) { if ( progressCallback ) callback ( { percent : 100 , total : changeSet . fileSizeNumber , loaded : changeSet . fileSizeNumber } ) ; return ; } await this . downloadChangeSetWithRetry ( accessToken , iModelId , changeSet , downloadPath , progressCallback ? callback : undefined ) ; } ) ) ; await queue . waitAll ( ) ; Logger . logTrace ( loggerCategory , `Finished downloading changesets chunk` ) ; } param iModelId param changeSet param downloadPath param changeSetProgress throws private async downloadChangeSetWithRetry ( accessToken , iModelId , changeSet , downloadPath , changeSetProgress ? ) { try { await this . downloadChangeSet ( accessToken , changeSet , downloadPath , changeSetProgress ) ; return ; } catch ( error ) { if ( ! ( error instanceof SasUrlExpired || error instanceof DownloadFailed ) ) throw error ; Logger . logWarning ( loggerCategory , `ChangeSet download failed, refreshing downloadUrl to retry download.` , ( ) => ( { iModelId , id : changeSet . id } ) ) ; const changeSetQuery = new ChangeSetQuery ( ) ; changeSetQuery . byId ( changeSet . id ! ) ; changeSetQuery . selectDownloadUrl ( ) ; const refreshedChangeSets = await this . get ( accessToken , iModelId , changeSetQuery ) ; if ( refreshedChangeSets . length === 0 ) throw error ; await this . downloadChangeSet ( accessToken , refreshedChangeSets [ 0 ] , downloadPath , changeSetProgress ) ; return ; } } param changeSet param downloadPath param changeSetProgress throws private async downloadChangeSet ( accessToken , changeSet , downloadPath , changeSetProgress ? ) { try { await this . _fileHandler ! . downloadFile ( accessToken , changeSet . downloadUrl ! , downloadPath , changeSet . fileSizeNumber , changeSetProgress ) ; } catch ( error ) { if ( this . wasChangeSetDownloaded ( downloadPath , changeSet . fileSizeNumber , changeSet ) ) return ; throw error ; } } param path param expectedFileSize param changeSet private wasChangeSetDownloaded ( path , expectedFileSize , changeSet ) { if ( ! this . _fileHandler ) return false ; if ( ! this . _fileHandler . exists ( path ) ) return false ; const actualFileSize = this . _fileHandler . getFileSize ( path ) ; if ( actualFileSize === expectedFileSize ) return true ; Logger . logError ( loggerCategory , `ChangeSet size ${ actualFileSize } does not match the expected size ${ expectedFileSize }. Deleting it so that it can be refetched` , ( ) => ( changeSet ) ) ; try { this . _fileHandler . unlink ( path ) ; } catch ( error ) { Logger . logError ( loggerCategory , `Cannot delete ChangeSet file at ${ path }` ) ; } return false ; } param iModelId param changeSet param path param progressCallback throws throws throws throws throws throws internal public async create ( accessToken , iModelId , changeSet , path , progressCallback ? ) { Logger . logInfo ( loggerCategory , "Started uploading ChangeSet" , ( ) => ( { iModelId , ... changeSet } ) ) ; ArgumentCheck . validGuid ( "iModelId" , iModelId ) ; ArgumentCheck . defined ( "changeSet" , changeSet ) ; ArgumentCheck . defined ( "path" , path ) ; if ( typeof window !== "undefined" ) throw IModelHubClientError . browser ( ) ; if ( ! this . _fileHandler ) throw IModelHubClientError . fileHandler ( ) ; if ( ! this . _fileHandler . exists ( path ) || this . _fileHandler . isDirectory ( path ) ) throw IModelHubClientError . fileNotFound ( ) ; const postChangeSet = await this . _handler . postInstance < ChangeSet > ( accessToken , ChangeSet , this . getRelativeUrl ( iModelId ) , changeSet ) ; await this . _fileHandler . uploadFile ( accessToken , postChangeSet . uploadUrl ! , path , progressCallback ) ; postChangeSet . uploadUrl = undefined ; postChangeSet . downloadUrl = undefined ; postChangeSet . isUploaded = true ; const confirmChangeSet = await this . _handler . postInstance < ChangeSet > ( accessToken , ChangeSet , this . getRelativeUrl ( iModelId , postChangeSet . id ) , postChangeSet ) ; changeSet . isUploaded = true ; Logger . logInfo ( loggerCategory , "Finished uploading ChangeSet" , ( ) => ( { iModelId , ... changeSet } ) ) ; return confirmChangeSet ; } }