@itwin/imodel-transformer 1.0.1-dev.0 → 1.1.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +17 -0
  3. package/lib/cjs/Algo.d.ts +7 -0
  4. package/lib/cjs/Algo.d.ts.map +1 -1
  5. package/lib/cjs/Algo.js +7 -0
  6. package/lib/cjs/Algo.js.map +1 -1
  7. package/lib/cjs/BigMap.d.ts +6 -1
  8. package/lib/cjs/BigMap.d.ts.map +1 -1
  9. package/lib/cjs/BigMap.js +28 -2
  10. package/lib/cjs/BigMap.js.map +1 -1
  11. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  12. package/lib/cjs/BranchProvenanceInitializer.js +2 -0
  13. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  14. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +1 -1
  15. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  16. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  17. package/lib/cjs/ElementCascadingDeleter.js.map +1 -1
  18. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  19. package/lib/cjs/EntityUnifier.js.map +1 -1
  20. package/lib/cjs/ExportElementAspectsStrategy.js.map +1 -1
  21. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +1 -1
  22. package/lib/cjs/IModelCloneContext.d.ts +1 -4
  23. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  24. package/lib/cjs/IModelCloneContext.js +16 -31
  25. package/lib/cjs/IModelCloneContext.js.map +1 -1
  26. package/lib/cjs/IModelExporter.d.ts +67 -62
  27. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  28. package/lib/cjs/IModelExporter.js +173 -83
  29. package/lib/cjs/IModelExporter.js.map +1 -1
  30. package/lib/cjs/IModelImporter.d.ts +31 -47
  31. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  32. package/lib/cjs/IModelImporter.js +62 -72
  33. package/lib/cjs/IModelImporter.js.map +1 -1
  34. package/lib/cjs/IModelTransformer.d.ts +146 -210
  35. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  36. package/lib/cjs/IModelTransformer.js +515 -657
  37. package/lib/cjs/IModelTransformer.js.map +1 -1
  38. package/lib/cjs/{transformer.d.ts → imodel-transformer.d.ts} +1 -1
  39. package/lib/cjs/imodel-transformer.d.ts.map +1 -0
  40. package/lib/cjs/{transformer.js → imodel-transformer.js} +13 -3
  41. package/lib/cjs/imodel-transformer.js.map +1 -0
  42. package/package.json +22 -20
  43. package/lib/cjs/PendingReferenceMap.d.ts +0 -37
  44. package/lib/cjs/PendingReferenceMap.d.ts.map +0 -1
  45. package/lib/cjs/PendingReferenceMap.js +0 -92
  46. package/lib/cjs/PendingReferenceMap.js.map +0 -1
  47. package/lib/cjs/transformer.d.ts.map +0 -1
  48. package/lib/cjs/transformer.js.map +0 -1
@@ -13,13 +13,12 @@ const Semver = require("semver");
13
13
  const nodeAssert = require("assert");
14
14
  const core_bentley_1 = require("@itwin/core-bentley");
15
15
  const core_geometry_1 = require("@itwin/core-geometry");
16
+ const coreBackendPkgJson = require("@itwin/core-backend/package.json");
16
17
  const core_backend_1 = require("@itwin/core-backend");
17
18
  const core_common_1 = require("@itwin/core-common");
18
19
  const IModelExporter_1 = require("./IModelExporter");
19
20
  const IModelImporter_1 = require("./IModelImporter");
20
21
  const TransformerLoggerCategory_1 = require("./TransformerLoggerCategory");
21
- const PendingReferenceMap_1 = require("./PendingReferenceMap");
22
- const EntityMap_1 = require("./EntityMap");
23
22
  const IModelCloneContext_1 = require("./IModelCloneContext");
24
23
  const EntityUnifier_1 = require("./EntityUnifier");
25
24
  const Algo_1 = require("./Algo");
@@ -30,30 +29,6 @@ const nullLastProvenanceEntityInfo = {
30
29
  aspectVersion: "",
31
30
  aspectKind: core_backend_1.ExternalSourceAspect.Kind.Element,
32
31
  };
33
- /**
34
- * A container for tracking the state of a partially committed entity and finalizing it when it's ready to be fully committed
35
- * @internal
36
- */
37
- class PartiallyCommittedEntity {
38
- constructor(
39
- /**
40
- * A set of "model|element ++ ID64" pairs, (e.g. `model0x11` or `element0x12`)
41
- * It is possible for the submodel of an element to be separately resolved from the actual element,
42
- * so its resolution must be tracked separately
43
- */
44
- _missingReferences, _onComplete) {
45
- this._missingReferences = _missingReferences;
46
- this._onComplete = _onComplete;
47
- }
48
- resolveReference(id) {
49
- this._missingReferences.delete(id);
50
- if (this._missingReferences.size === 0)
51
- this._onComplete();
52
- }
53
- forceComplete() {
54
- this._onComplete();
55
- }
56
- }
57
32
  /**
58
33
  * Apply a function to each Id64 in a supported container type of Id64s.
59
34
  * Currently only supports raw Id64String or RelatedElement-like objects containing an `id` property that is a Id64String,
@@ -80,7 +55,7 @@ function mapId64(idContainer, func) {
80
55
  }
81
56
  else {
82
57
  throw Error([
83
- `Id64 container '${idContainer}' is unsupported.`,
58
+ `Id64 container '${JSON.stringify(idContainer)}' is unsupported.`,
84
59
  "Currently only singular Id64 strings or prop-like objects containing an 'id' property are supported.",
85
60
  ].join("\n"));
86
61
  }
@@ -149,7 +124,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
149
124
  id: targetScopeElementId,
150
125
  relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
151
126
  },
152
- scope: { id: core_common_1.IModel.rootSubjectId },
127
+ scope: { id: core_common_1.IModel.rootSubjectId }, // the root Subject scopes scope elements
153
128
  identifier: sourceDb.iModelId,
154
129
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
155
130
  jsonProperties: undefined,
@@ -171,7 +146,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
171
146
  if (this._isProvenanceInitTransform) {
172
147
  return "forward";
173
148
  }
174
- if (!this._isSynchronization) {
149
+ if (!this._options.argsForProcessChanges) {
175
150
  return "not-sync";
176
151
  }
177
152
  try {
@@ -216,14 +191,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
216
191
  */
217
192
  constructor(source, target, options) {
218
193
  super();
219
- /** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
220
- * and have some helper methods below for now */
221
- this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
222
194
  /** a set of elements for which source provenance will be explicitly tracked by ExternalSourceAspects */
223
195
  this._elementsWithExplicitlyTrackedProvenance = new Set();
224
- /** map of partially committed entities to their partial commit progress */
225
- this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
226
- this._isSynchronization = false;
196
+ this._partiallyCommittedElementIds = new Set();
197
+ this._partiallyCommittedAspectIds = new Set();
227
198
  /**
228
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.
229
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.
@@ -232,10 +203,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
232
203
  */
233
204
  this._allowNoScopingESA = false;
234
205
  this._changesetRanges = undefined;
235
- /** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
236
- * @note Currently only tracks elements which were not exported.
237
- */
238
- this._skippedEntities = new Set();
239
206
  /**
240
207
  * Previously the transformer would insert provenance always pointing to the "target" relationship.
241
208
  * It should (and now by default does) instead insert provenance pointing to the provenanceSource
@@ -253,7 +220,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
253
220
  this._cachedSynchronizationVersion = undefined;
254
221
  this._targetClassNameToClassIdCache = new Map();
255
222
  // if undefined, it can be initialized by calling [[this.processChangesets]]
256
- this._hasElementChangedCache = undefined;
257
223
  this._deletedSourceRelationshipData = undefined;
258
224
  this._yieldManager = new core_bentley_1.YieldManager();
259
225
  /** The directory where schemas will be exported, a random temporary directory */
@@ -273,11 +239,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
273
239
  cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
274
240
  targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
275
241
  // eslint-disable-next-line deprecation/deprecation
276
- danglingReferencesBehavior: options?.danglingReferencesBehavior ??
277
- options?.danglingPredecessorsBehavior ??
278
- "reject",
242
+ danglingReferencesBehavior: options?.danglingReferencesBehavior ?? "reject",
279
243
  branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
244
+ skipPropagateChangesToRootElements: options?.skipPropagateChangesToRootElements ?? true,
280
245
  };
246
+ // check if authorization client is defined
247
+ if (core_backend_1.IModelHost.authorizationClient === undefined) {
248
+ core_bentley_1.Logger.logWarning(loggerCategory, "Authorization client is not set in IModelHost. If the transformer needs an accessToken, then it will fail.");
249
+ }
281
250
  this._isProvenanceInitTransform = this._options
282
251
  .wasSourceIModelCopiedToTarget
283
252
  ? true
@@ -361,8 +330,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
361
330
  core_bentley_1.Logger.logInfo(loggerCategory, `this._includeSourceProvenance=${this._options.includeSourceProvenance}`);
362
331
  core_bentley_1.Logger.logInfo(loggerCategory, `this._cloneUsingBinaryGeometry=${this._options.cloneUsingBinaryGeometry}`);
363
332
  core_bentley_1.Logger.logInfo(loggerCategory, `this._wasSourceIModelCopiedToTarget=${this._options.wasSourceIModelCopiedToTarget}`);
364
- core_bentley_1.Logger.logInfo(loggerCategory, `this._isReverseSynchronization=${this._options.isReverseSynchronization}`);
365
- core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.autoExtendProjectExtents=${this.importer.options.autoExtendProjectExtents}`);
333
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.autoExtendProjectExtents=${JSON.stringify(this.importer.options.autoExtendProjectExtents)}`);
366
334
  core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.simplifyElementGeometry=${this.importer.options.simplifyElementGeometry}`);
367
335
  }
368
336
  /** Return the IModelDb where IModelTransformer will store its provenance.
@@ -455,29 +423,30 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
455
423
  forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
456
424
  });
457
425
  }
458
- /** the changeset in the scoping element's source version found for this transformation
459
- * @note: the version depends on whether this is a reverse synchronization or not, as
460
- * it is stored separately for both synchronization directions.
461
- * @note: must call [[initScopeProvenance]] before using this property.
462
- * @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).
426
+ /**
427
+ * As of itwinjs 4.6.0, definitionContainers are now deleted as if they were DefinitionPartitions as opposed to Definitions.
428
+ * This variable being true will be used to special case the deletion of DefinitionContainers the same way DefinitionPartitions are deleted.
463
429
  */
464
- get _synchronizationVersion() {
465
- if (!this._cachedSynchronizationVersion) {
466
- nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
467
- const version = this.isReverseSynchronization
468
- ? this._targetScopeProvenanceProps.jsonProperties?.reverseSyncVersion
469
- : this._targetScopeProvenanceProps.version;
470
- nodeAssert(version !== undefined, "no version contained in target scope");
471
- const [id, index] = version === "" ? ["", -1] : version.split(";");
472
- this._cachedSynchronizationVersion = { index: Number(index), id };
473
- nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
430
+ get hasDefinitionContainerDeletionFeature() {
431
+ if (this._hasDefinitionContainerDeletionFeature === undefined) {
432
+ this._hasDefinitionContainerDeletionFeature = Semver.satisfies(coreBackendPkgJson.version, "^4.6.0");
474
433
  }
475
- return this._cachedSynchronizationVersion;
434
+ return this._hasDefinitionContainerDeletionFeature;
435
+ }
436
+ /**
437
+ * We cache the synchronization version to avoid querying the target scoping ESA multiple times.
438
+ * If the target scoping ESA is ever updated we need to clear any potentially cached sync version otherwise we will get stale values.
439
+ * Sets this._cachedSynchronizationVersion to undefined.
440
+ */
441
+ clearCachedSynchronizationVersion() {
442
+ this._cachedSynchronizationVersion = undefined;
476
443
  }
477
444
  /** the changeset in the scoping element's source version found for this transformation
478
- * @note: the version depends on whether this is a reverse synchronization or not, as
445
+ * @note the version depends on whether this is a reverse synchronization or not, as
479
446
  * it is stored separately for both synchronization directions.
480
- * @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).
447
+ * @note empty string and -1 for changeset and index if it has never been transformed
448
+ * @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".
449
+ * @throws if the version is not found in a preexisting scope aspect and @see [[IModelTransformOptions.branchRelationshipDataBehavior]] !== "unsafe-migrate"
481
450
  */
482
451
  get synchronizationVersion() {
483
452
  if (this._cachedSynchronizationVersion === undefined) {
@@ -488,10 +457,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
488
457
  const version = this.isReverseSynchronization
489
458
  ? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
490
459
  : provenanceScopeAspect.version;
491
- if (!version) {
460
+ if (!version &&
461
+ this._options.branchRelationshipDataBehavior === "unsafe-migrate") {
492
462
  return { index: -1, id: "" }; // previous synchronization was done before fed guid update.
493
463
  }
494
- const [id, index] = version.split(";");
464
+ if (version === undefined) {
465
+ 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.
466
+ Consider running the transformer with branchRelationshipDataBehavior set to 'unsafe-migrate'`);
467
+ }
468
+ const [id, index] = version === "" ? ["", -1] : version.split(";");
495
469
  if (Number.isNaN(Number(index)))
496
470
  throw new Error("Could not parse version data from scope aspect");
497
471
  this._cachedSynchronizationVersion = { index: Number(index), id }; // synchronization version found and cached.
@@ -530,7 +504,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
530
504
  id: this.targetScopeElementId,
531
505
  relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
532
506
  },
533
- scope: { id: core_common_1.IModel.rootSubjectId },
507
+ scope: { id: core_common_1.IModel.rootSubjectId }, // the root Subject scopes scope elements
534
508
  identifier: this.provenanceSourceDb.iModelId,
535
509
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
536
510
  jsonProperties: undefined,
@@ -567,29 +541,82 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
567
541
  jsonProperties: JSON.stringify(aspectProps.jsonProperties),
568
542
  });
569
543
  aspectProps.id = id;
544
+ // Busting a potential cached version
545
+ this.clearCachedSynchronizationVersion();
570
546
  }
571
547
  }
572
548
  else {
573
549
  // foundEsaProps is defined.
574
550
  aspectProps.id = foundEsaProps.aspectId;
575
- aspectProps.version =
576
- foundEsaProps.version ??
577
- (this._options.branchRelationshipDataBehavior === "unsafe-migrate"
578
- ? ""
579
- : undefined);
551
+ aspectProps.version = foundEsaProps.version;
580
552
  aspectProps.jsonProperties = foundEsaProps.jsonProperties
581
553
  ? JSON.parse(foundEsaProps.jsonProperties)
582
- : this._options.branchRelationshipDataBehavior === "unsafe-migrate"
583
- ? {
584
- pendingReverseSyncChangesetIndices: [],
585
- pendingSyncChangesetIndices: [],
586
- reverseSyncVersion: "",
587
- }
588
- : undefined;
554
+ : undefined;
555
+ // Clone oldProps incase they're changed for logging purposes
556
+ const oldProps = JSON.parse(JSON.stringify(aspectProps));
557
+ if (this.handleUnsafeMigrate(aspectProps)) {
558
+ 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 });
559
+ this.provenanceDb.elements.updateAspect({
560
+ ...aspectProps,
561
+ jsonProperties: JSON.stringify(aspectProps.jsonProperties),
562
+ });
563
+ // Busting a potential cached version
564
+ this.clearCachedSynchronizationVersion();
565
+ }
589
566
  }
590
567
  this._targetScopeProvenanceProps =
591
568
  aspectProps;
592
569
  }
570
+ /** Returns true if a change was made to the aspectProps. */
571
+ handleUnsafeMigrate(aspectProps) {
572
+ let madeChange = false;
573
+ if (this._options.branchRelationshipDataBehavior !== "unsafe-migrate")
574
+ return madeChange;
575
+ const fallbackSyncVersionToUse = this._options.argsForProcessChanges?.unsafeFallbackSyncVersion ?? "";
576
+ const fallbackReverseSyncVersionToUse = this._options.argsForProcessChanges?.unsafeFallbackReverseSyncVersion ??
577
+ "";
578
+ if (aspectProps.version === undefined ||
579
+ (aspectProps.version === "" &&
580
+ aspectProps.version !== fallbackSyncVersionToUse)) {
581
+ aspectProps.version = fallbackSyncVersionToUse;
582
+ madeChange = true;
583
+ }
584
+ if (aspectProps.jsonProperties === undefined) {
585
+ aspectProps.jsonProperties = {
586
+ pendingReverseSyncChangesetIndices: [],
587
+ pendingSyncChangesetIndices: [],
588
+ reverseSyncVersion: fallbackReverseSyncVersionToUse,
589
+ };
590
+ madeChange = true;
591
+ }
592
+ else if (aspectProps.jsonProperties.reverseSyncVersion === undefined ||
593
+ (aspectProps.jsonProperties.reverseSyncVersion === "" &&
594
+ aspectProps.jsonProperties.reverseSyncVersion !==
595
+ fallbackReverseSyncVersionToUse)) {
596
+ aspectProps.jsonProperties.reverseSyncVersion =
597
+ fallbackReverseSyncVersionToUse;
598
+ madeChange = true;
599
+ }
600
+ /**
601
+ * This case will only be hit when:
602
+ * - first transformation was performed on pre-fedguid transformer.
603
+ * - a second processAll transformation was performed on the same target-source iModels post-fedguid transformer.
604
+ * - change processing was invoked on for the second 'initial' transformation.
605
+ * NOTE: This case likely does not exist anymore, but we will keep it just to be sure.
606
+ */
607
+ if (aspectProps.jsonProperties.pendingReverseSyncChangesetIndices ===
608
+ undefined) {
609
+ core_bentley_1.Logger.logWarning(loggerCategory, "Property pendingReverseSyncChangesetIndices missing on the jsonProperties of the scoping ESA. Setting to [].");
610
+ aspectProps.jsonProperties.pendingReverseSyncChangesetIndices = [];
611
+ madeChange = true;
612
+ }
613
+ if (aspectProps.jsonProperties.pendingSyncChangesetIndices === undefined) {
614
+ core_bentley_1.Logger.logWarning(loggerCategory, "Property pendingSyncChangesetIndices missing on the jsonProperties of the scoping ESA. Setting to [].");
615
+ aspectProps.jsonProperties.pendingSyncChangesetIndices = [];
616
+ madeChange = true;
617
+ }
618
+ return madeChange;
619
+ }
593
620
  /**
594
621
  * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
595
622
  * and call a function for each one.
@@ -685,9 +712,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
685
712
  targetScopeElementId: this.targetScopeElementId,
686
713
  isReverseSynchronization: this.isReverseSynchronization,
687
714
  fn,
688
- skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
715
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? true,
689
716
  });
690
717
  }
718
+ /**
719
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
720
+ * The identifier on the ESA is the id of the element in the [[IModelTransformer.provenanceSourceDb]]
721
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
722
+ * @param entityInProvenanceSourceId
723
+ * @returns the elementId that the ESA is stored on, esa.Element.Id
724
+ */
691
725
  _queryProvenanceForElement(entityInProvenanceSourceId) {
692
726
  return this.provenanceDb.withPreparedStatement(`
693
727
  SELECT esa.Element.Id
@@ -705,6 +739,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
705
739
  return undefined;
706
740
  });
707
741
  }
742
+ /**
743
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
744
+ * The identifier on the ESA is the id of the relationship in the [[IModelTransformer.provenanceSourceDb]]
745
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
746
+ * @param entityInProvenanceSourceId
747
+ * @returns
748
+ */
708
749
  _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
709
750
  return this.provenanceDb.withPreparedStatement(`
710
751
  SELECT
@@ -793,7 +834,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
793
834
  }
794
835
  /** Returns `true` if *brute force* delete detections should be run.
795
836
  * @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
796
- * @note Not relevant for processChanges when change history is known.
837
+ * @note Not relevant for [[process]] when [[IModelTransformOptions.argsForProcessChanges]] are provided and change history is known.
797
838
  */
798
839
  shouldDetectDeletes() {
799
840
  nodeAssert(this._syncType !== undefined);
@@ -803,9 +844,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
803
844
  * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
804
845
  * in the source iModel.
805
846
  * @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
806
- * This method is only called during [[processAll]] when the option
847
+ * This method is only called during [[process]] when [[IModelTransformOptions.argsForProcessChanges]] is undefined and the option
807
848
  * [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
808
- * necessary when using [[processChanges]] since changeset information is sufficient.
849
+ * necessary when calling [[process]] with [[IModelTransformOptions.argsForProcessChanges]] defined, since changeset information is sufficient.
809
850
  * @note you do not need to call this directly unless processing a subset of an iModel.
810
851
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
811
852
  */
@@ -835,12 +876,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
835
876
  }
836
877
  });
837
878
  }
838
- /**
839
- * @deprecated in 3.x, this no longer has any effect except emitting a warning
840
- */
841
- skipElement(_sourceElement) {
842
- core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
843
- }
844
879
  /** Transform the specified sourceElement into ElementProps for the target iModel.
845
880
  * @param sourceElement The Element from the source iModel to transform.
846
881
  * @returns ElementProps for the target iModel.
@@ -850,6 +885,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
850
885
  onTransformElement(sourceElement) {
851
886
  core_bentley_1.Logger.logTrace(loggerCategory, `onTransformElement(${sourceElement.id}) "${sourceElement.getDisplayLabel()}"`);
852
887
  const targetElementProps = this.context.cloneElement(sourceElement, { binaryGeometry: this._options.cloneUsingBinaryGeometry });
888
+ // Special case: source element is the root subject
889
+ if (sourceElement.id === core_common_1.IModel.rootSubjectId) {
890
+ const targetElementId = this.context.findTargetElementId(sourceElement.id);
891
+ // When remapping rootSubject from source to non root subject in target, the code.scope gets remapped incorrectly.
892
+ // This is because the rootSubject has no parent and its code.scope is unique in that it is the id of itself.
893
+ // For all other subjects which do have parents the code.scope and its parent should be in agreement.
894
+ if (targetElementId !== core_bentley_1.Id64.invalid &&
895
+ targetElementId !== core_common_1.IModel.rootSubjectId) {
896
+ const targetElement = this.targetDb.elements.getElement(targetElementId);
897
+ targetElementProps.parent =
898
+ targetElement.parent ?? targetElementProps.parent;
899
+ targetElementProps.code.scope = targetElement.code.scope;
900
+ }
901
+ }
853
902
  if (sourceElement instanceof core_backend_1.Subject) {
854
903
  if (targetElementProps.jsonProperties?.Subject?.Job) {
855
904
  // don't propagate source channels into target (legacy bridge case)
@@ -860,97 +909,82 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
860
909
  }
861
910
  /** Returns true if a change within sourceElement is detected.
862
911
  * @param sourceElement The Element from the source iModel
863
- * @param targetElementId The Element from the target iModel to compare against.
864
912
  * @note A subclass can override this method to provide custom change detection behavior.
865
913
  */
866
- hasElementChanged(sourceElement, _targetElementId) {
867
- if (this._sourceChangeDataState === "no-changes")
868
- return false;
869
- if (this._sourceChangeDataState === "unconnected")
870
- return true;
871
- nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
872
- nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
873
- return this._hasElementChangedCache.has(sourceElement.id);
874
- }
875
- static transformCallbackFor(transformer, entity) {
876
- if (entity instanceof core_backend_1.Element)
877
- return transformer.onTransformElement; // eslint-disable-line @typescript-eslint/unbound-method
878
- else if (entity instanceof core_backend_1.Element)
879
- return transformer.onTransformModel; // eslint-disable-line @typescript-eslint/unbound-method
880
- else if (entity instanceof core_backend_1.Relationship)
881
- return transformer.onTransformRelationship; // eslint-disable-line @typescript-eslint/unbound-method
882
- else if (entity instanceof core_backend_1.ElementAspect)
883
- return transformer.onTransformElementAspect; // eslint-disable-line @typescript-eslint/unbound-method
884
- else
885
- (0, core_bentley_1.assert)(false, `unreachable; entity was '${entity.constructor.name}' not an Element, Relationship, or ElementAspect`);
886
- }
887
- /** callback to perform when a partial element says it's ready to be completed
888
- * transforms the source element with all references now valid, then updates the partial element with the results
889
- */
890
- makePartialEntityCompleter(sourceEntity) {
891
- return () => {
892
- const targetId = this.context.findTargetEntityId(core_backend_1.EntityReferences.from(sourceEntity));
893
- if (!core_backend_1.EntityReferences.isValid(targetId))
894
- throw Error(`${sourceEntity.id} has not been inserted into the target yet, the completer is invalid. This is a bug.`);
895
- const onEntityTransform = IModelTransformer.transformCallbackFor(this, sourceEntity);
896
- const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
897
- const targetProps = onEntityTransform.call(this, sourceEntity);
898
- if (sourceEntity instanceof core_backend_1.Relationship) {
899
- targetProps.sourceId =
900
- this.context.findTargetElementId(sourceEntity.sourceId);
901
- targetProps.targetId =
902
- this.context.findTargetElementId(sourceEntity.targetId);
914
+ hasElementChanged(sourceElement) {
915
+ const sourceDbChanges = this.exporter.sourceDbChanges;
916
+ return (!sourceDbChanges || // are we processing changes? if not then element is considered as changed
917
+ sourceDbChanges.element.insertIds.has(sourceElement.id) ||
918
+ sourceDbChanges.element.updateIds.has(sourceElement.id));
919
+ }
920
+ completePartiallyCommittedElements() {
921
+ for (const sourceElementId of this._partiallyCommittedElementIds) {
922
+ const sourceElement = this.sourceDb.elements.getElement({
923
+ id: sourceElementId,
924
+ wantGeometry: this.exporter.wantGeometry,
925
+ wantBRepData: this.exporter.wantGeometry,
926
+ });
927
+ const targetId = this.context.findTargetElementId(sourceElementId);
928
+ if (core_bentley_1.Id64.isInvalid(targetId)) {
929
+ throw new Error(`source-target element mapping not found for element "${sourceElementId}" when completing partially committed elements. This is a bug.`);
903
930
  }
904
- updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
905
- this._partiallyCommittedEntities.delete(sourceEntity);
906
- };
931
+ const targetProps = this.onTransformElement(sourceElement);
932
+ this.targetDb.elements.updateElement({ ...targetProps, id: targetId });
933
+ }
907
934
  }
908
- /** collect references this entity has that are yet to be mapped, and if there are any
909
- * create a [[PartiallyCommittedEntity]] to track resolution of those references
910
- */
911
- collectUnmappedReferences(entity) {
912
- const missingReferences = new core_common_1.EntityReferenceSet();
913
- let thisPartialElem;
914
- // eslint-disable-next-line deprecation/deprecation
915
- for (const referenceId of entity.getReferenceConcreteIds()) {
916
- // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
917
- const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
918
- const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
919
- this._skippedEntities.has(referenceId);
920
- if (alreadyProcessed)
921
- continue;
922
- core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
923
- const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
924
- entityReference: referenceId,
935
+ completePartiallyCommittedAspects() {
936
+ for (const sourceAspectId of this._partiallyCommittedAspectIds) {
937
+ const sourceAspect = this.sourceDb.elements.getAspect(sourceAspectId);
938
+ const targetAspectId = this.context.findTargetAspectId(sourceAspectId);
939
+ if (core_bentley_1.Id64.isInvalid(targetAspectId)) {
940
+ throw new Error(`source-target aspect mapping not found for aspect "${sourceAspectId}" when completing partially committed aspects. This is a bug.`);
941
+ }
942
+ const targetAspectProps = this.onTransformElementAspect(sourceAspect);
943
+ this.targetDb.elements.updateAspect({
944
+ ...targetAspectProps,
945
+ id: targetAspectId,
925
946
  });
926
- if (!referencedExistsInSource) {
927
- core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
928
- switch (this._options.danglingReferencesBehavior) {
929
- case "ignore":
930
- continue;
931
- case "reject":
932
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
933
- `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
934
- "This must have been caused by an upstream application that changed the iModel.",
935
- "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
936
- "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
937
- "transformer to remove the reference and fix affected elements may be suitable.",
938
- ].join("\n"));
947
+ }
948
+ }
949
+ doAllReferencesExistInTarget(entity) {
950
+ let allReferencesExist = true;
951
+ for (const referenceId of entity.getReferenceIds()) {
952
+ const referencedEntityId = core_backend_1.EntityReferences.toId64(referenceId);
953
+ if (referencedEntityId === core_common_1.IModel.repositoryModelId ||
954
+ referencedEntityId === core_common_1.IModel.dictionaryId ||
955
+ referencedEntityId === "0xe") {
956
+ continue;
957
+ }
958
+ if (allReferencesExist &&
959
+ !core_backend_1.EntityReferences.isValid(this.context.findTargetEntityId(referenceId))) {
960
+ // if we care about references existing then we cannot return early and must check all other references.
961
+ if (this._options.danglingReferencesBehavior === "ignore") {
962
+ return false;
939
963
  }
964
+ allReferencesExist = false;
940
965
  }
941
- if (thisPartialElem === undefined) {
942
- thisPartialElem = new PartiallyCommittedEntity(missingReferences, this.makePartialEntityCompleter(entity));
943
- if (!this._partiallyCommittedEntities.has(entity))
944
- this._partiallyCommittedEntities.set(entity, thisPartialElem);
966
+ if (this._options.danglingReferencesBehavior === "reject") {
967
+ this.assertReferenceExistsInSource(referenceId, entity);
945
968
  }
946
- missingReferences.add(referenceId);
947
- const entityReference = core_backend_1.EntityReferences.from(entity);
948
- this._pendingReferences.set({ referenced: referenceId, referencer: entityReference }, thisPartialElem);
969
+ }
970
+ return allReferencesExist;
971
+ }
972
+ assertReferenceExistsInSource(referenceId, entity) {
973
+ const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
974
+ entityReference: referenceId,
975
+ });
976
+ if (!referencedExistsInSource) {
977
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
978
+ `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
979
+ "This must have been caused by an upstream application that changed the iModel.",
980
+ "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this,",
981
+ `and the referenceId found on "${entity.id}" will not be carried over to corresponding target element.`,
982
+ ].join("\n"));
949
983
  }
950
984
  }
951
985
  /** Cause the specified Element and its child Elements (if applicable) to be exported from the source iModel and imported into the target iModel.
952
986
  * @param sourceElementId Identifies the Element from the source iModel to import.
953
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
987
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
954
988
  */
955
989
  async processElement(sourceElementId) {
956
990
  await this.initialize();
@@ -961,7 +995,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
961
995
  }
962
996
  /** Import child elements into the target IModelDb
963
997
  * @param sourceElementId Import the child elements of this element in the source IModelDb.
964
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
998
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
965
999
  */
966
1000
  async processChildElements(sourceElementId) {
967
1001
  await this.initialize();
@@ -973,24 +1007,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
973
1007
  shouldExportElement(_sourceElement) {
974
1008
  return true;
975
1009
  }
976
- onSkipElement(sourceElementId) {
977
- if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
978
- // element already has provenance
979
- return;
980
- }
981
- core_bentley_1.Logger.logInfo(loggerCategory, `Element '${sourceElementId}' won't be exported. Marking its references as resolved`);
982
- const elementKey = `e${sourceElementId}`;
983
- this._skippedEntities.add(elementKey);
984
- // Mark any existing pending references to the skipped element as resolved.
985
- for (const referencer of this._pendingReferences.getReferencersByEntityKey(elementKey)) {
986
- const key = PendingReferenceMap_1.PendingReference.from(referencer, elementKey);
987
- const pendingRef = this._pendingReferences.get(key);
988
- if (!pendingRef)
989
- continue;
990
- pendingRef.resolveReference(elementKey);
991
- this._pendingReferences.delete(key);
992
- }
993
- }
994
1010
  /**
995
1011
  * If they haven't been already, import all of the required references
996
1012
  * @internal do not call, override or implement this, it will be removed
@@ -1057,11 +1073,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1057
1073
  onExportElement(sourceElement) {
1058
1074
  let targetElementId;
1059
1075
  let targetElementProps;
1060
- if (this._options.preserveElementIdsForFiltering) {
1061
- targetElementId = sourceElement.id;
1062
- targetElementProps = this.onTransformElement(sourceElement);
1063
- }
1064
- else if (this._options.wasSourceIModelCopiedToTarget) {
1076
+ if (this._options.wasSourceIModelCopiedToTarget) {
1065
1077
  targetElementId = sourceElement.id;
1066
1078
  targetElementProps =
1067
1079
  this.targetDb.elements.getElementProps(targetElementId);
@@ -1098,20 +1110,44 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1098
1110
  }
1099
1111
  }
1100
1112
  }
1101
- if (core_bentley_1.Id64.isValid(targetElementId) &&
1102
- !this.hasElementChanged(sourceElement, targetElementId))
1113
+ if (!this.hasElementChanged(sourceElement)) {
1114
+ core_bentley_1.Logger.logTrace(loggerCategory, `Skipping unchanged element (${sourceElement.id}, ${sourceElement.getDisplayLabel()}).`);
1103
1115
  return;
1104
- this.collectUnmappedReferences(sourceElement);
1116
+ }
1117
+ if (!this.doAllReferencesExistInTarget(sourceElement)) {
1118
+ this._partiallyCommittedElementIds.add(sourceElement.id);
1119
+ }
1105
1120
  // targetElementId will be valid (indicating update) or undefined (indicating insert)
1106
1121
  targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
1107
1122
  ? targetElementId
1108
1123
  : undefined;
1124
+ if (this._options.preserveElementIdsForFiltering) {
1125
+ const isValid = core_bentley_1.Id64.isValid(targetElementId);
1126
+ if (isValid && targetElementId !== sourceElement.id) {
1127
+ // Element found with different id
1128
+ throw new Error(`Element id(${sourceElement.id}) cannot be preserved. Found a different mapping(${targetElementId}) from source element`);
1129
+ }
1130
+ else if (isValid && targetElementId === sourceElement.id) {
1131
+ // targetElementId is valid (indicating update)
1132
+ this.importer.markElementToUpdateDuringPreserveIds(sourceElement.id);
1133
+ }
1134
+ else if (!isValid) {
1135
+ const sourceInTargetElemProps = this.targetDb.elements.tryGetElementProps(sourceElement.id);
1136
+ // if we don't find mapping for source element in target(invalid) but another element with source id exists in target
1137
+ if (sourceInTargetElemProps) {
1138
+ // Element id is already taken by another element
1139
+ throw new Error(`Element id(${sourceElement.id}) cannot be preserved. An unrelated element in the target already uses id: ${sourceElement.id}`);
1140
+ }
1141
+ else {
1142
+ // Element id in target is available to be remapped
1143
+ targetElementProps.id = sourceElement.id;
1144
+ }
1145
+ }
1146
+ }
1109
1147
  if (!this._options.wasSourceIModelCopiedToTarget) {
1110
1148
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1111
1149
  }
1112
1150
  this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
1113
- // now that we've mapped this elem we can fix unmapped references to it
1114
- this.resolvePendingReferences(sourceElement);
1115
1151
  // the transformer does not currently 'split' or 'join' any elements, therefore, it does not
1116
1152
  // insert external source aspects because federation guids are sufficient for this.
1117
1153
  // Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
@@ -1139,16 +1175,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1139
1175
  this.markLastProvenance(provenance, { isRelationship: false });
1140
1176
  }
1141
1177
  }
1142
- resolvePendingReferences(entity) {
1143
- for (const referencer of this._pendingReferences.getReferencers(entity)) {
1144
- const key = PendingReferenceMap_1.PendingReference.from(referencer, entity);
1145
- const pendingRef = this._pendingReferences.get(key);
1146
- if (!pendingRef)
1147
- continue;
1148
- pendingRef.resolveReference(core_backend_1.EntityReferences.from(entity));
1149
- this._pendingReferences.delete(key);
1150
- }
1151
- }
1152
1178
  /** Override of [IModelExportHandler.onDeleteElement]($transformer) that is called when [IModelExporter]($transformer) detects that an Element has been deleted from the source iModel.
1153
1179
  * This override propagates the delete to the target iModel via [IModelImporter.deleteElement]($transformer).
1154
1180
  */
@@ -1168,12 +1194,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1168
1194
  const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
1169
1195
  // there can only be one repositoryModel per database, so ignore the repo model on remapped subjects
1170
1196
  const isRemappedRootSubject = sourceModel.id === core_common_1.IModel.repositoryModelId &&
1171
- targetModeledElementId != sourceModel.id;
1197
+ targetModeledElementId !== sourceModel.id;
1172
1198
  if (isRemappedRootSubject)
1173
1199
  return;
1174
1200
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
1175
1201
  this.importer.importModel(targetModelProps);
1176
- this.resolvePendingReferences(sourceModel);
1177
1202
  }
1178
1203
  /** Override of [IModelExportHandler.onDeleteModel]($transformer) that is called when [IModelExporter]($transformer) detects that a [Model]($backend) has been deleted from the source iModel. */
1179
1204
  onDeleteModel(sourceModelId) {
@@ -1183,13 +1208,28 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1183
1208
  const targetModelId = this.context.findTargetElementId(sourceModelId);
1184
1209
  if (!core_bentley_1.Id64.isValidId64(targetModelId))
1185
1210
  return;
1211
+ let sql;
1212
+ if (this.hasDefinitionContainerDeletionFeature) {
1213
+ sql = `
1214
+ SELECT 1
1215
+ FROM bis.DefinitionPartition
1216
+ WHERE ECInstanceId=:targetModelId
1217
+ UNION
1218
+ SELECT 1
1219
+ FROM bis.DefinitionContainer
1220
+ WHERE ECInstanceId=:targetModelId
1221
+ `;
1222
+ }
1223
+ else {
1224
+ sql = `
1225
+ SELECT 1
1226
+ FROM bis.DefinitionPartition
1227
+ WHERE ECInstanceId=:targetModelId
1228
+ `;
1229
+ }
1186
1230
  if (this.exporter.sourceDbChanges?.element.deleteIds.has(sourceModelId)) {
1187
- const isDefinitionPartition = this.targetDb.withPreparedStatement(`
1188
- SELECT 1
1189
- FROM bis.DefinitionPartition
1190
- WHERE ECInstanceId=?
1191
- `, (stmt) => {
1192
- stmt.bindId(1, targetModelId);
1231
+ const isDefinitionPartition = this.targetDb.withPreparedStatement(sql, (stmt) => {
1232
+ stmt.bindId("targetModelId", targetModelId);
1193
1233
  const val = stmt.step();
1194
1234
  switch (val) {
1195
1235
  case core_bentley_1.DbResult.BE_SQLITE_ROW:
@@ -1197,7 +1237,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1197
1237
  case core_bentley_1.DbResult.BE_SQLITE_DONE:
1198
1238
  return false;
1199
1239
  default:
1200
- (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
1240
+ (0, core_bentley_1.assert)(false, `unexpected db result: '${JSON.stringify(stmt)}'`);
1201
1241
  }
1202
1242
  });
1203
1243
  if (isDefinitionPartition) {
@@ -1232,7 +1272,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1232
1272
  }
1233
1273
  /** Cause the model container, contents, and sub-models to be exported from the source iModel and imported into the target iModel.
1234
1274
  * @param sourceModeledElementId Import this [Model]($backend) from the source IModelDb.
1235
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1275
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1236
1276
  */
1237
1277
  async processModel(sourceModeledElementId) {
1238
1278
  await this.initialize();
@@ -1242,7 +1282,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1242
1282
  * @param sourceModelId Import the contents of this model from the source IModelDb.
1243
1283
  * @param targetModelId Import into this model in the target IModelDb. The target model must exist prior to this call.
1244
1284
  * @param elementClassFullName Optional classFullName of an element subclass to limit import query against the source model.
1245
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1285
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1246
1286
  */
1247
1287
  async processModelContents(sourceModelId, targetModelId, elementClassFullName = core_backend_1.Element.classFullName) {
1248
1288
  await this.initialize();
@@ -1299,66 +1339,78 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1299
1339
  targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
1300
1340
  return targetModelProps;
1301
1341
  }
1302
- /** Import elements that were deferred in a prior pass.
1303
- * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
1304
- */
1305
- async processDeferredElements(_numRetries = 3) { }
1306
- /** called at the end ([[finalizeTransformation]]) of a transformation,
1342
+ /**
1343
+ * Called at the end of a transformation,
1307
1344
  * updates the target scope element to say that transformation up through the
1308
1345
  * source's changeset has been performed. Also stores all changesets that occurred
1309
1346
  * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
1310
1347
  *
1311
- * You generally should not call this function yourself and use [[processChanges]] instead.
1348
+ * You generally should not call this function yourself and use [[process]] with [[IModelTransformOptions.argsForProcessChanges]] provided instead.
1312
1349
  * It is public for unsupported use cases of custom synchronization transforms.
1313
- * @note if you are not running processChanges in this transformation, this will fail
1314
- * without setting the `force` option to `true`
1350
+ * @note If [[IModelTransformOptions.argsForProcessChanges]] is not defined in this transformation, this function will return early without updating the sync version,
1351
+ * unless the `initializeReverseSyncVersion` option is set to `true`
1352
+ *
1353
+ * The `initializeReverseSyncVersion` is added to set the reverse synchronization version during a forward synchronization.
1354
+ * 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.
1355
+ * 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.
1356
+ *
1357
+ * Note that typically, the reverseSyncVersion is saved as the last changeset merged from the branch into master.
1358
+ * 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.
1315
1359
  */
1316
- updateSynchronizationVersion({ force = false } = {}) {
1317
- const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
1318
- this._sourceChangeDataState !== "has-changes" &&
1319
- !this._isProvenanceInitTransform;
1320
- if (notForcedAndHasNoChangesAndIsntProvenanceInit)
1360
+ updateSynchronizationVersion({ initializeReverseSyncVersion = false, } = {}) {
1361
+ const shouldSkipSyncVersionUpdate = !initializeReverseSyncVersion &&
1362
+ this._sourceChangeDataState !== "has-changes";
1363
+ if (shouldSkipSyncVersionUpdate)
1321
1364
  return;
1322
1365
  nodeAssert(this._targetScopeProvenanceProps);
1323
1366
  const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1324
1367
  const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1325
- if (this._isProvenanceInitTransform) {
1326
- this._targetScopeProvenanceProps.version = sourceVersion;
1327
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1328
- targetVersion;
1329
- }
1330
- else if (this.isReverseSynchronization) {
1368
+ if (this.isReverseSynchronization) {
1331
1369
  const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1332
1370
  core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1333
1371
  this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1334
1372
  sourceVersion;
1335
1373
  }
1336
- else if (!this.isReverseSynchronization) {
1374
+ else {
1337
1375
  core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1338
1376
  this._targetScopeProvenanceProps.version = sourceVersion;
1377
+ // save reverse sync version
1378
+ if (initializeReverseSyncVersion) {
1379
+ core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse sync version from ${this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion} to ${targetVersion}`);
1380
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1381
+ targetVersion;
1382
+ }
1339
1383
  }
1340
- if (this._isSynchronization ||
1341
- (this._startingChangesetIndices && this._isProvenanceInitTransform)) {
1384
+ if (this._options.argsForProcessChanges ||
1385
+ (this._startingChangesetIndices && initializeReverseSyncVersion)) {
1342
1386
  nodeAssert(this.targetDb.changeset.index !== undefined &&
1343
1387
  this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1344
1388
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1345
1389
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1346
1390
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1347
- const [syncChangesetsToClear, syncChangesetsToUpdate] = this
1348
- .isReverseSynchronization
1349
- ? [
1350
- jsonProps.pendingReverseSyncChangesetIndices,
1351
- jsonProps.pendingSyncChangesetIndices,
1352
- ]
1353
- : [
1354
- jsonProps.pendingSyncChangesetIndices,
1355
- jsonProps.pendingReverseSyncChangesetIndices,
1356
- ];
1391
+ const pendingSyncChangesetIndicesKey = "pendingSyncChangesetIndices";
1392
+ const pendingReverseSyncChangesetIndicesKey = "pendingReverseSyncChangesetIndices";
1393
+ // Determine which keys to clear and update based on the synchronization direction
1394
+ let syncChangesetsToClearKey;
1395
+ let syncChangesetsToUpdateKey;
1396
+ if (this.isReverseSynchronization) {
1397
+ syncChangesetsToClearKey = pendingReverseSyncChangesetIndicesKey;
1398
+ syncChangesetsToUpdateKey = pendingSyncChangesetIndicesKey;
1399
+ }
1400
+ else {
1401
+ syncChangesetsToClearKey = pendingSyncChangesetIndicesKey;
1402
+ syncChangesetsToUpdateKey = pendingReverseSyncChangesetIndicesKey;
1403
+ }
1404
+ // NOTE that as documented in [[processChanges]], this assumes that right after
1405
+ // transformation finalization, the work will be saved immediately, otherwise we've
1406
+ // just marked this changeset as a synchronization to ignore, and the user can add other
1407
+ // stuff to it which would break future synchronizations
1357
1408
  for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1358
- syncChangesetsToUpdate.push(i);
1359
- // FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
1360
- // probably wrong, and we should filter it instead
1361
- syncChangesetsToClear.length = 0;
1409
+ jsonProps[syncChangesetsToUpdateKey].push(i);
1410
+ // Only keep the changeset indices which are greater than the source, this means they haven't been processed yet.
1411
+ jsonProps[syncChangesetsToClearKey] = jsonProps[syncChangesetsToClearKey].filter((csIndex) => {
1412
+ return csIndex > this._startingChangesetIndices.source;
1413
+ });
1362
1414
  // if reverse sync then we may have received provenance changes which should be marked as sync changes
1363
1415
  if (this.isReverseSynchronization) {
1364
1416
  nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
@@ -1372,25 +1424,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1372
1424
  ...this._targetScopeProvenanceProps,
1373
1425
  jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1374
1426
  });
1427
+ this.clearCachedSynchronizationVersion();
1375
1428
  }
1376
1429
  // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1377
- async finalizeTransformation(options) {
1430
+ finalizeTransformation() {
1378
1431
  this.importer.finalize();
1379
- this.updateSynchronizationVersion();
1380
- if (this._partiallyCommittedEntities.size > 0) {
1381
- const message = [
1382
- "The following elements were never fully resolved:",
1383
- [...this._partiallyCommittedEntities.keys()].join(","),
1384
- "This indicates that either some references were excluded from the transformation",
1385
- "or the source has dangling references.",
1386
- ].join("\n");
1387
- if (this._options.danglingReferencesBehavior === "reject")
1388
- throw new Error(message);
1389
- core_bentley_1.Logger.logWarning(loggerCategory, message);
1390
- for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
1391
- partiallyCommittedElem.forceComplete();
1392
- }
1393
- }
1432
+ this.updateSynchronizationVersion({
1433
+ initializeReverseSyncVersion: this._isProvenanceInitTransform,
1434
+ });
1394
1435
  // TODO: ignore if we remove change cache usage
1395
1436
  if (!this._options.noDetachChangeCache) {
1396
1437
  if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
@@ -1403,35 +1444,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1403
1444
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1404
1445
  }
1405
1446
  /* eslint-enable @itwin/no-internal */
1406
- const defaultSaveTargetChanges = () => this.targetDb.saveChanges();
1407
- await (options?.saveTargetChanges ?? defaultSaveTargetChanges)(this);
1408
- if (this.isReverseSynchronization)
1409
- this.sourceDb.saveChanges();
1410
- const description = `${this._isProvenanceInitTransform
1411
- ? options?.provenanceInitTransformChangesetDescription ??
1412
- `initialized branch provenance with master iModel: ${this.sourceDb.iModelId}`
1413
- : this.isForwardSynchronization
1414
- ? options?.forwardSyncBranchChangesetDescription ??
1415
- `Forward sync of iModel: ${this.sourceDb.iModelId}`
1416
- : options?.reverseSyncMasterChangesetDescription ??
1417
- `Reverse sync of iModel: ${this.sourceDb.iModelId}`}`;
1418
- if (this.targetDb.isBriefcaseDb()) {
1419
- // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1420
- await this.targetDb.pushChanges({
1421
- description,
1422
- });
1423
- }
1424
- if (this.isReverseSynchronization && this.sourceDb.isBriefcaseDb()) {
1425
- // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1426
- await this.sourceDb.pushChanges({
1427
- description: options?.reverseSyncBranchChangesetDescription ??
1428
- `Update provenance in response to a reverse sync to iModel: ${this.targetDb.iModelId}`,
1429
- });
1430
- }
1431
1447
  }
1432
1448
  /** Imports all relationships that subclass from the specified base class.
1433
1449
  * @param baseRelClassFullName The specified base relationship class.
1434
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1450
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1435
1451
  */
1436
1452
  async processRelationships(baseRelClassFullName) {
1437
1453
  await this.initialize();
@@ -1479,15 +1495,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1479
1495
  core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1480
1496
  return;
1481
1497
  }
1482
- const relArg = deletedRelData.relId ??
1483
- {
1498
+ const id = deletedRelData.relId ??
1499
+ this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, {
1484
1500
  sourceId: deletedRelData.sourceIdInTarget,
1485
1501
  targetId: deletedRelData.targetIdInTarget,
1486
- };
1487
- // FIXME: make importer.deleteRelationship not need full props
1488
- const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1489
- if (targetRelationship) {
1490
- this.importer.deleteRelationship(targetRelationship.toJSON());
1502
+ })?.id;
1503
+ if (id) {
1504
+ this.importer.deleteRelationship({
1505
+ id,
1506
+ classFullName: deletedRelData.classFullName,
1507
+ });
1491
1508
  }
1492
1509
  if (deletedRelData.provenanceAspectId) {
1493
1510
  try {
@@ -1503,8 +1520,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1503
1520
  }
1504
1521
  /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
1505
1522
  * @deprecated in 1.x. Don't use this anymore
1506
- * @see processChanges
1507
- * @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.
1523
+ * @see [[process]] with [[IModelTransformOptions.argsForProcessChanges]] provided.
1524
+ * @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.
1508
1525
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
1509
1526
  */
1510
1527
  async detectRelationshipDeletes() {
@@ -1531,8 +1548,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1531
1548
  const json = JSON.parse(statement.getValue(2).getString());
1532
1549
  const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
1533
1550
  if (targetRelInstanceId) {
1534
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
1535
- this.importer.deleteRelationship(targetRelationship.toJSON());
1551
+ this.importer.deleteRelationship({
1552
+ id: targetRelInstanceId,
1553
+ classFullName: core_backend_1.ElementRefersToElements.classFullName,
1554
+ });
1536
1555
  }
1537
1556
  aspectDeleteIds.push(statement.getValue(0).getId());
1538
1557
  }
@@ -1569,22 +1588,25 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1569
1588
  * This override calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel.
1570
1589
  */
1571
1590
  onExportElementUniqueAspect(sourceAspect) {
1572
- const targetElementId = this.context.findTargetElementId(sourceAspect.element.id);
1573
- const targetAspectProps = this.onTransformElementAspect(sourceAspect, targetElementId);
1574
- this.collectUnmappedReferences(sourceAspect);
1591
+ const targetAspectProps = this.onTransformElementAspect(sourceAspect);
1592
+ if (!this.doAllReferencesExistInTarget(sourceAspect)) {
1593
+ this._partiallyCommittedAspectIds.add(sourceAspect.id);
1594
+ }
1575
1595
  const targetId = this.importer.importElementUniqueAspect(targetAspectProps);
1576
1596
  this.context.remapElementAspect(sourceAspect.id, targetId);
1577
- this.resolvePendingReferences(sourceAspect);
1578
1597
  }
1579
1598
  /** Override of [IModelExportHandler.onExportElementMultiAspects]($transformer) that imports ElementMultiAspects into the target iModel when they are exported from the source iModel.
1580
1599
  * This override calls [[onTransformElementAspect]] for each ElementMultiAspect and then [IModelImporter.importElementMultiAspects]($transformer) to update the target iModel.
1581
1600
  * @note ElementMultiAspects are handled as a group to make it easier to differentiate between insert, update, and delete.
1582
1601
  */
1583
1602
  onExportElementMultiAspects(sourceAspects) {
1584
- const targetElementId = this.context.findTargetElementId(sourceAspects[0].element.id);
1585
1603
  // Transform source ElementMultiAspects into target ElementAspectProps
1586
- const targetAspectPropsArray = sourceAspects.map((srcA) => this.onTransformElementAspect(srcA, targetElementId));
1587
- sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
1604
+ const targetAspectPropsArray = sourceAspects.map((srcA) => this.onTransformElementAspect(srcA));
1605
+ sourceAspects.forEach((a) => {
1606
+ if (!this.doAllReferencesExistInTarget(a)) {
1607
+ this._partiallyCommittedAspectIds.add(a.id);
1608
+ }
1609
+ });
1588
1610
  // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
1589
1611
  const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
1590
1612
  const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
@@ -1594,16 +1616,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1594
1616
  });
1595
1617
  for (let i = 0; i < targetIds.length; ++i) {
1596
1618
  this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
1597
- this.resolvePendingReferences(sourceAspects[i]);
1598
1619
  }
1599
1620
  }
1600
1621
  /** Transform the specified sourceElementAspect into ElementAspectProps for the target iModel.
1601
1622
  * @param sourceElementAspect The ElementAspect from the source iModel to be transformed.
1602
- * @param _targetElementId The ElementId of the target Element that will own the ElementAspects after transformation.
1603
1623
  * @returns ElementAspectProps for the target iModel.
1604
1624
  * @note A subclass can override this method to provide custom transform behavior.
1605
1625
  */
1606
- onTransformElementAspect(sourceElementAspect, _targetElementId) {
1626
+ onTransformElementAspect(sourceElementAspect) {
1607
1627
  const targetElementAspectProps = this.context.cloneElementAspect(sourceElementAspect);
1608
1628
  return targetElementAspectProps;
1609
1629
  }
@@ -1629,7 +1649,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1629
1649
  let schemaFileName = schema.name + ext;
1630
1650
  // many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
1631
1651
  const systemMaxPathSegmentSize = 255;
1632
- if (schemaFileName.length > systemMaxPathSegmentSize) {
1652
+ // windows usually has a limit for the total path length of 260
1653
+ const windowsMaxPathLimit = 260;
1654
+ if (schemaFileName.length > systemMaxPathSegmentSize ||
1655
+ path.join(this._schemaExportDir, schemaFileName).length >=
1656
+ windowsMaxPathLimit) {
1633
1657
  // this name should be well under 255 bytes
1634
1658
  // ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
1635
1659
  // You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
@@ -1639,6 +1663,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1639
1663
  nodeAssert(schemaFileName.length <= systemMaxPathSegmentSize, "Schema name was still long. This is a bug.");
1640
1664
  this._longNamedSchemasMap.set(schema.name, schemaFileName);
1641
1665
  }
1666
+ /* eslint-disable-next-line deprecation/deprecation */
1642
1667
  this.sourceDb.nativeDb.exportSchema(schema.name, this._schemaExportDir, schemaFileName);
1643
1668
  return { schemaPath: path.join(this._schemaExportDir, schemaFileName) };
1644
1669
  }
@@ -1679,7 +1704,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1679
1704
  }
1680
1705
  }
1681
1706
  /** Cause all fonts to be exported from the source iModel and imported into the target iModel.
1682
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1707
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1683
1708
  */
1684
1709
  async processFonts() {
1685
1710
  // we do not need to initialize for this since no entities are exported
@@ -1691,14 +1716,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1691
1716
  this.context.importFont(font.id);
1692
1717
  }
1693
1718
  /** Cause all CodeSpecs to be exported from the source iModel and imported into the target iModel.
1694
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1719
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1695
1720
  */
1696
1721
  async processCodeSpecs() {
1697
1722
  await this.initialize();
1698
1723
  return this.exporter.exportCodeSpecs();
1699
1724
  }
1700
1725
  /** Cause a single CodeSpec to be exported from the source iModel and imported into the target iModel.
1701
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1726
+ * @note This method is called from [[process]], so it only needs to be called directly when processing a subset of an iModel.
1702
1727
  */
1703
1728
  async processCodeSpec(codeSpecName) {
1704
1729
  await this.initialize();
@@ -1722,29 +1747,30 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1722
1747
  this.context.remapElement(sourceSubjectId, targetSubjectId);
1723
1748
  await this.processChildElements(sourceSubjectId);
1724
1749
  await this.processSubjectSubModels(sourceSubjectId);
1725
- return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1750
+ this.completePartiallyCommittedElements();
1751
+ this.completePartiallyCommittedAspects();
1726
1752
  }
1727
1753
  /**
1728
1754
  * Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
1729
- * are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
1755
+ * 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.
1730
1756
  * @note Called by all `process*` functions implicitly.
1731
1757
  * Overriders must call `super.initialize()` first
1732
1758
  */
1733
- async initialize(args) {
1759
+ async initialize() {
1734
1760
  if (this._initialized)
1735
1761
  return;
1736
- await this._tryInitChangesetData(args);
1762
+ this.initScopeProvenance();
1763
+ await this._tryInitChangesetData(this._options.argsForProcessChanges);
1737
1764
  await this.context.initialize();
1738
1765
  // need exporter initialized to do remapdeletedsourceentities.
1739
- await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1766
+ await this.exporter.initialize(this.getExportInitOpts(this._options.argsForProcessChanges ?? {}));
1740
1767
  // 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).
1741
1768
  await this.processChangesets();
1742
1769
  this._initialized = true;
1743
1770
  }
1744
1771
  /**
1745
- * Reads all the changeset files in the private member of the transformer: _csFileProps and does two things with these changesets.
1746
- * Finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
1747
- * Populates this._hasElementChangedCache with a set of elementIds that have been updated or inserted into the database.
1772
+ * Reads all the changeset files in the private member of the transformer: _csFileProps
1773
+ * and finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
1748
1774
  * This function returns early if csFileProps is undefined or is of length 0.
1749
1775
  * @returns void
1750
1776
  */
@@ -1752,9 +1778,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1752
1778
  this.forEachTrackedElement((sourceElementId, targetElementId) => {
1753
1779
  this.context.remapElement(sourceElementId, targetElementId);
1754
1780
  });
1755
- if (this._csFileProps === undefined || this._csFileProps.length === 0)
1756
- return;
1757
- const hasElementChangedCache = new Set();
1781
+ if (this.exporter.sourceDbChanges)
1782
+ await this.addCustomChanges(this.exporter.sourceDbChanges);
1783
+ if (this._csFileProps === undefined || this._csFileProps.length === 0) {
1784
+ if (this.exporter.sourceDbChanges === undefined ||
1785
+ !this.exporter.sourceDbChanges.hasChanges)
1786
+ return;
1787
+ // our sourcedbChanges aren't empty (probably due to someone adding custom changes), change our sourceChangeDataState to has-changes
1788
+ if (this._sourceChangeDataState === "no-changes")
1789
+ this._sourceChangeDataState = "has-changes";
1790
+ }
1758
1791
  const relationshipECClassIdsToSkip = new Set();
1759
1792
  for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
1760
1793
  relationshipECClassIdsToSkip.add(row.ECInstanceId);
@@ -1777,7 +1810,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1777
1810
  alreadyImportedModelInserts.add(targetModelId);
1778
1811
  });
1779
1812
  this._deletedSourceRelationshipData = new Map();
1780
- for (const csFile of this._csFileProps) {
1813
+ for (const csFile of this._csFileProps ?? []) {
1781
1814
  const csReader = core_backend_1.SqliteChangesetReader.openFile({
1782
1815
  fileName: csFile.pathname,
1783
1816
  db: this.sourceDb,
@@ -1798,11 +1831,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1798
1831
  const changeType = change.$meta?.op;
1799
1832
  if (changeType === "Deleted" &&
1800
1833
  change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1801
- change.Scope.Id === this.targetScopeElementId) {
1834
+ change.Scope.Id === this.targetScopeElementId &&
1835
+ change.Kind === core_backend_1.ExternalSourceAspect.Kind.Element) {
1802
1836
  elemIdToScopeEsa.set(change.Element.Id, change);
1803
1837
  }
1804
- else if (changeType === "Inserted" || changeType === "Updated")
1805
- hasElementChangedCache.add(change.ECInstanceId);
1806
1838
  }
1807
1839
  // Loop to process deletes.
1808
1840
  for (const change of changes) {
@@ -1815,13 +1847,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1815
1847
  if (changeType !== "Deleted" ||
1816
1848
  relationshipECClassIdsToSkip.has(ecClassId))
1817
1849
  continue;
1818
- this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1850
+ await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1819
1851
  }
1820
1852
  csReader.close();
1821
1853
  }
1822
- this._hasElementChangedCache = hasElementChangedCache;
1823
1854
  return;
1824
1855
  }
1856
+ /**
1857
+ * This will be called when transformer is called with [[IModelTransformOptions.argsForProcessChanges]] to process changes.
1858
+ * It will be executed after changes in changesets are populated into `sourceDbChanges` and before data processing begins.
1859
+ * Remap table between the source and target iModels will be built at that time, meaning that functions like [[IModelTransformer.context.findTargetElementId]] will return meaningful results.
1860
+ * This function should be used to modify the `sourceDbChanges`, if necessary, using `add custom change` methods in [[ChangedInstanceIds]], such as [[ChangedInstanceIds.addCustomElementChange]], [[ChangedInstanceIds.addCustomModelChange]] and other.
1861
+ * @param sourceDbChanges the ChangedInstanceIds already populated by the exporter with the changes in source changesets, if any, passed to the transformer.
1862
+ * @note Its expected that this function be overridden by a subclass of transformer if it needs to modify sourceDbChanges.
1863
+ */
1864
+ async addCustomChanges(_sourceDbChanges) { }
1825
1865
  /**
1826
1866
  * Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
1827
1867
  * @param change the change to process, must be of changeType "Deleted"
@@ -1832,116 +1872,100 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1832
1872
  * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1833
1873
  * @returns void
1834
1874
  */
1835
- processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1875
+ async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1836
1876
  // we need a connected iModel with changes to remap elements with deletions
1837
1877
  const notConnectedModel = this.sourceDb.iTwinId === undefined;
1838
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1878
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index &&
1879
+ (this.exporter.sourceDbChanges === undefined ||
1880
+ !this.exporter.sourceDbChanges.hasChanges);
1839
1881
  if (notConnectedModel || noChanges)
1840
1882
  return;
1841
- // optimization: if we have provenance, use it to avoid more querying later
1842
- // eventually when itwin.js supports attaching a second iModelDb in JS,
1843
- // this won't have to be a conditional part of the query, and we can always have it by attaching
1844
- const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
1845
- const instId = change.ECInstanceId;
1846
- if (!isRelationship) {
1847
- const sourceElemFedGuid = change.FederationGuid;
1883
+ /**
1884
+ * if our ChangedECInstance is in the provenanceDb, then we can use the ids we find in the ChangedECInstance to query for ESAs.
1885
+ * This is because the ESAs are stored on an element Id thats present in the provenanceDb.
1886
+ */
1887
+ const changeDataInProvenanceDb = this.sourceDb === this.provenanceDb;
1888
+ const getTargetIdFromSourceId = async (id) => {
1848
1889
  let identifierValue;
1849
- if (queryCanAccessProvenance) {
1850
- const aspects = this.sourceDb.elements.getAspects(instId, core_backend_1.ExternalSourceAspect.classFullName);
1851
- for (const aspect of aspects) {
1852
- // look for aspect where the ecInstanceId = the aspect.element.id
1853
- if (aspect.element.id === instId &&
1854
- aspect.scope.id === this.targetScopeElementId)
1855
- identifierValue = aspect.identifier;
1856
- }
1857
- // Think I need to query the esas given the instId.. not sure what db to do it on though.. soruce or target.. or provenance?
1858
- // I need to know the id of the element dpeneding on which db its stored in.
1890
+ let element;
1891
+ if (isRelationship) {
1892
+ element = this.sourceDb.elements.tryGetElement(id);
1859
1893
  }
1860
- if (queryCanAccessProvenance && !identifierValue) {
1861
- if (mapOfDeletedElemIdToScopeEsas.get(instId) !== undefined)
1862
- identifierValue =
1863
- mapOfDeletedElemIdToScopeEsas.get(instId).Identifier;
1894
+ const fedGuid = isRelationship
1895
+ ? element?.federationGuid
1896
+ : change.FederationGuid;
1897
+ if (changeDataInProvenanceDb) {
1898
+ // TODO: clarify what happens if there are multiple (e.g. elements were merged)
1899
+ 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([
1900
+ this.targetScopeElementId,
1901
+ core_backend_1.ExternalSourceAspect.Kind.Element,
1902
+ id,
1903
+ ]))) {
1904
+ identifierValue = row.Identifier;
1905
+ }
1906
+ identifierValue =
1907
+ identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.Identifier;
1864
1908
  }
1865
- const targetId = (queryCanAccessProvenance && identifierValue) ||
1866
- // maybe batching these queries would perform better but we should
1867
- // try to attach the second db and query both together anyway
1868
- (sourceElemFedGuid &&
1869
- this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid)) ||
1870
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1871
- this._queryProvenanceForElement(instId);
1872
- // since we are processing one changeset at a time, we can see local source deletes
1873
- // of entities that were never synced and can be safely ignored
1874
- const deletionNotInTarget = !targetId;
1875
- if (deletionNotInTarget)
1876
- return;
1877
- this.context.remapElement(instId, targetId);
1878
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1879
- // In such case an entity update will be triggered and we no longer need to delete the entity.
1880
- if (alreadyImportedElementInserts.has(targetId)) {
1881
- this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
1909
+ // Check for targetId by an esa first
1910
+ if (changeDataInProvenanceDb && identifierValue) {
1911
+ const targetId = identifierValue;
1912
+ return targetId;
1882
1913
  }
1883
- if (alreadyImportedModelInserts.has(targetId)) {
1884
- this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
1914
+ // Check for targetId using sourceId's fedguid if we didn't find an esa.
1915
+ if (fedGuid) {
1916
+ const targetId = this._queryElemIdByFedGuid(this.targetDb, fedGuid);
1917
+ return targetId;
1885
1918
  }
1886
- }
1887
- else {
1888
- // is deleted relationship
1889
- const classFullName = change.$meta?.classFullName;
1919
+ return undefined;
1920
+ };
1921
+ const changedInstanceId = change.ECInstanceId;
1922
+ if (isRelationship) {
1890
1923
  const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1891
1924
  const targetIdOfRelationshipInSource = change.TargetECInstanceId;
1892
- const [sourceIdInTarget, targetIdInTarget] = [
1893
- sourceIdOfRelationshipInSource,
1894
- targetIdOfRelationshipInSource,
1895
- ].map((id) => {
1896
- let element;
1897
- try {
1898
- element = this.sourceDb.elements.getElement(id);
1899
- }
1900
- catch (err) {
1901
- return undefined;
1902
- }
1903
- const fedGuid = element.federationGuid;
1904
- let identifierValue;
1905
- if (queryCanAccessProvenance) {
1906
- const aspects = this.sourceDb.elements.getAspects(id, core_backend_1.ExternalSourceAspect.classFullName);
1907
- for (const aspect of aspects) {
1908
- if (aspect.element.id === id &&
1909
- aspect.scope.id === this.targetScopeElementId)
1910
- identifierValue = aspect.identifier;
1911
- }
1912
- if (identifierValue === undefined) {
1913
- if (mapOfDeletedElemIdToScopeEsas.get(id) !== undefined)
1914
- identifierValue =
1915
- mapOfDeletedElemIdToScopeEsas.get(id).Identifier;
1916
- }
1917
- }
1918
- return ((queryCanAccessProvenance && identifierValue) ||
1919
- // maybe batching these queries would perform better but we should
1920
- // try to attach the second db and query both together anyway
1921
- (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
1922
- });
1923
- if (sourceIdInTarget && targetIdInTarget) {
1924
- this._deletedSourceRelationshipData.set(instId, {
1925
+ const classFullName = change.$meta?.classFullName;
1926
+ const sourceIdOfRelationshipInTarget = await getTargetIdFromSourceId(sourceIdOfRelationshipInSource);
1927
+ const targetIdOfRelationshipInTarget = await getTargetIdFromSourceId(targetIdOfRelationshipInSource);
1928
+ if (sourceIdOfRelationshipInTarget && targetIdOfRelationshipInTarget) {
1929
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1925
1930
  classFullName: classFullName ?? "",
1926
- sourceIdInTarget,
1927
- targetIdInTarget,
1931
+ sourceIdInTarget: sourceIdOfRelationshipInTarget,
1932
+ targetIdInTarget: targetIdOfRelationshipInTarget,
1928
1933
  });
1929
1934
  }
1930
- else {
1931
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1932
- const relProvenance = this._queryProvenanceForRelationship(instId, {
1935
+ else if (this.sourceDb === this.provenanceSourceDb) {
1936
+ const relProvenance = this._queryProvenanceForRelationship(changedInstanceId, {
1933
1937
  classFullName: classFullName ?? "",
1934
1938
  sourceId: sourceIdOfRelationshipInSource,
1935
1939
  targetId: targetIdOfRelationshipInSource,
1936
1940
  });
1937
1941
  if (relProvenance && relProvenance.relationshipId)
1938
- this._deletedSourceRelationshipData.set(instId, {
1942
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1939
1943
  classFullName: classFullName ?? "",
1940
1944
  relId: relProvenance.relationshipId,
1941
1945
  provenanceAspectId: relProvenance.aspectId,
1942
1946
  });
1943
1947
  }
1944
1948
  }
1949
+ else {
1950
+ let targetId = await getTargetIdFromSourceId(changedInstanceId);
1951
+ if (targetId === undefined && this.sourceDb === this.provenanceSourceDb) {
1952
+ targetId = this._queryProvenanceForElement(changedInstanceId);
1953
+ }
1954
+ // since we are processing one changeset at a time, we can see local source deletes
1955
+ // of entities that were never synced and can be safely ignored
1956
+ const deletionNotInTarget = !targetId;
1957
+ if (deletionNotInTarget)
1958
+ return;
1959
+ this.context.remapElement(changedInstanceId, targetId);
1960
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1961
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
1962
+ if (alreadyImportedElementInserts.has(targetId)) {
1963
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(changedInstanceId);
1964
+ }
1965
+ if (alreadyImportedModelInserts.has(targetId)) {
1966
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(changedInstanceId);
1967
+ }
1968
+ }
1945
1969
  }
1946
1970
  async _tryInitChangesetData(args) {
1947
1971
  if (!args ||
@@ -1950,17 +1974,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1950
1974
  this._sourceChangeDataState = "unconnected";
1951
1975
  return;
1952
1976
  }
1953
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1977
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
1954
1978
  if (noChanges) {
1955
1979
  this._sourceChangeDataState = "no-changes";
1956
1980
  this._csFileProps = [];
1957
1981
  return;
1958
1982
  }
1983
+ const startChangeset = "startChangeset" in args ? args.startChangeset : undefined;
1959
1984
  // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1960
1985
  // to ignore those already processed changes
1961
- const startChangesetIndexOrId = args.startChangeset?.index ??
1962
- args.startChangeset?.id ??
1963
- this._synchronizationVersion.index + 1;
1986
+ const startChangesetIndexOrId = startChangeset?.index ??
1987
+ startChangeset?.id ??
1988
+ this.synchronizationVersion.index + 1;
1964
1989
  const endChangesetId = this.sourceDb.changeset.id;
1965
1990
  const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
1966
1991
  ? indexOrId
@@ -1969,20 +1994,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1969
1994
  iModelId: this.sourceDb.iModelId,
1970
1995
  // eslint-disable-next-line deprecation/deprecation
1971
1996
  changeset: { id: indexOrId },
1972
- accessToken: args.accessToken,
1973
1997
  })
1974
1998
  .then((changeset) => changeset.index)));
1975
- const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
1976
- if (!this._options.ignoreMissingChangesetsInSynchronizations &&
1977
- startChangesetIndex !== this._synchronizationVersion.index + 1 &&
1978
- this._synchronizationVersion.index !== -1) {
1999
+ const missingChangesets = startChangesetIndex > this.synchronizationVersion.index + 1;
2000
+ if (!this._options.argsForProcessChanges
2001
+ ?.ignoreMissingChangesetsInSynchronizations &&
2002
+ startChangesetIndex !== this.synchronizationVersion.index + 1 &&
2003
+ this.synchronizationVersion.index !== -1) {
1979
2004
  throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
1980
2005
  " startChangesetId should be" +
1981
2006
  " exactly the first changeset *after* the previous synchronization to not miss data." +
1982
2007
  ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}` +
1983
- ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'` +
1984
- ` which is changeset #${this._synchronizationVersion.index}. The transformer expected` +
1985
- ` #${this._synchronizationVersion.index + 1}.`);
2008
+ ` but the previous synchronization for this targetScopeElement was '${this.synchronizationVersion.id}'` +
2009
+ ` which is changeset #${this.synchronizationVersion.index}. The transformer expected` +
2010
+ ` #${this.synchronizationVersion.index + 1}.`);
1986
2011
  }
1987
2012
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1988
2013
  const changesetsToSkip = this.isReverseSynchronization
@@ -2004,19 +2029,47 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2004
2029
  csFileProps.push(...fileProps);
2005
2030
  }
2006
2031
  this._csFileProps = csFileProps;
2007
- this._sourceChangeDataState = "has-changes";
2032
+ /** Theres a possibility that our csFileProps length is still 0 here, since we skip cs indices found in the pendingSync and pendingReverseSync indices arrays. */
2033
+ this._sourceChangeDataState =
2034
+ this._csFileProps.length === 0 ? "no-changes" : "has-changes";
2035
+ }
2036
+ /**
2037
+ * The behavior of process is influenced by [[IModelTransformOptions.argsForProcessChanges]] being defined or not defined during construction passed of the IModelTransformer.
2038
+ * @section When argsForProcessChanges are defined:
2039
+ *
2040
+ * Export changes from the source iModel and import the transformed entities into the target iModel.
2041
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2042
+ *
2043
+ * Notes:
2044
+ * - the transformer assumes that you saveChanges after processing changes. You should not modify the iModel after processChanges until saveChanges,
2045
+ * failure to do so may result in corrupted
2046
+ * data loss in future branch operations
2047
+ * - if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2048
+ * will automatically be determined and used
2049
+ * - 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.
2050
+ *
2051
+ * @section When argsForProcessChanges are undefined:
2052
+ *
2053
+ * Export everything from the source iModel and import the transformed entities into the target iModel.
2054
+ *
2055
+ * Notes:
2056
+ * - [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2057
+ *
2058
+ */
2059
+ async process() {
2060
+ await this.initialize();
2061
+ this.logSettings();
2062
+ return this._options.argsForProcessChanges !== undefined
2063
+ ? this.processChanges(this._options.argsForProcessChanges)
2064
+ : this.processAll();
2008
2065
  }
2009
2066
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
2010
2067
  * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2011
2068
  */
2012
- async processAll(options) {
2013
- this.logSettings();
2014
- this.initScopeProvenance();
2015
- await this.initialize();
2069
+ async processAll() {
2016
2070
  await this.exporter.exportCodeSpecs();
2017
2071
  await this.exporter.exportFonts();
2018
2072
  if (this._options.skipPropagateChangesToRootElements) {
2019
- // FIXME<NICK>: This option in exportAll was a maybe.
2020
2073
  // The RepositoryModel and root Subject of the target iModel should not be transformed.
2021
2074
  await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
2022
2075
  await this.exporter.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true); // after the Subject hierarchy, process the other elements of the RepositoryModel
@@ -2025,18 +2078,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2025
2078
  else {
2026
2079
  await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
2027
2080
  }
2081
+ this.completePartiallyCommittedElements();
2028
2082
  await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
2083
+ this.completePartiallyCommittedAspects();
2029
2084
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
2030
- await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2031
2085
  if (this._options.forceExternalSourceAspectProvenance &&
2032
2086
  this.shouldDetectDeletes()) {
2087
+ // eslint-disable-next-line deprecation/deprecation
2033
2088
  await this.detectElementDeletes();
2089
+ // eslint-disable-next-line deprecation/deprecation
2034
2090
  await this.detectRelationshipDeletes();
2035
2091
  }
2036
2092
  if (this._options.optimizeGeometry)
2037
2093
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2038
2094
  this.importer.computeProjectExtents();
2039
- await this.finalizeTransformation(options);
2095
+ this.finalizeTransformation();
2040
2096
  }
2041
2097
  markLastProvenance(sourceAspect, { isRelationship = false }) {
2042
2098
  this._lastProvenanceEntityInfo =
@@ -2051,240 +2107,47 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2051
2107
  : core_backend_1.ExternalSourceAspect.Kind.Element,
2052
2108
  };
2053
2109
  }
2054
- /**
2055
- * Load the state of the active transformation from an open SQLiteDb
2056
- * You can override this if you'd like to load from custom tables in the resumable dump state, but you should call
2057
- * this super implementation
2058
- * @note the SQLiteDb must be open
2059
- */
2060
- loadStateFromDb(db) {
2061
- const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
2062
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
2063
- throw Error("expected row when getting lastProvenanceEntityId from target state table");
2064
- const entityId = stmt.getValueString(0);
2065
- const isGuidOrGuidPair = entityId.includes("-");
2066
- return isGuidOrGuidPair
2067
- ? entityId
2068
- : {
2069
- entityId,
2070
- aspectId: stmt.getValueString(1),
2071
- aspectVersion: stmt.getValueString(2),
2072
- aspectKind: stmt.getValueString(3),
2073
- };
2074
- });
2075
- /*
2076
- // TODO: maybe save transformer state resumption state based on target changset and require calls
2077
- // to saveChanges
2078
- if () {
2079
- const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
2080
- const isRelProvenance = targetFedGuid !== undefined;
2081
- const instanceId = isRelProvenance
2082
- ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
2083
- : "";
2084
- //const classId =
2085
- if (isRelProvenance) {
2086
- }
2087
- }
2088
- */
2089
- const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
2090
- // ignore provenance check if it's null since we can't bind those ids
2091
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
2092
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
2093
- this.provenanceDb.withPreparedStatement(`
2094
- SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
2095
- WHERE Scope.Id=:scopeId
2096
- AND ECInstanceId=:aspectId
2097
- AND Kind=:kind
2098
- AND Element.Id=:entityId
2099
- `, (statement) => {
2100
- statement.bindId("scopeId", this.targetScopeElementId);
2101
- statement.bindId("aspectId", lastProvenanceEntityInfo.aspectId);
2102
- statement.bindString("kind", lastProvenanceEntityInfo.aspectKind);
2103
- statement.bindId("entityId", lastProvenanceEntityInfo.entityId);
2104
- const stepResult = statement.step();
2105
- switch (stepResult) {
2106
- case core_bentley_1.DbResult.BE_SQLITE_ROW:
2107
- const version = statement.getValue(0).getString();
2108
- return version === lastProvenanceEntityInfo.aspectVersion;
2109
- case core_bentley_1.DbResult.BE_SQLITE_DONE:
2110
- return false;
2111
- default:
2112
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.SQLiteError, `got sql error ${stepResult}`);
2113
- }
2114
- });
2115
- if (!targetHasCorrectLastProvenance)
2116
- throw Error([
2117
- "Target for resuming from does not have the expected provenance ",
2118
- "from the target that the resume state was made with",
2119
- ].join("\n"));
2120
- this._lastProvenanceEntityInfo = lastProvenanceEntityInfo;
2121
- const state = db.withSqliteStatement(`SELECT data FROM ${IModelTransformer.jsStateTable}`, (stmt) => {
2122
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
2123
- throw Error("expected row when getting data from js state table");
2124
- return JSON.parse(stmt.getValueString(0));
2125
- });
2126
- if (state.transformerClass !== this.constructor.name)
2127
- throw Error("resuming from a differently named transformer class, it is not necessarily valid to resume with a different transformer class");
2128
- // force assign to readonly options since we do not know how the transformer subclass takes options to pass to the superclass
2129
- this._options = state.options;
2130
- this.context.loadStateFromDb(db);
2131
- this.importer.loadStateFromJson(state.importerState);
2132
- this.exporter.loadStateFromJson(state.exporterState);
2133
- this._elementsWithExplicitlyTrackedProvenance =
2134
- core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
2135
- this.loadAdditionalStateJson(state.additionalState);
2136
- }
2137
- /**
2138
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
2139
- * from the original changeset
2140
- *
2141
- * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
2142
- * This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
2143
- * again but the remapping state will cause already mapped elements to be skipped.
2144
- * To "resume" an iModel Transformation you need:
2145
- * - the sourceDb at the same changeset
2146
- * - the same targetDb in the state in which it was before
2147
- * @param statePath the path to the serialized state of the transformer, use [[IModelTransformer.saveStateToFile]] to get this from an existing transformer instance
2148
- * @param constructorArgs remaining arguments that you would normally pass to the Transformer subclass you are using, usually (sourceDb, targetDb)
2149
- * @note custom transformers with custom state may need to override this method in order to handle loading their own custom state somewhere
2150
- */
2151
- static resumeTransformation(statePath, ...constructorArgs) {
2152
- const transformer = new this(...constructorArgs);
2153
- const db = new core_backend_1.SQLiteDb();
2154
- db.openDb(statePath, core_bentley_1.OpenMode.Readonly);
2155
- try {
2156
- transformer.loadStateFromDb(db);
2157
- }
2158
- finally {
2159
- db.closeDb();
2160
- }
2161
- return transformer;
2162
- }
2163
- /**
2164
- * You may override this to store arbitrary json state in a transformer state dump, useful for some resumptions
2165
- * @see [[IModelTransformer.saveStateToFile]]
2166
- */
2167
- getAdditionalStateJson() {
2168
- return {};
2169
- }
2170
- /**
2171
- * You may override this to load arbitrary json state in a transformer state dump, useful for some resumptions
2172
- * @see [[IModelTransformer.loadStateFromFile]]
2173
- */
2174
- loadAdditionalStateJson(_additionalState) { }
2175
- /**
2176
- * Save the state of the active transformation to an open SQLiteDb
2177
- * You can override this if you'd like to write custom tables to the resumable dump state, but you should call
2178
- * this super implementation
2179
- * @note the SQLiteDb must be open
2180
- */
2181
- saveStateToDb(db) {
2182
- const jsonState = {
2183
- transformerClass: this.constructor.name,
2184
- options: this._options,
2185
- explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
2186
- importerState: this.importer.saveStateToJson(),
2187
- exporterState: this.exporter.saveStateToJson(),
2188
- additionalState: this.getAdditionalStateJson(),
2189
- };
2190
- this.context.saveStateToDb(db);
2191
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2192
- db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
2193
- throw Error("Failed to create the js state table in the state database");
2194
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2195
- db.executeSQL(`
2196
- CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
2197
- -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
2198
- entityId TEXT,
2199
- -- the following are only valid if the above entityId is a hex id representation
2200
- aspectId TEXT,
2201
- aspectVersion TEXT,
2202
- aspectKind TEXT
2203
- )
2204
- `))
2205
- throw Error("Failed to create the target state table in the state database");
2206
- db.saveChanges();
2207
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.jsStateTable} (data) VALUES (?)`, (stmt) => {
2208
- stmt.bindString(1, JSON.stringify(jsonState));
2209
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2210
- throw Error("Failed to insert options into the state database");
2211
- });
2212
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
2213
- const lastProvenanceEntityInfo = this
2214
- ._lastProvenanceEntityInfo;
2215
- stmt.bindString(1, lastProvenanceEntityInfo?.entityId ??
2216
- this._lastProvenanceEntityInfo);
2217
- stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
2218
- stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
2219
- stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
2220
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2221
- throw Error("Failed to insert options into the state database");
2222
- });
2223
- db.saveChanges();
2224
- }
2225
- /**
2226
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
2227
- * from the original changeset
2228
- *
2229
- * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
2230
- * This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
2231
- * The serialization format is a custom sqlite database.
2232
- * @note custom transformers with custom state may override [[IModelTransformer.saveStateToDb]] or [[IModelTransformer.getAdditionalStateJson]]
2233
- * and [[IModelTransformer.loadStateFromDb]] (with a super call) or [[IModelTransformer.loadAdditionalStateJson]]
2234
- * if they have custom state that needs to be stored with
2235
- * potentially inside the same sqlite file in separate tables
2236
- */
2237
- saveStateToFile(nativeStatePath) {
2238
- const db = new core_backend_1.SQLiteDb();
2239
- if (core_backend_1.IModelJsFs.existsSync(nativeStatePath))
2240
- core_backend_1.IModelJsFs.unlinkSync(nativeStatePath);
2241
- db.createDb(nativeStatePath);
2242
- try {
2243
- this.saveStateToDb(db);
2244
- db.saveChanges();
2245
- }
2246
- finally {
2247
- db.closeDb();
2248
- }
2249
- }
2250
2110
  /** Export changes from the source iModel and import the transformed entities into the target iModel.
2251
2111
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2252
- * @note the transformer saves and pushes changes when its work is complete.
2112
+ * @note the transformer assumes that you saveChanges after processing changes. You should not
2113
+ * modify the iModel after processChanges until saveChanges, failure to do so may result in corrupted
2114
+ * data loss in future branch operations
2253
2115
  * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2254
2116
  * will automatically be determined and used
2255
2117
  * @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.
2256
2118
  */
2257
2119
  async processChanges(options) {
2258
- this._isSynchronization = true;
2259
- this.initScopeProvenance();
2260
- this.logSettings();
2261
- await this.initialize(options);
2262
2120
  // must wait for initialization of synchronization provenance data
2263
2121
  await this.exporter.exportChanges(this.getExportInitOpts(options));
2264
- await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2122
+ this.completePartiallyCommittedElements();
2123
+ this.completePartiallyCommittedAspects();
2265
2124
  if (this._options.optimizeGeometry)
2266
2125
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2267
2126
  this.importer.computeProjectExtents();
2268
- await this.finalizeTransformation(options);
2127
+ this.finalizeTransformation();
2128
+ const defaultSaveTargetChanges = () => {
2129
+ this.targetDb.saveChanges();
2130
+ };
2131
+ await (options.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2269
2132
  }
2270
2133
  /** Changeset data must be initialized in order to build correct changeOptions.
2271
2134
  * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
2272
2135
  */
2273
2136
  getExportInitOpts(opts) {
2274
- if (!this._isSynchronization)
2137
+ if (!this._options.argsForProcessChanges)
2275
2138
  return {};
2139
+ const startChangeset = "startChangeset" in opts ? opts.startChangeset : undefined;
2276
2140
  return {
2277
- skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
2278
- accessToken: opts.accessToken,
2141
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
2279
2142
  ...(this._csFileProps
2280
2143
  ? { csFileProps: this._csFileProps }
2281
2144
  : this._changesetRanges
2282
2145
  ? { changesetRanges: this._changesetRanges }
2283
- : opts.startChangeset
2284
- ? { startChangeset: opts.startChangeset }
2146
+ : startChangeset
2147
+ ? { startChangeset }
2285
2148
  : {
2286
2149
  startChangeset: {
2287
- index: this._synchronizationVersion.index + 1,
2150
+ index: this.synchronizationVersion.index + 1,
2288
2151
  },
2289
2152
  }),
2290
2153
  };
@@ -2303,10 +2166,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2303
2166
  }
2304
2167
  exports.IModelTransformer = IModelTransformer;
2305
2168
  IModelTransformer.noEsaSyncDirectionErrorMessage = "Couldn't find an external source aspect to determine sync direction. This often means that the master->branch relationship has not been established. Consider running the transformer with wasSourceIModelCopiedToTarget set to true.";
2306
- /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
2307
- IModelTransformer.jsStateTable = "TransformerJsState";
2308
- /** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
2309
- IModelTransformer.lastProvenanceEntityInfoTable = "LastProvenanceEntityInfo";
2310
2169
  /** IModelTransformer that clones the contents of a template model.
2311
2170
  * @beta
2312
2171
  */
@@ -2367,8 +2226,7 @@ class TemplateModelCloner extends IModelTransformer {
2367
2226
  }
2368
2227
  /** Cloning from a template requires this override of onTransformElement. */
2369
2228
  onTransformElement(sourceElement) {
2370
- // eslint-disable-next-line deprecation/deprecation
2371
- const referenceIds = sourceElement.getReferenceConcreteIds();
2229
+ const referenceIds = sourceElement.getReferenceIds();
2372
2230
  referenceIds.forEach((referenceId) => {
2373
2231
  // TODO: consider going through all definition elements at once and remapping them to themselves
2374
2232
  if (!core_backend_1.EntityReferences.isValid(this.context.findTargetEntityId(referenceId))) {