@itwin/imodel-transformer 0.4.18-fedguidopt.6 → 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 (65) 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 +10 -4
  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 +29 -3
  9. package/lib/cjs/BigMap.js.map +1 -1
  10. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  11. package/lib/cjs/BranchProvenanceInitializer.js +15 -4
  12. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  13. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +1 -1
  14. package/lib/cjs/DetachedExportElementAspectsStrategy.js +12 -5
  15. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +1 -1
  16. package/lib/cjs/ECReferenceTypesCache.d.ts.map +1 -1
  17. package/lib/cjs/ECReferenceTypesCache.js +32 -18
  18. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  19. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts +1 -1
  20. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts.map +1 -1
  21. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js +7 -5
  22. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  23. package/lib/cjs/ElementCascadingDeleter.d.ts +3 -3
  24. package/lib/cjs/ElementCascadingDeleter.d.ts.map +1 -1
  25. package/lib/cjs/ElementCascadingDeleter.js +9 -7
  26. package/lib/cjs/ElementCascadingDeleter.js.map +1 -1
  27. package/lib/cjs/EntityMap.d.ts.map +1 -1
  28. package/lib/cjs/EntityMap.js.map +1 -1
  29. package/lib/cjs/EntityUnifier.d.ts +5 -0
  30. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  31. package/lib/cjs/EntityUnifier.js +22 -35
  32. package/lib/cjs/EntityUnifier.js.map +1 -1
  33. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +1 -1
  34. package/lib/cjs/ExportElementAspectsStrategy.js +5 -4
  35. package/lib/cjs/ExportElementAspectsStrategy.js.map +1 -1
  36. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +1 -1
  37. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +9 -5
  38. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +1 -1
  39. package/lib/cjs/IModelCloneContext.d.ts +1 -4
  40. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  41. package/lib/cjs/IModelCloneContext.js +37 -41
  42. package/lib/cjs/IModelCloneContext.js.map +1 -1
  43. package/lib/cjs/IModelExporter.d.ts +69 -73
  44. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  45. package/lib/cjs/IModelExporter.js +240 -176
  46. package/lib/cjs/IModelExporter.js.map +1 -1
  47. package/lib/cjs/IModelImporter.d.ts +17 -65
  48. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  49. package/lib/cjs/IModelImporter.js +79 -109
  50. package/lib/cjs/IModelImporter.js.map +1 -1
  51. package/lib/cjs/IModelTransformer.d.ts +151 -121
  52. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  53. package/lib/cjs/IModelTransformer.js +809 -751
  54. package/lib/cjs/IModelTransformer.js.map +1 -1
  55. package/lib/cjs/PendingReferenceMap.d.ts.map +1 -1
  56. package/lib/cjs/PendingReferenceMap.js +12 -6
  57. package/lib/cjs/PendingReferenceMap.js.map +1 -1
  58. package/lib/cjs/TransformerLoggerCategory.d.ts +2 -2
  59. package/lib/cjs/TransformerLoggerCategory.d.ts.map +1 -1
  60. package/lib/cjs/TransformerLoggerCategory.js +5 -5
  61. package/lib/cjs/TransformerLoggerCategory.js.map +1 -1
  62. package/lib/cjs/transformer.d.ts.map +1 -1
  63. package/lib/cjs/transformer.js +15 -10
  64. package/lib/cjs/transformer.js.map +1 -1
  65. package/package.json +22 -18
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TemplateModelCloner = exports.IModelTransformer = void 0;
4
4
  /*---------------------------------------------------------------------------------------------
5
- * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
6
- * See LICENSE.md in the project root for license terms and full copyright notice.
7
- *--------------------------------------------------------------------------------------------*/
5
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
6
+ * See LICENSE.md in the project root for license terms and full copyright notice.
7
+ *--------------------------------------------------------------------------------------------*/
8
8
  /** @packageDocumentation
9
9
  * @module iModels
10
10
  */
@@ -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
  }
@@ -95,15 +96,115 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
95
96
  get targetScopeElementId() {
96
97
  return this._options.targetScopeElementId;
97
98
  }
98
- get _isReverseSynchronization() {
99
- return this._isSynchronization && this._options.isReverseSynchronization;
99
+ /**
100
+ * Queries for an esa which matches the props in the provided aspectProps.
101
+ * @param dbToQuery db to run the query on for scope external source
102
+ * @param aspectProps aspectProps to search for @see ExternalSourceAspectProps
103
+ */
104
+ static queryScopeExternalSourceAspect(dbToQuery, aspectProps) {
105
+ const sql = `
106
+ SELECT ECInstanceId, Version, JsonProperties
107
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName}
108
+ WHERE Element.Id=:elementId
109
+ AND Scope.Id=:scopeId
110
+ AND Kind=:kind
111
+ AND Identifier=:identifier
112
+ LIMIT 1
113
+ `;
114
+ return dbToQuery.withPreparedStatement(sql, (statement) => {
115
+ statement.bindId("elementId", aspectProps.element.id);
116
+ if (aspectProps.scope === undefined)
117
+ return undefined; // return instead of binding an invalid id
118
+ statement.bindId("scopeId", aspectProps.scope.id);
119
+ statement.bindString("kind", aspectProps.kind);
120
+ statement.bindString("identifier", aspectProps.identifier);
121
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
122
+ return undefined;
123
+ const aspectId = statement.getValue(0).getId();
124
+ const versionValue = statement.getValue(1);
125
+ const version = versionValue.isNull
126
+ ? undefined
127
+ : versionValue.getString();
128
+ const jsonPropsValue = statement.getValue(2);
129
+ const jsonProperties = jsonPropsValue.isNull
130
+ ? undefined
131
+ : jsonPropsValue.getString();
132
+ return { aspectId, version, jsonProperties };
133
+ });
134
+ }
135
+ /**
136
+ * Determines the sync direction "forward" or "reverse" of a given sourceDb and targetDb by looking for the scoping ESA.
137
+ * If the sourceDb's iModelId is found as the identifier of the expected scoping ESA in the targetDb, then it is a forward synchronization.
138
+ * If the targetDb's iModelId is found as the identifier of the expected scoping ESA in the sourceDb, then it is a reverse synchronization.
139
+ * @throws if no scoping ESA can be found in either the sourceDb or targetDb which describes a master branch relationship between the two databases.
140
+ * @returns "forward" or "reverse"
141
+ */
142
+ static determineSyncType(sourceDb, targetDb,
143
+ /** @see [[IModelTransformOptions.targetScopeElementId]] */
144
+ targetScopeElementId) {
145
+ const aspectProps = {
146
+ id: undefined,
147
+ version: undefined,
148
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
149
+ element: {
150
+ id: targetScopeElementId,
151
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
152
+ },
153
+ scope: { id: core_common_1.IModel.rootSubjectId }, // the root Subject scopes scope elements
154
+ identifier: sourceDb.iModelId,
155
+ kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
156
+ jsonProperties: undefined,
157
+ };
158
+ /** First check if the targetDb is the branch (branch is the @see provenanceDb) */
159
+ const esaPropsFromTargetDb = this.queryScopeExternalSourceAspect(targetDb, aspectProps);
160
+ if (esaPropsFromTargetDb !== undefined) {
161
+ return "forward"; // we found an esa assuming targetDb is the provenanceDb/branch so this is a forward sync.
162
+ }
163
+ // Now check if the sourceDb is the branch
164
+ aspectProps.identifier = targetDb.iModelId;
165
+ const esaPropsFromSourceDb = this.queryScopeExternalSourceAspect(sourceDb, aspectProps);
166
+ if (esaPropsFromSourceDb !== undefined) {
167
+ return "reverse"; // we found an esa assuming sourceDb is the provenanceDb/branch so this is a reverse sync.
168
+ }
169
+ throw new Error(this.noEsaSyncDirectionErrorMessage);
170
+ }
171
+ determineSyncType() {
172
+ if (this._isProvenanceInitTransform) {
173
+ return "forward";
174
+ }
175
+ if (!this._isSynchronization) {
176
+ return "not-sync";
177
+ }
178
+ try {
179
+ return IModelTransformer.determineSyncType(this.sourceDb, this.targetDb, this.targetScopeElementId);
180
+ }
181
+ catch (err) {
182
+ if (err instanceof Error &&
183
+ err.message === IModelTransformer.noEsaSyncDirectionErrorMessage &&
184
+ this._allowNoScopingESA) {
185
+ return "forward";
186
+ }
187
+ throw err;
188
+ }
100
189
  }
101
- get _isForwardSynchronization() {
102
- return this._isSynchronization && !this._options.isReverseSynchronization;
190
+ get isReverseSynchronization() {
191
+ if (this._syncType === undefined)
192
+ this._syncType = this.determineSyncType();
193
+ return this._syncType === "reverse";
194
+ }
195
+ get isForwardSynchronization() {
196
+ if (this._syncType === undefined)
197
+ this._syncType = this.determineSyncType();
198
+ return this._syncType === "forward";
103
199
  }
104
200
  /** The element classes that are considered to define provenance in the iModel */
105
201
  static get provenanceElementClasses() {
106
- return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
202
+ return [
203
+ core_backend_1.FolderLink,
204
+ core_backend_1.SynchronizationConfigLink,
205
+ core_backend_1.ExternalSource,
206
+ core_backend_1.ExternalSourceAttachment,
207
+ ];
107
208
  }
108
209
  /** The element aspect classes that are considered to define provenance in the iModel */
109
210
  static get provenanceElementAspectClasses() {
@@ -124,12 +225,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
124
225
  /** map of partially committed entities to their partial commit progress */
125
226
  this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
126
227
  this._isSynchronization = false;
228
+ /**
229
+ * A private variable meant to be set by tests which have an outdated way of setting up transforms. In all synchronizations today we expect to find an ESA in the branch db which describes the master -> branch relationship.
230
+ * The exception to this is the first transform aka the provenance initializing transform which requires that the master imodel and the branch imodel are identical at the time of provenance initialization.
231
+ * A couple ofoutdated tests run their first transform providing a source and targetdb that are slightly different which is no longer supported. In order to not remove these tests which are still providing value
232
+ * this private property on the IModelTransformer exists.
233
+ */
234
+ this._allowNoScopingESA = false;
127
235
  this._changesetRanges = undefined;
128
236
  /** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
129
237
  * @note Currently only tracks elements which were not exported.
130
238
  */
131
239
  this._skippedEntities = new Set();
132
- // FIXME: add test transforming using this, then switching to new transform method
133
240
  /**
134
241
  * Previously the transformer would insert provenance always pointing to the "target" relationship.
135
242
  * It should (and now by default does) instead insert provenance pointing to the provenanceSource
@@ -146,7 +253,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
146
253
  this._startingChangesetIndices = undefined;
147
254
  this._cachedSynchronizationVersion = undefined;
148
255
  this._targetClassNameToClassIdCache = new Map();
149
- // if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
256
+ // if undefined, it can be initialized by calling [[this.processChangesets]]
150
257
  this._hasElementChangedCache = undefined;
151
258
  this._deletedSourceRelationshipData = undefined;
152
259
  this._yieldManager = new core_bentley_1.YieldManager();
@@ -155,9 +262,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
155
262
  this._longNamedSchemasMap = new Map();
156
263
  /** state to prevent reinitialization, @see [[initialize]] */
157
264
  this._initialized = false;
158
- /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
159
- this._changeSummaryIds = undefined;
160
265
  this._sourceChangeDataState = "uninited";
266
+ /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
267
+ this._csFileProps = undefined;
161
268
  /** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
162
269
  this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
163
270
  // initialize IModelTransformOptions
@@ -167,9 +274,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
167
274
  cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
168
275
  targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
169
276
  // eslint-disable-next-line deprecation/deprecation
170
- danglingReferencesBehavior: options?.danglingReferencesBehavior ?? options?.danglingPredecessorsBehavior ?? "reject",
277
+ danglingReferencesBehavior: options?.danglingReferencesBehavior ?? "reject",
278
+ branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
171
279
  };
172
- this._isFirstSynchronization = this._options.wasSourceIModelCopiedToTarget ? true : undefined;
280
+ this._isProvenanceInitTransform = this._options
281
+ .wasSourceIModelCopiedToTarget
282
+ ? true
283
+ : undefined;
173
284
  // initialize exporter and sourceDb
174
285
  if (source instanceof core_backend_1.IModelDb) {
175
286
  this.exporter = new IModelExporter_1.IModelExporter(source);
@@ -180,7 +291,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
180
291
  this.sourceDb = this.exporter.sourceDb;
181
292
  this.exporter.registerHandler(this);
182
293
  this.exporter.wantGeometry = options?.loadSourceGeometry ?? false; // optimization to not load source GeometryStreams by default
183
- if (!this._options.includeSourceProvenance) { // clone provenance from the source iModel into the target iModel?
294
+ if (!this._options.includeSourceProvenance) {
295
+ // clone provenance from the source iModel into the target iModel?
184
296
  IModelTransformer.provenanceElementClasses.forEach((cls) => this.exporter.excludeElementClass(cls.classFullName));
185
297
  IModelTransformer.provenanceElementAspectClasses.forEach((cls) => this.exporter.excludeElementAspectClass(cls.classFullName));
186
298
  }
@@ -188,26 +300,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
188
300
  this.exporter.excludeElementAspectClass("BisCore:TextAnnotationData"); // This ElementAspect is auto-created by the BisCore:TextAnnotation2d/3d element handlers
189
301
  // initialize importer and targetDb
190
302
  if (target instanceof core_backend_1.IModelDb) {
191
- this.importer = new IModelImporter_1.IModelImporter(target, { preserveElementIdsForFiltering: this._options.preserveElementIdsForFiltering });
303
+ this.importer = new IModelImporter_1.IModelImporter(target, {
304
+ preserveElementIdsForFiltering: this._options.preserveElementIdsForFiltering,
305
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
306
+ });
192
307
  }
193
308
  else {
194
309
  this.importer = target;
195
- /* eslint-disable deprecation/deprecation */
196
- if (Boolean(this._options.preserveElementIdsForFiltering) !== this.importer.preserveElementIdsForFiltering) {
197
- core_bentley_1.Logger.logWarning(loggerCategory, [
198
- "A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.",
199
- "The custom importer target's option will be force updated to use the transformer's value.",
200
- "This behavior is deprecated and will be removed in a future version, throwing an error if they are out of sync.",
201
- ].join("\n"));
202
- this.importer.preserveElementIdsForFiltering = Boolean(this._options.preserveElementIdsForFiltering);
203
- }
204
- /* eslint-enable deprecation/deprecation */
310
+ this.validateSharedOptionsMatch();
205
311
  }
206
312
  this.targetDb = this.importer.targetDb;
207
313
  // create the IModelCloneContext, it must be initialized later
208
314
  this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
209
315
  if (this.sourceDb.isBriefcase && this.targetDb.isBriefcase) {
210
- nodeAssert(this.sourceDb.changeset.index !== undefined && this.targetDb.changeset.index !== undefined, "database has no changeset index");
316
+ nodeAssert(this.sourceDb.changeset.index !== undefined &&
317
+ this.targetDb.changeset.index !== undefined, "database has no changeset index");
211
318
  this._startingChangesetIndices = {
212
319
  target: this.targetDb.changeset.index,
213
320
  source: this.sourceDb.changeset.index,
@@ -215,12 +322,27 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
215
322
  }
216
323
  // this internal is guaranteed stable for just transformer usage
217
324
  /* eslint-disable @itwin/no-internal */
218
- if ("codeValueBehavior" in this.sourceDb) {
325
+ if (("codeValueBehavior" in this.sourceDb)) {
219
326
  this.sourceDb.codeValueBehavior = "exact";
220
327
  this.targetDb.codeValueBehavior = "exact";
221
328
  }
222
329
  /* eslint-enable @itwin/no-internal */
223
330
  }
331
+ /** validates that the importer set on the transformer has the same values for its shared options as the transformer.
332
+ * @note This expects that the importer is already set on the transformer.
333
+ */
334
+ validateSharedOptionsMatch() {
335
+ if (Boolean(this._options.preserveElementIdsForFiltering) !==
336
+ this.importer.options.preserveElementIdsForFiltering) {
337
+ const errMessage = "A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.";
338
+ throw new Error(errMessage);
339
+ }
340
+ if (Boolean(this._options.skipPropagateChangesToRootElements) !==
341
+ this.importer.options.skipPropagateChangesToRootElements) {
342
+ const errMessage = "A custom importer was passed as a target but its 'skipPropagateChangesToRootElements' option is out of sync with the transformer's option.";
343
+ throw new Error(errMessage);
344
+ }
345
+ }
224
346
  /** Dispose any native resources associated with this IModelTransformer. */
225
347
  dispose() {
226
348
  core_bentley_1.Logger.logTrace(loggerCategory, "dispose()");
@@ -238,32 +360,41 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
238
360
  core_bentley_1.Logger.logInfo(loggerCategory, `this._includeSourceProvenance=${this._options.includeSourceProvenance}`);
239
361
  core_bentley_1.Logger.logInfo(loggerCategory, `this._cloneUsingBinaryGeometry=${this._options.cloneUsingBinaryGeometry}`);
240
362
  core_bentley_1.Logger.logInfo(loggerCategory, `this._wasSourceIModelCopiedToTarget=${this._options.wasSourceIModelCopiedToTarget}`);
241
- core_bentley_1.Logger.logInfo(loggerCategory, `this._isReverseSynchronization=${this._options.isReverseSynchronization}`);
242
- 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)}`);
243
367
  core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.simplifyElementGeometry=${this.importer.options.simplifyElementGeometry}`);
244
368
  }
245
369
  /** Return the IModelDb where IModelTransformer will store its provenance.
246
370
  * @note This will be [[targetDb]] except when it is a reverse synchronization. In that case it be [[sourceDb]].
247
371
  */
248
372
  get provenanceDb() {
249
- return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
373
+ return this.isReverseSynchronization ? this.sourceDb : this.targetDb;
250
374
  }
251
375
  /** Return the IModelDb where IModelTransformer looks for entities referred to by stored provenance.
252
376
  * @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
253
377
  */
254
378
  get provenanceSourceDb() {
255
- return this._options.isReverseSynchronization ? this.targetDb : this.sourceDb;
379
+ return this.isReverseSynchronization ? this.targetDb : this.sourceDb;
256
380
  }
257
381
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
258
382
  static initElementProvenanceOptions(sourceElementId, targetElementId, args) {
259
- const elementId = args.isReverseSynchronization ? sourceElementId : targetElementId;
383
+ const elementId = args.isReverseSynchronization
384
+ ? sourceElementId
385
+ : targetElementId;
260
386
  const version = args.isReverseSynchronization
261
387
  ? args.targetDb.elements.queryLastModifiedTime(targetElementId)
262
388
  : args.sourceDb.elements.queryLastModifiedTime(sourceElementId);
263
- const aspectIdentifier = args.isReverseSynchronization ? targetElementId : sourceElementId;
389
+ const aspectIdentifier = args.isReverseSynchronization
390
+ ? targetElementId
391
+ : sourceElementId;
264
392
  const aspectProps = {
265
393
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
266
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
394
+ element: {
395
+ id: elementId,
396
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
397
+ },
267
398
  scope: { id: args.targetScopeElementId },
268
399
  identifier: aspectIdentifier,
269
400
  kind: core_backend_1.ExternalSourceAspect.Kind.Element,
@@ -272,9 +403,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
272
403
  return aspectProps;
273
404
  }
274
405
  static initRelationshipProvenanceOptions(sourceRelInstanceId, targetRelInstanceId, args) {
275
- const provenanceDb = args.isReverseSynchronization ? args.sourceDb : args.targetDb;
276
- const aspectIdentifier = args.isReverseSynchronization ? targetRelInstanceId : sourceRelInstanceId;
277
- const provenanceRelInstanceId = args.isReverseSynchronization ? sourceRelInstanceId : targetRelInstanceId;
406
+ const provenanceDb = args.isReverseSynchronization
407
+ ? args.sourceDb
408
+ : args.targetDb;
409
+ const aspectIdentifier = args.isReverseSynchronization
410
+ ? targetRelInstanceId
411
+ : sourceRelInstanceId;
412
+ const provenanceRelInstanceId = args.isReverseSynchronization
413
+ ? sourceRelInstanceId
414
+ : targetRelInstanceId;
278
415
  const elementId = provenanceDb.withPreparedStatement("SELECT SourceECInstanceId FROM bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
279
416
  stmt.bindId(1, provenanceRelInstanceId);
280
417
  nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
@@ -285,7 +422,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
285
422
  : { provenanceRelInstanceId };
286
423
  const aspectProps = {
287
424
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
288
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
425
+ element: {
426
+ id: elementId,
427
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
428
+ },
289
429
  scope: { id: args.targetScopeElementId },
290
430
  identifier: aspectIdentifier,
291
431
  kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
@@ -296,7 +436,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
296
436
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
297
437
  initElementProvenance(sourceElementId, targetElementId) {
298
438
  return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
299
- isReverseSynchronization: !!this._options.isReverseSynchronization,
439
+ isReverseSynchronization: this.isReverseSynchronization,
300
440
  targetScopeElementId: this.targetScopeElementId,
301
441
  sourceDb: this.sourceDb,
302
442
  targetDb: this.targetDb,
@@ -311,36 +451,35 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
311
451
  return IModelTransformer.initRelationshipProvenanceOptions(sourceRelationship.id, targetRelInstanceId, {
312
452
  sourceDb: this.sourceDb,
313
453
  targetDb: this.targetDb,
314
- isReverseSynchronization: !!this._options.isReverseSynchronization,
454
+ isReverseSynchronization: this.isReverseSynchronization,
315
455
  targetScopeElementId: this.targetScopeElementId,
316
456
  forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
317
457
  });
318
458
  }
319
- /** the changeset in the scoping element's source version found for this transformation
320
- * @note: the version depends on whether this is a reverse synchronization or not, as
321
- * it is stored separately for both synchronization directions.
322
- * @note: must call [[initScopeProvenance]] before using this property.
323
- * @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.
324
462
  */
325
- get _synchronizationVersion() {
326
- if (!this._cachedSynchronizationVersion) {
327
- nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
328
- const version = this._options.isReverseSynchronization
329
- ? this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion
330
- : this._targetScopeProvenanceProps.version;
331
- nodeAssert(version !== undefined, "no version contained in target scope");
332
- const [id, index] = version === ""
333
- ? ["", -1]
334
- : version.split(";");
335
- this._cachedSynchronizationVersion = { index: Number(index), id };
336
- 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");
337
466
  }
338
- 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;
339
476
  }
340
477
  /** the changeset in the scoping element's source version found for this transformation
341
- * @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
342
479
  * it is stored separately for both synchronization directions.
343
- * @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"
344
483
  */
345
484
  get synchronizationVersion() {
346
485
  if (this._cachedSynchronizationVersion === undefined) {
@@ -348,13 +487,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
348
487
  if (!provenanceScopeAspect) {
349
488
  return { index: -1, id: "" }; // first synchronization.
350
489
  }
351
- const version = this._options.isReverseSynchronization
490
+ const version = this.isReverseSynchronization
352
491
  ? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
353
492
  : provenanceScopeAspect.version;
354
- if (!version) {
493
+ if (!version &&
494
+ this._options.branchRelationshipDataBehavior === "unsafe-migrate") {
355
495
  return { index: -1, id: "" }; // previous synchronization was done before fed guid update.
356
496
  }
357
- 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(";");
358
502
  if (Number.isNaN(Number(index)))
359
503
  throw new Error("Could not parse version data from scope aspect");
360
504
  this._cachedSynchronizationVersion = { index: Number(index), id }; // synchronization version found and cached.
@@ -366,15 +510,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
366
510
  * Provenance scope aspect is created and inserted into provenanceDb when [[initScopeProvenance]] is invoked.
367
511
  */
368
512
  tryGetProvenanceScopeAspect() {
369
- const scopeProvenanceAspectId = this.queryScopeExternalSource({
513
+ const scopeProvenanceAspectProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, {
514
+ id: undefined,
370
515
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
371
516
  scope: { id: core_common_1.IModel.rootSubjectId },
372
517
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
373
518
  element: { id: this.targetScopeElementId ?? core_common_1.IModel.rootSubjectId },
374
519
  identifier: this.provenanceSourceDb.iModelId,
375
520
  });
376
- return scopeProvenanceAspectId.aspectId
377
- ? this.provenanceDb.elements.getAspect(scopeProvenanceAspectId.aspectId)
521
+ return scopeProvenanceAspectProps !== undefined
522
+ ? this.provenanceDb.elements.getAspect(scopeProvenanceAspectProps.aspectId)
378
523
  : undefined;
379
524
  }
380
525
  /**
@@ -388,19 +533,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
388
533
  id: undefined,
389
534
  version: undefined,
390
535
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
391
- element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
392
- scope: { id: core_common_1.IModel.rootSubjectId },
536
+ element: {
537
+ id: this.targetScopeElementId,
538
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
539
+ },
540
+ scope: { id: core_common_1.IModel.rootSubjectId }, // the root Subject scopes scope elements
393
541
  identifier: this.provenanceSourceDb.iModelId,
394
542
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
395
543
  jsonProperties: undefined,
396
544
  };
397
- // FIXME: handle older transformed iModels which do NOT have the version. Add test where we don't set those and then start setting them.
398
- // or reverseSyncVersion set correctly
399
- const externalSource = this.queryScopeExternalSource(aspectProps, { getJsonProperties: true }); // this query includes "identifier"
400
- aspectProps.id = externalSource.aspectId;
401
- aspectProps.version = externalSource.version;
402
- aspectProps.jsonProperties = externalSource.jsonProperties ? JSON.parse(externalSource.jsonProperties) : {};
403
- if (undefined === aspectProps.id) {
545
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps); // this query includes "identifier"
546
+ if (foundEsaProps === undefined) {
404
547
  aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
405
548
  aspectProps.jsonProperties = {
406
549
  pendingReverseSyncChangesetIndices: [],
@@ -426,44 +569,85 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
426
569
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
427
570
  }
428
571
  if (!this._options.noProvenance) {
429
- this.provenanceDb.elements.insertAspect({
572
+ const id = this.provenanceDb.elements.insertAspect({
430
573
  ...aspectProps,
431
574
  jsonProperties: JSON.stringify(aspectProps.jsonProperties),
432
575
  });
576
+ aspectProps.id = id;
577
+ // Busting a potential cached version
578
+ this.clearCachedSynchronizationVersion();
433
579
  }
434
580
  }
435
- this._targetScopeProvenanceProps = aspectProps;
436
- }
437
- /**
438
- * @returns the id and version of an aspect with the given element, scope, kind, and identifier
439
- * May also return a reverseSyncVersion from json properties if requested
440
- */
441
- queryScopeExternalSource(aspectProps, { getJsonProperties = false } = {}) {
442
- const sql = `
443
- SELECT ECInstanceId, Version
444
- ${getJsonProperties ? ", JsonProperties" : ""}
445
- FROM ${core_backend_1.ExternalSourceAspect.classFullName}
446
- WHERE Element.Id=:elementId
447
- AND Scope.Id=:scopeId
448
- AND Kind=:kind
449
- AND Identifier=:identifier
450
- LIMIT 1
451
- `;
452
- const emptyResult = { aspectId: undefined, version: undefined, jsonProperties: undefined };
453
- return this.provenanceDb.withPreparedStatement(sql, (statement) => {
454
- statement.bindId("elementId", aspectProps.element.id);
455
- if (aspectProps.scope === undefined)
456
- return emptyResult; // return undefined instead of binding an invalid id
457
- statement.bindId("scopeId", aspectProps.scope.id);
458
- statement.bindString("kind", aspectProps.kind);
459
- statement.bindString("identifier", aspectProps.identifier);
460
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
461
- return emptyResult;
462
- const aspectId = statement.getValue(0).getId();
463
- const version = statement.getValue(1).getString();
464
- const jsonProperties = getJsonProperties ? statement.getValue(2).getString() : undefined;
465
- return { aspectId, version, jsonProperties };
466
- });
581
+ else {
582
+ // foundEsaProps is defined.
583
+ aspectProps.id = foundEsaProps.aspectId;
584
+ aspectProps.version = foundEsaProps.version;
585
+ aspectProps.jsonProperties = foundEsaProps.jsonProperties
586
+ ? JSON.parse(foundEsaProps.jsonProperties)
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
+ }
599
+ }
600
+ this._targetScopeProvenanceProps =
601
+ aspectProps;
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;
467
651
  }
468
652
  /**
469
653
  * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
@@ -477,13 +661,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
477
661
  if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
478
662
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
479
663
  }
480
- const sourceDb = args.isReverseSynchronization ? args.provenanceDb : args.provenanceSourceDb;
481
- const targetDb = args.isReverseSynchronization ? args.provenanceSourceDb : args.provenanceDb;
664
+ const sourceDb = args.isReverseSynchronization
665
+ ? args.provenanceDb
666
+ : args.provenanceSourceDb;
667
+ const targetDb = args.isReverseSynchronization
668
+ ? args.provenanceSourceDb
669
+ : args.provenanceDb;
482
670
  // query for provenanceDb
483
671
  const elementIdByFedGuidQuery = `
484
672
  SELECT e.ECInstanceId, FederationGuid
485
673
  FROM bis.Element e
486
- WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
674
+ ${args.skipPropagateChangesToRootElements
675
+ ? "WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements"
676
+ : ""}
487
677
  ORDER BY FederationGuid
488
678
  `;
489
679
  // iterate through sorted list of fed guids from both dbs to get the intersection
@@ -501,22 +691,22 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
501
691
  // and the fact that '0' < '9' < a' < 'f' in ascii/utf8
502
692
  while (true) {
503
693
  const currSourceRow = sourceRow, currTargetRow = targetRow;
504
- if (currSourceRow.federationGuid !== undefined
505
- && currTargetRow.federationGuid !== undefined
506
- && currSourceRow.federationGuid === currTargetRow.federationGuid) {
694
+ if (currSourceRow.federationGuid !== undefined &&
695
+ currTargetRow.federationGuid !== undefined &&
696
+ currSourceRow.federationGuid === currTargetRow.federationGuid) {
507
697
  // data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
508
698
  args.fn(sourceRow.id, targetRow.id);
509
699
  }
510
- if (currTargetRow.federationGuid === undefined
511
- || (currSourceRow.federationGuid !== undefined
512
- && currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
700
+ if (currTargetRow.federationGuid === undefined ||
701
+ (currSourceRow.federationGuid !== undefined &&
702
+ currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
513
703
  if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
514
704
  return;
515
705
  targetRow = targetStmt.getRow();
516
706
  }
517
- if (currSourceRow.federationGuid === undefined
518
- || (currTargetRow.federationGuid !== undefined
519
- && currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
707
+ if (currSourceRow.federationGuid === undefined ||
708
+ (currTargetRow.federationGuid !== undefined &&
709
+ currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
520
710
  if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
521
711
  return;
522
712
  sourceRow = sourceStmt.getRow();
@@ -534,7 +724,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
534
724
  // victims of the old provenance method that have both fedguids and an inserted aspect.
535
725
  // But this is a private function with one known caller where that doesn't matter
536
726
  args.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
537
- const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization ? args.fn(sourceId, targetId) : args.fn(targetId, sourceId);
727
+ const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization
728
+ ? args.fn(sourceId, targetId)
729
+ : args.fn(targetId, sourceId);
538
730
  stmt.bindId("scopeId", args.targetScopeElementId);
539
731
  stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
540
732
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
@@ -550,240 +742,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
550
742
  provenanceSourceDb: this.provenanceSourceDb,
551
743
  provenanceDb: this.provenanceDb,
552
744
  targetScopeElementId: this.targetScopeElementId,
553
- isReverseSynchronization: !!this._options.isReverseSynchronization,
745
+ isReverseSynchronization: this.isReverseSynchronization,
554
746
  fn,
747
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
555
748
  });
556
749
  }
557
- /** Initialize the source to target Element mapping from ExternalSourceAspects in the target iModel.
558
- * @note This method is called from all `process*` functions and should never need to be called directly.
559
- * @deprecated in 3.x. call [[initialize]] instead, it does the same thing among other initialization
560
- * @note Passing an [[InitFromExternalSourceAspectsArgs]] is required when processing changes, to remap any elements that may have been deleted.
561
- * You must await the returned promise as well in this case. The synchronous behavior has not changed but is deprecated and won't process everything.
562
- */
563
- initFromExternalSourceAspects(args) {
564
- this.forEachTrackedElement((sourceElementId, targetElementId) => {
565
- this.context.remapElement(sourceElementId, targetElementId);
566
- });
567
- if (args)
568
- return this.remapDeletedSourceEntities();
569
- }
570
750
  /**
571
- * Scan changesets for deleted entities, if in a reverse synchronization, provenance has
572
- * already been deleted, so we must scan for that as well.
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
573
756
  */
574
- async remapDeletedSourceEntities() {
575
- // we need a connected iModel with changes to remap elements with deletions
576
- const notConnectedModel = this.sourceDb.iTwinId === undefined;
577
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
578
- if (notConnectedModel || noChanges)
579
- return;
580
- this._deletedSourceRelationshipData = new Map();
581
- nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
582
- if (this._changeSummaryIds.length === 0)
583
- return;
584
- const alreadyImportedElementInserts = new Set();
585
- const alreadyImportedModelInserts = new Set();
586
- this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
587
- const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
588
- if (core_bentley_1.Id64.isValid(targetElementId))
589
- alreadyImportedElementInserts.add(targetElementId);
590
- });
591
- this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
592
- const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
593
- if (core_bentley_1.Id64.isValid(targetModelId))
594
- alreadyImportedModelInserts.add(targetModelId);
595
- });
596
- // optimization: if we have provenance, use it to avoid more querying later
597
- // eventually when itwin.js supports attaching a second iModelDb in JS,
598
- // this won't have to be a conditional part of the query, and we can always have it by attaching
599
- const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
600
- const deletedEntitySql = `
601
- SELECT
602
- 1 AS IsElemNotRel,
603
- ic.ChangedInstance.Id AS InstanceId,
604
- NULL AS InstId2, -- need these columns for relationship ends in the unioned query
605
- NULL AS InstId3,
606
- ec.FederationGuid AS FedGuid,
607
- NULL AS FedGuid2,
608
- ic.ChangedInstance.ClassId AS ClassId
609
- ${queryCanAccessProvenance ? `
610
- /*
611
- -- can't coalesce these due to a bug, so do it in JS
612
- , coalesce(
613
- IIF(esa.Scope.Id=:targetScopeElement, esa.Identifier, NULL),
614
- IIF(esac.Scope.Id=:targetScopeElement, esac.Identifier, NULL)
615
- ) AS Identifier1
616
- */
617
- , CASE WHEN esa.Scope.Id = ${this.targetScopeElementId} THEN esa.Identifier ELSE NULL END AS Identifier1A
618
- -- FIXME: using :targetScopeElement parameter in this second potential identifier breaks ecsql
619
- , CASE WHEN esac.Scope.Id = ${this.targetScopeElementId} THEN esac.Identifier ELSE NULL END AS Identifier1B
620
- , NULL AS Identifier2A
621
- , NULL AS Identifier2B
622
- ` : ""}
623
- FROM ecchange.change.InstanceChange ic
624
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
625
- ON ic.ChangedInstance.Id=ec.ECInstanceId
626
- ${queryCanAccessProvenance ? `
627
- LEFT JOIN bis.ExternalSourceAspect esa
628
- ON ec.ECInstanceId=esa.Element.Id
629
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
630
- ON ec.ECInstanceId=esac.Element.Id
631
- ` : ""}
632
- WHERE ic.OpCode=:opDelete
633
- AND ic.Summary.Id=:changeSummaryId
634
- AND ic.ChangedInstance.ClassId IS (BisCore.Element)
635
-
636
- UNION ALL
637
-
638
- SELECT
639
- 0 AS IsElemNotRel,
640
- ic.ChangedInstance.Id AS InstanceId,
641
- coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
642
- coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
643
- coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
644
- coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
645
- ic.ChangedInstance.ClassId AS ClassId
646
- ${queryCanAccessProvenance ? `
647
- , sesa.Identifier AS Identifier1A
648
- , sesac.Identifier AS Identifier1B
649
- , tesa.Identifier AS Identifier2A
650
- , tesac.Identifier AS Identifier2B
651
- ` : ""}
652
- FROM ecchange.change.InstanceChange ic
653
- LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
654
- ON ic.ChangedInstance.Id=ertec.ECInstanceId
655
- -- FIXME: test a deletion of both an element and a relationship at the same time
656
- LEFT JOIN bis.Element se
657
- ON se.ECInstanceId=ertec.SourceECInstanceId
658
- LEFT JOIN bis.Element te
659
- ON te.ECInstanceId=ertec.TargetECInstanceId
660
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
661
- ON sec.ECInstanceId=ertec.SourceECInstanceId
662
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
663
- ON tec.ECInstanceId=ertec.TargetECInstanceId
664
- ${queryCanAccessProvenance ? `
665
- -- NOTE: need to join on both se/te and sec/tec incase the element was deleted
666
- LEFT JOIN bis.ExternalSourceAspect sesa
667
- ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
668
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
669
- ON sec.ECInstanceId=sesac.Element.Id
670
- LEFT JOIN bis.ExternalSourceAspect tesa
671
- ON te.ECInstanceId=tesa.Element.Id
672
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
673
- ON tec.ECInstanceId=tesac.Element.Id
674
- ` : ""}
675
- WHERE ic.OpCode=:opDelete
676
- AND ic.Summary.Id=:changeSummaryId
677
- AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
678
- ${queryCanAccessProvenance ? `
679
- AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
680
- AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
681
- AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
682
- AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
683
- AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
684
- AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
685
- AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
686
- AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
687
- ` : ""}
688
- `;
689
- for (const changeSummaryId of this._changeSummaryIds) {
690
- this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
691
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
692
- if (queryCanAccessProvenance)
693
- stmt.bindId("targetScopeElement", this.targetScopeElementId);
694
- stmt.bindId("changeSummaryId", changeSummaryId);
695
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
696
- const isElemNotRel = stmt.getValue(0).getBoolean();
697
- const instId = stmt.getValue(1).getId();
698
- if (isElemNotRel) {
699
- const sourceElemFedGuid = stmt.getValue(4).getGuid();
700
- // "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
701
- let identifierValue;
702
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
703
- if (queryCanAccessProvenance) {
704
- identifierValue = stmt.getValue(7);
705
- if (identifierValue.isNull)
706
- identifierValue = stmt.getValue(8);
707
- }
708
- // TODO: if I could attach the second db, will probably be much faster to get target id
709
- // as part of the whole query rather than with _queryElemIdByFedGuid
710
- const targetId = (queryCanAccessProvenance && identifierValue
711
- && !identifierValue.isNull
712
- && identifierValue.getString())
713
- // maybe batching these queries would perform better but we should
714
- // try to attach the second db and query both together anyway
715
- || (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
716
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
717
- || this._queryProvenanceForElement(instId);
718
- // since we are processing one changeset at a time, we can see local source deletes
719
- // of entities that were never synced and can be safely ignored
720
- const deletionNotInTarget = !targetId;
721
- if (deletionNotInTarget)
722
- continue;
723
- this.context.remapElement(instId, targetId);
724
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
725
- // In such case an entity update will be triggered and we no longer need to delete the entity.
726
- if (alreadyImportedElementInserts.has(targetId)) {
727
- this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
728
- }
729
- if (alreadyImportedModelInserts.has(targetId)) {
730
- this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
731
- }
732
- }
733
- else { // is deleted relationship
734
- const classFullName = stmt.getValue(6).getClassNameForClassId();
735
- const [sourceIdInTarget, targetIdInTarget] = [
736
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
737
- { guidColumn: 4, identifierColumns: { a: 7, b: 8 }, isTarget: false },
738
- { guidColumn: 5, identifierColumns: { a: 9, b: 10 }, isTarget: true },
739
- ].map(({ guidColumn, identifierColumns }) => {
740
- const fedGuid = stmt.getValue(guidColumn).getGuid();
741
- let identifierValue;
742
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
743
- if (queryCanAccessProvenance) {
744
- identifierValue = stmt.getValue(identifierColumns.a);
745
- if (identifierValue.isNull)
746
- identifierValue = stmt.getValue(identifierColumns.b);
747
- }
748
- return ((queryCanAccessProvenance && identifierValue
749
- // FIXME: this is really far from idiomatic, try to undo that
750
- && !identifierValue.isNull
751
- && identifierValue.getString())
752
- // maybe batching these queries would perform better but we should
753
- // try to attach the second db and query both together anyway
754
- || (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
755
- });
756
- // since we are processing one changeset at a time, we can see local source deletes
757
- // of entities that were never synced and can be safely ignored
758
- if (sourceIdInTarget && targetIdInTarget) {
759
- this._deletedSourceRelationshipData.set(instId, {
760
- classFullName,
761
- sourceIdInTarget,
762
- targetIdInTarget,
763
- });
764
- }
765
- else {
766
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
767
- const relProvenance = this._queryProvenanceForRelationship(instId, {
768
- classFullName,
769
- sourceId: stmt.getValue(2).getId(),
770
- targetId: stmt.getValue(3).getId(),
771
- });
772
- if (relProvenance && relProvenance.relationshipId)
773
- this._deletedSourceRelationshipData.set(instId, {
774
- classFullName,
775
- relId: relProvenance.relationshipId,
776
- provenanceAspectId: relProvenance.aspectId,
777
- });
778
- }
779
- }
780
- }
781
- // NEXT: remap sourceId and targetId to target, get provenance there
782
- // NOTE: it is possible during a forward sync for the target to already have deleted
783
- // something that the source deleted, in which case we can safely ignore the gone provenance
784
- });
785
- }
786
- }
787
757
  _queryProvenanceForElement(entityInProvenanceSourceId) {
788
758
  return this.provenanceDb.withPreparedStatement(`
789
759
  SELECT esa.Element.Id
@@ -801,6 +771,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
801
771
  return undefined;
802
772
  });
803
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
+ */
804
781
  _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
805
782
  return this.provenanceDb.withPreparedStatement(`
806
783
  SELECT
@@ -833,7 +810,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
833
810
  sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
834
811
  targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
835
812
  };
836
- if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
813
+ if (targetRelInfo.sourceId === undefined ||
814
+ targetRelInfo.targetId === undefined)
837
815
  return undefined; // couldn't find an element, rel is invalid or deleted
838
816
  return this.targetDb.withPreparedStatement(`
839
817
  SELECT ECInstanceId
@@ -867,7 +845,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
867
845
  JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
868
846
  WHERE s.Name=? AND c.Name=?
869
847
  `, (stmt) => {
870
- const [schemaName, className] = classFullName.split(".");
848
+ const [schemaName, className] = classFullName.indexOf(".") !== -1
849
+ ? classFullName.split(".")
850
+ : classFullName.split(":");
871
851
  stmt.bindString(1, schemaName);
872
852
  stmt.bindString(2, className);
873
853
  if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
@@ -885,26 +865,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
885
865
  });
886
866
  }
887
867
  /** Returns `true` if *brute force* delete detections should be run.
868
+ * @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
888
869
  * @note Not relevant for processChanges when change history is known.
889
870
  */
890
871
  shouldDetectDeletes() {
891
- // FIXME: all synchronizations should mark this as false, but we can probably change this
892
- // to just follow the new deprecated option
893
- if (this._isFirstSynchronization)
894
- return false; // not necessary the first time since there are no deletes to detect
895
- if (this._options.isReverseSynchronization)
896
- return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
897
- // FIXME: do any tests fail? if not, consider using @see _isSynchronization
898
- if (this._isForwardSynchronization)
899
- return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
900
- return true;
872
+ nodeAssert(this._syncType !== undefined);
873
+ return this._syncType === "not-sync";
901
874
  }
902
875
  /**
903
876
  * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
904
877
  * in the source iModel.
905
878
  * @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
906
879
  * This method is only called during [[processAll]] when the option
907
- * [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
880
+ * [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
908
881
  * necessary when using [[processChanges]] since changeset information is sufficient.
909
882
  * @note you do not need to call this directly unless processing a subset of an iModel.
910
883
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
@@ -916,18 +889,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
916
889
  WHERE Scope.Id=:scopeId
917
890
  AND Kind=:kind
918
891
  `;
919
- nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
892
+ nodeAssert(!this.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
920
893
  this.provenanceDb.withPreparedStatement(sql, (stmt) => {
921
894
  stmt.bindId("scopeId", this.targetScopeElementId);
922
895
  stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
923
896
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
924
897
  // ExternalSourceAspect.Identifier is of type string
925
898
  const aspectIdentifier = stmt.getValue(0).getString();
926
- if (!core_bentley_1.Id64.isId64(aspectIdentifier)) {
899
+ if (!core_bentley_1.Id64.isValidId64(aspectIdentifier)) {
927
900
  continue;
928
901
  }
929
902
  const targetElemId = stmt.getValue(1).getId();
930
- const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
903
+ const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
904
+ entityReference: `e${aspectIdentifier}`,
905
+ });
931
906
  if (wasDeletedInSource)
932
907
  this.importer.deleteElement(targetElemId);
933
908
  }
@@ -937,7 +912,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
937
912
  * @deprecated in 3.x, this no longer has any effect except emitting a warning
938
913
  */
939
914
  skipElement(_sourceElement) {
940
- core_bentley_1.Logger.logWarning(loggerCategory, `Tried to defer/skip an element, which is no longer necessary`);
915
+ core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
941
916
  }
942
917
  /** Transform the specified sourceElement into ElementProps for the target iModel.
943
918
  * @param sourceElement The Element from the source iModel to transform.
@@ -948,6 +923,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
948
923
  onTransformElement(sourceElement) {
949
924
  core_bentley_1.Logger.logTrace(loggerCategory, `onTransformElement(${sourceElement.id}) "${sourceElement.getDisplayLabel()}"`);
950
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
+ }
951
940
  if (sourceElement instanceof core_backend_1.Subject) {
952
941
  if (targetElementProps.jsonProperties?.Subject?.Job) {
953
942
  // don't propagate source channels into target (legacy bridge case)
@@ -956,52 +945,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
956
945
  }
957
946
  return targetElementProps;
958
947
  }
959
- // TODO: this is a PoC, see if we minimize memory usage
960
- _cacheSourceChanges() {
961
- nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
962
- this._hasElementChangedCache = new Set();
963
- const query = `
964
- SELECT
965
- ic.ChangedInstance.Id AS InstId
966
- FROM ecchange.change.InstanceChange ic
967
- JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
968
- -- TODO: do relationship entities also need this cache optimization?
969
- WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
970
- AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
971
- -- ignore deleted, we take care of those in remapDeletedSourceEntities
972
- -- include inserted since inserted code-colliding elements should be considered
973
- -- a change so that the colliding element is exported to the target
974
- AND ic.OpCode<>:opDelete
975
- `;
976
- // there is a single mega-query multi-join+coalescing hack that I used originally to get around
977
- // only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
978
- // tables in a join. Need to talk to core about .Changes being able to take a set of changesets
979
- // You can find this version in the `federation-guid-optimization-megaquery` branch
980
- // I wouldn't use it unless we prove via profiling that it speeds things up significantly
981
- // And even then let's first try scanning the raw changesets instead of applying them as these queries
982
- // require
983
- this.sourceDb.withPreparedStatement(query, (stmt) => {
984
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
985
- stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
986
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
987
- const instId = stmt.getValue(0).getId();
988
- this._hasElementChangedCache.add(instId);
989
- }
990
- });
991
- }
992
948
  /** Returns true if a change within sourceElement is detected.
993
949
  * @param sourceElement The Element from the source iModel
994
- * @param targetElementId The Element from the target iModel to compare against.
995
950
  * @note A subclass can override this method to provide custom change detection behavior.
996
951
  */
997
- hasElementChanged(sourceElement, _targetElementId) {
952
+ hasElementChanged(sourceElement) {
998
953
  if (this._sourceChangeDataState === "no-changes")
999
954
  return false;
1000
955
  if (this._sourceChangeDataState === "unconnected")
1001
956
  return true;
1002
957
  nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
1003
- if (this._hasElementChangedCache === undefined)
1004
- this._cacheSourceChanges();
958
+ nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
1005
959
  return this._hasElementChangedCache.has(sourceElement.id);
1006
960
  }
1007
961
  static transformCallbackFor(transformer, entity) {
@@ -1028,8 +982,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1028
982
  const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
1029
983
  const targetProps = onEntityTransform.call(this, sourceEntity);
1030
984
  if (sourceEntity instanceof core_backend_1.Relationship) {
1031
- targetProps.sourceId = this.context.findTargetElementId(sourceEntity.sourceId);
1032
- targetProps.targetId = this.context.findTargetElementId(sourceEntity.targetId);
985
+ targetProps.sourceId =
986
+ this.context.findTargetElementId(sourceEntity.sourceId);
987
+ targetProps.targetId =
988
+ this.context.findTargetElementId(sourceEntity.targetId);
1033
989
  }
1034
990
  updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
1035
991
  this._partiallyCommittedEntities.delete(sourceEntity);
@@ -1045,11 +1001,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1045
1001
  for (const referenceId of entity.getReferenceConcreteIds()) {
1046
1002
  // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
1047
1003
  const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
1048
- const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) || this._skippedEntities.has(referenceId);
1004
+ const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
1005
+ this._skippedEntities.has(referenceId);
1049
1006
  if (alreadyProcessed)
1050
1007
  continue;
1051
1008
  core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
1052
- const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: referenceId });
1009
+ const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
1010
+ entityReference: referenceId,
1011
+ });
1053
1012
  if (!referencedExistsInSource) {
1054
1013
  core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
1055
1014
  switch (this._options.danglingReferencesBehavior) {
@@ -1059,7 +1018,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1059
1018
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
1060
1019
  `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
1061
1020
  "This must have been caused by an upstream application that changed the iModel.",
1062
- "You can set the IModelTransformerOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
1021
+ "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
1063
1022
  "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
1064
1023
  "transformer to remove the reference and fix affected elements may be suitable.",
1065
1024
  ].join("\n"));
@@ -1097,7 +1056,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1097
1056
  /** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel.
1098
1057
  * @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter.
1099
1058
  */
1100
- shouldExportElement(_sourceElement) { return true; }
1059
+ shouldExportElement(_sourceElement) {
1060
+ return true;
1061
+ }
1101
1062
  onSkipElement(sourceElementId) {
1102
1063
  if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
1103
1064
  // element already has provenance
@@ -1128,7 +1089,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1128
1089
  const referenceType = elemClass.requiredReferenceKeyTypeMap[referenceKey];
1129
1090
  // For now we just consider all required references to be elements (as they are in biscore), and do not support
1130
1091
  // entities that refuse to be inserted without a different kind of entity (e.g. aspect or relationship) first being inserted
1131
- (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element || referenceType === core_common_1.ConcreteEntityTypes.Model);
1092
+ (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element ||
1093
+ referenceType === core_common_1.ConcreteEntityTypes.Model);
1132
1094
  return mapId64(idContainer, (id) => {
1133
1095
  if (id === core_bentley_1.Id64.invalid || id === core_common_1.IModel.rootSubjectId)
1134
1096
  return undefined; // not allowed to directly export the root subject
@@ -1137,13 +1099,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1137
1099
  // This is relied upon by the TemplateModelCloner
1138
1100
  // TODO: extract this out to only be in the TemplateModelCloner
1139
1101
  const asDefinitionElem = this.sourceDb.elements.tryGetElement(id, core_backend_1.DefinitionElement);
1140
- if (asDefinitionElem && !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
1102
+ if (asDefinitionElem &&
1103
+ !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
1141
1104
  this.context.remapElement(id, id);
1142
1105
  }
1143
1106
  }
1144
1107
  return id;
1145
- })
1146
- .filter((sourceReferenceId) => {
1108
+ }).filter((sourceReferenceId) => {
1147
1109
  if (sourceReferenceId === undefined)
1148
1110
  return false;
1149
1111
  const referenceInTargetId = this.context.findTargetElementId(sourceReferenceId);
@@ -1171,7 +1133,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1171
1133
  const isSubModeled = dbHasModel(this.sourceDb, elementId);
1172
1134
  const idOfElemInTarget = this.context.findTargetElementId(elementId);
1173
1135
  const isElemInTarget = core_bentley_1.Id64.invalid !== idOfElemInTarget;
1174
- const needsModelImport = isSubModeled && (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
1136
+ const needsModelImport = isSubModeled &&
1137
+ (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
1175
1138
  return { needsElemImport: !isElemInTarget, needsModelImport };
1176
1139
  }
1177
1140
  /** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel.
@@ -1186,27 +1149,33 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1186
1149
  }
1187
1150
  else if (this._options.wasSourceIModelCopiedToTarget) {
1188
1151
  targetElementId = sourceElement.id;
1189
- targetElementProps = this.targetDb.elements.getElementProps(targetElementId);
1152
+ targetElementProps =
1153
+ this.targetDb.elements.getElementProps(targetElementId);
1190
1154
  }
1191
1155
  else {
1192
1156
  targetElementId = this.context.findTargetElementId(sourceElement.id);
1193
1157
  targetElementProps = this.onTransformElement(sourceElement);
1194
1158
  }
1195
1159
  // if an existing remapping was not yet found, check by FederationGuid
1196
- if (this.context.isBetweenIModels && !core_bentley_1.Id64.isValid(targetElementId) && sourceElement.federationGuid !== undefined) {
1197
- targetElementId = this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1160
+ if (this.context.isBetweenIModels &&
1161
+ !core_bentley_1.Id64.isValid(targetElementId) &&
1162
+ sourceElement.federationGuid !== undefined) {
1163
+ targetElementId =
1164
+ this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1198
1165
  if (core_bentley_1.Id64.isValid(targetElementId))
1199
1166
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
1200
1167
  }
1201
1168
  // if an existing remapping was not yet found, check by Code as long as the CodeScope is valid (invalid means a missing reference so not worth checking)
1202
- if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1169
+ if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
1170
+ core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1203
1171
  // respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
1204
1172
  // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
1205
1173
  targetElementProps.code.value = targetElementProps.code.value ?? "";
1206
1174
  const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
1207
1175
  if (undefined !== maybeTargetElementId) {
1208
1176
  const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
1209
- if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1177
+ if (maybeTargetElem.classFullName === targetElementProps.classFullName) {
1178
+ // ensure code remapping doesn't change the target class
1210
1179
  targetElementId = maybeTargetElementId;
1211
1180
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
1212
1181
  }
@@ -1215,14 +1184,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1215
1184
  }
1216
1185
  }
1217
1186
  }
1218
- if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
1187
+ if (!this.hasElementChanged(sourceElement))
1219
1188
  return;
1220
1189
  this.collectUnmappedReferences(sourceElement);
1221
1190
  // targetElementId will be valid (indicating update) or undefined (indicating insert)
1222
- targetElementProps.id
1223
- = core_bentley_1.Id64.isValid(targetElementId)
1224
- ? targetElementId
1225
- : undefined;
1191
+ targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
1192
+ ? targetElementId
1193
+ : undefined;
1226
1194
  if (!this._options.wasSourceIModelCopiedToTarget) {
1227
1195
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1228
1196
  }
@@ -1237,17 +1205,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1237
1205
  // FIXME: verify at finalization time that we don't lose provenance on new elements
1238
1206
  // FIXME: make public and improve `initElementProvenance` API for usage by consolidators
1239
1207
  if (!this._options.noProvenance) {
1240
- let provenance = this._options.forceExternalSourceAspectProvenance || this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1208
+ let provenance = this._options.forceExternalSourceAspectProvenance ||
1209
+ this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1241
1210
  ? undefined
1242
1211
  : sourceElement.federationGuid;
1243
1212
  if (!provenance) {
1244
1213
  const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
1245
- const aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
1246
- if (aspectId === undefined) {
1214
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1215
+ if (foundEsaProps === undefined)
1247
1216
  aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1248
- }
1249
1217
  else {
1250
- aspectProps.id = aspectId;
1218
+ // Since initElementProvenance sets a property 'version' on the aspectProps that we wish to persist in the provenanceDb, only grab the id from the foundEsaProps.
1219
+ aspectProps.id = foundEsaProps.aspectId;
1251
1220
  this.provenanceDb.elements.updateAspect(aspectProps);
1252
1221
  }
1253
1222
  provenance = aspectProps;
@@ -1278,10 +1247,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1278
1247
  * This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel.
1279
1248
  */
1280
1249
  onExportModel(sourceModel) {
1281
- if (core_common_1.IModel.repositoryModelId === sourceModel.id) {
1250
+ if (this._options.skipPropagateChangesToRootElements &&
1251
+ core_common_1.IModel.repositoryModelId === sourceModel.id)
1282
1252
  return; // The RepositoryModel should not be directly imported
1283
- }
1284
1253
  const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
1254
+ // there can only be one repositoryModel per database, so ignore the repo model on remapped subjects
1255
+ const isRemappedRootSubject = sourceModel.id === core_common_1.IModel.repositoryModelId &&
1256
+ targetModeledElementId !== sourceModel.id;
1257
+ if (isRemappedRootSubject)
1258
+ return;
1285
1259
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
1286
1260
  this.importer.importModel(targetModelProps);
1287
1261
  this.resolvePendingReferences(sourceModel);
@@ -1294,18 +1268,36 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1294
1268
  const targetModelId = this.context.findTargetElementId(sourceModelId);
1295
1269
  if (!core_bentley_1.Id64.isValidId64(targetModelId))
1296
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
+ }
1297
1290
  if (this.exporter.sourceDbChanges?.element.deleteIds.has(sourceModelId)) {
1298
- const isDefinitionPartition = this.targetDb.withPreparedStatement(`
1299
- SELECT 1
1300
- FROM bis.DefinitionPartition
1301
- WHERE ECInstanceId=?
1302
- `, (stmt) => {
1303
- stmt.bindId(1, targetModelId);
1291
+ const isDefinitionPartition = this.targetDb.withPreparedStatement(sql, (stmt) => {
1292
+ stmt.bindId("targetModelId", targetModelId);
1304
1293
  const val = stmt.step();
1305
1294
  switch (val) {
1306
- case core_bentley_1.DbResult.BE_SQLITE_ROW: return true;
1307
- case core_bentley_1.DbResult.BE_SQLITE_DONE: return false;
1308
- default: (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
1295
+ case core_bentley_1.DbResult.BE_SQLITE_ROW:
1296
+ return true;
1297
+ case core_bentley_1.DbResult.BE_SQLITE_DONE:
1298
+ return false;
1299
+ default:
1300
+ (0, core_bentley_1.assert)(false, `unexpected db result: '${JSON.stringify(stmt)}'`);
1309
1301
  }
1310
1302
  });
1311
1303
  if (isDefinitionPartition) {
@@ -1319,7 +1311,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1319
1311
  this.importer.deleteModel(targetModelId);
1320
1312
  }
1321
1313
  catch (error) {
1322
- const isDeletionProhibitedErr = error instanceof core_common_1.IModelError && (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited || error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
1314
+ const isDeletionProhibitedErr = error instanceof core_common_1.IModelError &&
1315
+ (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited ||
1316
+ error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
1323
1317
  if (!isDeletionProhibitedErr)
1324
1318
  throw error;
1325
1319
  // Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
@@ -1330,7 +1324,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1330
1324
  }
1331
1325
  /** Schedule modeled partition deletion */
1332
1326
  scheduleModeledPartitionDeletion(sourceModelId) {
1333
- const deletedElements = this.exporter.sourceDbChanges?.element.deleteIds;
1327
+ const deletedElements = this.exporter.sourceDbChanges?.element
1328
+ .deleteIds;
1334
1329
  if (!deletedElements.has(sourceModelId)) {
1335
1330
  deletedElements.add(sourceModelId);
1336
1331
  }
@@ -1396,7 +1391,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1396
1391
  onTransformModel(sourceModel, targetModeledElementId) {
1397
1392
  const targetModelProps = sourceModel.toJSON();
1398
1393
  // don't directly edit deep object since toJSON performs a shallow clone
1399
- targetModelProps.modeledElement = { ...targetModelProps.modeledElement, id: targetModeledElementId };
1394
+ targetModelProps.modeledElement = {
1395
+ ...targetModelProps.modeledElement,
1396
+ id: targetModeledElementId,
1397
+ };
1400
1398
  targetModelProps.id = targetModeledElementId;
1401
1399
  targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
1402
1400
  return targetModelProps;
@@ -1405,10 +1403,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1405
1403
  * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
1406
1404
  */
1407
1405
  async processDeferredElements(_numRetries = 3) { }
1408
- /** called at the end ([[finalizeTransformation]]) of a transformation,
1406
+ /** called at the end of a transformation,
1409
1407
  * updates the target scope element to say that transformation up through the
1410
1408
  * source's changeset has been performed. Also stores all changesets that occurred
1411
- * during the transformation as "pending synchronization changeset indices"
1409
+ * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
1412
1410
  *
1413
1411
  * You generally should not call this function yourself and use [[processChanges]] instead.
1414
1412
  * It is public for unsupported use cases of custom synchronization transforms.
@@ -1416,45 +1414,61 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1416
1414
  * without setting the `force` option to `true`
1417
1415
  */
1418
1416
  updateSynchronizationVersion({ force = false } = {}) {
1419
- if (!force && (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization))
1417
+ const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
1418
+ this._sourceChangeDataState !== "has-changes" &&
1419
+ !this._isProvenanceInitTransform;
1420
+ if (notForcedAndHasNoChangesAndIsntProvenanceInit)
1420
1421
  return;
1421
1422
  nodeAssert(this._targetScopeProvenanceProps);
1422
1423
  const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1423
1424
  const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1424
- if (this._isFirstSynchronization) {
1425
+ if (this._isProvenanceInitTransform) {
1425
1426
  this._targetScopeProvenanceProps.version = sourceVersion;
1426
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = targetVersion;
1427
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1428
+ targetVersion;
1427
1429
  }
1428
- else if (this._options.isReverseSynchronization) {
1430
+ else if (this.isReverseSynchronization) {
1429
1431
  const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1430
1432
  core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1431
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = sourceVersion;
1433
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1434
+ sourceVersion;
1432
1435
  }
1433
- else if (!this._options.isReverseSynchronization) {
1436
+ else if (!this.isReverseSynchronization) {
1434
1437
  core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1435
1438
  this._targetScopeProvenanceProps.version = sourceVersion;
1436
1439
  }
1437
- if (this._isSynchronization) {
1438
- (0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1440
+ if (this._isSynchronization ||
1441
+ (this._startingChangesetIndices && this._isProvenanceInitTransform)) {
1442
+ nodeAssert(this.targetDb.changeset.index !== undefined &&
1443
+ this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1439
1444
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1440
1445
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1441
1446
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1442
- const [syncChangesetsToClear, syncChangesetsToUpdate] = this._isReverseSynchronization
1443
- ? [jsonProps.pendingReverseSyncChangesetIndices, jsonProps.pendingSyncChangesetIndices]
1444
- : [jsonProps.pendingSyncChangesetIndices, jsonProps.pendingReverseSyncChangesetIndices];
1447
+ const pendingSyncChangesetIndicesKey = "pendingSyncChangesetIndices";
1448
+ const pendingReverseSyncChangesetIndicesKey = "pendingReverseSyncChangesetIndices";
1449
+ const [syncChangesetsToClearKey, syncChangesetsToUpdateKey] = this
1450
+ .isReverseSynchronization
1451
+ ? [
1452
+ pendingReverseSyncChangesetIndicesKey,
1453
+ pendingSyncChangesetIndicesKey,
1454
+ ]
1455
+ : [
1456
+ pendingSyncChangesetIndicesKey,
1457
+ pendingReverseSyncChangesetIndicesKey,
1458
+ ];
1445
1459
  // NOTE that as documented in [[processChanges]], this assumes that right after
1446
1460
  // transformation finalization, the work will be saved immediately, otherwise we've
1447
1461
  // just marked this changeset as a synchronization to ignore, and the user can add other
1448
1462
  // stuff to it which would break future synchronizations
1449
- // FIXME: force save for the user to prevent that
1450
1463
  for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1451
- syncChangesetsToUpdate.push(i);
1452
- // FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
1453
- // probably wrong, and we should filter it instead
1454
- 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
+ });
1455
1469
  // if reverse sync then we may have received provenance changes which should be marked as sync changes
1456
- if (this._isReverseSynchronization) {
1457
- nodeAssert(this.sourceDb.changeset.index, "changeset didn't exist");
1470
+ if (this.isReverseSynchronization) {
1471
+ nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
1458
1472
  for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
1459
1473
  jsonProps.pendingReverseSyncChangesetIndices.push(i);
1460
1474
  }
@@ -1465,19 +1479,22 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1465
1479
  ...this._targetScopeProvenanceProps,
1466
1480
  jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1467
1481
  });
1482
+ this.clearCachedSynchronizationVersion();
1468
1483
  }
1469
1484
  // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1470
1485
  finalizeTransformation() {
1471
1486
  this.importer.finalize();
1472
1487
  this.updateSynchronizationVersion();
1473
1488
  if (this._partiallyCommittedEntities.size > 0) {
1474
- // FIXME: throw in this case if danglingReferenceBehavior === reject
1475
- core_bentley_1.Logger.logWarning(loggerCategory, [
1489
+ const message = [
1476
1490
  "The following elements were never fully resolved:",
1477
1491
  [...this._partiallyCommittedEntities.keys()].join(","),
1478
1492
  "This indicates that either some references were excluded from the transformation",
1479
1493
  "or the source has dangling references.",
1480
- ].join("\n"));
1494
+ ].join("\n");
1495
+ if (this._options.danglingReferencesBehavior === "reject")
1496
+ throw new Error(message);
1497
+ core_bentley_1.Logger.logWarning(loggerCategory, message);
1481
1498
  for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
1482
1499
  partiallyCommittedElem.forceComplete();
1483
1500
  }
@@ -1489,7 +1506,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1489
1506
  }
1490
1507
  // this internal is guaranteed stable for just transformer usage
1491
1508
  /* eslint-disable @itwin/no-internal */
1492
- if ("codeValueBehavior" in this.sourceDb) {
1509
+ if (("codeValueBehavior" in this.sourceDb)) {
1493
1510
  this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
1494
1511
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1495
1512
  }
@@ -1506,7 +1523,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1506
1523
  /** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported.
1507
1524
  * @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer).
1508
1525
  */
1509
- shouldExportRelationship(_sourceRelationship) { return true; }
1526
+ shouldExportRelationship(_sourceRelationship) {
1527
+ return true;
1528
+ }
1510
1529
  /** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel.
1511
1530
  * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
1512
1531
  */
@@ -1515,14 +1534,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1515
1534
  const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
1516
1535
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
1517
1536
  const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
1518
- if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1537
+ if (!this._options.noProvenance &&
1538
+ core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1519
1539
  let provenance = !this._options.forceExternalSourceAspectProvenance
1520
1540
  ? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
1521
1541
  : undefined;
1522
1542
  if (!provenance) {
1523
1543
  const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
1524
- aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
1525
- if (undefined === aspectProps.id) {
1544
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1545
+ // onExportRelationship doesn't need to call updateAspect if esaProps were found, because relationship provenance doesn't have the same concept of a version as element provenance (which uses last mod time on the elements).
1546
+ if (undefined === foundEsaProps) {
1526
1547
  aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1527
1548
  }
1528
1549
  provenance = aspectProps;
@@ -1541,14 +1562,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1541
1562
  core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1542
1563
  return;
1543
1564
  }
1544
- const relArg = deletedRelData.relId ?? {
1545
- sourceId: deletedRelData.sourceIdInTarget,
1546
- targetId: deletedRelData.targetIdInTarget,
1547
- };
1548
- // FIXME: make importer.deleteRelationship not need full props
1549
- const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1550
- if (targetRelationship) {
1551
- this.importer.deleteRelationship(targetRelationship.toJSON());
1565
+ const id = deletedRelData.relId ??
1566
+ this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, {
1567
+ sourceId: deletedRelData.sourceIdInTarget,
1568
+ targetId: deletedRelData.targetIdInTarget,
1569
+ })?.id;
1570
+ if (id) {
1571
+ this.importer.deleteRelationship({
1572
+ id,
1573
+ classFullName: deletedRelData.classFullName,
1574
+ });
1552
1575
  }
1553
1576
  if (deletedRelData.provenanceAspectId) {
1554
1577
  try {
@@ -1569,7 +1592,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1569
1592
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
1570
1593
  */
1571
1594
  async detectRelationshipDeletes() {
1572
- if (this._options.isReverseSynchronization) {
1595
+ if (this.isReverseSynchronization) {
1573
1596
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
1574
1597
  }
1575
1598
  const aspectDeleteIds = [];
@@ -1584,14 +1607,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1584
1607
  statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
1585
1608
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
1586
1609
  const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
1587
- if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1610
+ if (undefined ===
1611
+ this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1588
1612
  // this function exists only to support some in-imodel transformations, which must
1589
1613
  // use the old (external source aspect) provenance method anyway so we don't need to support
1590
1614
  // new provenance
1591
1615
  const json = JSON.parse(statement.getValue(2).getString());
1592
- if (undefined !== json.targetRelInstanceId) {
1593
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
1594
- this.importer.deleteRelationship(targetRelationship.toJSON());
1616
+ const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
1617
+ if (targetRelInstanceId) {
1618
+ this.importer.deleteRelationship({
1619
+ id: targetRelInstanceId,
1620
+ classFullName: core_backend_1.ElementRefersToElements.classFullName,
1621
+ });
1595
1622
  }
1596
1623
  aspectDeleteIds.push(statement.getValue(0).getId());
1597
1624
  }
@@ -1611,8 +1638,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1611
1638
  targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
1612
1639
  // TODO: move to cloneRelationship in IModelCloneContext
1613
1640
  sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
1614
- if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
1615
- targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
1641
+ if (core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType &&
1642
+ "Id" === propertyMetaData.extendedType) {
1643
+ targetRelationshipProps[propertyName] =
1644
+ this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
1616
1645
  }
1617
1646
  });
1618
1647
  return targetRelationshipProps;
@@ -1644,8 +1673,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1644
1673
  sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
1645
1674
  // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
1646
1675
  const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
1647
- const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect && a.scope?.id === this.targetScopeElementId;
1648
- return !this._options.includeSourceProvenance || !isExternalSourceAspectFromTransformer;
1676
+ const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
1677
+ a.scope?.id === this.targetScopeElementId;
1678
+ return (!this._options.includeSourceProvenance ||
1679
+ !isExternalSourceAspectFromTransformer);
1649
1680
  });
1650
1681
  for (let i = 0; i < targetIds.length; ++i) {
1651
1682
  this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
@@ -1686,7 +1717,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1686
1717
  const systemMaxPathSegmentSize = 255;
1687
1718
  // windows usually has a limit for the total path length of 260
1688
1719
  const windowsMaxPathLimit = 260;
1689
- if (schemaFileName.length > systemMaxPathSegmentSize || path.join(this._schemaExportDir, schemaFileName).length >= windowsMaxPathLimit) {
1720
+ if (schemaFileName.length > systemMaxPathSegmentSize ||
1721
+ path.join(this._schemaExportDir, schemaFileName).length >=
1722
+ windowsMaxPathLimit) {
1690
1723
  // this name should be well under 255 bytes
1691
1724
  // ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
1692
1725
  // You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
@@ -1726,7 +1759,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1726
1759
  const maybeLongNameResolvingSchemaCtx = this._longNamedSchemasMap.size > 0
1727
1760
  ? this._makeLongNameResolvingSchemaCtx()
1728
1761
  : undefined;
1729
- return await this.targetDb.importSchemas(schemaFullPaths, { ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx });
1762
+ return await this.targetDb.importSchemas(schemaFullPaths, {
1763
+ ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx,
1764
+ });
1730
1765
  }
1731
1766
  finally {
1732
1767
  core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
@@ -1734,8 +1769,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1734
1769
  }
1735
1770
  }
1736
1771
  /** Cause all fonts to be exported from the source iModel and imported into the target iModel.
1737
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1738
- */
1772
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1773
+ */
1739
1774
  async processFonts() {
1740
1775
  // we do not need to initialize for this since no entities are exported
1741
1776
  await this.initialize();
@@ -1762,7 +1797,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1762
1797
  /** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel.
1763
1798
  * @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer).
1764
1799
  */
1765
- shouldExportCodeSpec(_sourceCodeSpec) { return true; }
1800
+ shouldExportCodeSpec(_sourceCodeSpec) {
1801
+ return true;
1802
+ }
1766
1803
  /** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */
1767
1804
  onExportCodeSpec(sourceCodeSpec) {
1768
1805
  this.context.importCodeSpec(sourceCodeSpec.id);
@@ -1786,33 +1823,224 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1786
1823
  async initialize(args) {
1787
1824
  if (this._initialized)
1788
1825
  return;
1789
- await this.context.initialize();
1790
1826
  await this._tryInitChangesetData(args);
1827
+ await this.context.initialize();
1828
+ // need exporter initialized to do remapdeletedsourceentities.
1791
1829
  await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1792
- // Exporter must be initialized prior to `initFromExternalSourceAspects` in order to handle entity recreations.
1793
- // eslint-disable-next-line deprecation/deprecation
1794
- await this.initFromExternalSourceAspects(args);
1830
+ // Exporter must be initialized prior to processing changesets in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
1831
+ await this.processChangesets();
1795
1832
  this._initialized = true;
1796
1833
  }
1834
+ /**
1835
+ * Reads all the changeset files in the private member of the transformer: _csFileProps and does two things with these changesets.
1836
+ * Finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
1837
+ * Populates this._hasElementChangedCache with a set of elementIds that have been updated or inserted into the database.
1838
+ * This function returns early if csFileProps is undefined or is of length 0.
1839
+ * @returns void
1840
+ */
1841
+ async processChangesets() {
1842
+ this.forEachTrackedElement((sourceElementId, targetElementId) => {
1843
+ this.context.remapElement(sourceElementId, targetElementId);
1844
+ });
1845
+ if (this._csFileProps === undefined || this._csFileProps.length === 0)
1846
+ return;
1847
+ const hasElementChangedCache = new Set();
1848
+ const relationshipECClassIdsToSkip = new Set();
1849
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
1850
+ relationshipECClassIdsToSkip.add(row.ECInstanceId);
1851
+ }
1852
+ const relationshipECClassIds = new Set();
1853
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)")) {
1854
+ relationshipECClassIds.add(row.ECInstanceId);
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
+ }
1860
+ // For later use when processing deletes.
1861
+ const alreadyImportedElementInserts = new Set();
1862
+ const alreadyImportedModelInserts = new Set();
1863
+ this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
1864
+ const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
1865
+ if (core_bentley_1.Id64.isValid(targetElementId))
1866
+ alreadyImportedElementInserts.add(targetElementId);
1867
+ });
1868
+ this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
1869
+ const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
1870
+ if (core_bentley_1.Id64.isValid(targetModelId))
1871
+ alreadyImportedModelInserts.add(targetModelId);
1872
+ });
1873
+ this._deletedSourceRelationshipData = new Map();
1874
+ for (const csFile of this._csFileProps) {
1875
+ const csReader = core_backend_1.SqliteChangesetReader.openFile({
1876
+ fileName: csFile.pathname,
1877
+ db: this.sourceDb,
1878
+ disableSchemaCheck: true,
1879
+ });
1880
+ const csAdaptor = new core_backend_1.ChangesetECAdaptor(csReader);
1881
+ const ecChangeUnifier = new core_backend_1.PartialECChangeUnifier();
1882
+ while (csAdaptor.step()) {
1883
+ ecChangeUnifier.appendFrom(csAdaptor);
1884
+ }
1885
+ const changes = [...ecChangeUnifier.instances];
1886
+ /** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
1887
+ const elemIdToScopeEsa = new Map();
1888
+ for (const change of changes) {
1889
+ if (change.ECClassId !== undefined &&
1890
+ relationshipECClassIdsToSkip.has(change.ECClassId))
1891
+ continue;
1892
+ const changeType = change.$meta?.op;
1893
+ if (changeType === "Deleted" &&
1894
+ change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1895
+ change.Scope.Id === this.targetScopeElementId) {
1896
+ elemIdToScopeEsa.set(change.Element.Id, change);
1897
+ }
1898
+ else if ((changeType === "Inserted" || changeType === "Updated") &&
1899
+ change.ECClassId !== undefined &&
1900
+ elementECClassIds.has(change.ECClassId))
1901
+ hasElementChangedCache.add(change.ECInstanceId);
1902
+ }
1903
+ // Loop to process deletes.
1904
+ for (const change of changes) {
1905
+ const changeType = change.$meta?.op;
1906
+ const ecClassId = change.ECClassId ?? change.$meta?.fallbackClassId;
1907
+ if (ecClassId === undefined)
1908
+ throw new Error(`ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change?.$meta?.tables}`);
1909
+ if (changeType === undefined)
1910
+ throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
1911
+ if (changeType !== "Deleted" ||
1912
+ relationshipECClassIdsToSkip.has(ecClassId))
1913
+ continue;
1914
+ await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1915
+ }
1916
+ csReader.close();
1917
+ }
1918
+ this._hasElementChangedCache = hasElementChangedCache;
1919
+ return;
1920
+ }
1921
+ /**
1922
+ * Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
1923
+ * @param change the change to process, must be of changeType "Deleted"
1924
+ * @param mapOfDeletedElemIdToScopeEsas a map of elementIds to changedECInstances (which are ESAs). the elementId is not the id of the esa itself, but the elementid that the esa was stored on before the esa's deletion.
1925
+ * All ESAs in this map are part of the transformer's scope / ESA data and are tracked in case the ESA is deleted in the target.
1926
+ * @param isRelationship is relationship or not
1927
+ * @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
1928
+ * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1929
+ * @returns void
1930
+ */
1931
+ async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1932
+ // we need a connected iModel with changes to remap elements with deletions
1933
+ const notConnectedModel = this.sourceDb.iTwinId === undefined;
1934
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
1935
+ if (notConnectedModel || noChanges)
1936
+ return;
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) => {
1943
+ let identifierValue;
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;
1959
+ }
1960
+ identifierValue =
1961
+ identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.Identifier;
1962
+ }
1963
+ // Check for targetId by an esa first
1964
+ if (changeDataInProvenanceDb && identifierValue) {
1965
+ const targetId = identifierValue;
1966
+ return targetId;
1967
+ }
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;
1972
+ }
1973
+ return undefined;
1974
+ };
1975
+ const changedInstanceId = change.ECInstanceId;
1976
+ if (isRelationship) {
1977
+ const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1978
+ const targetIdOfRelationshipInSource = change.TargetECInstanceId;
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, {
1984
+ classFullName: classFullName ?? "",
1985
+ sourceIdInTarget: sourceIdOfRelationshipInTarget,
1986
+ targetIdInTarget: targetIdOfRelationshipInTarget,
1987
+ });
1988
+ }
1989
+ else if (this.sourceDb === this.provenanceSourceDb) {
1990
+ const relProvenance = this._queryProvenanceForRelationship(changedInstanceId, {
1991
+ classFullName: classFullName ?? "",
1992
+ sourceId: sourceIdOfRelationshipInSource,
1993
+ targetId: targetIdOfRelationshipInSource,
1994
+ });
1995
+ if (relProvenance && relProvenance.relationshipId)
1996
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1997
+ classFullName: classFullName ?? "",
1998
+ relId: relProvenance.relationshipId,
1999
+ provenanceAspectId: relProvenance.aspectId,
2000
+ });
2001
+ }
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
+ }
2023
+ }
1797
2024
  async _tryInitChangesetData(args) {
1798
- if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
2025
+ if (!args ||
2026
+ this.sourceDb.iTwinId === undefined ||
2027
+ this.sourceDb.changeset.index === undefined) {
1799
2028
  this._sourceChangeDataState = "unconnected";
1800
2029
  return;
1801
2030
  }
1802
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
2031
+ const noChanges = this.synchronizationVersion.index === this.sourceDb.changeset.index;
1803
2032
  if (noChanges) {
1804
2033
  this._sourceChangeDataState = "no-changes";
1805
- this._changeSummaryIds = [];
2034
+ this._csFileProps = [];
1806
2035
  return;
1807
2036
  }
1808
2037
  // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1809
2038
  // to ignore those already processed changes
1810
- const startChangesetIndexOrId = args.startChangeset?.index
1811
- ?? args.startChangeset?.id
1812
- ?? this._synchronizationVersion.index + 1;
2039
+ const startChangesetIndexOrId = args.startChangeset?.index ??
2040
+ args.startChangeset?.id ??
2041
+ this.synchronizationVersion.index + 1;
1813
2042
  const endChangesetId = this.sourceDb.changeset.id;
1814
- const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
1815
- .map(async (indexOrId) => typeof indexOrId === "number"
2043
+ const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
1816
2044
  ? indexOrId
1817
2045
  : core_backend_1.IModelHost.hubAccess
1818
2046
  .queryChangeset({
@@ -1822,55 +2050,68 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1822
2050
  accessToken: args.accessToken,
1823
2051
  })
1824
2052
  .then((changeset) => changeset.index)));
1825
- const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
1826
- if (!this._options.ignoreMissingChangesetsInSynchronizations
1827
- && startChangesetIndex !== this._synchronizationVersion.index + 1
1828
- && this._synchronizationVersion.index !== -1) {
1829
- throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
1830
- + " startChangesetId should be"
1831
- + " exactly the first changeset *after* the previous synchronization to not miss data."
1832
- + ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}`
1833
- + ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'`
1834
- + ` which is changeset #${this._synchronizationVersion.index}. The transformer expected`
1835
- + ` #${this._synchronizationVersion.index + 1}.`);
2053
+ const missingChangesets = startChangesetIndex > this.synchronizationVersion.index + 1;
2054
+ if (!this._options.ignoreMissingChangesetsInSynchronizations &&
2055
+ startChangesetIndex !== this.synchronizationVersion.index + 1 &&
2056
+ this.synchronizationVersion.index !== -1) {
2057
+ throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
2058
+ " startChangesetId should be" +
2059
+ " exactly the first changeset *after* the previous synchronization to not miss data." +
2060
+ ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}` +
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}.`);
1836
2064
  }
1837
2065
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1838
- const changesetsToSkip = this._isReverseSynchronization
1839
- ? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
1840
- : this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
2066
+ const changesetsToSkip = this.isReverseSynchronization
2067
+ ? this._targetScopeProvenanceProps.jsonProperties
2068
+ .pendingReverseSyncChangesetIndices
2069
+ : this._targetScopeProvenanceProps.jsonProperties
2070
+ .pendingSyncChangesetIndices;
1841
2071
  core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1842
2072
  this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1843
2073
  core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
2074
+ const csFileProps = [];
1844
2075
  for (const [first, end] of this._changesetRanges) {
1845
- this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
1846
- accessToken: args.accessToken,
2076
+ // TODO: should the first changeset in a reverse sync really be included even though its 'initialized branch provenance'? The answer is no, its a bug that needs to be fixed.
2077
+ const fileProps = await core_backend_1.IModelHost.hubAccess.downloadChangesets({
1847
2078
  iModelId: this.sourceDb.iModelId,
1848
- iTwinId: this.sourceDb.iTwinId,
2079
+ targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
1849
2080
  range: { first, end },
1850
2081
  });
2082
+ csFileProps.push(...fileProps);
1851
2083
  }
1852
- core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
1853
- this._sourceChangeDataState = "has-changes";
2084
+ this._csFileProps = csFileProps;
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";
1854
2088
  }
1855
2089
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
1856
- * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
1857
- */
2090
+ * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2091
+ */
1858
2092
  async processAll() {
1859
2093
  this.logSettings();
1860
2094
  this.initScopeProvenance();
1861
2095
  await this.initialize();
1862
2096
  await this.exporter.exportCodeSpecs();
1863
2097
  await this.exporter.exportFonts();
1864
- // The RepositoryModel and root Subject of the target iModel should not be transformed.
1865
- await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
1866
- 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
1867
- await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
2098
+ if (this._options.skipPropagateChangesToRootElements) {
2099
+ // The RepositoryModel and root Subject of the target iModel should not be transformed.
2100
+ await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
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
2102
+ await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
2103
+ }
2104
+ else {
2105
+ await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
2106
+ }
1868
2107
  await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1869
2108
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
1870
2109
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1871
- // FIXME: add a deprecated option to force run these, don't otherwise
1872
- if (this.shouldDetectDeletes()) {
2110
+ if (this._options.forceExternalSourceAspectProvenance &&
2111
+ this.shouldDetectDeletes()) {
2112
+ // eslint-disable-next-line deprecation/deprecation
1873
2113
  await this.detectElementDeletes();
2114
+ // eslint-disable-next-line deprecation/deprecation
1874
2115
  await this.detectRelationshipDeletes();
1875
2116
  }
1876
2117
  if (this._options.optimizeGeometry)
@@ -1879,222 +2120,34 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1879
2120
  this.finalizeTransformation();
1880
2121
  }
1881
2122
  markLastProvenance(sourceAspect, { isRelationship = false }) {
1882
- this._lastProvenanceEntityInfo
1883
- = typeof sourceAspect === "string"
2123
+ this._lastProvenanceEntityInfo =
2124
+ typeof sourceAspect === "string"
1884
2125
  ? sourceAspect
1885
2126
  : {
1886
2127
  entityId: sourceAspect.element.id,
1887
2128
  aspectId: sourceAspect.id,
1888
2129
  aspectVersion: sourceAspect.version ?? "",
1889
- aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
2130
+ aspectKind: isRelationship
2131
+ ? core_backend_1.ExternalSourceAspect.Kind.Relationship
2132
+ : core_backend_1.ExternalSourceAspect.Kind.Element,
1890
2133
  };
1891
2134
  }
1892
- /**
1893
- * Load the state of the active transformation from an open SQLiteDb
1894
- * You can override this if you'd like to load from custom tables in the resumable dump state, but you should call
1895
- * this super implementation
1896
- * @note the SQLiteDb must be open
2135
+ /** Export changes from the source iModel and import the transformed entities into the target iModel.
2136
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
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
2140
+ * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2141
+ * will automatically be determined and used
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.
1897
2143
  */
1898
- loadStateFromDb(db) {
1899
- const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
1900
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1901
- throw Error("expected row when getting lastProvenanceEntityId from target state table");
1902
- const entityId = stmt.getValueString(0);
1903
- const isGuidOrGuidPair = entityId.includes("-");
1904
- return isGuidOrGuidPair
1905
- ? entityId
1906
- : {
1907
- entityId,
1908
- aspectId: stmt.getValueString(1),
1909
- aspectVersion: stmt.getValueString(2),
1910
- aspectKind: stmt.getValueString(3),
1911
- };
1912
- });
1913
- /*
1914
- // TODO: maybe save transformer state resumption state based on target changset and require calls
1915
- // to saveChanges
1916
- if () {
1917
- const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
1918
- const isRelProvenance = targetFedGuid !== undefined;
1919
- const instanceId = isRelProvenance
1920
- ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
1921
- : "";
1922
- //const classId =
1923
- if (isRelProvenance) {
1924
- }
1925
- }
1926
- */
1927
- const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
1928
- // ignore provenance check if it's null since we can't bind those ids
1929
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
1930
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1931
- this.provenanceDb.withPreparedStatement(`
1932
- SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
1933
- WHERE Scope.Id=:scopeId
1934
- AND ECInstanceId=:aspectId
1935
- AND Kind=:kind
1936
- AND Element.Id=:entityId
1937
- `, (statement) => {
1938
- statement.bindId("scopeId", this.targetScopeElementId);
1939
- statement.bindId("aspectId", lastProvenanceEntityInfo.aspectId);
1940
- statement.bindString("kind", lastProvenanceEntityInfo.aspectKind);
1941
- statement.bindId("entityId", lastProvenanceEntityInfo.entityId);
1942
- const stepResult = statement.step();
1943
- switch (stepResult) {
1944
- case core_bentley_1.DbResult.BE_SQLITE_ROW:
1945
- const version = statement.getValue(0).getString();
1946
- return version === lastProvenanceEntityInfo.aspectVersion;
1947
- case core_bentley_1.DbResult.BE_SQLITE_DONE:
1948
- return false;
1949
- default:
1950
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.SQLiteError, `got sql error ${stepResult}`);
1951
- }
1952
- });
1953
- if (!targetHasCorrectLastProvenance)
1954
- throw Error([
1955
- "Target for resuming from does not have the expected provenance ",
1956
- "from the target that the resume state was made with",
1957
- ].join("\n"));
1958
- this._lastProvenanceEntityInfo = lastProvenanceEntityInfo;
1959
- const state = db.withSqliteStatement(`SELECT data FROM ${IModelTransformer.jsStateTable}`, (stmt) => {
1960
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1961
- throw Error("expected row when getting data from js state table");
1962
- return JSON.parse(stmt.getValueString(0));
1963
- });
1964
- if (state.transformerClass !== this.constructor.name)
1965
- throw Error("resuming from a differently named transformer class, it is not necessarily valid to resume with a different transformer class");
1966
- // force assign to readonly options since we do not know how the transformer subclass takes options to pass to the superclass
1967
- this._options = state.options;
1968
- this.context.loadStateFromDb(db);
1969
- this.importer.loadStateFromJson(state.importerState);
1970
- this.exporter.loadStateFromJson(state.exporterState);
1971
- this._elementsWithExplicitlyTrackedProvenance = core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
1972
- this.loadAdditionalStateJson(state.additionalState);
1973
- }
1974
- /**
1975
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1976
- * from the original changeset
1977
- *
1978
- * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
1979
- * This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
1980
- * again but the remapping state will cause already mapped elements to be skipped.
1981
- * To "resume" an iModel Transformation you need:
1982
- * - the sourceDb at the same changeset
1983
- * - the same targetDb in the state in which it was before
1984
- * @param statePath the path to the serialized state of the transformer, use [[IModelTransformer.saveStateToFile]] to get this from an existing transformer instance
1985
- * @param constructorArgs remaining arguments that you would normally pass to the Transformer subclass you are using, usually (sourceDb, targetDb)
1986
- * @note custom transformers with custom state may need to override this method in order to handle loading their own custom state somewhere
1987
- */
1988
- static resumeTransformation(statePath, ...constructorArgs) {
1989
- const transformer = new this(...constructorArgs);
1990
- const db = new core_backend_1.SQLiteDb();
1991
- db.openDb(statePath, core_bentley_1.OpenMode.Readonly);
1992
- try {
1993
- transformer.loadStateFromDb(db);
1994
- }
1995
- finally {
1996
- db.closeDb();
1997
- }
1998
- return transformer;
1999
- }
2000
- /**
2001
- * You may override this to store arbitrary json state in a transformer state dump, useful for some resumptions
2002
- * @see [[IModelTransformer.saveStateToFile]]
2003
- */
2004
- getAdditionalStateJson() {
2005
- return {};
2006
- }
2007
- /**
2008
- * You may override this to load arbitrary json state in a transformer state dump, useful for some resumptions
2009
- * @see [[IModelTransformer.loadStateFromFile]]
2010
- */
2011
- loadAdditionalStateJson(_additionalState) { }
2012
- /**
2013
- * Save the state of the active transformation to an open SQLiteDb
2014
- * You can override this if you'd like to write custom tables to the resumable dump state, but you should call
2015
- * this super implementation
2016
- * @note the SQLiteDb must be open
2017
- */
2018
- saveStateToDb(db) {
2019
- const jsonState = {
2020
- transformerClass: this.constructor.name,
2021
- options: this._options,
2022
- explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
2023
- importerState: this.importer.saveStateToJson(),
2024
- exporterState: this.exporter.saveStateToJson(),
2025
- additionalState: this.getAdditionalStateJson(),
2026
- };
2027
- this.context.saveStateToDb(db);
2028
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
2029
- throw Error("Failed to create the js state table in the state database");
2030
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
2031
- CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
2032
- -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
2033
- entityId TEXT,
2034
- -- the following are only valid if the above entityId is a hex id representation
2035
- aspectId TEXT,
2036
- aspectVersion TEXT,
2037
- aspectKind TEXT
2038
- )
2039
- `))
2040
- throw Error("Failed to create the target state table in the state database");
2041
- db.saveChanges();
2042
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.jsStateTable} (data) VALUES (?)`, (stmt) => {
2043
- stmt.bindString(1, JSON.stringify(jsonState));
2044
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2045
- throw Error("Failed to insert options into the state database");
2046
- });
2047
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
2048
- const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
2049
- stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
2050
- stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
2051
- stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
2052
- stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
2053
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2054
- throw Error("Failed to insert options into the state database");
2055
- });
2056
- db.saveChanges();
2057
- }
2058
- /**
2059
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
2060
- * from the original changeset
2061
- *
2062
- * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
2063
- * This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
2064
- * The serialization format is a custom sqlite database.
2065
- * @note custom transformers with custom state may override [[IModelTransformer.saveStateToDb]] or [[IModelTransformer.getAdditionalStateJson]]
2066
- * and [[IModelTransformer.loadStateFromDb]] (with a super call) or [[IModelTransformer.loadAdditionalStateJson]]
2067
- * if they have custom state that needs to be stored with
2068
- * potentially inside the same sqlite file in separate tables
2069
- */
2070
- saveStateToFile(nativeStatePath) {
2071
- const db = new core_backend_1.SQLiteDb();
2072
- if (core_backend_1.IModelJsFs.existsSync(nativeStatePath))
2073
- core_backend_1.IModelJsFs.unlinkSync(nativeStatePath);
2074
- db.createDb(nativeStatePath);
2075
- try {
2076
- this.saveStateToDb(db);
2077
- db.saveChanges();
2078
- }
2079
- finally {
2080
- db.closeDb();
2081
- }
2082
- }
2083
- async processChanges(optionsOrAccessToken, startChangesetId) {
2144
+ async processChanges(options) {
2084
2145
  this._isSynchronization = true;
2085
2146
  this.initScopeProvenance();
2086
- const args = typeof optionsOrAccessToken === "string"
2087
- ? {
2088
- accessToken: optionsOrAccessToken,
2089
- startChangeset: startChangesetId
2090
- ? { id: startChangesetId }
2091
- : { index: this._synchronizationVersion.index + 1 },
2092
- }
2093
- : optionsOrAccessToken;
2094
2147
  this.logSettings();
2095
- await this.initialize(args);
2148
+ await this.initialize(options);
2096
2149
  // must wait for initialization of synchronization provenance data
2097
- await this.exporter.exportChanges(this.getExportInitOpts(args));
2150
+ await this.exporter.exportChanges(this.getExportInitOpts(options));
2098
2151
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2099
2152
  if (this._options.optimizeGeometry)
2100
2153
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
@@ -2103,7 +2156,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2103
2156
  const defaultSaveTargetChanges = () => {
2104
2157
  this.targetDb.saveChanges();
2105
2158
  };
2106
- await (args.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2159
+ await (options.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2107
2160
  }
2108
2161
  /** Changeset data must be initialized in order to build correct changeOptions.
2109
2162
  * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
@@ -2112,12 +2165,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2112
2165
  if (!this._isSynchronization)
2113
2166
  return {};
2114
2167
  return {
2168
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
2115
2169
  accessToken: opts.accessToken,
2116
- ...this._changesetRanges
2117
- ? { changesetRanges: this._changesetRanges }
2118
- : opts.startChangeset
2119
- ? { startChangeset: opts.startChangeset }
2120
- : { startChangeset: { index: this._synchronizationVersion.index + 1 } },
2170
+ ...(this._csFileProps
2171
+ ? { csFileProps: this._csFileProps }
2172
+ : this._changesetRanges
2173
+ ? { changesetRanges: this._changesetRanges }
2174
+ : opts.startChangeset
2175
+ ? { startChangeset: opts.startChangeset }
2176
+ : {
2177
+ startChangeset: {
2178
+ index: this.synchronizationVersion.index + 1,
2179
+ },
2180
+ }),
2121
2181
  };
2122
2182
  }
2123
2183
  /** Combine an array of source elements into a single target element.
@@ -2133,10 +2193,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2133
2193
  }
2134
2194
  }
2135
2195
  exports.IModelTransformer = IModelTransformer;
2136
- /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
2137
- IModelTransformer.jsStateTable = "TransformerJsState";
2138
- /** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
2139
- IModelTransformer.lastProvenanceEntityInfoTable = "LastProvenanceEntityInfo";
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.";
2140
2197
  /** IModelTransformer that clones the contents of a template model.
2141
2198
  * @beta
2142
2199
  */
@@ -2207,7 +2264,8 @@ class TemplateModelCloner extends IModelTransformer {
2207
2264
  }
2208
2265
  else {
2209
2266
  const definitionElement = this.sourceDb.elements.tryGetElement(referenceId, core_backend_1.DefinitionElement);
2210
- if (definitionElement && !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
2267
+ if (definitionElement &&
2268
+ !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
2211
2269
  this.context.remapElement(referenceId, referenceId); // when in the same iModel, can use existing DefinitionElements without remapping
2212
2270
  }
2213
2271
  else {
@@ -2222,7 +2280,7 @@ class TemplateModelCloner extends IModelTransformer {
2222
2280
  if (sourceElement instanceof core_backend_1.GeometricElement) {
2223
2281
  const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
2224
2282
  const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
2225
- const placement = (placementClass).fromJSON(targetElementProps.placement);
2283
+ const placement = placementClass.fromJSON(targetElementProps.placement);
2226
2284
  if (placement.isValid) {
2227
2285
  nodeAssert(this._transform3d);
2228
2286
  placement.multiplyTransform(this._transform3d);