@itwin/imodel-transformer 1.0.0-dev.9 → 1.0.1-customchanges.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
  }
@@ -605,8 +573,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
605
573
  let madeChange = false;
606
574
  if (this._options.branchRelationshipDataBehavior !== "unsafe-migrate")
607
575
  return madeChange;
608
- const fallbackSyncVersionToUse = this._options.unsafeFallbackSyncVersion ?? "";
609
- const fallbackReverseSyncVersionToUse = this._options.unsafeFallbackReverseSyncVersion ?? "";
576
+ const fallbackSyncVersionToUse = this._options.argsForProcessChanges?.unsafeFallbackSyncVersion ?? "";
577
+ const fallbackReverseSyncVersionToUse = this._options.argsForProcessChanges?.unsafeFallbackReverseSyncVersion ??
578
+ "";
610
579
  if (aspectProps.version === undefined ||
611
580
  (aspectProps.version === "" &&
612
581
  aspectProps.version !== fallbackSyncVersionToUse)) {
@@ -744,7 +713,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
744
713
  targetScopeElementId: this.targetScopeElementId,
745
714
  isReverseSynchronization: this.isReverseSynchronization,
746
715
  fn,
747
- skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
716
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? true,
748
717
  });
749
718
  }
750
719
  /**
@@ -866,7 +835,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
866
835
  }
867
836
  /** Returns `true` if *brute force* delete detections should be run.
868
837
  * @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
869
- * @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.
870
839
  */
871
840
  shouldDetectDeletes() {
872
841
  nodeAssert(this._syncType !== undefined);
@@ -876,9 +845,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
876
845
  * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
877
846
  * in the source iModel.
878
847
  * @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
879
- * 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
880
849
  * [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
881
- * necessary when using [[processChanges]] since changeset information is sufficient.
850
+ * necessary when calling [[process]] with [[IModelTransformOptions.argsForProcessChanges]] defined, since changeset information is sufficient.
882
851
  * @note you do not need to call this directly unless processing a subset of an iModel.
883
852
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
884
853
  */
@@ -908,12 +877,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
908
877
  }
909
878
  });
910
879
  }
911
- /**
912
- * @deprecated in 3.x, this no longer has any effect except emitting a warning
913
- */
914
- skipElement(_sourceElement) {
915
- core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
916
- }
917
880
  /** Transform the specified sourceElement into ElementProps for the target iModel.
918
881
  * @param sourceElement The Element from the source iModel to transform.
919
882
  * @returns ElementProps for the target iModel.
@@ -958,85 +921,74 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
958
921
  nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
959
922
  return this._hasElementChangedCache.has(sourceElement.id);
960
923
  }
961
- static transformCallbackFor(transformer, entity) {
962
- if (entity instanceof core_backend_1.Element)
963
- return transformer.onTransformElement; // eslint-disable-line @typescript-eslint/unbound-method
964
- else if (entity instanceof core_backend_1.Element)
965
- return transformer.onTransformModel; // eslint-disable-line @typescript-eslint/unbound-method
966
- else if (entity instanceof core_backend_1.Relationship)
967
- return transformer.onTransformRelationship; // eslint-disable-line @typescript-eslint/unbound-method
968
- else if (entity instanceof core_backend_1.ElementAspect)
969
- return transformer.onTransformElementAspect; // eslint-disable-line @typescript-eslint/unbound-method
970
- else
971
- (0, core_bentley_1.assert)(false, `unreachable; entity was '${entity.constructor.name}' not an Element, Relationship, or ElementAspect`);
972
- }
973
- /** callback to perform when a partial element says it's ready to be completed
974
- * transforms the source element with all references now valid, then updates the partial element with the results
975
- */
976
- makePartialEntityCompleter(sourceEntity) {
977
- return () => {
978
- const targetId = this.context.findTargetEntityId(core_backend_1.EntityReferences.from(sourceEntity));
979
- if (!core_backend_1.EntityReferences.isValid(targetId))
980
- throw Error(`${sourceEntity.id} has not been inserted into the target yet, the completer is invalid. This is a bug.`);
981
- const onEntityTransform = IModelTransformer.transformCallbackFor(this, sourceEntity);
982
- const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
983
- const targetProps = onEntityTransform.call(this, sourceEntity);
984
- if (sourceEntity instanceof core_backend_1.Relationship) {
985
- targetProps.sourceId =
986
- this.context.findTargetElementId(sourceEntity.sourceId);
987
- targetProps.targetId =
988
- 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.`);
989
934
  }
990
- updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
991
- this._partiallyCommittedEntities.delete(sourceEntity);
992
- };
935
+ const targetProps = this.onTransformElement(sourceElement);
936
+ this.targetDb.elements.updateElement({ ...targetProps, id: targetId });
937
+ }
993
938
  }
994
- /** collect references this entity has that are yet to be mapped, and if there are any
995
- * create a [[PartiallyCommittedEntity]] to track resolution of those references
996
- */
997
- collectUnmappedReferences(entity) {
998
- const missingReferences = new core_common_1.EntityReferenceSet();
999
- let thisPartialElem;
1000
- // eslint-disable-next-line deprecation/deprecation
1001
- for (const referenceId of entity.getReferenceConcreteIds()) {
1002
- // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
1003
- const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
1004
- const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
1005
- this._skippedEntities.has(referenceId);
1006
- if (alreadyProcessed)
1007
- continue;
1008
- core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
1009
- const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
1010
- 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,
1011
950
  });
1012
- if (!referencedExistsInSource) {
1013
- core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
1014
- switch (this._options.danglingReferencesBehavior) {
1015
- case "ignore":
1016
- continue;
1017
- case "reject":
1018
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
1019
- `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
1020
- "This must have been caused by an upstream application that changed the iModel.",
1021
- "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
1022
- "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
1023
- "transformer to remove the reference and fix affected elements may be suitable.",
1024
- ].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;
1025
967
  }
968
+ allReferencesExist = false;
1026
969
  }
1027
- if (thisPartialElem === undefined) {
1028
- thisPartialElem = new PartiallyCommittedEntity(missingReferences, this.makePartialEntityCompleter(entity));
1029
- if (!this._partiallyCommittedEntities.has(entity))
1030
- this._partiallyCommittedEntities.set(entity, thisPartialElem);
970
+ if (this._options.danglingReferencesBehavior === "reject") {
971
+ this.assertReferenceExistsInSource(referenceId, entity);
1031
972
  }
1032
- missingReferences.add(referenceId);
1033
- const entityReference = core_backend_1.EntityReferences.from(entity);
1034
- 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"));
1035
987
  }
1036
988
  }
1037
989
  /** Cause the specified Element and its child Elements (if applicable) to be exported from the source iModel and imported into the target iModel.
1038
990
  * @param sourceElementId Identifies the Element from the source iModel to import.
1039
- * @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.
1040
992
  */
1041
993
  async processElement(sourceElementId) {
1042
994
  await this.initialize();
@@ -1047,7 +999,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1047
999
  }
1048
1000
  /** Import child elements into the target IModelDb
1049
1001
  * @param sourceElementId Import the child elements of this element in the source IModelDb.
1050
- * @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.
1051
1003
  */
1052
1004
  async processChildElements(sourceElementId) {
1053
1005
  await this.initialize();
@@ -1059,24 +1011,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1059
1011
  shouldExportElement(_sourceElement) {
1060
1012
  return true;
1061
1013
  }
1062
- onSkipElement(sourceElementId) {
1063
- if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
1064
- // element already has provenance
1065
- return;
1066
- }
1067
- core_bentley_1.Logger.logInfo(loggerCategory, `Element '${sourceElementId}' won't be exported. Marking its references as resolved`);
1068
- const elementKey = `e${sourceElementId}`;
1069
- this._skippedEntities.add(elementKey);
1070
- // Mark any existing pending references to the skipped element as resolved.
1071
- for (const referencer of this._pendingReferences.getReferencersByEntityKey(elementKey)) {
1072
- const key = PendingReferenceMap_1.PendingReference.from(referencer, elementKey);
1073
- const pendingRef = this._pendingReferences.get(key);
1074
- if (!pendingRef)
1075
- continue;
1076
- pendingRef.resolveReference(elementKey);
1077
- this._pendingReferences.delete(key);
1078
- }
1079
- }
1080
1014
  /**
1081
1015
  * If they haven't been already, import all of the required references
1082
1016
  * @internal do not call, override or implement this, it will be removed
@@ -1143,11 +1077,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1143
1077
  onExportElement(sourceElement) {
1144
1078
  let targetElementId;
1145
1079
  let targetElementProps;
1146
- if (this._options.preserveElementIdsForFiltering) {
1147
- targetElementId = sourceElement.id;
1148
- targetElementProps = this.onTransformElement(sourceElement);
1149
- }
1150
- else if (this._options.wasSourceIModelCopiedToTarget) {
1080
+ if (this._options.wasSourceIModelCopiedToTarget) {
1151
1081
  targetElementId = sourceElement.id;
1152
1082
  targetElementProps =
1153
1083
  this.targetDb.elements.getElementProps(targetElementId);
@@ -1186,17 +1116,40 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1186
1116
  }
1187
1117
  if (!this.hasElementChanged(sourceElement))
1188
1118
  return;
1189
- this.collectUnmappedReferences(sourceElement);
1119
+ if (!this.doAllReferencesExistInTarget(sourceElement)) {
1120
+ this._partiallyCommittedElementIds.add(sourceElement.id);
1121
+ }
1190
1122
  // targetElementId will be valid (indicating update) or undefined (indicating insert)
1191
1123
  targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
1192
1124
  ? targetElementId
1193
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
+ }
1194
1149
  if (!this._options.wasSourceIModelCopiedToTarget) {
1195
1150
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1196
1151
  }
1197
1152
  this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
1198
- // now that we've mapped this elem we can fix unmapped references to it
1199
- this.resolvePendingReferences(sourceElement);
1200
1153
  // the transformer does not currently 'split' or 'join' any elements, therefore, it does not
1201
1154
  // insert external source aspects because federation guids are sufficient for this.
1202
1155
  // Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
@@ -1224,16 +1177,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1224
1177
  this.markLastProvenance(provenance, { isRelationship: false });
1225
1178
  }
1226
1179
  }
1227
- resolvePendingReferences(entity) {
1228
- for (const referencer of this._pendingReferences.getReferencers(entity)) {
1229
- const key = PendingReferenceMap_1.PendingReference.from(referencer, entity);
1230
- const pendingRef = this._pendingReferences.get(key);
1231
- if (!pendingRef)
1232
- continue;
1233
- pendingRef.resolveReference(core_backend_1.EntityReferences.from(entity));
1234
- this._pendingReferences.delete(key);
1235
- }
1236
- }
1237
1180
  /** Override of [IModelExportHandler.onDeleteElement]($transformer) that is called when [IModelExporter]($transformer) detects that an Element has been deleted from the source iModel.
1238
1181
  * This override propagates the delete to the target iModel via [IModelImporter.deleteElement]($transformer).
1239
1182
  */
@@ -1258,7 +1201,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1258
1201
  return;
1259
1202
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
1260
1203
  this.importer.importModel(targetModelProps);
1261
- this.resolvePendingReferences(sourceModel);
1262
1204
  }
1263
1205
  /** Override of [IModelExportHandler.onDeleteModel]($transformer) that is called when [IModelExporter]($transformer) detects that a [Model]($backend) has been deleted from the source iModel. */
1264
1206
  onDeleteModel(sourceModelId) {
@@ -1332,7 +1274,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1332
1274
  }
1333
1275
  /** Cause the model container, contents, and sub-models to be exported from the source iModel and imported into the target iModel.
1334
1276
  * @param sourceModeledElementId Import this [Model]($backend) from the source IModelDb.
1335
- * @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.
1336
1278
  */
1337
1279
  async processModel(sourceModeledElementId) {
1338
1280
  await this.initialize();
@@ -1342,7 +1284,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1342
1284
  * @param sourceModelId Import the contents of this model from the source IModelDb.
1343
1285
  * @param targetModelId Import into this model in the target IModelDb. The target model must exist prior to this call.
1344
1286
  * @param elementClassFullName Optional classFullName of an element subclass to limit import query against the source model.
1345
- * @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.
1346
1288
  */
1347
1289
  async processModelContents(sourceModelId, targetModelId, elementClassFullName = core_backend_1.Element.classFullName) {
1348
1290
  await this.initialize();
@@ -1399,46 +1341,50 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1399
1341
  targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
1400
1342
  return targetModelProps;
1401
1343
  }
1402
- /** Import elements that were deferred in a prior pass.
1403
- * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
1404
- */
1405
- async processDeferredElements(_numRetries = 3) { }
1406
- /** called at the end of a transformation,
1344
+ /**
1345
+ * Called at the end of a transformation,
1407
1346
  * updates the target scope element to say that transformation up through the
1408
1347
  * source's changeset has been performed. Also stores all changesets that occurred
1409
1348
  * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
1410
1349
  *
1411
- * 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.
1412
1351
  * It is public for unsupported use cases of custom synchronization transforms.
1413
- * @note if you are not running processChanges in this transformation, this will fail
1414
- * 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.
1415
1361
  */
1416
- updateSynchronizationVersion({ force = false } = {}) {
1417
- const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
1418
- this._sourceChangeDataState !== "has-changes" &&
1419
- !this._isProvenanceInitTransform;
1420
- if (notForcedAndHasNoChangesAndIsntProvenanceInit)
1362
+ updateSynchronizationVersion({ initializeReverseSyncVersion = false, } = {}) {
1363
+ const shouldSkipSyncVersionUpdate = !initializeReverseSyncVersion &&
1364
+ this._sourceChangeDataState !== "has-changes";
1365
+ if (shouldSkipSyncVersionUpdate)
1421
1366
  return;
1422
1367
  nodeAssert(this._targetScopeProvenanceProps);
1423
1368
  const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1424
1369
  const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1425
- if (this._isProvenanceInitTransform) {
1426
- this._targetScopeProvenanceProps.version = sourceVersion;
1427
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1428
- targetVersion;
1429
- }
1430
- else if (this.isReverseSynchronization) {
1370
+ if (this.isReverseSynchronization) {
1431
1371
  const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1432
1372
  core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1433
1373
  this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1434
1374
  sourceVersion;
1435
1375
  }
1436
- else if (!this.isReverseSynchronization) {
1376
+ else {
1437
1377
  core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1438
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
+ }
1439
1385
  }
1440
- if (this._isSynchronization ||
1441
- (this._startingChangesetIndices && this._isProvenanceInitTransform)) {
1386
+ if (this._options.argsForProcessChanges ||
1387
+ (this._startingChangesetIndices && initializeReverseSyncVersion)) {
1442
1388
  nodeAssert(this.targetDb.changeset.index !== undefined &&
1443
1389
  this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1444
1390
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
@@ -1446,16 +1392,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1446
1392
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1447
1393
  const pendingSyncChangesetIndicesKey = "pendingSyncChangesetIndices";
1448
1394
  const pendingReverseSyncChangesetIndicesKey = "pendingReverseSyncChangesetIndices";
1449
- const [syncChangesetsToClearKey, syncChangesetsToUpdateKey] = this
1450
- .isReverseSynchronization
1451
- ? [
1452
- pendingReverseSyncChangesetIndicesKey,
1453
- pendingSyncChangesetIndicesKey,
1454
- ]
1455
- : [
1456
- pendingSyncChangesetIndicesKey,
1457
- pendingReverseSyncChangesetIndicesKey,
1458
- ];
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
1459
1410
  for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1460
1411
  jsonProps[syncChangesetsToUpdateKey].push(i);
1461
1412
  // Only keep the changeset indices which are greater than the source, this means they haven't been processed yet.
@@ -1478,23 +1429,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1478
1429
  this.clearCachedSynchronizationVersion();
1479
1430
  }
1480
1431
  // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1481
- async finalizeTransformation(options) {
1432
+ finalizeTransformation() {
1482
1433
  this.importer.finalize();
1483
- this.updateSynchronizationVersion();
1484
- if (this._partiallyCommittedEntities.size > 0) {
1485
- const message = [
1486
- "The following elements were never fully resolved:",
1487
- [...this._partiallyCommittedEntities.keys()].join(","),
1488
- "This indicates that either some references were excluded from the transformation",
1489
- "or the source has dangling references.",
1490
- ].join("\n");
1491
- if (this._options.danglingReferencesBehavior === "reject")
1492
- throw new Error(message);
1493
- core_bentley_1.Logger.logWarning(loggerCategory, message);
1494
- for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
1495
- partiallyCommittedElem.forceComplete();
1496
- }
1497
- }
1434
+ this.updateSynchronizationVersion({
1435
+ initializeReverseSyncVersion: this._isProvenanceInitTransform,
1436
+ });
1498
1437
  // TODO: ignore if we remove change cache usage
1499
1438
  if (!this._options.noDetachChangeCache) {
1500
1439
  if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
@@ -1507,35 +1446,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1507
1446
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1508
1447
  }
1509
1448
  /* eslint-enable @itwin/no-internal */
1510
- const defaultSaveTargetChanges = () => this.targetDb.saveChanges();
1511
- await (options?.saveTargetChanges ?? defaultSaveTargetChanges)(this);
1512
- if (this.isReverseSynchronization)
1513
- this.sourceDb.saveChanges();
1514
- const description = `${this._isProvenanceInitTransform
1515
- ? options?.provenanceInitTransformChangesetDescription ??
1516
- `initialized branch provenance with master iModel: ${this.sourceDb.iModelId}`
1517
- : this.isForwardSynchronization
1518
- ? options?.forwardSyncBranchChangesetDescription ??
1519
- `Forward sync of iModel: ${this.sourceDb.iModelId}`
1520
- : options?.reverseSyncMasterChangesetDescription ??
1521
- `Reverse sync of iModel: ${this.sourceDb.iModelId}`}`;
1522
- if (this.targetDb.isBriefcaseDb()) {
1523
- // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1524
- await this.targetDb.pushChanges({
1525
- description,
1526
- });
1527
- }
1528
- if (this.isReverseSynchronization && this.sourceDb.isBriefcaseDb()) {
1529
- // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1530
- await this.sourceDb.pushChanges({
1531
- description: options?.reverseSyncBranchChangesetDescription ??
1532
- `Update provenance in response to a reverse sync to iModel: ${this.targetDb.iModelId}`,
1533
- });
1534
- }
1535
1449
  }
1536
1450
  /** Imports all relationships that subclass from the specified base class.
1537
1451
  * @param baseRelClassFullName The specified base relationship class.
1538
- * @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.
1539
1453
  */
1540
1454
  async processRelationships(baseRelClassFullName) {
1541
1455
  await this.initialize();
@@ -1608,8 +1522,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1608
1522
  }
1609
1523
  /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
1610
1524
  * @deprecated in 1.x. Don't use this anymore
1611
- * @see processChanges
1612
- * @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.
1613
1527
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
1614
1528
  */
1615
1529
  async detectRelationshipDeletes() {
@@ -1676,22 +1590,25 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1676
1590
  * This override calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel.
1677
1591
  */
1678
1592
  onExportElementUniqueAspect(sourceAspect) {
1679
- const targetElementId = this.context.findTargetElementId(sourceAspect.element.id);
1680
- const targetAspectProps = this.onTransformElementAspect(sourceAspect, targetElementId);
1681
- this.collectUnmappedReferences(sourceAspect);
1593
+ const targetAspectProps = this.onTransformElementAspect(sourceAspect);
1594
+ if (!this.doAllReferencesExistInTarget(sourceAspect)) {
1595
+ this._partiallyCommittedAspectIds.add(sourceAspect.id);
1596
+ }
1682
1597
  const targetId = this.importer.importElementUniqueAspect(targetAspectProps);
1683
1598
  this.context.remapElementAspect(sourceAspect.id, targetId);
1684
- this.resolvePendingReferences(sourceAspect);
1685
1599
  }
1686
1600
  /** Override of [IModelExportHandler.onExportElementMultiAspects]($transformer) that imports ElementMultiAspects into the target iModel when they are exported from the source iModel.
1687
1601
  * This override calls [[onTransformElementAspect]] for each ElementMultiAspect and then [IModelImporter.importElementMultiAspects]($transformer) to update the target iModel.
1688
1602
  * @note ElementMultiAspects are handled as a group to make it easier to differentiate between insert, update, and delete.
1689
1603
  */
1690
1604
  onExportElementMultiAspects(sourceAspects) {
1691
- const targetElementId = this.context.findTargetElementId(sourceAspects[0].element.id);
1692
1605
  // Transform source ElementMultiAspects into target ElementAspectProps
1693
- const targetAspectPropsArray = sourceAspects.map((srcA) => this.onTransformElementAspect(srcA, targetElementId));
1694
- 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
+ });
1695
1612
  // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
1696
1613
  const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
1697
1614
  const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
@@ -1701,16 +1618,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1701
1618
  });
1702
1619
  for (let i = 0; i < targetIds.length; ++i) {
1703
1620
  this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
1704
- this.resolvePendingReferences(sourceAspects[i]);
1705
1621
  }
1706
1622
  }
1707
1623
  /** Transform the specified sourceElementAspect into ElementAspectProps for the target iModel.
1708
1624
  * @param sourceElementAspect The ElementAspect from the source iModel to be transformed.
1709
- * @param _targetElementId The ElementId of the target Element that will own the ElementAspects after transformation.
1710
1625
  * @returns ElementAspectProps for the target iModel.
1711
1626
  * @note A subclass can override this method to provide custom transform behavior.
1712
1627
  */
1713
- onTransformElementAspect(sourceElementAspect, _targetElementId) {
1628
+ onTransformElementAspect(sourceElementAspect) {
1714
1629
  const targetElementAspectProps = this.context.cloneElementAspect(sourceElementAspect);
1715
1630
  return targetElementAspectProps;
1716
1631
  }
@@ -1750,6 +1665,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1750
1665
  nodeAssert(schemaFileName.length <= systemMaxPathSegmentSize, "Schema name was still long. This is a bug.");
1751
1666
  this._longNamedSchemasMap.set(schema.name, schemaFileName);
1752
1667
  }
1668
+ /* eslint-disable-next-line deprecation/deprecation */
1753
1669
  this.sourceDb.nativeDb.exportSchema(schema.name, this._schemaExportDir, schemaFileName);
1754
1670
  return { schemaPath: path.join(this._schemaExportDir, schemaFileName) };
1755
1671
  }
@@ -1790,7 +1706,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1790
1706
  }
1791
1707
  }
1792
1708
  /** Cause all fonts to be exported from the source iModel and imported into the target iModel.
1793
- * @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.
1794
1710
  */
1795
1711
  async processFonts() {
1796
1712
  // we do not need to initialize for this since no entities are exported
@@ -1802,14 +1718,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1802
1718
  this.context.importFont(font.id);
1803
1719
  }
1804
1720
  /** Cause all CodeSpecs to be exported from the source iModel and imported into the target iModel.
1805
- * @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.
1806
1722
  */
1807
1723
  async processCodeSpecs() {
1808
1724
  await this.initialize();
1809
1725
  return this.exporter.exportCodeSpecs();
1810
1726
  }
1811
1727
  /** Cause a single CodeSpec to be exported from the source iModel and imported into the target iModel.
1812
- * @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.
1813
1729
  */
1814
1730
  async processCodeSpec(codeSpecName) {
1815
1731
  await this.initialize();
@@ -1833,25 +1749,56 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1833
1749
  this.context.remapElement(sourceSubjectId, targetSubjectId);
1834
1750
  await this.processChildElements(sourceSubjectId);
1835
1751
  await this.processSubjectSubModels(sourceSubjectId);
1836
- return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1752
+ this.completePartiallyCommittedElements();
1753
+ this.completePartiallyCommittedAspects();
1837
1754
  }
1838
1755
  /**
1839
1756
  * Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
1840
- * 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.
1841
1758
  * @note Called by all `process*` functions implicitly.
1842
1759
  * Overriders must call `super.initialize()` first
1843
1760
  */
1844
- async initialize(args) {
1761
+ async initialize() {
1845
1762
  if (this._initialized)
1846
1763
  return;
1847
- await this._tryInitChangesetData(args);
1764
+ this.initScopeProvenance();
1765
+ await this._tryInitChangesetData(this._options.argsForProcessChanges);
1848
1766
  await this.context.initialize();
1849
1767
  // need exporter initialized to do remapdeletedsourceentities.
1850
- await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1768
+ await this.exporter.initialize(this.getExportInitOpts(this._options.argsForProcessChanges ?? {}));
1851
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).
1852
1770
  await this.processChangesets();
1853
1771
  this._initialized = true;
1854
1772
  }
1773
+ async handleCustomChanges(hasElementChangedCache, deleteIdsProcessed) {
1774
+ // The hasElementChangedCache gets populated by changes from this._csFileProps.
1775
+ // Because there is a possibility that someone could manually add ids to exporter.sourceDbChanges, we must separately process exporter.sourceDbChanges and add them to our hasElementChangedCache.
1776
+ // Without this change we risk onExportElement returning early because we use hasElementChangedCache to decide if an element has changed or not.
1777
+ this.exporter.sourceDbChanges?.element.updateIds.forEach((id) => hasElementChangedCache.add(id));
1778
+ this.exporter.sourceDbChanges?.element.insertIds.forEach((id) => hasElementChangedCache.add(id));
1779
+ // This loop is to process all custom deleteIds. Unclear if the special logic is still necessary for relationships or not (TODO!!). For all other entities, we assume that the element is still present in the sourceDb because it is not
1780
+ // a real delete and instead a simulated delete to update filtering criteria between source and target. Since the element is still present, we do not need to call processDeletedOp to find the corresponding targetId.
1781
+ // We can instead rely on `forEachTrackedElement` at the top of processChangesets to find the corresponding targetId.
1782
+ // Note this also assumes we don't need to handle entity recreation for these custom deletes. I.e. a caller of API would not be able to add a custom delete for an entity that was recreated.
1783
+ // a delete followed by an insert.
1784
+ // ASSUME: If a changeset has a deleteId then custom change will never reference it. Is this still true if it was re-inserted? (TODO!!)
1785
+ if (this.exporter.sourceDbChanges?.hasCustomChanges) {
1786
+ for (const id of this.exporter.sourceDbChanges?.relationship.deleteIds.keys() ??
1787
+ []) {
1788
+ if (deleteIdsProcessed?.has(id))
1789
+ continue;
1790
+ const customData = this.exporter.sourceDbChanges?.getCustomRelationshipDataFromId(id, "relationship");
1791
+ if (customData === undefined) {
1792
+ core_bentley_1.Logger.logError(loggerCategory, "Custom data not found for relationship.", { id });
1793
+ continue;
1794
+ }
1795
+ const classFullName = customData.classFullName;
1796
+ const sourceIdOfRelationshipInSource = customData?.sourceIdOfRelationship;
1797
+ const targetIdOfRelationshipInSource = customData?.targetIdOfRelationship;
1798
+ await this.processRelationshipDeleteOp(id, classFullName, sourceIdOfRelationshipInSource, targetIdOfRelationshipInSource);
1799
+ }
1800
+ }
1801
+ }
1855
1802
  /**
1856
1803
  * Reads all the changeset files in the private member of the transformer: _csFileProps and does two things with these changesets.
1857
1804
  * Finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
@@ -1863,8 +1810,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1863
1810
  this.forEachTrackedElement((sourceElementId, targetElementId) => {
1864
1811
  this.context.remapElement(sourceElementId, targetElementId);
1865
1812
  });
1866
- if (this._csFileProps === undefined || this._csFileProps.length === 0)
1867
- return;
1813
+ this.exporter.addCustomChanges();
1814
+ if (this._csFileProps === undefined || this._csFileProps.length === 0) {
1815
+ if (this.exporter.sourceDbChanges?.isEmpty)
1816
+ return;
1817
+ // our sourcedbChanges aren't empty (probably due to someone adding custom changes), change our sourceChangeDataState to has-changes
1818
+ if (this._sourceChangeDataState === "no-changes")
1819
+ this._sourceChangeDataState = "has-changes";
1820
+ }
1868
1821
  const hasElementChangedCache = new Set();
1869
1822
  const relationshipECClassIdsToSkip = new Set();
1870
1823
  for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
@@ -1892,7 +1845,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1892
1845
  alreadyImportedModelInserts.add(targetModelId);
1893
1846
  });
1894
1847
  this._deletedSourceRelationshipData = new Map();
1895
- for (const csFile of this._csFileProps) {
1848
+ /** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
1849
+ const elemIdToScopeEsa = new Map();
1850
+ const deleteIdsProcessed = new Set();
1851
+ for (const csFile of this._csFileProps ?? []) {
1896
1852
  const csReader = core_backend_1.SqliteChangesetReader.openFile({
1897
1853
  fileName: csFile.pathname,
1898
1854
  db: this.sourceDb,
@@ -1904,8 +1860,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1904
1860
  ecChangeUnifier.appendFrom(csAdaptor);
1905
1861
  }
1906
1862
  const changes = [...ecChangeUnifier.instances];
1907
- /** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
1908
- const elemIdToScopeEsa = new Map();
1909
1863
  for (const change of changes) {
1910
1864
  if (change.ECClassId !== undefined &&
1911
1865
  relationshipECClassIdsToSkip.has(change.ECClassId))
@@ -1913,7 +1867,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1913
1867
  const changeType = change.$meta?.op;
1914
1868
  if (changeType === "Deleted" &&
1915
1869
  change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1916
- change.Scope.Id === this.targetScopeElementId) {
1870
+ change.Scope.Id === this.targetScopeElementId &&
1871
+ change.Kind === core_backend_1.ExternalSourceAspect.Kind.Element) {
1917
1872
  elemIdToScopeEsa.set(change.Element.Id, change);
1918
1873
  }
1919
1874
  else if ((changeType === "Inserted" || changeType === "Updated") &&
@@ -1932,115 +1887,151 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1932
1887
  if (changeType !== "Deleted" ||
1933
1888
  relationshipECClassIdsToSkip.has(ecClassId))
1934
1889
  continue;
1935
- await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1890
+ if (relationshipECClassIds.has(ecClassId)) {
1891
+ if (change.$meta?.classFullName === undefined) {
1892
+ core_bentley_1.Logger.logError(loggerCategory, "ClassFullName was not found for relationship when reading changes. Relationship delete will not propagate.", { relationshipId: change.ECInstanceId, ecClassId });
1893
+ continue;
1894
+ }
1895
+ if (change.SourceECInstanceId === undefined ||
1896
+ change.TargetECInstanceId === undefined) {
1897
+ core_bentley_1.Logger.logError(loggerCategory, "SourceECInstanceId or TargetECInstanceId was not found for relationship when reading changes. Relationship delete will not propagate.", {
1898
+ relationshipId: change.ECInstanceId,
1899
+ ecClassId,
1900
+ classFullName: change.$meta.classFullName,
1901
+ });
1902
+ continue;
1903
+ }
1904
+ await this.processRelationshipDeleteOp(change.ECInstanceId, change.$meta.classFullName, change.SourceECInstanceId, change.TargetECInstanceId);
1905
+ }
1906
+ else {
1907
+ await this.processElementDeleteOp(change.ECInstanceId, alreadyImportedElementInserts, alreadyImportedModelInserts, elemIdToScopeEsa, change.FederationGuid);
1908
+ }
1909
+ deleteIdsProcessed.add(change.ECInstanceId);
1936
1910
  }
1937
1911
  csReader.close();
1938
1912
  }
1913
+ await this.handleCustomChanges(hasElementChangedCache, deleteIdsProcessed);
1939
1914
  this._hasElementChangedCache = hasElementChangedCache;
1940
1915
  return;
1941
1916
  }
1917
+ /**
1918
+ * Helper function for processChangesets.
1919
+ * Populates the '_deletedSourceRelationshipData' map, whose key is the id of the relationship in the source and the value is an object used to find that relationship in the target.
1920
+ * @param changedInstanceId The id of the relationship that was deleted
1921
+ * @param classFullName classFullName of relationship
1922
+ * @param sourceIdOfRelationshipInSource the element Id acting as the source of the relationship in the sourceDb
1923
+ * @param targetIdOfRelationshipInSource the element Id acting as the target of the relationship in the sourceDb
1924
+ * @returns
1925
+ */
1926
+ async processRelationshipDeleteOp(changedInstanceId, classFullName, sourceIdOfRelationshipInSource, targetIdOfRelationshipInSource) {
1927
+ // we need a connected iModel with changes to remap elements with deletions
1928
+ const notConnectedModel = this.sourceDb.iTwinId === undefined;
1929
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index &&
1930
+ this.exporter.sourceDbChanges?.isEmpty;
1931
+ if (notConnectedModel || noChanges)
1932
+ return;
1933
+ const sourceIdOfRelationshipInTarget = await this.getTargetIdFromSourceId(sourceIdOfRelationshipInSource, true);
1934
+ const targetIdOfRelationshipInTarget = await this.getTargetIdFromSourceId(targetIdOfRelationshipInSource, true);
1935
+ if (sourceIdOfRelationshipInTarget && targetIdOfRelationshipInTarget) {
1936
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1937
+ classFullName,
1938
+ sourceIdInTarget: sourceIdOfRelationshipInTarget,
1939
+ targetIdInTarget: targetIdOfRelationshipInTarget,
1940
+ });
1941
+ }
1942
+ else if (this.sourceDb === this.provenanceSourceDb) {
1943
+ const relProvenance = this._queryProvenanceForRelationship(changedInstanceId, {
1944
+ classFullName,
1945
+ sourceId: sourceIdOfRelationshipInSource,
1946
+ targetId: targetIdOfRelationshipInSource,
1947
+ });
1948
+ if (relProvenance && relProvenance.relationshipId)
1949
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1950
+ classFullName,
1951
+ relId: relProvenance.relationshipId,
1952
+ provenanceAspectId: relProvenance.aspectId,
1953
+ });
1954
+ }
1955
+ }
1942
1956
  /**
1943
1957
  * Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
1944
1958
  * @param change the change to process, must be of changeType "Deleted"
1945
1959
  * @param mapOfDeletedElemIdToScopeEsas a map of elementIds to changedECInstances (which are ESAs). the elementId is not the id of the esa itself, but the elementid that the esa was stored on before the esa's deletion.
1946
1960
  * All ESAs in this map are part of the transformer's scope / ESA data and are tracked in case the ESA is deleted in the target.
1947
- * @param isRelationship is relationship or not
1948
1961
  * @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
1949
1962
  * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1950
1963
  * @returns void
1951
1964
  */
1952
- async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1965
+ async processElementDeleteOp(changedInstanceId, alreadyImportedElementInserts, alreadyImportedModelInserts, mapOfDeletedElemIdToScopeEsas, federationGuid) {
1953
1966
  // we need a connected iModel with changes to remap elements with deletions
1954
1967
  const notConnectedModel = this.sourceDb.iTwinId === undefined;
1955
- const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
1968
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index &&
1969
+ this.exporter.sourceDbChanges?.isEmpty;
1956
1970
  if (notConnectedModel || noChanges)
1957
1971
  return;
1972
+ let targetId = await this.getTargetIdFromSourceId(changedInstanceId, false, mapOfDeletedElemIdToScopeEsas, federationGuid);
1973
+ if (targetId === undefined && this.sourceDb === this.provenanceSourceDb) {
1974
+ targetId = this._queryProvenanceForElement(changedInstanceId);
1975
+ }
1976
+ // since we are processing one changeset at a time, we can see local source deletes
1977
+ // of entities that were never synced and can be safely ignored
1978
+ const deletionNotInTarget = !targetId;
1979
+ if (deletionNotInTarget)
1980
+ return;
1981
+ this.context.remapElement(changedInstanceId, targetId);
1982
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1983
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
1984
+ if (alreadyImportedElementInserts.has(targetId)) {
1985
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(changedInstanceId);
1986
+ }
1987
+ if (alreadyImportedModelInserts.has(targetId)) {
1988
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(changedInstanceId);
1989
+ }
1990
+ }
1991
+ /**
1992
+ * Find the corresponding id in the targetDb given a id from the sourceDb
1993
+ * @param id the id in the source that we want to find the target id for
1994
+ * @param isRelationship Changes the way we look for the federationGuid , if true we look for the federationGuid on the element itself, if false we expect it to be passed in because it was part of the ChangedECInstance.
1995
+ * Typically the source and targetIds of the relationship and not the relationshipId itself is passed to this function
1996
+ * @param mapOfDeletedElemIdToScopeEsas a map of elementIds to changedECInstances (which are ESAs). the elementId is not the id of the esa itself, but the elementid that the esa was stored on before the esa's deletion.
1997
+ * All ESAs in this map are part of the transformer's scope / ESA data and are tracked in case the ESA is deleted in the target.
1998
+ * @param federationGuid
1999
+ * @returns id of the corresponding entity in the targetDb or undefined if not found
2000
+ */
2001
+ async getTargetIdFromSourceId(id, isRelationship, mapOfDeletedElemIdToScopeEsas, federationGuid) {
1958
2002
  /**
1959
2003
  * if our ChangedECInstance is in the provenanceDb, then we can use the ids we find in the ChangedECInstance to query for ESAs.
1960
2004
  * This is because the ESAs are stored on an element Id thats present in the provenanceDb.
1961
2005
  */
1962
2006
  const changeDataInProvenanceDb = this.sourceDb === this.provenanceDb;
1963
- const getTargetIdFromSourceId = async (id) => {
1964
- let identifierValue;
1965
- let element;
1966
- if (isRelationship) {
1967
- element = this.sourceDb.elements.tryGetElement(id);
1968
- }
1969
- const fedGuid = isRelationship
1970
- ? element?.federationGuid
1971
- : change.FederationGuid;
1972
- if (changeDataInProvenanceDb) {
1973
- // TODO: clarify what happens if there are multiple (e.g. elements were merged)
1974
- for await (const row of this.sourceDb.createQueryReader("SELECT esa.Identifier FROM bis.ExternalSourceAspect esa WHERE Scope.Id=:scopeId AND Kind=:kind AND Element.Id=:relatedElementId LIMIT 1", core_common_1.QueryBinder.from([
1975
- this.targetScopeElementId,
1976
- core_backend_1.ExternalSourceAspect.Kind.Element,
1977
- id,
1978
- ]))) {
1979
- identifierValue = row.Identifier;
1980
- }
1981
- identifierValue =
1982
- identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.Identifier;
1983
- }
1984
- // Check for targetId by an esa first
1985
- if (changeDataInProvenanceDb && identifierValue) {
1986
- const targetId = identifierValue;
1987
- return targetId;
1988
- }
1989
- // Check for targetId using sourceId's fedguid if we didn't find an esa.
1990
- if (fedGuid) {
1991
- const targetId = this._queryElemIdByFedGuid(this.targetDb, fedGuid);
1992
- return targetId;
1993
- }
1994
- return undefined;
1995
- };
1996
- const changedInstanceId = change.ECInstanceId;
2007
+ let identifierValue;
2008
+ let element;
1997
2009
  if (isRelationship) {
1998
- const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1999
- const targetIdOfRelationshipInSource = change.TargetECInstanceId;
2000
- const classFullName = change.$meta?.classFullName;
2001
- const sourceIdOfRelationshipInTarget = await getTargetIdFromSourceId(sourceIdOfRelationshipInSource);
2002
- const targetIdOfRelationshipInTarget = await getTargetIdFromSourceId(targetIdOfRelationshipInSource);
2003
- if (sourceIdOfRelationshipInTarget && targetIdOfRelationshipInTarget) {
2004
- this._deletedSourceRelationshipData.set(changedInstanceId, {
2005
- classFullName: classFullName ?? "",
2006
- sourceIdInTarget: sourceIdOfRelationshipInTarget,
2007
- targetIdInTarget: targetIdOfRelationshipInTarget,
2008
- });
2009
- }
2010
- else if (this.sourceDb === this.provenanceSourceDb) {
2011
- const relProvenance = this._queryProvenanceForRelationship(changedInstanceId, {
2012
- classFullName: classFullName ?? "",
2013
- sourceId: sourceIdOfRelationshipInSource,
2014
- targetId: targetIdOfRelationshipInSource,
2015
- });
2016
- if (relProvenance && relProvenance.relationshipId)
2017
- this._deletedSourceRelationshipData.set(changedInstanceId, {
2018
- classFullName: classFullName ?? "",
2019
- relId: relProvenance.relationshipId,
2020
- provenanceAspectId: relProvenance.aspectId,
2021
- });
2022
- }
2010
+ element = this.sourceDb.elements.tryGetElement(id);
2023
2011
  }
2024
- else {
2025
- let targetId = await getTargetIdFromSourceId(changedInstanceId);
2026
- if (targetId === undefined && this.sourceDb === this.provenanceSourceDb) {
2027
- targetId = this._queryProvenanceForElement(changedInstanceId);
2028
- }
2029
- // since we are processing one changeset at a time, we can see local source deletes
2030
- // of entities that were never synced and can be safely ignored
2031
- const deletionNotInTarget = !targetId;
2032
- if (deletionNotInTarget)
2033
- return;
2034
- this.context.remapElement(changedInstanceId, targetId);
2035
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
2036
- // In such case an entity update will be triggered and we no longer need to delete the entity.
2037
- if (alreadyImportedElementInserts.has(targetId)) {
2038
- this.exporter.sourceDbChanges?.element.deleteIds.delete(changedInstanceId);
2039
- }
2040
- if (alreadyImportedModelInserts.has(targetId)) {
2041
- this.exporter.sourceDbChanges?.model.deleteIds.delete(changedInstanceId);
2012
+ const fedGuid = isRelationship ? element?.federationGuid : federationGuid;
2013
+ // Check for targetId using sourceId's fedguid
2014
+ if (fedGuid) {
2015
+ const targetId = this._queryElemIdByFedGuid(this.targetDb, fedGuid);
2016
+ if (targetId !== undefined)
2017
+ return targetId;
2018
+ }
2019
+ // Check for targetId by esa
2020
+ if (changeDataInProvenanceDb) {
2021
+ // TODO: clarify what happens if there are multiple (e.g. elements were merged)
2022
+ for await (const row of this.sourceDb.createQueryReader("SELECT esa.Identifier FROM bis.ExternalSourceAspect esa WHERE Scope.Id=:scopeId AND Kind=:kind AND Element.Id=:relatedElementId LIMIT 1", core_common_1.QueryBinder.from([
2023
+ this.targetScopeElementId,
2024
+ core_backend_1.ExternalSourceAspect.Kind.Element,
2025
+ id,
2026
+ ]))) {
2027
+ identifierValue = row.Identifier;
2042
2028
  }
2029
+ identifierValue =
2030
+ identifierValue ?? mapOfDeletedElemIdToScopeEsas?.get(id)?.Identifier;
2031
+ if (identifierValue)
2032
+ return identifierValue;
2043
2033
  }
2034
+ return undefined;
2044
2035
  }
2045
2036
  async _tryInitChangesetData(args) {
2046
2037
  if (!args ||
@@ -2055,10 +2046,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2055
2046
  this._csFileProps = [];
2056
2047
  return;
2057
2048
  }
2049
+ const startChangeset = "startChangeset" in args ? args.startChangeset : undefined;
2058
2050
  // NOTE: that we do NOT download the changesummary for the last transformed version, we want
2059
2051
  // to ignore those already processed changes
2060
- const startChangesetIndexOrId = args.startChangeset?.index ??
2061
- args.startChangeset?.id ??
2052
+ const startChangesetIndexOrId = startChangeset?.index ??
2053
+ startChangeset?.id ??
2062
2054
  this.synchronizationVersion.index + 1;
2063
2055
  const endChangesetId = this.sourceDb.changeset.id;
2064
2056
  const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
@@ -2068,11 +2060,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2068
2060
  iModelId: this.sourceDb.iModelId,
2069
2061
  // eslint-disable-next-line deprecation/deprecation
2070
2062
  changeset: { id: indexOrId },
2071
- accessToken: args.accessToken,
2072
2063
  })
2073
2064
  .then((changeset) => changeset.index)));
2074
2065
  const missingChangesets = startChangesetIndex > this.synchronizationVersion.index + 1;
2075
- if (!this._options.ignoreMissingChangesetsInSynchronizations &&
2066
+ if (!this._options.argsForProcessChanges
2067
+ ?.ignoreMissingChangesetsInSynchronizations &&
2076
2068
  startChangesetIndex !== this.synchronizationVersion.index + 1 &&
2077
2069
  this.synchronizationVersion.index !== -1) {
2078
2070
  throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
@@ -2107,13 +2099,40 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2107
2099
  this._sourceChangeDataState =
2108
2100
  this._csFileProps.length === 0 ? "no-changes" : "has-changes";
2109
2101
  }
2102
+ /**
2103
+ * The behavior of process is influenced by [[IModelTransformOptions.argsForProcessChanges]] being defined or not defined during construction passed of the IModelTransformer.
2104
+ * @section When argsForProcessChanges are defined:
2105
+ *
2106
+ * Export changes from the source iModel and import the transformed entities into the target iModel.
2107
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2108
+ *
2109
+ * Notes:
2110
+ * - the transformer assumes that you saveChanges after processing changes. You should not modify the iModel after processChanges until saveChanges,
2111
+ * failure to do so may result in corrupted
2112
+ * data loss in future branch operations
2113
+ * - if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2114
+ * will automatically be determined and used
2115
+ * - 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.
2116
+ *
2117
+ * @section When argsForProcessChanges are undefined:
2118
+ *
2119
+ * Export everything from the source iModel and import the transformed entities into the target iModel.
2120
+ *
2121
+ * Notes:
2122
+ * - [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2123
+ *
2124
+ */
2125
+ async process() {
2126
+ await this.initialize();
2127
+ this.logSettings();
2128
+ return this._options.argsForProcessChanges !== undefined
2129
+ ? this.processChanges(this._options.argsForProcessChanges)
2130
+ : this.processAll();
2131
+ }
2110
2132
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
2111
2133
  * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2112
2134
  */
2113
- async processAll(options) {
2114
- this.logSettings();
2115
- this.initScopeProvenance();
2116
- await this.initialize();
2135
+ async processAll() {
2117
2136
  await this.exporter.exportCodeSpecs();
2118
2137
  await this.exporter.exportFonts();
2119
2138
  if (this._options.skipPropagateChangesToRootElements) {
@@ -2125,9 +2144,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2125
2144
  else {
2126
2145
  await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
2127
2146
  }
2147
+ this.completePartiallyCommittedElements();
2128
2148
  await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
2149
+ this.completePartiallyCommittedAspects();
2129
2150
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
2130
- await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2131
2151
  if (this._options.forceExternalSourceAspectProvenance &&
2132
2152
  this.shouldDetectDeletes()) {
2133
2153
  // eslint-disable-next-line deprecation/deprecation
@@ -2138,7 +2158,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2138
2158
  if (this._options.optimizeGeometry)
2139
2159
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2140
2160
  this.importer.computeProjectExtents();
2141
- await this.finalizeTransformation(options);
2161
+ this.finalizeTransformation();
2142
2162
  }
2143
2163
  markLastProvenance(sourceAspect, { isRelationship = false }) {
2144
2164
  this._lastProvenanceEntityInfo =
@@ -2155,39 +2175,42 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2155
2175
  }
2156
2176
  /** Export changes from the source iModel and import the transformed entities into the target iModel.
2157
2177
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2158
- * @note the transformer saves and pushes changes when its work is complete.
2178
+ * @note the transformer assumes that you saveChanges after processing changes. You should not
2179
+ * modify the iModel after processChanges until saveChanges, failure to do so may result in corrupted
2180
+ * data loss in future branch operations
2159
2181
  * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2160
2182
  * will automatically be determined and used
2161
2183
  * @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.
2162
2184
  */
2163
2185
  async processChanges(options) {
2164
- this._isSynchronization = true;
2165
- this.initScopeProvenance();
2166
- this.logSettings();
2167
- await this.initialize(options);
2168
2186
  // must wait for initialization of synchronization provenance data
2169
2187
  await this.exporter.exportChanges(this.getExportInitOpts(options));
2170
- await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2188
+ this.completePartiallyCommittedElements();
2189
+ this.completePartiallyCommittedAspects();
2171
2190
  if (this._options.optimizeGeometry)
2172
2191
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2173
2192
  this.importer.computeProjectExtents();
2174
- await this.finalizeTransformation(options);
2193
+ this.finalizeTransformation();
2194
+ const defaultSaveTargetChanges = () => {
2195
+ this.targetDb.saveChanges();
2196
+ };
2197
+ await (options.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2175
2198
  }
2176
2199
  /** Changeset data must be initialized in order to build correct changeOptions.
2177
2200
  * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
2178
2201
  */
2179
2202
  getExportInitOpts(opts) {
2180
- if (!this._isSynchronization)
2203
+ if (!this._options.argsForProcessChanges)
2181
2204
  return {};
2205
+ const startChangeset = "startChangeset" in opts ? opts.startChangeset : undefined;
2182
2206
  return {
2183
- skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
2184
- accessToken: opts.accessToken,
2207
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
2185
2208
  ...(this._csFileProps
2186
2209
  ? { csFileProps: this._csFileProps }
2187
2210
  : this._changesetRanges
2188
2211
  ? { changesetRanges: this._changesetRanges }
2189
- : opts.startChangeset
2190
- ? { startChangeset: opts.startChangeset }
2212
+ : startChangeset
2213
+ ? { startChangeset }
2191
2214
  : {
2192
2215
  startChangeset: {
2193
2216
  index: this.synchronizationVersion.index + 1,
@@ -2269,8 +2292,7 @@ class TemplateModelCloner extends IModelTransformer {
2269
2292
  }
2270
2293
  /** Cloning from a template requires this override of onTransformElement. */
2271
2294
  onTransformElement(sourceElement) {
2272
- // eslint-disable-next-line deprecation/deprecation
2273
- const referenceIds = sourceElement.getReferenceConcreteIds();
2295
+ const referenceIds = sourceElement.getReferenceIds();
2274
2296
  referenceIds.forEach((referenceId) => {
2275
2297
  // TODO: consider going through all definition elements at once and remapping them to themselves
2276
2298
  if (!core_backend_1.EntityReferences.isValid(this.context.findTargetEntityId(referenceId))) {