Skip to content

Breakup IModelTransformer class#295

Open
DanRod1999 wants to merge 5 commits intoitwinjs-v5from
dan/breakup-imodeltransformer-class
Open

Breakup IModelTransformer class#295
DanRod1999 wants to merge 5 commits intoitwinjs-v5from
dan/breakup-imodeltransformer-class

Conversation

@DanRod1999
Copy link
Copy Markdown
Contributor

This was a suggestion by copilot and I think aligned a little with one of the comments in the service's teams issue about reducing size of some of the files. I reviewed this work as copilot was making the changes, and I feel its fine. However, I would like to get the perspective of the service team. These are pretty dramatic changes, and I don't necessarily know if I agree that its worthwhile.

I had Copilot do a write up for what the documentation on this would look like

@itwin/imodel-transformer — Internal Architecture: Decompose IModelTransformer

Summary

The IModelTransformer class has been decomposed from a ~3,500-line monolith into a focused orchestrator (~2,500 lines) backed by three new internal classes. This is a non-breaking change — the public API surface is unchanged.

What Changed

Internal Class Extractions

Three new internal (non-exported) classes were extracted from IModelTransformer:

Class Responsibility Lines
SyncTypeResolver Determines synchronization direction (forward vs. reverse) by inspecting External Source Aspects on the target scope element. ~150
ProvenanceManager Manages all provenance concerns — scope ESA lifecycle, synchronization versioning, element/relationship provenance creation, provenance queries, and tracked-element iteration. ~900
ElementResolver Resolves which target element corresponds to a given source element using a 4-strategy fallback: remap → FederationGuid → Code → create new. ~130

Removed Items (Dead Code)

If you referenced any of the following (unlikely — they were undocumented internals), they no longer exist:

  • _lastProvenanceEntityInfo
  • markLastProvenance()
  • nullLastProvenanceEntityInfo
  • LastProvenanceEntityInfo

Design Decisions

  • No backward compatibility shims — this is a 2.0 major version.
  • Extracted classes are not exported — they are internal implementation details. This preserves freedom to refactor further without public API churn.
  • Delegate pattern for public API — all public/protected methods remain on IModelTransformer and forward to the extracted class. This keeps the public surface stable.
  • Callback injection over importsProvenanceManager receives capabilities like getIsReverseSynchronization and queryTargetRelationshipId as constructor callbacks, avoiding circular dependencies between extracted classes.
  • Live options referenceProvenanceManager holds a reference to the transformer's _options object (not a copy) so that post-construction mutations (common in tests) remain visible.
  • ChangeDetector extraction was deferred — analysis showed it requires ~12 callbacks and mixes changeset planning with delete-remapping and context mutation. Not a clean extraction boundary. May be revisited as a smaller ChangesetPlanner in a future iteration.

@DanRod1999 DanRod1999 linked an issue Apr 24, 2026 that may be closed by this pull request
@aruniverse aruniverse requested a review from khanaffan April 24, 2026 20:11
Copy link
Copy Markdown
Contributor

@JulijaRamoskiene JulijaRamoskiene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Feel like classes are still tightly coupled. Left comments.
  • No tests for new classes. Refactoring of old tests might be required.
  • All classes have options, not sure why those are needed.

targetScopeElementId: Id64String;
isProvenanceInitTransform?: boolean;
allowNoScopingESA?: boolean;
hasArgsForProcessChanges?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Who hasArgsForProcessChanges ? Name makes no sense in SyncTypeResolver class.
  • Why all these values are put into options? Why those are not passed separately into constructor?
  • Why determineSyncType() is static and public?

It seems that there are some rules how to use this class, but it is not intuitive to grasp from code / implementation. I see that getResolvedSyncType() throws if resolve() was not called before, but when is the right time to call resolve? Maybe better implementation would help to lift that requirement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cleaned up this a bit. Removed options as I agree, they weren't necessary (I actually thought I removed them at first but what copilot actually did was remove move the options interface it created from imodelTransformer to the new files)

determineSyncType was always static and public I believe, so I kept it the same. Do you think it should change to private now that I moved it into its own class? I believe this design decision was originally made so users could potentially do this:

  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
  public static initElementProvenanceOptions(
    sourceElementId: Id64String,
    targetElementId: Id64String,
    args: {
      sourceDb: IModelDb;
      targetDb: IModelDb;
      // TODO: Consider making it optional and determining it through ESAs if not provided. This gives opportunity for people to determine it themselves using public static determineSyncType function.
      isReverseSynchronization: boolean;
      targetScopeElementId: Id64String;
    }
  ): ExternalSourceAspectProps {
    return ProvenanceManager.initElementProvenanceOptions(
      sourceElementId,
      targetElementId,
      args
    );
  }

/** Options for constructing a ProvenanceManager.
* @internal
*/
export interface ProvenanceManagerOptions {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Why all these properties are put under ProvenanceManagerOptions instead of passing them separately into ctor?
  • Why ProvenanceManagerOptions has transformerOptions, does it help to decouple implementation?
  • Design and usage of getIsReverseSynchronization() / queryTargetRelationshipId() feels over-complicated:
Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design and usage of getIsReverseSynchronization() / queryTargetRelationshipId() feels over-complicated

I didn't like this either, but these 2 functions depend on imodelTransformer context that the provenance manager doesn't have. I feel like queryTargerRelId could be moved to manager, but that function depends on the transformer context, which I don't think we want to pass to the manager.

Similar with getIsReverseSync, we would need to pass in the sync type resolver to the provenance manager

(this.targetDb as any).codeValueBehavior = "exact";
}
/* eslint-enable @itwin/no-internal */
this._syncTypeResolver = new SyncTypeResolver({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the plan for unit tests? Will it be easy to test / mock when values are not passed trough ctor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my new impl should make make testing/mocking easy, but lmk if that's not what you were referring to

@DanRod1999 DanRod1999 marked this pull request as ready for review May 7, 2026 15:29
@DanRod1999 DanRod1999 requested review from a team as code owners May 7, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Break up IModelTransformer Class

3 participants