diff --git a/clients/imodels-client-authoring/src/base/types/apiEntities/ChangesetInterfaces.ts b/clients/imodels-client-authoring/src/base/types/apiEntities/ChangesetInterfaces.ts index d5387383..0d43feb3 100644 --- a/clients/imodels-client-authoring/src/base/types/apiEntities/ChangesetInterfaces.ts +++ b/clients/imodels-client-authoring/src/base/types/apiEntities/ChangesetInterfaces.ts @@ -8,3 +8,5 @@ import { DownloadedFileProps } from "../CommonInterfaces"; /** Changeset metadata along with the downloaded file path. */ export type DownloadedChangeset = Changeset & DownloadedFileProps; + +export type DownloadedChangedElements = DownloadedChangeset; diff --git a/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperationParams.ts b/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperationParams.ts index c6ce5b8b..06571e86 100644 --- a/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperationParams.ts +++ b/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperationParams.ts @@ -12,6 +12,7 @@ import { import { DownloadProgressParam, + GenericAbortSignal, RetryParams, TargetDirectoryParam, } from "../../base/types"; @@ -59,6 +60,11 @@ export interface CreateChangesetParams extends IModelScopedOperationParams { changesetProperties: ChangesetPropertiesForCreate; } +export type DownloadChangedElementsFileParams = GetSingleChangesetParams & + TargetDirectoryParam & { + abortSignal?: GenericAbortSignal; + }; + /** Parameters for single Changeset download operation. */ export type DownloadSingleChangesetParams = GetSingleChangesetParams & TargetDirectoryParam & diff --git a/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperations.ts b/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperations.ts index b46cd272..0f477190 100644 --- a/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperations.ts +++ b/clients/imodels-client-authoring/src/operations/changeset/ChangesetOperations.ts @@ -18,6 +18,7 @@ import { import { DownloadProgressParam, + DownloadedChangedElements, DownloadedChangeset, GenericAbortSignal, RetryParams, @@ -31,6 +32,7 @@ import { CreateChangesetParams, DownloadChangesetListParams, DownloadSingleChangesetParams, + DownloadChangedElementsFileParams, } from "./ChangesetOperationParams"; import { LimitedParallelQueue } from "./LimitedParallelQueue"; @@ -127,6 +129,24 @@ export class ChangesetOperations< return this.downloadChangeset({ ...params, changeset }); } + /** + * Downloads a changed elements file from a changeset identified by either index or id. If the file does not exist + * in the changeset, an error is thrown. This operation does not retry the download on failure and does not check if the + * file already exists in the target directory. + * @param {DownloadSingleChangesetParams} params parameters for this operation. See {@link DownloadSingleChangesetParams}. + * @returns downloaded changed element. See {@link DownloadedChangedElements}. + */ + public async downloadChangedElements( + params: DownloadChangedElementsFileParams + ): Promise { + await this._options.localFileSystem.createDirectory( + params.targetDirectoryPath + ); + + const changeset: Changeset = await this.querySingleInternal(params); + return this.downloadChangedElementsFile({ ...params, changeset }); + } + /** * Downloads Changeset list. Internally the method uses {@link ChangesetOperations.getRepresentationList} to query the * Changeset collection so this operation supports most of the the same url parameters to specify what Changesets to @@ -214,6 +234,47 @@ export class ChangesetOperations< }; } + private async downloadChangedElementsFile( + params: IModelScopedOperationParams & + DownloadChangedElementsFileParams & { changeset: Changeset } + ): Promise { + const changedElementsWithPath: DownloadedChangedElements = { + ...params.changeset, + filePath: path.join( + params.targetDirectoryPath, + this.createChangedElementsFileName(params.changeset.id) + ), + }; + + let loggedError: Error | undefined; + try { + const downloadLink = params.changeset._links.download; + assertLink(downloadLink); + await downloadFile({ + storage: this._options.cloudStorage, + localPath: params.targetDirectoryPath, + abortSignal: params.abortSignal, + url: downloadLink.href, + storageType: downloadLink.storageType, + }); + return changedElementsWithPath; + } catch (error) { + this.throwIfAbortError(error, params.changeset, params.abortSignal); + if (error instanceof Error) loggedError = error; + throw new IModelsErrorImpl({ + code: IModelsErrorCode.ChangedElementsDownloadFailed, + message: `Failed to download changedElements File. Changeset id: ${ + params.changeset.id + }, changeset index: ${params.changeset.index}, error: ${JSON.stringify( + loggedError + )}.`, + originalError: loggedError, + statusCode: undefined, + details: undefined, + }); + } + } + private async downloadChangeset( params: IModelScopedOperationParams & TargetDirectoryParam & { changeset: Changeset } & DownloadProgressParam & @@ -344,6 +405,10 @@ export class ChangesetOperations< return `${changesetId}.cs`; } + private createChangedElementsFileName(changesetId: string): string { + return `${changesetId}.ndjson.gz`; + } + private async provideDownloadCallbacks( params: DownloadChangesetListParams ): Promise<[DownloadCallback, DownloadFailedCallback] | undefined> { diff --git a/clients/imodels-client-management/src/base/types/IModelsErrorInterfaces.ts b/clients/imodels-client-management/src/base/types/IModelsErrorInterfaces.ts index ca14e1ea..2118cb9d 100644 --- a/clients/imodels-client-management/src/base/types/IModelsErrorInterfaces.ts +++ b/clients/imodels-client-management/src/base/types/IModelsErrorInterfaces.ts @@ -15,6 +15,7 @@ export enum IModelsErrorCode { BriefcaseNotFound = "BriefcaseNotFound", CannotAcquire = "CannotAcquire", ChangesetDownloadFailed = "ChangesetDownloadFailed", + ChangedElementsDownloadFailed = "ChangedElementsDownloadFailed", ChangesetExists = "ChangesetExists", ChangesetExtendedDataNotFound = "ChangesetExtendedDataNotFound", ChangesetGroupNotFound = "ChangesetGroupNotFound", diff --git a/clients/imodels-client-management/src/base/types/apiEntities/ChangesetInterfaces.ts b/clients/imodels-client-management/src/base/types/apiEntities/ChangesetInterfaces.ts index fa2af448..10456e1e 100644 --- a/clients/imodels-client-management/src/base/types/apiEntities/ChangesetInterfaces.ts +++ b/clients/imodels-client-management/src/base/types/apiEntities/ChangesetInterfaces.ts @@ -94,6 +94,11 @@ export interface ChangesetLinks extends MinimalChangesetLinks { currentOrPrecedingCheckpoint: Link | null; /** Link from where to download the Changeset file. Link points to a remote storage. */ download: StorageLink | null; + /** Link from where to download the changed elements file. Link points to a remote storage. + * + * Note: This property is experimental. + */ + changedElements: StorageLink | null; /** * Link where to upload the Changeset file. Link points to a remote storage. IMPORTANT: this link * is never present in any of the Changeset instances returned from methods in this client. This property diff --git a/tests/imodels-clients-tests/src/integration/authoring/ChangesetOperations.test.ts b/tests/imodels-clients-tests/src/integration/authoring/ChangesetOperations.test.ts index 98ae426b..9f7f0c3f 100644 --- a/tests/imodels-clients-tests/src/integration/authoring/ChangesetOperations.test.ts +++ b/tests/imodels-clients-tests/src/integration/authoring/ChangesetOperations.test.ts @@ -169,6 +169,7 @@ describe("[Authoring] ChangesetOperations", () => { expectedLinks: { namedVersion: false, checkpoint: false, + elements: false, }, isGetResponse: false, }); @@ -229,6 +230,7 @@ describe("[Authoring] ChangesetOperations", () => { expectedLinks: { namedVersion: false, checkpoint: false, + elements: false, }, isGetResponse: false, }); @@ -285,6 +287,7 @@ describe("[Authoring] ChangesetOperations", () => { expectedLinks: { namedVersion: changesetHasNamedVersion, checkpoint: true, + elements: true, }, expectedTestChangesetFile: testChangesetFile, }); @@ -349,6 +352,7 @@ describe("[Authoring] ChangesetOperations", () => { expectedLinks: { namedVersion: changesetHasNamedVersion, checkpoint: true, + elements: true, }, expectedTestChangesetFile: testChangesetFile, }); @@ -423,6 +427,7 @@ describe("[Authoring] ChangesetOperations", () => { expectedLinks: { namedVersion: changesetHasNamedVersion, checkpoint: true, + elements: true, }, expectedTestChangesetFile: testChangesetFile, }); diff --git a/tests/imodels-clients-tests/src/integration/management/ChangesetOperations.test.ts b/tests/imodels-clients-tests/src/integration/management/ChangesetOperations.test.ts index 28fa9862..321bb163 100644 --- a/tests/imodels-clients-tests/src/integration/management/ChangesetOperations.test.ts +++ b/tests/imodels-clients-tests/src/integration/management/ChangesetOperations.test.ts @@ -267,6 +267,7 @@ describe("[Management] ChangesetOperations", () => { expectedLinks: { namedVersion: true, checkpoint: true, + elements: true, }, isGetResponse: true, }); @@ -308,6 +309,7 @@ describe("[Management] ChangesetOperations", () => { expectedLinks: { namedVersion: true, checkpoint: true, + elements: true, }, isGetResponse: true, }); @@ -349,6 +351,7 @@ describe("[Management] ChangesetOperations", () => { expectedLinks: { namedVersion: changesetHasNamedVersion, checkpoint: true, + elements: true, }, isGetResponse: true, }); diff --git a/utils/imodels-client-test-utils/src/assertions/NodeOnlyAssertions.ts b/utils/imodels-client-test-utils/src/assertions/NodeOnlyAssertions.ts index 3a0a316a..7aa9a5e2 100644 --- a/utils/imodels-client-test-utils/src/assertions/NodeOnlyAssertions.ts +++ b/utils/imodels-client-test-utils/src/assertions/NodeOnlyAssertions.ts @@ -121,6 +121,7 @@ export async function assertChangeset(params: { expectedLinks: { namedVersion: boolean; checkpoint: boolean; + elements: boolean; }; isGetResponse: boolean; }): Promise { @@ -151,6 +152,11 @@ export async function assertChangeset(params: { shouldLinkExist: params.expectedLinks.checkpoint, }); + assertOptionalLink({ + actualLink: params.actualChangeset._links.changedElements, + shouldLinkExist: params.expectedLinks.elements, + }); + if (params.isGetResponse) { expect(params.actualChangeset._links.download).to.exist; expect(params.actualChangeset._links.download!.href).to.not.be.empty; @@ -173,6 +179,7 @@ export async function assertDownloadedChangeset(params: { expectedLinks: { namedVersion: boolean; checkpoint: boolean; + elements: boolean; }; }): Promise { await assertChangeset({