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

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