From 929b8134f87d94017987b5c3d6a1bf373c76148e Mon Sep 17 00:00:00 2001 From: Daniel Rodriguez Date: Thu, 23 Jul 2026 16:25:33 -0400 Subject: [PATCH] Decouple transformer export handling Register a dedicated export handler for IModelTransformer while preserving existing callback dispatch and exporter customization. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...l-transformer-decouple-export-handler.json | 7 + common/api/imodel-transformer.api.md | 6 +- docs/changehistory/NEXT_VERSION.md | 23 +++ docs/learning/transformer/index.md | 1 + packages/transformer/src/IModelExporter.ts | 5 +- packages/transformer/src/IModelTransformer.ts | 107 +++++++------- .../src/IModelTransformerExportHandler.ts | 138 ++++++++++++++++++ .../test/standalone/IModelTransformer.test.ts | 74 ++++++++++ 8 files changed, 304 insertions(+), 57 deletions(-) create mode 100644 change/@itwin-imodel-transformer-decouple-export-handler.json create mode 100644 packages/transformer/src/IModelTransformerExportHandler.ts diff --git a/change/@itwin-imodel-transformer-decouple-export-handler.json b/change/@itwin-imodel-transformer-decouple-export-handler.json new file mode 100644 index 00000000..07f937e6 --- /dev/null +++ b/change/@itwin-imodel-transformer-decouple-export-handler.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "IModelTransformer now uses a dedicated IModelExportHandler instead of inheriting from IModelExportHandler.", + "packageName": "@itwin/imodel-transformer", + "email": "44824788+DanRod1999@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/common/api/imodel-transformer.api.md b/common/api/imodel-transformer.api.md index 00aae561..8cf24a48 100644 --- a/common/api/imodel-transformer.api.md +++ b/common/api/imodel-transformer.api.md @@ -151,6 +151,8 @@ export class IModelExporter { exportCodeSpecByName(codeSpecName: string): Promise; exportCodeSpecs(): Promise; exportElement(elementId: Id64String): Promise; + // @internal + exportElementAspects(): Promise; exportFontByFontFamilyDescriptor(fontFamily: FontFamilyDescriptor): Promise; exportFontByFontProps(fontProps: FontProps): Promise; exportFontByName(fontName: string): Promise; @@ -254,7 +256,7 @@ export interface IModelTransformArgs { } // @beta -export class IModelTransformer extends IModelExportHandler { +export class IModelTransformer { constructor(args: IModelTransformArgs, options?: IModelTransformOptions); protected addCustomChanges(_sourceDbChanges: ChangedInstanceIds): Promise; calculateEcefTransform(): Transform | undefined; @@ -291,6 +293,8 @@ export class IModelTransformer extends IModelExportHandler { onExportModel(sourceModel: Model): Promise; onExportRelationship(sourceRelationship: Relationship): Promise; onExportSchema(schema: ECSchemaMetaData.Schema): Promise; + onProgress(): Promise; + onSkipElement(_sourceElementId: Id64String): Promise; onTransformElement(sourceElement: Element_2): Promise; protected onTransformElementAspect(sourceElementAspect: ElementAspect): Promise; onTransformModel(sourceModel: Model, targetModeledElementId: Id64String): ModelProps; diff --git a/docs/changehistory/NEXT_VERSION.md b/docs/changehistory/NEXT_VERSION.md index 5398a802..acecdf23 100644 --- a/docs/changehistory/NEXT_VERSION.md +++ b/docs/changehistory/NEXT_VERSION.md @@ -1,5 +1,28 @@ # Next release notes +## Breaking change: `IModelTransformer` no longer extends `IModelExportHandler` + +`IModelTransformer` now registers a dedicated internal `IModelExportHandler` with its exporter instead of acting as the export handler itself. Existing transformer subclass overrides such as `shouldExportElement()`, `onExportElement()`, `onProgress()`, and `onSkipElement()` continue to be invoked with the same behavior. + +Code that treated an `IModelTransformer` instance as an `IModelExportHandler` must use a separate handler for direct `IModelExporter` workflows: + +```ts +const transformer = new IModelTransformer({ + source: sourceDb, + target: targetEditTxn, +}); + +// The transformer configures its own internal export handler. +await transformer.process(); + +// Direct exporter workflows require their own handler. +const exporter = new IModelExporter(sourceDb); +exporter.registerHandler(myExportHandler); +await exporter.exportAll(); +``` + +The `IModelTransformer.exporter` property and support for passing a preconfigured exporter as `IModelTransformArgs.source` are unchanged. + ## Breaking change: transformer errors now have stable identifiers Errors detected and owned by `@itwin/imodel-transformer` now use `ITwinError` with scope `@itwin/imodel-transformer` and a key from `IModelTransformerError`. These errors previously used a mix of `IModelError` and plain `Error`. diff --git a/docs/learning/transformer/index.md b/docs/learning/transformer/index.md index 681522dd..ad710b7a 100644 --- a/docs/learning/transformer/index.md +++ b/docs/learning/transformer/index.md @@ -82,6 +82,7 @@ iModel entities are highly related to each other. Therefore, *cloning* an entity The [IModelTransformer]($transformer) base class is used when the **source** and **target** in an ETL workflow are both/different iModels and some sort of data transformation is needed in the middle. An instance of `IModelTransformer` holds instances of `IModelExporter`, `IModelImporter`, and `IModelElementCloneContext`. This means that customization is possible at the export stage, the transformation stage, and the import stage of the overall ETL process. +`IModelTransformer` configures a dedicated internal `IModelExportHandler` for its exporter; the transformer itself is not an export handler. Potential transformations include: diff --git a/packages/transformer/src/IModelExporter.ts b/packages/transformer/src/IModelExporter.ts index 5cdf6d1b..2d3cea04 100644 --- a/packages/transformer/src/IModelExporter.ts +++ b/packages/transformer/src/IModelExporter.ts @@ -512,7 +512,7 @@ export class IModelExporter { } else { await this.exportModel(IModel.repositoryModelId); } - await this.exportAllAspects(); + await this.exportElementAspects(); await this.exportRelationships(ElementRefersToElements.classFullName); // handle deletes @@ -976,8 +976,9 @@ export class IModelExporter { } /** Exports all aspects present in the iModel. + * @internal */ - private async exportAllAspects(): Promise { + public async exportElementAspects(): Promise { return this._exportElementAspectsStrategy.exportAllElementAspects(); } diff --git a/packages/transformer/src/IModelTransformer.ts b/packages/transformer/src/IModelTransformer.ts index 5209ca31..358b6c4c 100644 --- a/packages/transformer/src/IModelTransformer.ts +++ b/packages/transformer/src/IModelTransformer.ts @@ -103,7 +103,6 @@ import { ExporterInitOptions, ExportSchemaResult, IModelExporter, - IModelExportHandler, } from "./IModelExporter"; import { IModelImporter, OptimizeGeometryOptions } from "./IModelImporter"; import { TransformerLoggerCategory } from "./TransformerLoggerCategory"; @@ -116,6 +115,7 @@ import { IModelTransformerError, IModelTransformerErrorScope, } from "./IModelTransformerError"; +import { IModelTransformerExportHandler } from "./IModelTransformerExportHandler"; const loggerCategory: string = TransformerLoggerCategory.IModelTransformer; @@ -394,9 +394,10 @@ export interface RelationshipPropsForDelete { /** Base class used to transform a source iModel into a different target iModel. * @see [iModel Transformation and Data Exchange]($docs/learning/transformer/index.md), [IModelExporter]($transformer), [IModelImporter]($transformer) + * @note The transformer configures a dedicated internal [[IModelExportHandler]] for its exporter and is not itself an export handler. * @beta */ -export class IModelTransformer extends IModelExportHandler { +export class IModelTransformer { /** The IModelExporter that will export from the source iModel. */ public readonly exporter: IModelExporter; /** The IModelImporter that will import into the target iModel. */ @@ -488,8 +489,6 @@ export class IModelTransformer extends IModelExportHandler { args: IModelTransformArgs, options?: IModelTransformOptions ) { - super(); - const { source, target } = args; // initialize IModelTransformOptions this._options = { @@ -524,7 +523,7 @@ export class IModelTransformer extends IModelExportHandler { this.exporter = source; } this.sourceDb = this.exporter.sourceDb; - this.exporter.registerHandler(this); + this.exporter.registerHandler(new IModelTransformerExportHandler(this)); this.exporter.wantGeometry = options?.loadSourceGeometry ?? false; // optimization to not load source GeometryStreams by default if (!this._options.includeSourceProvenance) { // clone provenance from the source iModel into the target iModel? @@ -1161,12 +1160,20 @@ export class IModelTransformer extends IModelExportHandler { return this.exporter.exportChildElements(sourceElementId); } - /** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel. + /** Called when an element is skipped instead of transformed. + * @note A subclass may override this method to react to skipped elements. The base implementation does nothing. + */ + public async onSkipElement(_sourceElementId: Id64String): Promise {} + + /** Called when the exporter has made incremental progress. + * @note A subclass may override this method to report custom progress. The base implementation does nothing. + */ + public async onProgress(): Promise {} + + /** Called by the transformer's export handler to determine if an element should be exported from the source iModel. * @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter. */ - public override async shouldExportElement( - _sourceElement: Element - ): Promise { + public async shouldExportElement(_sourceElement: Element): Promise { return true; } @@ -1174,9 +1181,7 @@ export class IModelTransformer extends IModelExportHandler { * If they haven't been already, import all of the required references * @internal do not call, override or implement this, it will be removed */ - public override async preExportElement( - sourceElement: Element - ): Promise { + public async preExportElement(sourceElement: Element): Promise { if (!this.hasElementChanged(sourceElement)) { Logger.logTrace( loggerCategory, @@ -1299,10 +1304,10 @@ export class IModelTransformer extends IModelExportHandler { return (await queryReader.step()) ? queryReader.current[0] : undefined; } - /** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel. - * This override calls [[onTransformElement]] and then [IModelImporter.importElement]($transformer) to update the target iModel. + /** Called by the transformer's export handler to import an element into the target iModel. + * This method calls [[onTransformElement]] and then [IModelImporter.importElement]($transformer) to update the target iModel. */ - public override async onExportElement(sourceElement: Element): Promise { + public async onExportElement(sourceElement: Element): Promise { let targetElementId: Id64String = Id64.invalid; let targetElementProps: ElementProps; if (this._options.wasSourceIModelCopiedToTarget) { @@ -1459,12 +1464,10 @@ export class IModelTransformer extends IModelExportHandler { } } - /** Override of [IModelExportHandler.onDeleteElement]($transformer) that is called when [IModelExporter]($transformer) detects that an Element has been deleted from the source iModel. - * This override propagates the delete to the target iModel via [IModelImporter.deleteElement]($transformer). + /** Called by the transformer's export handler when [IModelExporter]($transformer) detects that an Element has been deleted from the source iModel. + * This method propagates the delete to the target iModel via [IModelImporter.deleteElement]($transformer). */ - public override async onDeleteElement( - sourceElementId: Id64String - ): Promise { + public async onDeleteElement(sourceElementId: Id64String): Promise { const targetElementId: Id64String = this.context.findTargetElementId(sourceElementId); if (Id64.isValidId64(targetElementId)) { @@ -1476,10 +1479,10 @@ export class IModelTransformer extends IModelExportHandler { } } - /** Override of [IModelExportHandler.onExportModel]($transformer) that is called when a Model should be exported from the source iModel. - * This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel. + /** Called by the transformer's export handler when a Model should be exported from the source iModel. + * This method calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel. */ - public override async onExportModel(sourceModel: Model): Promise { + public async onExportModel(sourceModel: Model): Promise { if ( this._options.skipPropagateChangesToRootElements && IModel.repositoryModelId === sourceModel.id @@ -1505,10 +1508,8 @@ export class IModelTransformer extends IModelExportHandler { this._targetModelsImportedInCurrentTransform.add(targetModelProps.id); } - /** Override of [IModelExportHandler.onDeleteModel]($transformer) that is called when [IModelExporter]($transformer) detects that a [Model]($backend) has been deleted from the source iModel. */ - public override async onDeleteModel( - sourceModelId: Id64String - ): Promise { + /** Called by the transformer's export handler when [IModelExporter]($transformer) detects that a [Model]($backend) has been deleted from the source iModel. */ + public async onDeleteModel(sourceModelId: Id64String): Promise { // It is possible and apparently occasionally sensical to delete a model without deleting its underlying element. // - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists. // - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid @@ -1715,19 +1716,19 @@ export class IModelTransformer extends IModelExportHandler { return this.exporter.exportRelationships(baseRelClassFullName); } - /** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported. + /** Called by the transformer's export handler to determine if a [Relationship]($backend) should be exported. * @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer). */ - public override async shouldExportRelationship( + public async shouldExportRelationship( _sourceRelationship: Relationship ): Promise { return true; } - /** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel. - * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel. + /** Called by the transformer's export handler to import a relationship into the target iModel. + * This method calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel. */ - public override async onExportRelationship( + public async onExportRelationship( sourceRelationship: Relationship ): Promise { const sourceFedGuid = this.sourceDb.elements.getFederationGuidFromId( @@ -1774,10 +1775,10 @@ export class IModelTransformer extends IModelExportHandler { } } - /** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel. + /** Called by the transformer's export handler when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel. * This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer). */ - public override async onDeleteRelationship( + public async onDeleteRelationship( sourceRelInstanceId: Id64String ): Promise { nodeAssert( @@ -1862,7 +1863,7 @@ export class IModelTransformer extends IModelExportHandler { return targetRelationshipProps; } - public override async shouldExportElementAspect( + public async shouldExportElementAspect( aspect: ElementAspect ): Promise { // This override is needed to ensure that aspects are not exported if their element is not exported. @@ -1870,10 +1871,10 @@ export class IModelTransformer extends IModelExportHandler { return this.context.findTargetElementId(aspect.element.id) !== Id64.invalid; } - /** Override of [IModelExportHandler.onExportElementUniqueAspect]($transformer) that imports an ElementUniqueAspect into the target iModel when it is exported from the source iModel. - * This override calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel. + /** Called by the transformer's export handler to import an ElementUniqueAspect into the target iModel. + * This method calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel. */ - public override async onExportElementUniqueAspect( + public async onExportElementUniqueAspect( sourceAspect: ElementUniqueAspect ): Promise { const targetAspectProps = await this.onTransformElementAspect(sourceAspect); @@ -1885,11 +1886,11 @@ export class IModelTransformer extends IModelExportHandler { this.context.remapElementAspect(sourceAspect.id, targetId); } - /** Override of [IModelExportHandler.onExportElementMultiAspects]($transformer) that imports ElementMultiAspects into the target iModel when they are exported from the source iModel. - * This override calls [[onTransformElementAspect]] for each ElementMultiAspect and then [IModelImporter.importElementMultiAspects]($transformer) to update the target iModel. + /** Called by the transformer's export handler to import ElementMultiAspects into the target iModel. + * This method calls [[onTransformElementAspect]] for each ElementMultiAspect and then [IModelImporter.importElementMultiAspects]($transformer) to update the target iModel. * @note ElementMultiAspects are handled as a group to make it easier to differentiate between insert, update, and delete. */ - public override async onExportElementMultiAspects( + public async onExportElementMultiAspects( sourceAspects: ElementMultiAspect[] ): Promise { // Transform source ElementMultiAspects into target ElementAspectProps @@ -1938,10 +1939,10 @@ export class IModelTransformer extends IModelExportHandler { Guid.createValue() ); - /** Override of [IModelExportHandler.shouldExportSchema]($transformer) that is called to determine if a schema should be exported + /** Called by the transformer's export handler to determine if a schema should be exported. * @note the default behavior doesn't import schemas older than those already in the target */ - public override async shouldExportSchema( + public async shouldExportSchema( schemaKey: ECSchemaMetaData.SchemaKey ): Promise { const versionInTarget = this.targetDb.querySchemaVersion(schemaKey.name); @@ -1954,7 +1955,7 @@ export class IModelTransformer extends IModelExportHandler { private _longNamedSchemasMap = new Map(); - /** Override of [IModelExportHandler.onExportSchema]($transformer) that serializes a schema to disk for [[processSchemas]] to import into + /** Called by the transformer's export handler to serialize a schema to disk for [[processSchemas]] to import into * the target iModel when it is exported from the source iModel. * @returns {Promise} Although the type is possibly void for backwards compatibility of subclasses, * `IModelTransformer.onExportSchema` always returns an[[IModelExportHandler.ExportSchemaResult]] @@ -1962,7 +1963,7 @@ export class IModelTransformer extends IModelExportHandler { * Schemas are *not* guaranteed to be written to [[IModelTransformer._schemaExportDir]] by a * known pattern derivable from the schema's name, so you must use this to find it. */ - public override async onExportSchema( + public async onExportSchema( schema: ECSchemaMetaData.Schema ): Promise { const ext = ".ecschema.xml"; @@ -2045,8 +2046,8 @@ export class IModelTransformer extends IModelExportHandler { return this.exporter.exportFonts(); } - /** Override of [IModelExportHandler.onExportFont]($transformer) that imports a font into the target iModel when it is exported from the source iModel. */ - public override async onExportFont( + /** Called by the transformer's export handler to import a font into the target iModel. */ + public async onExportFont( font: FontProps, _isUpdate: boolean | undefined ): Promise { @@ -2069,19 +2070,17 @@ export class IModelTransformer extends IModelExportHandler { return this.exporter.exportCodeSpecByName(codeSpecName); } - /** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel. + /** Called by the transformer's export handler to determine if a CodeSpec should be exported from the source iModel. * @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer). */ - public override async shouldExportCodeSpec( + public async shouldExportCodeSpec( _sourceCodeSpec: CodeSpec ): Promise { return true; } - /** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */ - public override async onExportCodeSpec( - sourceCodeSpec: CodeSpec - ): Promise { + /** Called by the transformer's export handler to import a CodeSpec into the target iModel. */ + public async onExportCodeSpec(sourceCodeSpec: CodeSpec): Promise { this.context.importCodeSpec(sourceCodeSpec.id); } @@ -2609,7 +2608,7 @@ export class IModelTransformer extends IModelExportHandler { await this.exporter.exportModel(IModel.repositoryModelId); } await this.completePartiallyCommittedElements(); - await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation + await this.exporter.exportElementAspects(); await this.completePartiallyCommittedAspects(); await this.exporter.exportRelationships( ElementRefersToElements.classFullName diff --git a/packages/transformer/src/IModelTransformerExportHandler.ts b/packages/transformer/src/IModelTransformerExportHandler.ts new file mode 100644 index 00000000..3943fd45 --- /dev/null +++ b/packages/transformer/src/IModelTransformerExportHandler.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { + // eslint-disable-next-line @typescript-eslint/no-redeclare + Element, + ElementAspect, + ElementMultiAspect, + ElementUniqueAspect, + Model, + Relationship, +} from "@itwin/core-backend"; +import { Id64String } from "@itwin/core-bentley"; +import { CodeSpec, FontProps } from "@itwin/core-common"; +import { Schema, SchemaKey } from "@itwin/ecschema-metadata"; +import { ExportSchemaResult, IModelExportHandler } from "./IModelExporter"; +import type { IModelTransformer } from "./IModelTransformer"; + +/** Adapts IModelExporter callbacks to IModelTransformer operations. + * @internal + */ +export class IModelTransformerExportHandler extends IModelExportHandler { + public constructor(private readonly _transformer: IModelTransformer) { + super(); + } + + public override async shouldExportCodeSpec( + codeSpec: CodeSpec + ): Promise { + return this._transformer.shouldExportCodeSpec(codeSpec); + } + + public override async onExportCodeSpec( + codeSpec: CodeSpec, + _isUpdate: boolean | undefined + ): Promise { + return this._transformer.onExportCodeSpec(codeSpec); + } + + public override async onExportFont( + font: FontProps, + isUpdate: boolean | undefined + ): Promise { + return this._transformer.onExportFont(font, isUpdate); + } + + public override async onExportModel( + model: Model, + _isUpdate: boolean | undefined + ): Promise { + return this._transformer.onExportModel(model); + } + + public override async onDeleteModel(modelId: Id64String): Promise { + return this._transformer.onDeleteModel(modelId); + } + + public override async shouldExportElement( + element: Element + ): Promise { + return this._transformer.shouldExportElement(element); + } + + public override async onSkipElement(elementId: Id64String): Promise { + return this._transformer.onSkipElement(elementId); + } + + public override async preExportElement(element: Element): Promise { + return this._transformer.preExportElement(element); + } + + public override async onExportElement( + element: Element, + _isUpdate: boolean | undefined + ): Promise { + return this._transformer.onExportElement(element); + } + + public override async onDeleteElement(elementId: Id64String): Promise { + return this._transformer.onDeleteElement(elementId); + } + + public override async shouldExportElementAspect( + aspect: ElementAspect + ): Promise { + return this._transformer.shouldExportElementAspect(aspect); + } + + public override async onExportElementUniqueAspect( + aspect: ElementUniqueAspect, + _isUpdate: boolean | undefined + ): Promise { + return this._transformer.onExportElementUniqueAspect(aspect); + } + + public override async onExportElementMultiAspects( + aspects: ElementMultiAspect[] + ): Promise { + return this._transformer.onExportElementMultiAspects(aspects); + } + + public override async shouldExportRelationship( + relationship: Relationship + ): Promise { + return this._transformer.shouldExportRelationship(relationship); + } + + public override async onExportRelationship( + relationship: Relationship, + _isUpdate: boolean | undefined + ): Promise { + return this._transformer.onExportRelationship(relationship); + } + + public override async onDeleteRelationship( + relInstanceId: Id64String + ): Promise { + return this._transformer.onDeleteRelationship(relInstanceId); + } + + public override async shouldExportSchema( + schemaKey: SchemaKey + ): Promise { + return this._transformer.shouldExportSchema(schemaKey); + } + + public override async onExportSchema( + schema: Schema + ): Promise { + return this._transformer.onExportSchema(schema); + } + + public override async onProgress(): Promise { + return this._transformer.onProgress(); + } +} diff --git a/packages/transformer/src/test/standalone/IModelTransformer.test.ts b/packages/transformer/src/test/standalone/IModelTransformer.test.ts index d8ede951..dc8314ef 100644 --- a/packages/transformer/src/test/standalone/IModelTransformer.test.ts +++ b/packages/transformer/src/test/standalone/IModelTransformer.test.ts @@ -209,6 +209,80 @@ describe("IModelTransformer", () => { await ReusedSnapshots.cleanup(); }); + it("should use a dedicated export handler and preserve callback dispatch", async () => { + const sourceDbFile = IModelTransformerTestUtils.prepareOutputFile( + "IModelTransformer", + "DedicatedExportHandler-Source.bim" + ); + const sourceDb = SnapshotDb.createEmpty(sourceDbFile, { + rootSubject: { name: "DedicatedExportHandler-Source" }, + }); + const [excludedElementId] = withEditTxn( + sourceDb, + "insert test subjects", + (txn) => [ + Subject.insert(txn, IModel.rootSubjectId, "Excluded"), + Subject.insert(txn, IModel.rootSubjectId, "Exported"), + ] + ); + + const targetDbFile = IModelTransformerTestUtils.prepareOutputFile( + "IModelTransformer", + "DedicatedExportHandler-Target.bim" + ); + const targetDb = SnapshotDb.createEmpty(targetDbFile, { + rootSubject: { name: "DedicatedExportHandler-Target" }, + }); + + class CapturingExporter extends IModelExporter { + public registeredHandler?: IModelExportHandler; + + public override registerHandler(handler: IModelExportHandler): void { + this.registeredHandler = handler; + super.registerHandler(handler); + } + } + + class CallbackTransformer extends IModelTransformer { + public progressCount = 0; + public skippedElementIds: Id64String[] = []; + + public override async onProgress(): Promise { + ++this.progressCount; + } + + public override async onSkipElement( + sourceElementId: Id64String + ): Promise { + this.skippedElementIds.push(sourceElementId); + } + } + + const exporter = new CapturingExporter(sourceDb); + exporter.excludeElement(excludedElementId); + exporter.progressInterval = 1; + const editTxn = new EditTxn(targetDb, "dedicated export handler"); + editTxn.start(); + const transformer = new CallbackTransformer( + { source: exporter, target: editTxn }, + { noProvenance: true } + ); + + expect(exporter.registeredHandler).to.be.instanceOf(IModelExportHandler); + expect(exporter.registeredHandler).not.to.equal(transformer); + expect(transformer).not.to.be.instanceOf(IModelExportHandler); + + await transformer.processChildElements(IModel.rootSubjectId); + + expect(transformer.skippedElementIds).to.deep.equal([excludedElementId]); + expect(transformer.progressCount).to.be.greaterThan(0); + + transformer.dispose(); + editTxn.end(); + sourceDb.close(); + targetDb.close(); + }); + it("should transform changes from source to target", async () => { // Source IModelDb const sourceDbFile = IModelTransformerTestUtils.prepareOutputFile(