@itwin/imodel-transformer 1.0.0-dev.1 → 1.0.0-dev.10

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 (38) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/lib/cjs/Algo.d.ts +7 -0
  3. package/lib/cjs/Algo.d.ts.map +1 -1
  4. package/lib/cjs/Algo.js +7 -0
  5. package/lib/cjs/Algo.js.map +1 -1
  6. package/lib/cjs/BigMap.d.ts +6 -1
  7. package/lib/cjs/BigMap.d.ts.map +1 -1
  8. package/lib/cjs/BigMap.js +28 -2
  9. package/lib/cjs/BigMap.js.map +1 -1
  10. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  11. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +1 -1
  12. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  13. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  14. package/lib/cjs/ElementCascadingDeleter.js.map +1 -1
  15. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  16. package/lib/cjs/EntityUnifier.js.map +1 -1
  17. package/lib/cjs/ExportElementAspectsStrategy.js.map +1 -1
  18. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +1 -1
  19. package/lib/cjs/IModelCloneContext.d.ts +1 -4
  20. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  21. package/lib/cjs/IModelCloneContext.js +16 -31
  22. package/lib/cjs/IModelCloneContext.js.map +1 -1
  23. package/lib/cjs/IModelExporter.d.ts +5 -52
  24. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  25. package/lib/cjs/IModelExporter.js +6 -60
  26. package/lib/cjs/IModelExporter.js.map +1 -1
  27. package/lib/cjs/IModelImporter.d.ts +5 -44
  28. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  29. package/lib/cjs/IModelImporter.js +4 -52
  30. package/lib/cjs/IModelImporter.js.map +1 -1
  31. package/lib/cjs/IModelTransformer.d.ts +59 -111
  32. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  33. package/lib/cjs/IModelTransformer.js +287 -400
  34. package/lib/cjs/IModelTransformer.js.map +1 -1
  35. package/lib/cjs/PendingReferenceMap.js.map +1 -1
  36. package/lib/cjs/transformer.js +2 -1
  37. package/lib/cjs/transformer.js.map +1 -1
  38. package/package.json +17 -16
@@ -13,6 +13,7 @@ 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");
@@ -80,7 +81,7 @@ function mapId64(idContainer, func) {
80
81
  }
81
82
  else {
82
83
  throw Error([
83
- `Id64 container '${idContainer}' is unsupported.`,
84
+ `Id64 container '${JSON.stringify(idContainer)}' is unsupported.`,
84
85
  "Currently only singular Id64 strings or prop-like objects containing an 'id' property are supported.",
85
86
  ].join("\n"));
86
87
  }
@@ -149,7 +150,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
149
150
  id: targetScopeElementId,
150
151
  relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
151
152
  },
152
- scope: { id: core_common_1.IModel.rootSubjectId },
153
+ scope: { id: core_common_1.IModel.rootSubjectId }, // the root Subject scopes scope elements
153
154
  identifier: sourceDb.iModelId,
154
155
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
155
156
  jsonProperties: undefined,
@@ -273,9 +274,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
273
274
  cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
274
275
  targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
275
276
  // eslint-disable-next-line deprecation/deprecation
276
- danglingReferencesBehavior: options?.danglingReferencesBehavior ??
277
- options?.danglingPredecessorsBehavior ??
278
- "reject",
277
+ danglingReferencesBehavior: options?.danglingReferencesBehavior ?? "reject",
279
278
  branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
280
279
  };
281
280
  this._isProvenanceInitTransform = this._options
@@ -361,8 +360,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
361
360
  core_bentley_1.Logger.logInfo(loggerCategory, `this._includeSourceProvenance=${this._options.includeSourceProvenance}`);
362
361
  core_bentley_1.Logger.logInfo(loggerCategory, `this._cloneUsingBinaryGeometry=${this._options.cloneUsingBinaryGeometry}`);
363
362
  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}`);
363
+ core_bentley_1.Logger.logInfo(loggerCategory,
364
+ // eslint-disable-next-line deprecation/deprecation
365
+ `this._isReverseSynchronization=${this._options.isReverseSynchronization}`);
366
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.autoExtendProjectExtents=${JSON.stringify(this.importer.options.autoExtendProjectExtents)}`);
366
367
  core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.simplifyElementGeometry=${this.importer.options.simplifyElementGeometry}`);
367
368
  }
368
369
  /** Return the IModelDb where IModelTransformer will store its provenance.
@@ -455,29 +456,30 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
455
456
  forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
456
457
  });
457
458
  }
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).
459
+ /**
460
+ * As of itwinjs 4.6.0, definitionContainers are now deleted as if they were DefinitionPartitions as opposed to Definitions.
461
+ * This variable being true will be used to special case the deletion of DefinitionContainers the same way DefinitionPartitions are deleted.
463
462
  */
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");
463
+ get hasDefinitionContainerDeletionFeature() {
464
+ if (this._hasDefinitionContainerDeletionFeature === undefined) {
465
+ this._hasDefinitionContainerDeletionFeature = Semver.satisfies(coreBackendPkgJson.version, "^4.6.0");
474
466
  }
475
- return this._cachedSynchronizationVersion;
467
+ return this._hasDefinitionContainerDeletionFeature;
468
+ }
469
+ /**
470
+ * We cache the synchronization version to avoid querying the target scoping ESA multiple times.
471
+ * If the target scoping ESA is ever updated we need to clear any potentially cached sync version otherwise we will get stale values.
472
+ * Sets this._cachedSynchronizationVersion to undefined.
473
+ */
474
+ clearCachedSynchronizationVersion() {
475
+ this._cachedSynchronizationVersion = undefined;
476
476
  }
477
477
  /** 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
478
+ * @note the version depends on whether this is a reverse synchronization or not, as
479
479
  * 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).
480
+ * @note empty string and -1 for changeset and index if it has never been transformed
481
+ * @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".
482
+ * @throws if the version is not found in a preexisting scope aspect and @see [[IModelTransformOptions.branchRelationshipDataBehavior]] !== "unsafe-migrate"
481
483
  */
482
484
  get synchronizationVersion() {
483
485
  if (this._cachedSynchronizationVersion === undefined) {
@@ -488,10 +490,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
488
490
  const version = this.isReverseSynchronization
489
491
  ? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
490
492
  : provenanceScopeAspect.version;
491
- if (!version) {
493
+ if (!version &&
494
+ this._options.branchRelationshipDataBehavior === "unsafe-migrate") {
492
495
  return { index: -1, id: "" }; // previous synchronization was done before fed guid update.
493
496
  }
494
- const [id, index] = version.split(";");
497
+ if (version === undefined) {
498
+ 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.
499
+ Consider running the transformer with branchRelationshipDataBehavior set to 'unsafe-migrate'`);
500
+ }
501
+ const [id, index] = version === "" ? ["", -1] : version.split(";");
495
502
  if (Number.isNaN(Number(index)))
496
503
  throw new Error("Could not parse version data from scope aspect");
497
504
  this._cachedSynchronizationVersion = { index: Number(index), id }; // synchronization version found and cached.
@@ -530,7 +537,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
530
537
  id: this.targetScopeElementId,
531
538
  relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
532
539
  },
533
- scope: { id: core_common_1.IModel.rootSubjectId },
540
+ scope: { id: core_common_1.IModel.rootSubjectId }, // the root Subject scopes scope elements
534
541
  identifier: this.provenanceSourceDb.iModelId,
535
542
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
536
543
  jsonProperties: undefined,
@@ -567,29 +574,81 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
567
574
  jsonProperties: JSON.stringify(aspectProps.jsonProperties),
568
575
  });
569
576
  aspectProps.id = id;
577
+ // Busting a potential cached version
578
+ this.clearCachedSynchronizationVersion();
570
579
  }
571
580
  }
572
581
  else {
573
582
  // foundEsaProps is defined.
574
583
  aspectProps.id = foundEsaProps.aspectId;
575
- aspectProps.version =
576
- foundEsaProps.version ??
577
- (this._options.branchRelationshipDataBehavior === "unsafe-migrate"
578
- ? ""
579
- : undefined);
584
+ aspectProps.version = foundEsaProps.version;
580
585
  aspectProps.jsonProperties = foundEsaProps.jsonProperties
581
586
  ? JSON.parse(foundEsaProps.jsonProperties)
582
- : this._options.branchRelationshipDataBehavior === "unsafe-migrate"
583
- ? {
584
- pendingReverseSyncChangesetIndices: [],
585
- pendingSyncChangesetIndices: [],
586
- reverseSyncVersion: "",
587
- }
588
- : undefined;
587
+ : undefined;
588
+ // Clone oldProps incase they're changed for logging purposes
589
+ const oldProps = JSON.parse(JSON.stringify(aspectProps));
590
+ if (this.handleUnsafeMigrate(aspectProps)) {
591
+ 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 });
592
+ this.provenanceDb.elements.updateAspect({
593
+ ...aspectProps,
594
+ jsonProperties: JSON.stringify(aspectProps.jsonProperties),
595
+ });
596
+ // Busting a potential cached version
597
+ this.clearCachedSynchronizationVersion();
598
+ }
589
599
  }
590
600
  this._targetScopeProvenanceProps =
591
601
  aspectProps;
592
602
  }
603
+ /** Returns true if a change was made to the aspectProps. */
604
+ handleUnsafeMigrate(aspectProps) {
605
+ let madeChange = false;
606
+ if (this._options.branchRelationshipDataBehavior !== "unsafe-migrate")
607
+ return madeChange;
608
+ const fallbackSyncVersionToUse = this._options.unsafeFallbackSyncVersion ?? "";
609
+ const fallbackReverseSyncVersionToUse = this._options.unsafeFallbackReverseSyncVersion ?? "";
610
+ if (aspectProps.version === undefined ||
611
+ (aspectProps.version === "" &&
612
+ aspectProps.version !== fallbackSyncVersionToUse)) {
613
+ aspectProps.version = fallbackSyncVersionToUse;
614
+ madeChange = true;
615
+ }
616
+ if (aspectProps.jsonProperties === undefined) {
617
+ aspectProps.jsonProperties = {
618
+ pendingReverseSyncChangesetIndices: [],
619
+ pendingSyncChangesetIndices: [],
620
+ reverseSyncVersion: fallbackReverseSyncVersionToUse,
621
+ };
622
+ madeChange = true;
623
+ }
624
+ else if (aspectProps.jsonProperties.reverseSyncVersion === undefined ||
625
+ (aspectProps.jsonProperties.reverseSyncVersion === "" &&
626
+ aspectProps.jsonProperties.reverseSyncVersion !==
627
+ fallbackReverseSyncVersionToUse)) {
628
+ aspectProps.jsonProperties.reverseSyncVersion =
629
+ fallbackReverseSyncVersionToUse;
630
+ madeChange = true;
631
+ }
632
+ /**
633
+ * This case will only be hit when:
634
+ * - first transformation was performed on pre-fedguid transformer.
635
+ * - a second processAll transformation was performed on the same target-source iModels post-fedguid transformer.
636
+ * - change processing was invoked on for the second 'initial' transformation.
637
+ * NOTE: This case likely does not exist anymore, but we will keep it just to be sure.
638
+ */
639
+ if (aspectProps.jsonProperties.pendingReverseSyncChangesetIndices ===
640
+ undefined) {
641
+ core_bentley_1.Logger.logWarning(loggerCategory, "Property pendingReverseSyncChangesetIndices missing on the jsonProperties of the scoping ESA. Setting to [].");
642
+ aspectProps.jsonProperties.pendingReverseSyncChangesetIndices = [];
643
+ madeChange = true;
644
+ }
645
+ if (aspectProps.jsonProperties.pendingSyncChangesetIndices === undefined) {
646
+ core_bentley_1.Logger.logWarning(loggerCategory, "Property pendingSyncChangesetIndices missing on the jsonProperties of the scoping ESA. Setting to [].");
647
+ aspectProps.jsonProperties.pendingSyncChangesetIndices = [];
648
+ madeChange = true;
649
+ }
650
+ return madeChange;
651
+ }
593
652
  /**
594
653
  * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
595
654
  * and call a function for each one.
@@ -688,6 +747,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
688
747
  skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
689
748
  });
690
749
  }
750
+ /**
751
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
752
+ * The identifier on the ESA is the id of the element in the [[IModelTransformer.provenanceSourceDb]]
753
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
754
+ * @param entityInProvenanceSourceId
755
+ * @returns the elementId that the ESA is stored on, esa.Element.Id
756
+ */
691
757
  _queryProvenanceForElement(entityInProvenanceSourceId) {
692
758
  return this.provenanceDb.withPreparedStatement(`
693
759
  SELECT esa.Element.Id
@@ -705,6 +771,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
705
771
  return undefined;
706
772
  });
707
773
  }
774
+ /**
775
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
776
+ * The identifier on the ESA is the id of the relationship in the [[IModelTransformer.provenanceSourceDb]]
777
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
778
+ * @param entityInProvenanceSourceId
779
+ * @returns
780
+ */
708
781
  _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
709
782
  return this.provenanceDb.withPreparedStatement(`
710
783
  SELECT
@@ -850,6 +923,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
850
923
  onTransformElement(sourceElement) {
851
924
  core_bentley_1.Logger.logTrace(loggerCategory, `onTransformElement(${sourceElement.id}) "${sourceElement.getDisplayLabel()}"`);
852
925
  const targetElementProps = this.context.cloneElement(sourceElement, { binaryGeometry: this._options.cloneUsingBinaryGeometry });
926
+ // Special case: source element is the root subject
927
+ if (sourceElement.id === core_common_1.IModel.rootSubjectId) {
928
+ const targetElementId = this.context.findTargetElementId(sourceElement.id);
929
+ // When remapping rootSubject from source to non root subject in target, the code.scope gets remapped incorrectly.
930
+ // This is because the rootSubject has no parent and its code.scope is unique in that it is the id of itself.
931
+ // For all other subjects which do have parents the code.scope and its parent should be in agreement.
932
+ if (targetElementId !== core_bentley_1.Id64.invalid &&
933
+ targetElementId !== core_common_1.IModel.rootSubjectId) {
934
+ const targetElement = this.targetDb.elements.getElement(targetElementId);
935
+ targetElementProps.parent =
936
+ targetElement.parent ?? targetElementProps.parent;
937
+ targetElementProps.code.scope = targetElement.code.scope;
938
+ }
939
+ }
853
940
  if (sourceElement instanceof core_backend_1.Subject) {
854
941
  if (targetElementProps.jsonProperties?.Subject?.Job) {
855
942
  // don't propagate source channels into target (legacy bridge case)
@@ -860,10 +947,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
860
947
  }
861
948
  /** Returns true if a change within sourceElement is detected.
862
949
  * @param sourceElement The Element from the source iModel
863
- * @param targetElementId The Element from the target iModel to compare against.
864
950
  * @note A subclass can override this method to provide custom change detection behavior.
865
951
  */
866
- hasElementChanged(sourceElement, _targetElementId) {
952
+ hasElementChanged(sourceElement) {
867
953
  if (this._sourceChangeDataState === "no-changes")
868
954
  return false;
869
955
  if (this._sourceChangeDataState === "unconnected")
@@ -1098,8 +1184,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1098
1184
  }
1099
1185
  }
1100
1186
  }
1101
- if (core_bentley_1.Id64.isValid(targetElementId) &&
1102
- !this.hasElementChanged(sourceElement, targetElementId))
1187
+ if (!this.hasElementChanged(sourceElement))
1103
1188
  return;
1104
1189
  this.collectUnmappedReferences(sourceElement);
1105
1190
  // targetElementId will be valid (indicating update) or undefined (indicating insert)
@@ -1168,7 +1253,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1168
1253
  const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
1169
1254
  // there can only be one repositoryModel per database, so ignore the repo model on remapped subjects
1170
1255
  const isRemappedRootSubject = sourceModel.id === core_common_1.IModel.repositoryModelId &&
1171
- targetModeledElementId != sourceModel.id;
1256
+ targetModeledElementId !== sourceModel.id;
1172
1257
  if (isRemappedRootSubject)
1173
1258
  return;
1174
1259
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
@@ -1183,13 +1268,28 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1183
1268
  const targetModelId = this.context.findTargetElementId(sourceModelId);
1184
1269
  if (!core_bentley_1.Id64.isValidId64(targetModelId))
1185
1270
  return;
1271
+ let sql;
1272
+ if (this.hasDefinitionContainerDeletionFeature) {
1273
+ sql = `
1274
+ SELECT 1
1275
+ FROM bis.DefinitionPartition
1276
+ WHERE ECInstanceId=:targetModelId
1277
+ UNION
1278
+ SELECT 1
1279
+ FROM bis.DefinitionContainer
1280
+ WHERE ECInstanceId=:targetModelId
1281
+ `;
1282
+ }
1283
+ else {
1284
+ sql = `
1285
+ SELECT 1
1286
+ FROM bis.DefinitionPartition
1287
+ WHERE ECInstanceId=:targetModelId
1288
+ `;
1289
+ }
1186
1290
  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);
1291
+ const isDefinitionPartition = this.targetDb.withPreparedStatement(sql, (stmt) => {
1292
+ stmt.bindId("targetModelId", targetModelId);
1193
1293
  const val = stmt.step();
1194
1294
  switch (val) {
1195
1295
  case core_bentley_1.DbResult.BE_SQLITE_ROW:
@@ -1197,7 +1297,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1197
1297
  case core_bentley_1.DbResult.BE_SQLITE_DONE:
1198
1298
  return false;
1199
1299
  default:
1200
- (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
1300
+ (0, core_bentley_1.assert)(false, `unexpected db result: '${JSON.stringify(stmt)}'`);
1201
1301
  }
1202
1302
  });
1203
1303
  if (isDefinitionPartition) {
@@ -1303,7 +1403,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1303
1403
  * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
1304
1404
  */
1305
1405
  async processDeferredElements(_numRetries = 3) { }
1306
- /** called at the end ([[finalizeTransformation]]) of a transformation,
1406
+ /** called at the end of a transformation,
1307
1407
  * updates the target scope element to say that transformation up through the
1308
1408
  * source's changeset has been performed. Also stores all changesets that occurred
1309
1409
  * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
@@ -1344,21 +1444,28 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1344
1444
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1345
1445
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1346
1446
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1347
- const [syncChangesetsToClear, syncChangesetsToUpdate] = this
1447
+ const pendingSyncChangesetIndicesKey = "pendingSyncChangesetIndices";
1448
+ const pendingReverseSyncChangesetIndicesKey = "pendingReverseSyncChangesetIndices";
1449
+ const [syncChangesetsToClearKey, syncChangesetsToUpdateKey] = this
1348
1450
  .isReverseSynchronization
1349
1451
  ? [
1350
- jsonProps.pendingReverseSyncChangesetIndices,
1351
- jsonProps.pendingSyncChangesetIndices,
1452
+ pendingReverseSyncChangesetIndicesKey,
1453
+ pendingSyncChangesetIndicesKey,
1352
1454
  ]
1353
1455
  : [
1354
- jsonProps.pendingSyncChangesetIndices,
1355
- jsonProps.pendingReverseSyncChangesetIndices,
1456
+ pendingSyncChangesetIndicesKey,
1457
+ pendingReverseSyncChangesetIndicesKey,
1356
1458
  ];
1459
+ // NOTE that as documented in [[processChanges]], this assumes that right after
1460
+ // transformation finalization, the work will be saved immediately, otherwise we've
1461
+ // just marked this changeset as a synchronization to ignore, and the user can add other
1462
+ // stuff to it which would break future synchronizations
1357
1463
  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;
1464
+ jsonProps[syncChangesetsToUpdateKey].push(i);
1465
+ // Only keep the changeset indices which are greater than the source, this means they haven't been processed yet.
1466
+ jsonProps[syncChangesetsToClearKey] = jsonProps[syncChangesetsToClearKey].filter((csIndex) => {
1467
+ return csIndex > this._startingChangesetIndices.source;
1468
+ });
1362
1469
  // if reverse sync then we may have received provenance changes which should be marked as sync changes
1363
1470
  if (this.isReverseSynchronization) {
1364
1471
  nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
@@ -1372,9 +1479,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1372
1479
  ...this._targetScopeProvenanceProps,
1373
1480
  jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1374
1481
  });
1482
+ this.clearCachedSynchronizationVersion();
1375
1483
  }
1376
1484
  // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1377
- async finalizeTransformation(options) {
1485
+ finalizeTransformation() {
1378
1486
  this.importer.finalize();
1379
1487
  this.updateSynchronizationVersion();
1380
1488
  if (this._partiallyCommittedEntities.size > 0) {
@@ -1403,31 +1511,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1403
1511
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1404
1512
  }
1405
1513
  /* 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
1514
  }
1432
1515
  /** Imports all relationships that subclass from the specified base class.
1433
1516
  * @param baseRelClassFullName The specified base relationship class.
@@ -1479,15 +1562,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1479
1562
  core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1480
1563
  return;
1481
1564
  }
1482
- const relArg = deletedRelData.relId ??
1483
- {
1565
+ const id = deletedRelData.relId ??
1566
+ this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, {
1484
1567
  sourceId: deletedRelData.sourceIdInTarget,
1485
1568
  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());
1569
+ })?.id;
1570
+ if (id) {
1571
+ this.importer.deleteRelationship({
1572
+ id,
1573
+ classFullName: deletedRelData.classFullName,
1574
+ });
1491
1575
  }
1492
1576
  if (deletedRelData.provenanceAspectId) {
1493
1577
  try {
@@ -1531,8 +1615,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1531
1615
  const json = JSON.parse(statement.getValue(2).getString());
1532
1616
  const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
1533
1617
  if (targetRelInstanceId) {
1534
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
1535
- this.importer.deleteRelationship(targetRelationship.toJSON());
1618
+ this.importer.deleteRelationship({
1619
+ id: targetRelInstanceId,
1620
+ classFullName: core_backend_1.ElementRefersToElements.classFullName,
1621
+ });
1536
1622
  }
1537
1623
  aspectDeleteIds.push(statement.getValue(0).getId());
1538
1624
  }
@@ -1629,7 +1715,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1629
1715
  let schemaFileName = schema.name + ext;
1630
1716
  // many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
1631
1717
  const systemMaxPathSegmentSize = 255;
1632
- if (schemaFileName.length > systemMaxPathSegmentSize) {
1718
+ // windows usually has a limit for the total path length of 260
1719
+ const windowsMaxPathLimit = 260;
1720
+ if (schemaFileName.length > systemMaxPathSegmentSize ||
1721
+ path.join(this._schemaExportDir, schemaFileName).length >=
1722
+ windowsMaxPathLimit) {
1633
1723
  // this name should be well under 255 bytes
1634
1724
  // ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
1635
1725
  // You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
@@ -1763,6 +1853,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1763
1853
  for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)")) {
1764
1854
  relationshipECClassIds.add(row.ECInstanceId);
1765
1855
  }
1856
+ const elementECClassIds = new Set();
1857
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.Element)")) {
1858
+ elementECClassIds.add(row.ECInstanceId);
1859
+ }
1766
1860
  // For later use when processing deletes.
1767
1861
  const alreadyImportedElementInserts = new Set();
1768
1862
  const alreadyImportedModelInserts = new Set();
@@ -1801,7 +1895,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1801
1895
  change.Scope.Id === this.targetScopeElementId) {
1802
1896
  elemIdToScopeEsa.set(change.Element.Id, change);
1803
1897
  }
1804
- else if (changeType === "Inserted" || changeType === "Updated")
1898
+ else if ((changeType === "Inserted" || changeType === "Updated") &&
1899
+ change.ECClassId !== undefined &&
1900
+ elementECClassIds.has(change.ECClassId))
1805
1901
  hasElementChangedCache.add(change.ECInstanceId);
1806
1902
  }
1807
1903
  // Loop to process deletes.
@@ -1815,7 +1911,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1815
1911
  if (changeType !== "Deleted" ||
1816
1912
  relationshipECClassIdsToSkip.has(ecClassId))
1817
1913
  continue;
1818
- this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1914
+ await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1819
1915
  }
1820
1916
  csReader.close();
1821
1917
  }
@@ -1832,116 +1928,98 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1832
1928
  * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1833
1929
  * @returns void
1834
1930
  */
1835
- processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1931
+ async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1836
1932
  // we need a connected iModel with changes to remap elements with deletions
1837
1933
  const notConnectedModel = this.sourceDb.iTwinId === undefined;
1838
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1934
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
1839
1935
  if (notConnectedModel || noChanges)
1840
1936
  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;
1937
+ /**
1938
+ * if our ChangedECInstance is in the provenanceDb, then we can use the ids we find in the ChangedECInstance to query for ESAs.
1939
+ * This is because the ESAs are stored on an element Id thats present in the provenanceDb.
1940
+ */
1941
+ const changeDataInProvenanceDb = this.sourceDb === this.provenanceDb;
1942
+ const getTargetIdFromSourceId = async (id) => {
1848
1943
  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;
1944
+ let element;
1945
+ if (isRelationship) {
1946
+ element = this.sourceDb.elements.tryGetElement(id);
1947
+ }
1948
+ const fedGuid = isRelationship
1949
+ ? element?.federationGuid
1950
+ : change.FederationGuid;
1951
+ if (changeDataInProvenanceDb) {
1952
+ // TODO: clarify what happens if there are multiple (e.g. elements were merged)
1953
+ 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([
1954
+ this.targetScopeElementId,
1955
+ core_backend_1.ExternalSourceAspect.Kind.Element,
1956
+ id,
1957
+ ]))) {
1958
+ identifierValue = row.Identifier;
1856
1959
  }
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.
1960
+ identifierValue =
1961
+ identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.Identifier;
1859
1962
  }
1860
- if (queryCanAccessProvenance && !identifierValue) {
1861
- if (mapOfDeletedElemIdToScopeEsas.get(instId) !== undefined)
1862
- identifierValue =
1863
- mapOfDeletedElemIdToScopeEsas.get(instId).Identifier;
1963
+ // Check for targetId by an esa first
1964
+ if (changeDataInProvenanceDb && identifierValue) {
1965
+ const targetId = identifierValue;
1966
+ return targetId;
1864
1967
  }
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);
1882
- }
1883
- if (alreadyImportedModelInserts.has(targetId)) {
1884
- this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
1968
+ // Check for targetId using sourceId's fedguid if we didn't find an esa.
1969
+ if (fedGuid) {
1970
+ const targetId = this._queryElemIdByFedGuid(this.targetDb, fedGuid);
1971
+ return targetId;
1885
1972
  }
1886
- }
1887
- else {
1888
- // is deleted relationship
1889
- const classFullName = change.$meta?.classFullName;
1973
+ return undefined;
1974
+ };
1975
+ const changedInstanceId = change.ECInstanceId;
1976
+ if (isRelationship) {
1890
1977
  const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1891
1978
  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, {
1979
+ const classFullName = change.$meta?.classFullName;
1980
+ const sourceIdOfRelationshipInTarget = await getTargetIdFromSourceId(sourceIdOfRelationshipInSource);
1981
+ const targetIdOfRelationshipInTarget = await getTargetIdFromSourceId(targetIdOfRelationshipInSource);
1982
+ if (sourceIdOfRelationshipInTarget && targetIdOfRelationshipInTarget) {
1983
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1925
1984
  classFullName: classFullName ?? "",
1926
- sourceIdInTarget,
1927
- targetIdInTarget,
1985
+ sourceIdInTarget: sourceIdOfRelationshipInTarget,
1986
+ targetIdInTarget: targetIdOfRelationshipInTarget,
1928
1987
  });
1929
1988
  }
1930
- else {
1931
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1932
- const relProvenance = this._queryProvenanceForRelationship(instId, {
1989
+ else if (this.sourceDb === this.provenanceSourceDb) {
1990
+ const relProvenance = this._queryProvenanceForRelationship(changedInstanceId, {
1933
1991
  classFullName: classFullName ?? "",
1934
1992
  sourceId: sourceIdOfRelationshipInSource,
1935
1993
  targetId: targetIdOfRelationshipInSource,
1936
1994
  });
1937
1995
  if (relProvenance && relProvenance.relationshipId)
1938
- this._deletedSourceRelationshipData.set(instId, {
1996
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1939
1997
  classFullName: classFullName ?? "",
1940
1998
  relId: relProvenance.relationshipId,
1941
1999
  provenanceAspectId: relProvenance.aspectId,
1942
2000
  });
1943
2001
  }
1944
2002
  }
2003
+ else {
2004
+ let targetId = await getTargetIdFromSourceId(changedInstanceId);
2005
+ if (targetId === undefined && this.sourceDb === this.provenanceSourceDb) {
2006
+ targetId = this._queryProvenanceForElement(changedInstanceId);
2007
+ }
2008
+ // since we are processing one changeset at a time, we can see local source deletes
2009
+ // of entities that were never synced and can be safely ignored
2010
+ const deletionNotInTarget = !targetId;
2011
+ if (deletionNotInTarget)
2012
+ return;
2013
+ this.context.remapElement(changedInstanceId, targetId);
2014
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
2015
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
2016
+ if (alreadyImportedElementInserts.has(targetId)) {
2017
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(changedInstanceId);
2018
+ }
2019
+ if (alreadyImportedModelInserts.has(targetId)) {
2020
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(changedInstanceId);
2021
+ }
2022
+ }
1945
2023
  }
1946
2024
  async _tryInitChangesetData(args) {
1947
2025
  if (!args ||
@@ -1950,7 +2028,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1950
2028
  this._sourceChangeDataState = "unconnected";
1951
2029
  return;
1952
2030
  }
1953
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
2031
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
1954
2032
  if (noChanges) {
1955
2033
  this._sourceChangeDataState = "no-changes";
1956
2034
  this._csFileProps = [];
@@ -1960,7 +2038,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1960
2038
  // to ignore those already processed changes
1961
2039
  const startChangesetIndexOrId = args.startChangeset?.index ??
1962
2040
  args.startChangeset?.id ??
1963
- this._synchronizationVersion.index + 1;
2041
+ this.synchronizationVersion.index + 1;
1964
2042
  const endChangesetId = this.sourceDb.changeset.id;
1965
2043
  const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
1966
2044
  ? indexOrId
@@ -1972,17 +2050,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1972
2050
  accessToken: args.accessToken,
1973
2051
  })
1974
2052
  .then((changeset) => changeset.index)));
1975
- const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
2053
+ const missingChangesets = startChangesetIndex > this.synchronizationVersion.index + 1;
1976
2054
  if (!this._options.ignoreMissingChangesetsInSynchronizations &&
1977
- startChangesetIndex !== this._synchronizationVersion.index + 1 &&
1978
- this._synchronizationVersion.index !== -1) {
2055
+ startChangesetIndex !== this.synchronizationVersion.index + 1 &&
2056
+ this.synchronizationVersion.index !== -1) {
1979
2057
  throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
1980
2058
  " startChangesetId should be" +
1981
2059
  " exactly the first changeset *after* the previous synchronization to not miss data." +
1982
2060
  ` 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}.`);
2061
+ ` but the previous synchronization for this targetScopeElement was '${this.synchronizationVersion.id}'` +
2062
+ ` which is changeset #${this.synchronizationVersion.index}. The transformer expected` +
2063
+ ` #${this.synchronizationVersion.index + 1}.`);
1986
2064
  }
1987
2065
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1988
2066
  const changesetsToSkip = this.isReverseSynchronization
@@ -2004,19 +2082,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2004
2082
  csFileProps.push(...fileProps);
2005
2083
  }
2006
2084
  this._csFileProps = csFileProps;
2007
- this._sourceChangeDataState = "has-changes";
2085
+ /** Theres a possibility that our csFileProps length is still 0 here, since we skip cs indices found in the pendingSync and pendingReverseSync indices arrays. */
2086
+ this._sourceChangeDataState =
2087
+ this._csFileProps.length === 0 ? "no-changes" : "has-changes";
2008
2088
  }
2009
2089
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
2010
2090
  * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2011
2091
  */
2012
- async processAll(options) {
2092
+ async processAll() {
2013
2093
  this.logSettings();
2014
2094
  this.initScopeProvenance();
2015
2095
  await this.initialize();
2016
2096
  await this.exporter.exportCodeSpecs();
2017
2097
  await this.exporter.exportFonts();
2018
2098
  if (this._options.skipPropagateChangesToRootElements) {
2019
- // FIXME<NICK>: This option in exportAll was a maybe.
2020
2099
  // The RepositoryModel and root Subject of the target iModel should not be transformed.
2021
2100
  await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
2022
2101
  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
@@ -2030,13 +2109,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2030
2109
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2031
2110
  if (this._options.forceExternalSourceAspectProvenance &&
2032
2111
  this.shouldDetectDeletes()) {
2112
+ // eslint-disable-next-line deprecation/deprecation
2033
2113
  await this.detectElementDeletes();
2114
+ // eslint-disable-next-line deprecation/deprecation
2034
2115
  await this.detectRelationshipDeletes();
2035
2116
  }
2036
2117
  if (this._options.optimizeGeometry)
2037
2118
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2038
2119
  this.importer.computeProjectExtents();
2039
- await this.finalizeTransformation(options);
2120
+ this.finalizeTransformation();
2040
2121
  }
2041
2122
  markLastProvenance(sourceAspect, { isRelationship = false }) {
2042
2123
  this._lastProvenanceEntityInfo =
@@ -2051,205 +2132,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2051
2132
  : core_backend_1.ExternalSourceAspect.Kind.Element,
2052
2133
  };
2053
2134
  }
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
2135
  /** Export changes from the source iModel and import the transformed entities into the target iModel.
2251
2136
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2252
- * @note the transformer saves and pushes changes when its work is complete.
2137
+ * @note the transformer assumes that you saveChanges after processing changes. You should not
2138
+ * modify the iModel after processChanges until saveChanges, failure to do so may result in corrupted
2139
+ * data loss in future branch operations
2253
2140
  * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2254
2141
  * will automatically be determined and used
2255
2142
  * @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.
@@ -2265,7 +2152,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2265
2152
  if (this._options.optimizeGeometry)
2266
2153
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2267
2154
  this.importer.computeProjectExtents();
2268
- await this.finalizeTransformation(options);
2155
+ this.finalizeTransformation();
2156
+ const defaultSaveTargetChanges = () => {
2157
+ this.targetDb.saveChanges();
2158
+ };
2159
+ await (options.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2269
2160
  }
2270
2161
  /** Changeset data must be initialized in order to build correct changeOptions.
2271
2162
  * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
@@ -2284,7 +2175,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2284
2175
  ? { startChangeset: opts.startChangeset }
2285
2176
  : {
2286
2177
  startChangeset: {
2287
- index: this._synchronizationVersion.index + 1,
2178
+ index: this.synchronizationVersion.index + 1,
2288
2179
  },
2289
2180
  }),
2290
2181
  };
@@ -2303,10 +2194,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2303
2194
  }
2304
2195
  exports.IModelTransformer = IModelTransformer;
2305
2196
  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
2197
  /** IModelTransformer that clones the contents of a template model.
2311
2198
  * @beta
2312
2199
  */