@itwin/imodel-transformer 1.0.0-dev.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,8 +19,6 @@ const core_common_1 = require("@itwin/core-common");
19
19
  const IModelExporter_1 = require("./IModelExporter");
20
20
  const IModelImporter_1 = require("./IModelImporter");
21
21
  const TransformerLoggerCategory_1 = require("./TransformerLoggerCategory");
22
- const PendingReferenceMap_1 = require("./PendingReferenceMap");
23
- const EntityMap_1 = require("./EntityMap");
24
22
  const IModelCloneContext_1 = require("./IModelCloneContext");
25
23
  const EntityUnifier_1 = require("./EntityUnifier");
26
24
  const Algo_1 = require("./Algo");
@@ -31,30 +29,6 @@ const nullLastProvenanceEntityInfo = {
31
29
  aspectVersion: "",
32
30
  aspectKind: core_backend_1.ExternalSourceAspect.Kind.Element,
33
31
  };
34
- /**
35
- * A container for tracking the state of a partially committed entity and finalizing it when it's ready to be fully committed
36
- * @internal
37
- */
38
- class PartiallyCommittedEntity {
39
- constructor(
40
- /**
41
- * A set of "model|element ++ ID64" pairs, (e.g. `model0x11` or `element0x12`)
42
- * It is possible for the submodel of an element to be separately resolved from the actual element,
43
- * so its resolution must be tracked separately
44
- */
45
- _missingReferences, _onComplete) {
46
- this._missingReferences = _missingReferences;
47
- this._onComplete = _onComplete;
48
- }
49
- resolveReference(id) {
50
- this._missingReferences.delete(id);
51
- if (this._missingReferences.size === 0)
52
- this._onComplete();
53
- }
54
- forceComplete() {
55
- this._onComplete();
56
- }
57
- }
58
32
  /**
59
33
  * Apply a function to each Id64 in a supported container type of Id64s.
60
34
  * Currently only supports raw Id64String or RelatedElement-like objects containing an `id` property that is a Id64String,
@@ -172,7 +146,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
172
146
  if (this._isProvenanceInitTransform) {
173
147
  return "forward";
174
148
  }
175
- if (!this._isSynchronization) {
149
+ if (!this._options.argsForProcessChanges) {
176
150
  return "not-sync";
177
151
  }
178
152
  try {
@@ -217,14 +191,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
217
191
  */
218
192
  constructor(source, target, options) {
219
193
  super();
220
- /** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
221
- * and have some helper methods below for now */
222
- this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
223
194
  /** a set of elements for which source provenance will be explicitly tracked by ExternalSourceAspects */
224
195
  this._elementsWithExplicitlyTrackedProvenance = new Set();
225
- /** map of partially committed entities to their partial commit progress */
226
- this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
227
- this._isSynchronization = false;
196
+ this._partiallyCommittedElementIds = new Set();
197
+ this._partiallyCommittedAspectIds = new Set();
228
198
  /**
229
199
  * A private variable meant to be set by tests which have an outdated way of setting up transforms. In all synchronizations today we expect to find an ESA in the branch db which describes the master -> branch relationship.
230
200
  * The exception to this is the first transform aka the provenance initializing transform which requires that the master imodel and the branch imodel are identical at the time of provenance initialization.
@@ -233,10 +203,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
233
203
  */
234
204
  this._allowNoScopingESA = false;
235
205
  this._changesetRanges = undefined;
236
- /** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
237
- * @note Currently only tracks elements which were not exported.
238
- */
239
- this._skippedEntities = new Set();
240
206
  /**
241
207
  * Previously the transformer would insert provenance always pointing to the "target" relationship.
242
208
  * It should (and now by default does) instead insert provenance pointing to the provenanceSource
@@ -276,7 +242,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
276
242
  // eslint-disable-next-line deprecation/deprecation
277
243
  danglingReferencesBehavior: options?.danglingReferencesBehavior ?? "reject",
278
244
  branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
245
+ skipPropagateChangesToRootElements: options?.skipPropagateChangesToRootElements ?? true,
279
246
  };
247
+ // check if authorization client is defined
248
+ if (core_backend_1.IModelHost.authorizationClient === undefined) {
249
+ core_bentley_1.Logger.logWarning(loggerCategory, "Authorization client is not set in IModelHost. If the transformer needs an accessToken, then it will fail.");
250
+ }
280
251
  this._isProvenanceInitTransform = this._options
281
252
  .wasSourceIModelCopiedToTarget
282
253
  ? true
@@ -360,9 +331,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
360
331
  core_bentley_1.Logger.logInfo(loggerCategory, `this._includeSourceProvenance=${this._options.includeSourceProvenance}`);
361
332
  core_bentley_1.Logger.logInfo(loggerCategory, `this._cloneUsingBinaryGeometry=${this._options.cloneUsingBinaryGeometry}`);
362
333
  core_bentley_1.Logger.logInfo(loggerCategory, `this._wasSourceIModelCopiedToTarget=${this._options.wasSourceIModelCopiedToTarget}`);
363
- core_bentley_1.Logger.logInfo(loggerCategory,
364
- // eslint-disable-next-line deprecation/deprecation
365
- `this._isReverseSynchronization=${this._options.isReverseSynchronization}`);
366
334
  core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.autoExtendProjectExtents=${JSON.stringify(this.importer.options.autoExtendProjectExtents)}`);
367
335
  core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.simplifyElementGeometry=${this.importer.options.simplifyElementGeometry}`);
368
336
  }
@@ -456,25 +424,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
456
424
  forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
457
425
  });
458
426
  }
459
- /** the changeset in the scoping element's source version found for this transformation
460
- * @note: the version depends on whether this is a reverse synchronization or not, as
461
- * it is stored separately for both synchronization directions.
462
- * @note: must call [[initScopeProvenance]] before using this property.
463
- * @note: empty string and -1 for changeset and index if it has never been transformed or was transformed before federation guid update (pre 1.x).
464
- */
465
- get _synchronizationVersion() {
466
- if (!this._cachedSynchronizationVersion) {
467
- nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
468
- const version = this.isReverseSynchronization
469
- ? this._targetScopeProvenanceProps.jsonProperties?.reverseSyncVersion
470
- : this._targetScopeProvenanceProps.version;
471
- nodeAssert(version !== undefined, "no version contained in target scope");
472
- const [id, index] = version === "" ? ["", -1] : version.split(";");
473
- this._cachedSynchronizationVersion = { index: Number(index), id };
474
- nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
475
- }
476
- return this._cachedSynchronizationVersion;
477
- }
478
427
  /**
479
428
  * As of itwinjs 4.6.0, definitionContainers are now deleted as if they were DefinitionPartitions as opposed to Definitions.
480
429
  * This variable being true will be used to special case the deletion of DefinitionContainers the same way DefinitionPartitions are deleted.
@@ -485,10 +434,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
485
434
  }
486
435
  return this._hasDefinitionContainerDeletionFeature;
487
436
  }
437
+ /**
438
+ * We cache the synchronization version to avoid querying the target scoping ESA multiple times.
439
+ * If the target scoping ESA is ever updated we need to clear any potentially cached sync version otherwise we will get stale values.
440
+ * Sets this._cachedSynchronizationVersion to undefined.
441
+ */
442
+ clearCachedSynchronizationVersion() {
443
+ this._cachedSynchronizationVersion = undefined;
444
+ }
488
445
  /** the changeset in the scoping element's source version found for this transformation
489
- * @note: the version depends on whether this is a reverse synchronization or not, as
446
+ * @note the version depends on whether this is a reverse synchronization or not, as
490
447
  * it is stored separately for both synchronization directions.
491
- * @note: empty string and -1 for changeset and index if it has never been transformed, or was transformed before federation guid update (pre 1.x).
448
+ * @note empty string and -1 for changeset and index if it has never been transformed
449
+ * @note empty string and -1 for changeset and index if it was transformed before federation guid update (pre 1.x) and @see [[IModelTransformOptions.branchRelationshipDataBehavior]] === "unsafe-migrate".
450
+ * @throws if the version is not found in a preexisting scope aspect and @see [[IModelTransformOptions.branchRelationshipDataBehavior]] !== "unsafe-migrate"
492
451
  */
493
452
  get synchronizationVersion() {
494
453
  if (this._cachedSynchronizationVersion === undefined) {
@@ -499,10 +458,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
499
458
  const version = this.isReverseSynchronization
500
459
  ? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
501
460
  : provenanceScopeAspect.version;
502
- if (!version) {
461
+ if (!version &&
462
+ this._options.branchRelationshipDataBehavior === "unsafe-migrate") {
503
463
  return { index: -1, id: "" }; // previous synchronization was done before fed guid update.
504
464
  }
505
- const [id, index] = version.split(";");
465
+ if (version === undefined) {
466
+ throw new Error(`Could not find synchronization version in scope aspect. This may be due to the last successful run of the transformer being done with an older version.
467
+ Consider running the transformer with branchRelationshipDataBehavior set to 'unsafe-migrate'`);
468
+ }
469
+ const [id, index] = version === "" ? ["", -1] : version.split(";");
506
470
  if (Number.isNaN(Number(index)))
507
471
  throw new Error("Could not parse version data from scope aspect");
508
472
  this._cachedSynchronizationVersion = { index: Number(index), id }; // synchronization version found and cached.
@@ -578,6 +542,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
578
542
  jsonProperties: JSON.stringify(aspectProps.jsonProperties),
579
543
  });
580
544
  aspectProps.id = id;
545
+ // Busting a potential cached version
546
+ this.clearCachedSynchronizationVersion();
581
547
  }
582
548
  }
583
549
  else {
@@ -587,29 +553,50 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
587
553
  aspectProps.jsonProperties = foundEsaProps.jsonProperties
588
554
  ? JSON.parse(foundEsaProps.jsonProperties)
589
555
  : undefined;
590
- this.handleUnsafeMigrate(aspectProps);
556
+ // Clone oldProps incase they're changed for logging purposes
557
+ const oldProps = JSON.parse(JSON.stringify(aspectProps));
558
+ if (this.handleUnsafeMigrate(aspectProps)) {
559
+ core_bentley_1.Logger.logInfo(loggerCategory, "Unsafe migrate made a change to the target scope's external source aspect. Updating aspect in database.", { oldProps, newProps: aspectProps });
560
+ this.provenanceDb.elements.updateAspect({
561
+ ...aspectProps,
562
+ jsonProperties: JSON.stringify(aspectProps.jsonProperties),
563
+ });
564
+ // Busting a potential cached version
565
+ this.clearCachedSynchronizationVersion();
566
+ }
591
567
  }
592
568
  this._targetScopeProvenanceProps =
593
569
  aspectProps;
594
570
  }
571
+ /** Returns true if a change was made to the aspectProps. */
595
572
  handleUnsafeMigrate(aspectProps) {
573
+ let madeChange = false;
596
574
  if (this._options.branchRelationshipDataBehavior !== "unsafe-migrate")
597
- return;
598
- const fallbackSyncVersionToUse = this._options.unsafeFallbackSyncVersion ?? "";
599
- const fallbackReverseSyncVersionToUse = this._options.unsafeFallbackReverseSyncVersion ?? "";
600
- if (aspectProps.version === undefined || aspectProps.version === "")
575
+ return madeChange;
576
+ const fallbackSyncVersionToUse = this._options.argsForProcessChanges?.unsafeFallbackSyncVersion ?? "";
577
+ const fallbackReverseSyncVersionToUse = this._options.argsForProcessChanges?.unsafeFallbackReverseSyncVersion ??
578
+ "";
579
+ if (aspectProps.version === undefined ||
580
+ (aspectProps.version === "" &&
581
+ aspectProps.version !== fallbackSyncVersionToUse)) {
601
582
  aspectProps.version = fallbackSyncVersionToUse;
583
+ madeChange = true;
584
+ }
602
585
  if (aspectProps.jsonProperties === undefined) {
603
586
  aspectProps.jsonProperties = {
604
587
  pendingReverseSyncChangesetIndices: [],
605
588
  pendingSyncChangesetIndices: [],
606
589
  reverseSyncVersion: fallbackReverseSyncVersionToUse,
607
590
  };
591
+ madeChange = true;
608
592
  }
609
593
  else if (aspectProps.jsonProperties.reverseSyncVersion === undefined ||
610
- aspectProps.jsonProperties.reverseSyncVersion === "") {
594
+ (aspectProps.jsonProperties.reverseSyncVersion === "" &&
595
+ aspectProps.jsonProperties.reverseSyncVersion !==
596
+ fallbackReverseSyncVersionToUse)) {
611
597
  aspectProps.jsonProperties.reverseSyncVersion =
612
598
  fallbackReverseSyncVersionToUse;
599
+ madeChange = true;
613
600
  }
614
601
  /**
615
602
  * This case will only be hit when:
@@ -622,11 +609,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
622
609
  undefined) {
623
610
  core_bentley_1.Logger.logWarning(loggerCategory, "Property pendingReverseSyncChangesetIndices missing on the jsonProperties of the scoping ESA. Setting to [].");
624
611
  aspectProps.jsonProperties.pendingReverseSyncChangesetIndices = [];
612
+ madeChange = true;
625
613
  }
626
614
  if (aspectProps.jsonProperties.pendingSyncChangesetIndices === undefined) {
627
615
  core_bentley_1.Logger.logWarning(loggerCategory, "Property pendingSyncChangesetIndices missing on the jsonProperties of the scoping ESA. Setting to [].");
628
616
  aspectProps.jsonProperties.pendingSyncChangesetIndices = [];
617
+ madeChange = true;
629
618
  }
619
+ return madeChange;
630
620
  }
631
621
  /**
632
622
  * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
@@ -723,7 +713,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
723
713
  targetScopeElementId: this.targetScopeElementId,
724
714
  isReverseSynchronization: this.isReverseSynchronization,
725
715
  fn,
726
- skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
716
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? true,
727
717
  });
728
718
  }
729
719
  /**
@@ -845,7 +835,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
845
835
  }
846
836
  /** Returns `true` if *brute force* delete detections should be run.
847
837
  * @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
848
- * @note Not relevant for processChanges when change history is known.
838
+ * @note Not relevant for [[process]] when [[IModelTransformOptions.argsForProcessChanges]] are provided and change history is known.
849
839
  */
850
840
  shouldDetectDeletes() {
851
841
  nodeAssert(this._syncType !== undefined);
@@ -855,9 +845,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
855
845
  * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
856
846
  * in the source iModel.
857
847
  * @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
858
- * This method is only called during [[processAll]] when the option
848
+ * This method is only called during [[process]] when [[IModelTransformOptions.argsForProcessChanges]] is undefined and the option
859
849
  * [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
860
- * necessary when using [[processChanges]] since changeset information is sufficient.
850
+ * necessary when calling [[process]] with [[IModelTransformOptions.argsForProcessChanges]] defined, since changeset information is sufficient.
861
851
  * @note you do not need to call this directly unless processing a subset of an iModel.
862
852
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
863
853
  */
@@ -887,12 +877,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
887
877
  }
888
878
  });
889
879
  }
890
- /**
891
- * @deprecated in 3.x, this no longer has any effect except emitting a warning
892
- */
893
- skipElement(_sourceElement) {
894
- core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
895
- }
896
880
  /** Transform the specified sourceElement into ElementProps for the target iModel.
897
881
  * @param sourceElement The Element from the source iModel to transform.
898
882
  * @returns ElementProps for the target iModel.
@@ -937,85 +921,74 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
937
921
  nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
938
922
  return this._hasElementChangedCache.has(sourceElement.id);
939
923
  }
940
- static transformCallbackFor(transformer, entity) {
941
- if (entity instanceof core_backend_1.Element)
942
- return transformer.onTransformElement; // eslint-disable-line @typescript-eslint/unbound-method
943
- else if (entity instanceof core_backend_1.Element)
944
- return transformer.onTransformModel; // eslint-disable-line @typescript-eslint/unbound-method
945
- else if (entity instanceof core_backend_1.Relationship)
946
- return transformer.onTransformRelationship; // eslint-disable-line @typescript-eslint/unbound-method
947
- else if (entity instanceof core_backend_1.ElementAspect)
948
- return transformer.onTransformElementAspect; // eslint-disable-line @typescript-eslint/unbound-method
949
- else
950
- (0, core_bentley_1.assert)(false, `unreachable; entity was '${entity.constructor.name}' not an Element, Relationship, or ElementAspect`);
951
- }
952
- /** callback to perform when a partial element says it's ready to be completed
953
- * transforms the source element with all references now valid, then updates the partial element with the results
954
- */
955
- makePartialEntityCompleter(sourceEntity) {
956
- return () => {
957
- const targetId = this.context.findTargetEntityId(core_backend_1.EntityReferences.from(sourceEntity));
958
- if (!core_backend_1.EntityReferences.isValid(targetId))
959
- throw Error(`${sourceEntity.id} has not been inserted into the target yet, the completer is invalid. This is a bug.`);
960
- const onEntityTransform = IModelTransformer.transformCallbackFor(this, sourceEntity);
961
- const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
962
- const targetProps = onEntityTransform.call(this, sourceEntity);
963
- if (sourceEntity instanceof core_backend_1.Relationship) {
964
- targetProps.sourceId =
965
- this.context.findTargetElementId(sourceEntity.sourceId);
966
- targetProps.targetId =
967
- this.context.findTargetElementId(sourceEntity.targetId);
924
+ completePartiallyCommittedElements() {
925
+ for (const sourceElementId of this._partiallyCommittedElementIds) {
926
+ const sourceElement = this.sourceDb.elements.getElement({
927
+ id: sourceElementId,
928
+ wantGeometry: this.exporter.wantGeometry,
929
+ wantBRepData: this.exporter.wantGeometry,
930
+ });
931
+ const targetId = this.context.findTargetElementId(sourceElementId);
932
+ if (core_bentley_1.Id64.isInvalid(targetId)) {
933
+ throw new Error(`source-target element mapping not found for element "${sourceElementId}" when completing partially committed elements. This is a bug.`);
968
934
  }
969
- updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
970
- this._partiallyCommittedEntities.delete(sourceEntity);
971
- };
935
+ const targetProps = this.onTransformElement(sourceElement);
936
+ this.targetDb.elements.updateElement({ ...targetProps, id: targetId });
937
+ }
972
938
  }
973
- /** collect references this entity has that are yet to be mapped, and if there are any
974
- * create a [[PartiallyCommittedEntity]] to track resolution of those references
975
- */
976
- collectUnmappedReferences(entity) {
977
- const missingReferences = new core_common_1.EntityReferenceSet();
978
- let thisPartialElem;
979
- // eslint-disable-next-line deprecation/deprecation
980
- for (const referenceId of entity.getReferenceConcreteIds()) {
981
- // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
982
- const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
983
- const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
984
- this._skippedEntities.has(referenceId);
985
- if (alreadyProcessed)
986
- continue;
987
- core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
988
- const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
989
- entityReference: referenceId,
939
+ completePartiallyCommittedAspects() {
940
+ for (const sourceAspectId of this._partiallyCommittedAspectIds) {
941
+ const sourceAspect = this.sourceDb.elements.getAspect(sourceAspectId);
942
+ const targetAspectId = this.context.findTargetAspectId(sourceAspectId);
943
+ if (core_bentley_1.Id64.isInvalid(targetAspectId)) {
944
+ throw new Error(`source-target aspect mapping not found for aspect "${sourceAspectId}" when completing partially committed aspects. This is a bug.`);
945
+ }
946
+ const targetAspectProps = this.onTransformElementAspect(sourceAspect);
947
+ this.targetDb.elements.updateAspect({
948
+ ...targetAspectProps,
949
+ id: targetAspectId,
990
950
  });
991
- if (!referencedExistsInSource) {
992
- core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
993
- switch (this._options.danglingReferencesBehavior) {
994
- case "ignore":
995
- continue;
996
- case "reject":
997
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
998
- `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
999
- "This must have been caused by an upstream application that changed the iModel.",
1000
- "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
1001
- "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
1002
- "transformer to remove the reference and fix affected elements may be suitable.",
1003
- ].join("\n"));
951
+ }
952
+ }
953
+ doAllReferencesExistInTarget(entity) {
954
+ let allReferencesExist = true;
955
+ for (const referenceId of entity.getReferenceIds()) {
956
+ const referencedEntityId = core_backend_1.EntityReferences.toId64(referenceId);
957
+ if (referencedEntityId === core_common_1.IModel.repositoryModelId ||
958
+ referencedEntityId === core_common_1.IModel.dictionaryId ||
959
+ referencedEntityId === "0xe") {
960
+ continue;
961
+ }
962
+ if (allReferencesExist &&
963
+ !core_backend_1.EntityReferences.isValid(this.context.findTargetEntityId(referenceId))) {
964
+ // if we care about references existing then we cannot return early and must check all other references.
965
+ if (this._options.danglingReferencesBehavior === "ignore") {
966
+ return false;
1004
967
  }
968
+ allReferencesExist = false;
1005
969
  }
1006
- if (thisPartialElem === undefined) {
1007
- thisPartialElem = new PartiallyCommittedEntity(missingReferences, this.makePartialEntityCompleter(entity));
1008
- if (!this._partiallyCommittedEntities.has(entity))
1009
- this._partiallyCommittedEntities.set(entity, thisPartialElem);
970
+ if (this._options.danglingReferencesBehavior === "reject") {
971
+ this.assertReferenceExistsInSource(referenceId, entity);
1010
972
  }
1011
- missingReferences.add(referenceId);
1012
- const entityReference = core_backend_1.EntityReferences.from(entity);
1013
- this._pendingReferences.set({ referenced: referenceId, referencer: entityReference }, thisPartialElem);
973
+ }
974
+ return allReferencesExist;
975
+ }
976
+ assertReferenceExistsInSource(referenceId, entity) {
977
+ const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
978
+ entityReference: referenceId,
979
+ });
980
+ if (!referencedExistsInSource) {
981
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
982
+ `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
983
+ "This must have been caused by an upstream application that changed the iModel.",
984
+ "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this,",
985
+ `and the referenceId found on "${entity.id}" will not be carried over to corresponding target element.`,
986
+ ].join("\n"));
1014
987
  }
1015
988
  }
1016
989
  /** Cause the specified Element and its child Elements (if applicable) to be exported from the source iModel and imported into the target iModel.
1017
990
  * @param sourceElementId Identifies the Element from the source iModel to import.
1018
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
991
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1019
992
  */
1020
993
  async processElement(sourceElementId) {
1021
994
  await this.initialize();
@@ -1026,7 +999,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1026
999
  }
1027
1000
  /** Import child elements into the target IModelDb
1028
1001
  * @param sourceElementId Import the child elements of this element in the source IModelDb.
1029
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1002
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1030
1003
  */
1031
1004
  async processChildElements(sourceElementId) {
1032
1005
  await this.initialize();
@@ -1038,24 +1011,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1038
1011
  shouldExportElement(_sourceElement) {
1039
1012
  return true;
1040
1013
  }
1041
- onSkipElement(sourceElementId) {
1042
- if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
1043
- // element already has provenance
1044
- return;
1045
- }
1046
- core_bentley_1.Logger.logInfo(loggerCategory, `Element '${sourceElementId}' won't be exported. Marking its references as resolved`);
1047
- const elementKey = `e${sourceElementId}`;
1048
- this._skippedEntities.add(elementKey);
1049
- // Mark any existing pending references to the skipped element as resolved.
1050
- for (const referencer of this._pendingReferences.getReferencersByEntityKey(elementKey)) {
1051
- const key = PendingReferenceMap_1.PendingReference.from(referencer, elementKey);
1052
- const pendingRef = this._pendingReferences.get(key);
1053
- if (!pendingRef)
1054
- continue;
1055
- pendingRef.resolveReference(elementKey);
1056
- this._pendingReferences.delete(key);
1057
- }
1058
- }
1059
1014
  /**
1060
1015
  * If they haven't been already, import all of the required references
1061
1016
  * @internal do not call, override or implement this, it will be removed
@@ -1122,11 +1077,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1122
1077
  onExportElement(sourceElement) {
1123
1078
  let targetElementId;
1124
1079
  let targetElementProps;
1125
- if (this._options.preserveElementIdsForFiltering) {
1126
- targetElementId = sourceElement.id;
1127
- targetElementProps = this.onTransformElement(sourceElement);
1128
- }
1129
- else if (this._options.wasSourceIModelCopiedToTarget) {
1080
+ if (this._options.wasSourceIModelCopiedToTarget) {
1130
1081
  targetElementId = sourceElement.id;
1131
1082
  targetElementProps =
1132
1083
  this.targetDb.elements.getElementProps(targetElementId);
@@ -1165,17 +1116,40 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1165
1116
  }
1166
1117
  if (!this.hasElementChanged(sourceElement))
1167
1118
  return;
1168
- this.collectUnmappedReferences(sourceElement);
1119
+ if (!this.doAllReferencesExistInTarget(sourceElement)) {
1120
+ this._partiallyCommittedElementIds.add(sourceElement.id);
1121
+ }
1169
1122
  // targetElementId will be valid (indicating update) or undefined (indicating insert)
1170
1123
  targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
1171
1124
  ? targetElementId
1172
1125
  : undefined;
1126
+ if (this._options.preserveElementIdsForFiltering) {
1127
+ const isValid = core_bentley_1.Id64.isValid(targetElementId);
1128
+ if (isValid && targetElementId !== sourceElement.id) {
1129
+ // Element found with different id
1130
+ throw new Error(`Element id(${sourceElement.id}) cannot be preserved. Found a different mapping(${targetElementId}) from source element`);
1131
+ }
1132
+ else if (isValid && targetElementId === sourceElement.id) {
1133
+ // targetElementId is valid (indicating update)
1134
+ this.importer.markElementToUpdateDuringPreserveIds(sourceElement.id);
1135
+ }
1136
+ else if (!isValid) {
1137
+ const sourceInTargetElemProps = this.targetDb.elements.tryGetElementProps(sourceElement.id);
1138
+ // if we don't find mapping for source element in target(invalid) but another element with source id exists in target
1139
+ if (sourceInTargetElemProps) {
1140
+ // Element id is already taken by another element
1141
+ throw new Error(`Element id(${sourceElement.id}) cannot be preserved. An unrelated element in the target already uses id: ${sourceElement.id}`);
1142
+ }
1143
+ else {
1144
+ // Element id in target is available to be remapped
1145
+ targetElementProps.id = sourceElement.id;
1146
+ }
1147
+ }
1148
+ }
1173
1149
  if (!this._options.wasSourceIModelCopiedToTarget) {
1174
1150
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1175
1151
  }
1176
1152
  this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
1177
- // now that we've mapped this elem we can fix unmapped references to it
1178
- this.resolvePendingReferences(sourceElement);
1179
1153
  // the transformer does not currently 'split' or 'join' any elements, therefore, it does not
1180
1154
  // insert external source aspects because federation guids are sufficient for this.
1181
1155
  // Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
@@ -1203,16 +1177,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1203
1177
  this.markLastProvenance(provenance, { isRelationship: false });
1204
1178
  }
1205
1179
  }
1206
- resolvePendingReferences(entity) {
1207
- for (const referencer of this._pendingReferences.getReferencers(entity)) {
1208
- const key = PendingReferenceMap_1.PendingReference.from(referencer, entity);
1209
- const pendingRef = this._pendingReferences.get(key);
1210
- if (!pendingRef)
1211
- continue;
1212
- pendingRef.resolveReference(core_backend_1.EntityReferences.from(entity));
1213
- this._pendingReferences.delete(key);
1214
- }
1215
- }
1216
1180
  /** Override of [IModelExportHandler.onDeleteElement]($transformer) that is called when [IModelExporter]($transformer) detects that an Element has been deleted from the source iModel.
1217
1181
  * This override propagates the delete to the target iModel via [IModelImporter.deleteElement]($transformer).
1218
1182
  */
@@ -1237,7 +1201,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1237
1201
  return;
1238
1202
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
1239
1203
  this.importer.importModel(targetModelProps);
1240
- this.resolvePendingReferences(sourceModel);
1241
1204
  }
1242
1205
  /** Override of [IModelExportHandler.onDeleteModel]($transformer) that is called when [IModelExporter]($transformer) detects that a [Model]($backend) has been deleted from the source iModel. */
1243
1206
  onDeleteModel(sourceModelId) {
@@ -1311,7 +1274,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1311
1274
  }
1312
1275
  /** Cause the model container, contents, and sub-models to be exported from the source iModel and imported into the target iModel.
1313
1276
  * @param sourceModeledElementId Import this [Model]($backend) from the source IModelDb.
1314
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1277
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1315
1278
  */
1316
1279
  async processModel(sourceModeledElementId) {
1317
1280
  await this.initialize();
@@ -1321,7 +1284,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1321
1284
  * @param sourceModelId Import the contents of this model from the source IModelDb.
1322
1285
  * @param targetModelId Import into this model in the target IModelDb. The target model must exist prior to this call.
1323
1286
  * @param elementClassFullName Optional classFullName of an element subclass to limit import query against the source model.
1324
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1287
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1325
1288
  */
1326
1289
  async processModelContents(sourceModelId, targetModelId, elementClassFullName = core_backend_1.Element.classFullName) {
1327
1290
  await this.initialize();
@@ -1378,46 +1341,50 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1378
1341
  targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
1379
1342
  return targetModelProps;
1380
1343
  }
1381
- /** Import elements that were deferred in a prior pass.
1382
- * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
1383
- */
1384
- async processDeferredElements(_numRetries = 3) { }
1385
- /** called at the end of a transformation,
1344
+ /**
1345
+ * Called at the end of a transformation,
1386
1346
  * updates the target scope element to say that transformation up through the
1387
1347
  * source's changeset has been performed. Also stores all changesets that occurred
1388
1348
  * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
1389
1349
  *
1390
- * You generally should not call this function yourself and use [[processChanges]] instead.
1350
+ * You generally should not call this function yourself and use [[process]] with [[IModelTransformOptions.argsForProcessChanges]] provided instead.
1391
1351
  * It is public for unsupported use cases of custom synchronization transforms.
1392
- * @note if you are not running processChanges in this transformation, this will fail
1393
- * without setting the `force` option to `true`
1352
+ * @note If [[IModelTransformOptions.argsForProcessChanges]] is not defined in this transformation, this function will return early without updating the sync version,
1353
+ * unless the `initializeReverseSyncVersion` option is set to `true`
1354
+ *
1355
+ * The `initializeReverseSyncVersion` is added to set the reverse synchronization version during a forward synchronization.
1356
+ * When set to `true`, it saves the reverse sync version as the current changeset of the targetDb. This is typically used for the first transformation between a master and branch iModel.
1357
+ * Setting `initializeReverseSyncVersion` to `true` has the effect of making it so any changesets in the branch iModel at the time of the first transformation will be ignored during any future reverse synchronizations from the branch to the master iModel.
1358
+ *
1359
+ * Note that typically, the reverseSyncVersion is saved as the last changeset merged from the branch into master.
1360
+ * Setting initializeReverseSyncVersion to true during a forward transformation could overwrite this correct reverseSyncVersion and should only be done during the first transformation between a master and branch iModel.
1394
1361
  */
1395
- updateSynchronizationVersion({ force = false } = {}) {
1396
- const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
1397
- this._sourceChangeDataState !== "has-changes" &&
1398
- !this._isProvenanceInitTransform;
1399
- if (notForcedAndHasNoChangesAndIsntProvenanceInit)
1362
+ updateSynchronizationVersion({ initializeReverseSyncVersion = false, } = {}) {
1363
+ const shouldSkipSyncVersionUpdate = !initializeReverseSyncVersion &&
1364
+ this._sourceChangeDataState !== "has-changes";
1365
+ if (shouldSkipSyncVersionUpdate)
1400
1366
  return;
1401
1367
  nodeAssert(this._targetScopeProvenanceProps);
1402
1368
  const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1403
1369
  const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1404
- if (this._isProvenanceInitTransform) {
1405
- this._targetScopeProvenanceProps.version = sourceVersion;
1406
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1407
- targetVersion;
1408
- }
1409
- else if (this.isReverseSynchronization) {
1370
+ if (this.isReverseSynchronization) {
1410
1371
  const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1411
1372
  core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1412
1373
  this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1413
1374
  sourceVersion;
1414
1375
  }
1415
- else if (!this.isReverseSynchronization) {
1376
+ else {
1416
1377
  core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1417
1378
  this._targetScopeProvenanceProps.version = sourceVersion;
1379
+ // save reverse sync version
1380
+ if (initializeReverseSyncVersion) {
1381
+ core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse sync version from ${this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion} to ${targetVersion}`);
1382
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1383
+ targetVersion;
1384
+ }
1418
1385
  }
1419
- if (this._isSynchronization ||
1420
- (this._startingChangesetIndices && this._isProvenanceInitTransform)) {
1386
+ if (this._options.argsForProcessChanges ||
1387
+ (this._startingChangesetIndices && initializeReverseSyncVersion)) {
1421
1388
  nodeAssert(this.targetDb.changeset.index !== undefined &&
1422
1389
  this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1423
1390
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
@@ -1425,16 +1392,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1425
1392
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1426
1393
  const pendingSyncChangesetIndicesKey = "pendingSyncChangesetIndices";
1427
1394
  const pendingReverseSyncChangesetIndicesKey = "pendingReverseSyncChangesetIndices";
1428
- const [syncChangesetsToClearKey, syncChangesetsToUpdateKey] = this
1429
- .isReverseSynchronization
1430
- ? [
1431
- pendingReverseSyncChangesetIndicesKey,
1432
- pendingSyncChangesetIndicesKey,
1433
- ]
1434
- : [
1435
- pendingSyncChangesetIndicesKey,
1436
- pendingReverseSyncChangesetIndicesKey,
1437
- ];
1395
+ // Determine which keys to clear and update based on the synchronization direction
1396
+ let syncChangesetsToClearKey;
1397
+ let syncChangesetsToUpdateKey;
1398
+ if (this.isReverseSynchronization) {
1399
+ syncChangesetsToClearKey = pendingReverseSyncChangesetIndicesKey;
1400
+ syncChangesetsToUpdateKey = pendingSyncChangesetIndicesKey;
1401
+ }
1402
+ else {
1403
+ syncChangesetsToClearKey = pendingSyncChangesetIndicesKey;
1404
+ syncChangesetsToUpdateKey = pendingReverseSyncChangesetIndicesKey;
1405
+ }
1406
+ // NOTE that as documented in [[processChanges]], this assumes that right after
1407
+ // transformation finalization, the work will be saved immediately, otherwise we've
1408
+ // just marked this changeset as a synchronization to ignore, and the user can add other
1409
+ // stuff to it which would break future synchronizations
1438
1410
  for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1439
1411
  jsonProps[syncChangesetsToUpdateKey].push(i);
1440
1412
  // Only keep the changeset indices which are greater than the source, this means they haven't been processed yet.
@@ -1454,25 +1426,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1454
1426
  ...this._targetScopeProvenanceProps,
1455
1427
  jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1456
1428
  });
1429
+ this.clearCachedSynchronizationVersion();
1457
1430
  }
1458
1431
  // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1459
- async finalizeTransformation(options) {
1432
+ finalizeTransformation() {
1460
1433
  this.importer.finalize();
1461
- this.updateSynchronizationVersion();
1462
- if (this._partiallyCommittedEntities.size > 0) {
1463
- const message = [
1464
- "The following elements were never fully resolved:",
1465
- [...this._partiallyCommittedEntities.keys()].join(","),
1466
- "This indicates that either some references were excluded from the transformation",
1467
- "or the source has dangling references.",
1468
- ].join("\n");
1469
- if (this._options.danglingReferencesBehavior === "reject")
1470
- throw new Error(message);
1471
- core_bentley_1.Logger.logWarning(loggerCategory, message);
1472
- for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
1473
- partiallyCommittedElem.forceComplete();
1474
- }
1475
- }
1434
+ this.updateSynchronizationVersion({
1435
+ initializeReverseSyncVersion: this._isProvenanceInitTransform,
1436
+ });
1476
1437
  // TODO: ignore if we remove change cache usage
1477
1438
  if (!this._options.noDetachChangeCache) {
1478
1439
  if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
@@ -1485,35 +1446,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1485
1446
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1486
1447
  }
1487
1448
  /* eslint-enable @itwin/no-internal */
1488
- const defaultSaveTargetChanges = () => this.targetDb.saveChanges();
1489
- await (options?.saveTargetChanges ?? defaultSaveTargetChanges)(this);
1490
- if (this.isReverseSynchronization)
1491
- this.sourceDb.saveChanges();
1492
- const description = `${this._isProvenanceInitTransform
1493
- ? options?.provenanceInitTransformChangesetDescription ??
1494
- `initialized branch provenance with master iModel: ${this.sourceDb.iModelId}`
1495
- : this.isForwardSynchronization
1496
- ? options?.forwardSyncBranchChangesetDescription ??
1497
- `Forward sync of iModel: ${this.sourceDb.iModelId}`
1498
- : options?.reverseSyncMasterChangesetDescription ??
1499
- `Reverse sync of iModel: ${this.sourceDb.iModelId}`}`;
1500
- if (this.targetDb.isBriefcaseDb()) {
1501
- // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1502
- await this.targetDb.pushChanges({
1503
- description,
1504
- });
1505
- }
1506
- if (this.isReverseSynchronization && this.sourceDb.isBriefcaseDb()) {
1507
- // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1508
- await this.sourceDb.pushChanges({
1509
- description: options?.reverseSyncBranchChangesetDescription ??
1510
- `Update provenance in response to a reverse sync to iModel: ${this.targetDb.iModelId}`,
1511
- });
1512
- }
1513
1449
  }
1514
1450
  /** Imports all relationships that subclass from the specified base class.
1515
1451
  * @param baseRelClassFullName The specified base relationship class.
1516
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1452
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1517
1453
  */
1518
1454
  async processRelationships(baseRelClassFullName) {
1519
1455
  await this.initialize();
@@ -1586,8 +1522,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1586
1522
  }
1587
1523
  /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
1588
1524
  * @deprecated in 1.x. Don't use this anymore
1589
- * @see processChanges
1590
- * @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
1525
+ * @see [[process]] with [[IModelTransformOptions.argsForProcessChanges]] provided.
1526
+ * @note This method is called from [[process]] when [[IModelTransformOptions.argsForProcessChanges]] are undefined, so it only needs to be called directly when processing a subset of an iModel.
1591
1527
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
1592
1528
  */
1593
1529
  async detectRelationshipDeletes() {
@@ -1654,22 +1590,25 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1654
1590
  * This override calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel.
1655
1591
  */
1656
1592
  onExportElementUniqueAspect(sourceAspect) {
1657
- const targetElementId = this.context.findTargetElementId(sourceAspect.element.id);
1658
- const targetAspectProps = this.onTransformElementAspect(sourceAspect, targetElementId);
1659
- this.collectUnmappedReferences(sourceAspect);
1593
+ const targetAspectProps = this.onTransformElementAspect(sourceAspect);
1594
+ if (!this.doAllReferencesExistInTarget(sourceAspect)) {
1595
+ this._partiallyCommittedAspectIds.add(sourceAspect.id);
1596
+ }
1660
1597
  const targetId = this.importer.importElementUniqueAspect(targetAspectProps);
1661
1598
  this.context.remapElementAspect(sourceAspect.id, targetId);
1662
- this.resolvePendingReferences(sourceAspect);
1663
1599
  }
1664
1600
  /** Override of [IModelExportHandler.onExportElementMultiAspects]($transformer) that imports ElementMultiAspects into the target iModel when they are exported from the source iModel.
1665
1601
  * This override calls [[onTransformElementAspect]] for each ElementMultiAspect and then [IModelImporter.importElementMultiAspects]($transformer) to update the target iModel.
1666
1602
  * @note ElementMultiAspects are handled as a group to make it easier to differentiate between insert, update, and delete.
1667
1603
  */
1668
1604
  onExportElementMultiAspects(sourceAspects) {
1669
- const targetElementId = this.context.findTargetElementId(sourceAspects[0].element.id);
1670
1605
  // Transform source ElementMultiAspects into target ElementAspectProps
1671
- const targetAspectPropsArray = sourceAspects.map((srcA) => this.onTransformElementAspect(srcA, targetElementId));
1672
- sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
1606
+ const targetAspectPropsArray = sourceAspects.map((srcA) => this.onTransformElementAspect(srcA));
1607
+ sourceAspects.forEach((a) => {
1608
+ if (!this.doAllReferencesExistInTarget(a)) {
1609
+ this._partiallyCommittedAspectIds.add(a.id);
1610
+ }
1611
+ });
1673
1612
  // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
1674
1613
  const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
1675
1614
  const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
@@ -1679,16 +1618,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1679
1618
  });
1680
1619
  for (let i = 0; i < targetIds.length; ++i) {
1681
1620
  this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
1682
- this.resolvePendingReferences(sourceAspects[i]);
1683
1621
  }
1684
1622
  }
1685
1623
  /** Transform the specified sourceElementAspect into ElementAspectProps for the target iModel.
1686
1624
  * @param sourceElementAspect The ElementAspect from the source iModel to be transformed.
1687
- * @param _targetElementId The ElementId of the target Element that will own the ElementAspects after transformation.
1688
1625
  * @returns ElementAspectProps for the target iModel.
1689
1626
  * @note A subclass can override this method to provide custom transform behavior.
1690
1627
  */
1691
- onTransformElementAspect(sourceElementAspect, _targetElementId) {
1628
+ onTransformElementAspect(sourceElementAspect) {
1692
1629
  const targetElementAspectProps = this.context.cloneElementAspect(sourceElementAspect);
1693
1630
  return targetElementAspectProps;
1694
1631
  }
@@ -1728,6 +1665,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1728
1665
  nodeAssert(schemaFileName.length <= systemMaxPathSegmentSize, "Schema name was still long. This is a bug.");
1729
1666
  this._longNamedSchemasMap.set(schema.name, schemaFileName);
1730
1667
  }
1668
+ /* eslint-disable-next-line deprecation/deprecation */
1731
1669
  this.sourceDb.nativeDb.exportSchema(schema.name, this._schemaExportDir, schemaFileName);
1732
1670
  return { schemaPath: path.join(this._schemaExportDir, schemaFileName) };
1733
1671
  }
@@ -1768,7 +1706,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1768
1706
  }
1769
1707
  }
1770
1708
  /** Cause all fonts to be exported from the source iModel and imported into the target iModel.
1771
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1709
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1772
1710
  */
1773
1711
  async processFonts() {
1774
1712
  // we do not need to initialize for this since no entities are exported
@@ -1780,14 +1718,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1780
1718
  this.context.importFont(font.id);
1781
1719
  }
1782
1720
  /** Cause all CodeSpecs to be exported from the source iModel and imported into the target iModel.
1783
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1721
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1784
1722
  */
1785
1723
  async processCodeSpecs() {
1786
1724
  await this.initialize();
1787
1725
  return this.exporter.exportCodeSpecs();
1788
1726
  }
1789
1727
  /** Cause a single CodeSpec to be exported from the source iModel and imported into the target iModel.
1790
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1728
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1791
1729
  */
1792
1730
  async processCodeSpec(codeSpecName) {
1793
1731
  await this.initialize();
@@ -1811,21 +1749,23 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1811
1749
  this.context.remapElement(sourceSubjectId, targetSubjectId);
1812
1750
  await this.processChildElements(sourceSubjectId);
1813
1751
  await this.processSubjectSubModels(sourceSubjectId);
1814
- return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1752
+ this.completePartiallyCommittedElements();
1753
+ this.completePartiallyCommittedAspects();
1815
1754
  }
1816
1755
  /**
1817
1756
  * Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
1818
- * are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
1757
+ * are intending to process changes. Callers may wish to explicitly call initialize if they need to execute code after initialize but before [[process]] is called.
1819
1758
  * @note Called by all `process*` functions implicitly.
1820
1759
  * Overriders must call `super.initialize()` first
1821
1760
  */
1822
- async initialize(args) {
1761
+ async initialize() {
1823
1762
  if (this._initialized)
1824
1763
  return;
1825
- await this._tryInitChangesetData(args);
1764
+ this.initScopeProvenance();
1765
+ await this._tryInitChangesetData(this._options.argsForProcessChanges);
1826
1766
  await this.context.initialize();
1827
1767
  // need exporter initialized to do remapdeletedsourceentities.
1828
- await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1768
+ await this.exporter.initialize(this.getExportInitOpts(this._options.argsForProcessChanges ?? {}));
1829
1769
  // Exporter must be initialized prior to processing changesets in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
1830
1770
  await this.processChangesets();
1831
1771
  this._initialized = true;
@@ -1891,7 +1831,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1891
1831
  const changeType = change.$meta?.op;
1892
1832
  if (changeType === "Deleted" &&
1893
1833
  change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1894
- change.Scope.Id === this.targetScopeElementId) {
1834
+ change.Scope.Id === this.targetScopeElementId &&
1835
+ change.Kind === core_backend_1.ExternalSourceAspect.Kind.Element) {
1895
1836
  elemIdToScopeEsa.set(change.Element.Id, change);
1896
1837
  }
1897
1838
  else if ((changeType === "Inserted" || changeType === "Updated") &&
@@ -1930,7 +1871,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1930
1871
  async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1931
1872
  // we need a connected iModel with changes to remap elements with deletions
1932
1873
  const notConnectedModel = this.sourceDb.iTwinId === undefined;
1933
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1874
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
1934
1875
  if (notConnectedModel || noChanges)
1935
1876
  return;
1936
1877
  /**
@@ -2027,17 +1968,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2027
1968
  this._sourceChangeDataState = "unconnected";
2028
1969
  return;
2029
1970
  }
2030
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1971
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
2031
1972
  if (noChanges) {
2032
1973
  this._sourceChangeDataState = "no-changes";
2033
1974
  this._csFileProps = [];
2034
1975
  return;
2035
1976
  }
1977
+ const startChangeset = "startChangeset" in args ? args.startChangeset : undefined;
2036
1978
  // NOTE: that we do NOT download the changesummary for the last transformed version, we want
2037
1979
  // to ignore those already processed changes
2038
- const startChangesetIndexOrId = args.startChangeset?.index ??
2039
- args.startChangeset?.id ??
2040
- this._synchronizationVersion.index + 1;
1980
+ const startChangesetIndexOrId = startChangeset?.index ??
1981
+ startChangeset?.id ??
1982
+ this.synchronizationVersion.index + 1;
2041
1983
  const endChangesetId = this.sourceDb.changeset.id;
2042
1984
  const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
2043
1985
  ? indexOrId
@@ -2046,20 +1988,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2046
1988
  iModelId: this.sourceDb.iModelId,
2047
1989
  // eslint-disable-next-line deprecation/deprecation
2048
1990
  changeset: { id: indexOrId },
2049
- accessToken: args.accessToken,
2050
1991
  })
2051
1992
  .then((changeset) => changeset.index)));
2052
- const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
2053
- if (!this._options.ignoreMissingChangesetsInSynchronizations &&
2054
- startChangesetIndex !== this._synchronizationVersion.index + 1 &&
2055
- this._synchronizationVersion.index !== -1) {
1993
+ const missingChangesets = startChangesetIndex > this.synchronizationVersion.index + 1;
1994
+ if (!this._options.argsForProcessChanges
1995
+ ?.ignoreMissingChangesetsInSynchronizations &&
1996
+ startChangesetIndex !== this.synchronizationVersion.index + 1 &&
1997
+ this.synchronizationVersion.index !== -1) {
2056
1998
  throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
2057
1999
  " startChangesetId should be" +
2058
2000
  " exactly the first changeset *after* the previous synchronization to not miss data." +
2059
2001
  ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}` +
2060
- ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'` +
2061
- ` which is changeset #${this._synchronizationVersion.index}. The transformer expected` +
2062
- ` #${this._synchronizationVersion.index + 1}.`);
2002
+ ` but the previous synchronization for this targetScopeElement was '${this.synchronizationVersion.id}'` +
2003
+ ` which is changeset #${this.synchronizationVersion.index}. The transformer expected` +
2004
+ ` #${this.synchronizationVersion.index + 1}.`);
2063
2005
  }
2064
2006
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
2065
2007
  const changesetsToSkip = this.isReverseSynchronization
@@ -2081,15 +2023,44 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2081
2023
  csFileProps.push(...fileProps);
2082
2024
  }
2083
2025
  this._csFileProps = csFileProps;
2084
- this._sourceChangeDataState = "has-changes";
2026
+ /** Theres a possibility that our csFileProps length is still 0 here, since we skip cs indices found in the pendingSync and pendingReverseSync indices arrays. */
2027
+ this._sourceChangeDataState =
2028
+ this._csFileProps.length === 0 ? "no-changes" : "has-changes";
2029
+ }
2030
+ /**
2031
+ * The behavior of process is influenced by [[IModelTransformOptions.argsForProcessChanges]] being defined or not defined during construction passed of the IModelTransformer.
2032
+ * @section When argsForProcessChanges are defined:
2033
+ *
2034
+ * Export changes from the source iModel and import the transformed entities into the target iModel.
2035
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2036
+ *
2037
+ * Notes:
2038
+ * - the transformer assumes that you saveChanges after processing changes. You should not modify the iModel after processChanges until saveChanges,
2039
+ * failure to do so may result in corrupted
2040
+ * data loss in future branch operations
2041
+ * - if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2042
+ * will automatically be determined and used
2043
+ * - To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired range and open the source iModel as of the end (inclusive) of the desired range.
2044
+ *
2045
+ * @section When argsForProcessChanges are undefined:
2046
+ *
2047
+ * Export everything from the source iModel and import the transformed entities into the target iModel.
2048
+ *
2049
+ * Notes:
2050
+ * - [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2051
+ *
2052
+ */
2053
+ async process() {
2054
+ await this.initialize();
2055
+ this.logSettings();
2056
+ return this._options.argsForProcessChanges !== undefined
2057
+ ? this.processChanges(this._options.argsForProcessChanges)
2058
+ : this.processAll();
2085
2059
  }
2086
2060
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
2087
2061
  * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2088
2062
  */
2089
- async processAll(options) {
2090
- this.logSettings();
2091
- this.initScopeProvenance();
2092
- await this.initialize();
2063
+ async processAll() {
2093
2064
  await this.exporter.exportCodeSpecs();
2094
2065
  await this.exporter.exportFonts();
2095
2066
  if (this._options.skipPropagateChangesToRootElements) {
@@ -2101,9 +2072,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2101
2072
  else {
2102
2073
  await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
2103
2074
  }
2075
+ this.completePartiallyCommittedElements();
2104
2076
  await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
2077
+ this.completePartiallyCommittedAspects();
2105
2078
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
2106
- await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2107
2079
  if (this._options.forceExternalSourceAspectProvenance &&
2108
2080
  this.shouldDetectDeletes()) {
2109
2081
  // eslint-disable-next-line deprecation/deprecation
@@ -2114,7 +2086,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2114
2086
  if (this._options.optimizeGeometry)
2115
2087
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2116
2088
  this.importer.computeProjectExtents();
2117
- await this.finalizeTransformation(options);
2089
+ this.finalizeTransformation();
2118
2090
  }
2119
2091
  markLastProvenance(sourceAspect, { isRelationship = false }) {
2120
2092
  this._lastProvenanceEntityInfo =
@@ -2131,42 +2103,45 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2131
2103
  }
2132
2104
  /** Export changes from the source iModel and import the transformed entities into the target iModel.
2133
2105
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2134
- * @note the transformer saves and pushes changes when its work is complete.
2106
+ * @note the transformer assumes that you saveChanges after processing changes. You should not
2107
+ * modify the iModel after processChanges until saveChanges, failure to do so may result in corrupted
2108
+ * data loss in future branch operations
2135
2109
  * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2136
2110
  * will automatically be determined and used
2137
2111
  * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired range and open the source iModel as of the end (inclusive) of the desired range.
2138
2112
  */
2139
2113
  async processChanges(options) {
2140
- this._isSynchronization = true;
2141
- this.initScopeProvenance();
2142
- this.logSettings();
2143
- await this.initialize(options);
2144
2114
  // must wait for initialization of synchronization provenance data
2145
2115
  await this.exporter.exportChanges(this.getExportInitOpts(options));
2146
- await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2116
+ this.completePartiallyCommittedElements();
2117
+ this.completePartiallyCommittedAspects();
2147
2118
  if (this._options.optimizeGeometry)
2148
2119
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2149
2120
  this.importer.computeProjectExtents();
2150
- await this.finalizeTransformation(options);
2121
+ this.finalizeTransformation();
2122
+ const defaultSaveTargetChanges = () => {
2123
+ this.targetDb.saveChanges();
2124
+ };
2125
+ await (options.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2151
2126
  }
2152
2127
  /** Changeset data must be initialized in order to build correct changeOptions.
2153
2128
  * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
2154
2129
  */
2155
2130
  getExportInitOpts(opts) {
2156
- if (!this._isSynchronization)
2131
+ if (!this._options.argsForProcessChanges)
2157
2132
  return {};
2133
+ const startChangeset = "startChangeset" in opts ? opts.startChangeset : undefined;
2158
2134
  return {
2159
- skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
2160
- accessToken: opts.accessToken,
2135
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
2161
2136
  ...(this._csFileProps
2162
2137
  ? { csFileProps: this._csFileProps }
2163
2138
  : this._changesetRanges
2164
2139
  ? { changesetRanges: this._changesetRanges }
2165
- : opts.startChangeset
2166
- ? { startChangeset: opts.startChangeset }
2140
+ : startChangeset
2141
+ ? { startChangeset }
2167
2142
  : {
2168
2143
  startChangeset: {
2169
- index: this._synchronizationVersion.index + 1,
2144
+ index: this.synchronizationVersion.index + 1,
2170
2145
  },
2171
2146
  }),
2172
2147
  };
@@ -2245,8 +2220,7 @@ class TemplateModelCloner extends IModelTransformer {
2245
2220
  }
2246
2221
  /** Cloning from a template requires this override of onTransformElement. */
2247
2222
  onTransformElement(sourceElement) {
2248
- // eslint-disable-next-line deprecation/deprecation
2249
- const referenceIds = sourceElement.getReferenceConcreteIds();
2223
+ const referenceIds = sourceElement.getReferenceIds();
2250
2224
  referenceIds.forEach((referenceId) => {
2251
2225
  // TODO: consider going through all definition elements at once and remapping them to themselves
2252
2226
  if (!core_backend_1.EntityReferences.isValid(this.context.findTargetEntityId(referenceId))) {