@itwin/imodel-transformer 0.4.18-fedguidopt.6 → 1.0.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/lib/cjs/Algo.d.ts.map +1 -1
  2. package/lib/cjs/Algo.js +3 -4
  3. package/lib/cjs/Algo.js.map +1 -1
  4. package/lib/cjs/BigMap.d.ts.map +1 -1
  5. package/lib/cjs/BigMap.js +1 -1
  6. package/lib/cjs/BigMap.js.map +1 -1
  7. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  8. package/lib/cjs/BranchProvenanceInitializer.js +15 -4
  9. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  10. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +1 -1
  11. package/lib/cjs/DetachedExportElementAspectsStrategy.js +12 -5
  12. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +1 -1
  13. package/lib/cjs/ECReferenceTypesCache.d.ts.map +1 -1
  14. package/lib/cjs/ECReferenceTypesCache.js +32 -18
  15. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  16. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts +1 -1
  17. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts.map +1 -1
  18. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js +7 -5
  19. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  20. package/lib/cjs/ElementCascadingDeleter.d.ts +3 -3
  21. package/lib/cjs/ElementCascadingDeleter.d.ts.map +1 -1
  22. package/lib/cjs/ElementCascadingDeleter.js +9 -7
  23. package/lib/cjs/ElementCascadingDeleter.js.map +1 -1
  24. package/lib/cjs/EntityMap.d.ts.map +1 -1
  25. package/lib/cjs/EntityMap.js.map +1 -1
  26. package/lib/cjs/EntityUnifier.d.ts +5 -0
  27. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  28. package/lib/cjs/EntityUnifier.js +22 -35
  29. package/lib/cjs/EntityUnifier.js.map +1 -1
  30. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +1 -1
  31. package/lib/cjs/ExportElementAspectsStrategy.js +5 -4
  32. package/lib/cjs/ExportElementAspectsStrategy.js.map +1 -1
  33. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +1 -1
  34. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +9 -5
  35. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +1 -1
  36. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  37. package/lib/cjs/IModelCloneContext.js +23 -12
  38. package/lib/cjs/IModelCloneContext.js.map +1 -1
  39. package/lib/cjs/IModelExporter.d.ts +68 -25
  40. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  41. package/lib/cjs/IModelExporter.js +241 -123
  42. package/lib/cjs/IModelExporter.js.map +1 -1
  43. package/lib/cjs/IModelImporter.d.ts +13 -22
  44. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  45. package/lib/cjs/IModelImporter.js +80 -62
  46. package/lib/cjs/IModelImporter.js.map +1 -1
  47. package/lib/cjs/IModelTransformer.d.ts +126 -44
  48. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  49. package/lib/cjs/IModelTransformer.js +706 -535
  50. package/lib/cjs/IModelTransformer.js.map +1 -1
  51. package/lib/cjs/PendingReferenceMap.d.ts.map +1 -1
  52. package/lib/cjs/PendingReferenceMap.js +12 -6
  53. package/lib/cjs/PendingReferenceMap.js.map +1 -1
  54. package/lib/cjs/TransformerLoggerCategory.d.ts +2 -2
  55. package/lib/cjs/TransformerLoggerCategory.d.ts.map +1 -1
  56. package/lib/cjs/TransformerLoggerCategory.js +5 -5
  57. package/lib/cjs/TransformerLoggerCategory.js.map +1 -1
  58. package/lib/cjs/transformer.d.ts.map +1 -1
  59. package/lib/cjs/transformer.js +14 -10
  60. package/lib/cjs/transformer.js.map +1 -1
  61. package/package.json +20 -17
@@ -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
  */
@@ -95,15 +95,115 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
95
95
  get targetScopeElementId() {
96
96
  return this._options.targetScopeElementId;
97
97
  }
98
- get _isReverseSynchronization() {
99
- return this._isSynchronization && this._options.isReverseSynchronization;
98
+ /**
99
+ * Queries for an esa which matches the props in the provided aspectProps.
100
+ * @param dbToQuery db to run the query on for scope external source
101
+ * @param aspectProps aspectProps to search for @see ExternalSourceAspectProps
102
+ */
103
+ static queryScopeExternalSourceAspect(dbToQuery, aspectProps) {
104
+ const sql = `
105
+ SELECT ECInstanceId, Version, JsonProperties
106
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName}
107
+ WHERE Element.Id=:elementId
108
+ AND Scope.Id=:scopeId
109
+ AND Kind=:kind
110
+ AND Identifier=:identifier
111
+ LIMIT 1
112
+ `;
113
+ return dbToQuery.withPreparedStatement(sql, (statement) => {
114
+ statement.bindId("elementId", aspectProps.element.id);
115
+ if (aspectProps.scope === undefined)
116
+ return undefined; // return instead of binding an invalid id
117
+ statement.bindId("scopeId", aspectProps.scope.id);
118
+ statement.bindString("kind", aspectProps.kind);
119
+ statement.bindString("identifier", aspectProps.identifier);
120
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
121
+ return undefined;
122
+ const aspectId = statement.getValue(0).getId();
123
+ const versionValue = statement.getValue(1);
124
+ const version = versionValue.isNull
125
+ ? undefined
126
+ : versionValue.getString();
127
+ const jsonPropsValue = statement.getValue(2);
128
+ const jsonProperties = jsonPropsValue.isNull
129
+ ? undefined
130
+ : jsonPropsValue.getString();
131
+ return { aspectId, version, jsonProperties };
132
+ });
133
+ }
134
+ /**
135
+ * Determines the sync direction "forward" or "reverse" of a given sourceDb and targetDb by looking for the scoping ESA.
136
+ * If the sourceDb's iModelId is found as the identifier of the expected scoping ESA in the targetDb, then it is a forward synchronization.
137
+ * If the targetDb's iModelId is found as the identifier of the expected scoping ESA in the sourceDb, then it is a reverse synchronization.
138
+ * @throws if no scoping ESA can be found in either the sourceDb or targetDb which describes a master branch relationship between the two databases.
139
+ * @returns "forward" or "reverse"
140
+ */
141
+ static determineSyncType(sourceDb, targetDb,
142
+ /** @see [[IModelTransformOptions.targetScopeElementId]] */
143
+ targetScopeElementId) {
144
+ const aspectProps = {
145
+ id: undefined,
146
+ version: undefined,
147
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
148
+ element: {
149
+ id: targetScopeElementId,
150
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
151
+ },
152
+ scope: { id: core_common_1.IModel.rootSubjectId },
153
+ identifier: sourceDb.iModelId,
154
+ kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
155
+ jsonProperties: undefined,
156
+ };
157
+ /** First check if the targetDb is the branch (branch is the @see provenanceDb) */
158
+ const esaPropsFromTargetDb = this.queryScopeExternalSourceAspect(targetDb, aspectProps);
159
+ if (esaPropsFromTargetDb !== undefined) {
160
+ return "forward"; // we found an esa assuming targetDb is the provenanceDb/branch so this is a forward sync.
161
+ }
162
+ // Now check if the sourceDb is the branch
163
+ aspectProps.identifier = targetDb.iModelId;
164
+ const esaPropsFromSourceDb = this.queryScopeExternalSourceAspect(sourceDb, aspectProps);
165
+ if (esaPropsFromSourceDb !== undefined) {
166
+ return "reverse"; // we found an esa assuming sourceDb is the provenanceDb/branch so this is a reverse sync.
167
+ }
168
+ throw new Error(this.noEsaSyncDirectionErrorMessage);
169
+ }
170
+ determineSyncType() {
171
+ if (this._isProvenanceInitTransform) {
172
+ return "forward";
173
+ }
174
+ if (!this._isSynchronization) {
175
+ return "not-sync";
176
+ }
177
+ try {
178
+ return IModelTransformer.determineSyncType(this.sourceDb, this.targetDb, this.targetScopeElementId);
179
+ }
180
+ catch (err) {
181
+ if (err instanceof Error &&
182
+ err.message === IModelTransformer.noEsaSyncDirectionErrorMessage &&
183
+ this._allowNoScopingESA) {
184
+ return "forward";
185
+ }
186
+ throw err;
187
+ }
100
188
  }
101
- get _isForwardSynchronization() {
102
- return this._isSynchronization && !this._options.isReverseSynchronization;
189
+ get isReverseSynchronization() {
190
+ if (this._syncType === undefined)
191
+ this._syncType = this.determineSyncType();
192
+ return this._syncType === "reverse";
193
+ }
194
+ get isForwardSynchronization() {
195
+ if (this._syncType === undefined)
196
+ this._syncType = this.determineSyncType();
197
+ return this._syncType === "forward";
103
198
  }
104
199
  /** The element classes that are considered to define provenance in the iModel */
105
200
  static get provenanceElementClasses() {
106
- return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
201
+ return [
202
+ core_backend_1.FolderLink,
203
+ core_backend_1.SynchronizationConfigLink,
204
+ core_backend_1.ExternalSource,
205
+ core_backend_1.ExternalSourceAttachment,
206
+ ];
107
207
  }
108
208
  /** The element aspect classes that are considered to define provenance in the iModel */
109
209
  static get provenanceElementAspectClasses() {
@@ -124,12 +224,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
124
224
  /** map of partially committed entities to their partial commit progress */
125
225
  this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
126
226
  this._isSynchronization = false;
227
+ /**
228
+ * A private variable meant to be set by tests which have an outdated way of setting up transforms. In all synchronizations today we expect to find an ESA in the branch db which describes the master -> branch relationship.
229
+ * 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.
230
+ * 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
231
+ * this private property on the IModelTransformer exists.
232
+ */
233
+ this._allowNoScopingESA = false;
127
234
  this._changesetRanges = undefined;
128
235
  /** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
129
236
  * @note Currently only tracks elements which were not exported.
130
237
  */
131
238
  this._skippedEntities = new Set();
132
- // FIXME: add test transforming using this, then switching to new transform method
133
239
  /**
134
240
  * Previously the transformer would insert provenance always pointing to the "target" relationship.
135
241
  * It should (and now by default does) instead insert provenance pointing to the provenanceSource
@@ -146,7 +252,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
146
252
  this._startingChangesetIndices = undefined;
147
253
  this._cachedSynchronizationVersion = undefined;
148
254
  this._targetClassNameToClassIdCache = new Map();
149
- // if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
255
+ // if undefined, it can be initialized by calling [[this.processChangesets]]
150
256
  this._hasElementChangedCache = undefined;
151
257
  this._deletedSourceRelationshipData = undefined;
152
258
  this._yieldManager = new core_bentley_1.YieldManager();
@@ -155,9 +261,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
155
261
  this._longNamedSchemasMap = new Map();
156
262
  /** state to prevent reinitialization, @see [[initialize]] */
157
263
  this._initialized = false;
158
- /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
159
- this._changeSummaryIds = undefined;
160
264
  this._sourceChangeDataState = "uninited";
265
+ /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
266
+ this._csFileProps = undefined;
161
267
  /** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
162
268
  this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
163
269
  // initialize IModelTransformOptions
@@ -167,9 +273,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
167
273
  cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
168
274
  targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
169
275
  // eslint-disable-next-line deprecation/deprecation
170
- danglingReferencesBehavior: options?.danglingReferencesBehavior ?? options?.danglingPredecessorsBehavior ?? "reject",
276
+ danglingReferencesBehavior: options?.danglingReferencesBehavior ??
277
+ options?.danglingPredecessorsBehavior ??
278
+ "reject",
279
+ branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
171
280
  };
172
- this._isFirstSynchronization = this._options.wasSourceIModelCopiedToTarget ? true : undefined;
281
+ this._isProvenanceInitTransform = this._options
282
+ .wasSourceIModelCopiedToTarget
283
+ ? true
284
+ : undefined;
173
285
  // initialize exporter and sourceDb
174
286
  if (source instanceof core_backend_1.IModelDb) {
175
287
  this.exporter = new IModelExporter_1.IModelExporter(source);
@@ -180,7 +292,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
180
292
  this.sourceDb = this.exporter.sourceDb;
181
293
  this.exporter.registerHandler(this);
182
294
  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?
295
+ if (!this._options.includeSourceProvenance) {
296
+ // clone provenance from the source iModel into the target iModel?
184
297
  IModelTransformer.provenanceElementClasses.forEach((cls) => this.exporter.excludeElementClass(cls.classFullName));
185
298
  IModelTransformer.provenanceElementAspectClasses.forEach((cls) => this.exporter.excludeElementAspectClass(cls.classFullName));
186
299
  }
@@ -188,26 +301,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
188
301
  this.exporter.excludeElementAspectClass("BisCore:TextAnnotationData"); // This ElementAspect is auto-created by the BisCore:TextAnnotation2d/3d element handlers
189
302
  // initialize importer and targetDb
190
303
  if (target instanceof core_backend_1.IModelDb) {
191
- this.importer = new IModelImporter_1.IModelImporter(target, { preserveElementIdsForFiltering: this._options.preserveElementIdsForFiltering });
304
+ this.importer = new IModelImporter_1.IModelImporter(target, {
305
+ preserveElementIdsForFiltering: this._options.preserveElementIdsForFiltering,
306
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
307
+ });
192
308
  }
193
309
  else {
194
310
  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 */
311
+ this.validateSharedOptionsMatch();
205
312
  }
206
313
  this.targetDb = this.importer.targetDb;
207
314
  // create the IModelCloneContext, it must be initialized later
208
315
  this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
209
316
  if (this.sourceDb.isBriefcase && this.targetDb.isBriefcase) {
210
- nodeAssert(this.sourceDb.changeset.index !== undefined && this.targetDb.changeset.index !== undefined, "database has no changeset index");
317
+ nodeAssert(this.sourceDb.changeset.index !== undefined &&
318
+ this.targetDb.changeset.index !== undefined, "database has no changeset index");
211
319
  this._startingChangesetIndices = {
212
320
  target: this.targetDb.changeset.index,
213
321
  source: this.sourceDb.changeset.index,
@@ -215,12 +323,27 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
215
323
  }
216
324
  // this internal is guaranteed stable for just transformer usage
217
325
  /* eslint-disable @itwin/no-internal */
218
- if ("codeValueBehavior" in this.sourceDb) {
326
+ if (("codeValueBehavior" in this.sourceDb)) {
219
327
  this.sourceDb.codeValueBehavior = "exact";
220
328
  this.targetDb.codeValueBehavior = "exact";
221
329
  }
222
330
  /* eslint-enable @itwin/no-internal */
223
331
  }
332
+ /** validates that the importer set on the transformer has the same values for its shared options as the transformer.
333
+ * @note This expects that the importer is already set on the transformer.
334
+ */
335
+ validateSharedOptionsMatch() {
336
+ if (Boolean(this._options.preserveElementIdsForFiltering) !==
337
+ this.importer.options.preserveElementIdsForFiltering) {
338
+ const errMessage = "A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.";
339
+ throw new Error(errMessage);
340
+ }
341
+ if (Boolean(this._options.skipPropagateChangesToRootElements) !==
342
+ this.importer.options.skipPropagateChangesToRootElements) {
343
+ const errMessage = "A custom importer was passed as a target but its 'skipPropagateChangesToRootElements' option is out of sync with the transformer's option.";
344
+ throw new Error(errMessage);
345
+ }
346
+ }
224
347
  /** Dispose any native resources associated with this IModelTransformer. */
225
348
  dispose() {
226
349
  core_bentley_1.Logger.logTrace(loggerCategory, "dispose()");
@@ -246,24 +369,31 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
246
369
  * @note This will be [[targetDb]] except when it is a reverse synchronization. In that case it be [[sourceDb]].
247
370
  */
248
371
  get provenanceDb() {
249
- return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
372
+ return this.isReverseSynchronization ? this.sourceDb : this.targetDb;
250
373
  }
251
374
  /** Return the IModelDb where IModelTransformer looks for entities referred to by stored provenance.
252
375
  * @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
253
376
  */
254
377
  get provenanceSourceDb() {
255
- return this._options.isReverseSynchronization ? this.targetDb : this.sourceDb;
378
+ return this.isReverseSynchronization ? this.targetDb : this.sourceDb;
256
379
  }
257
380
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
258
381
  static initElementProvenanceOptions(sourceElementId, targetElementId, args) {
259
- const elementId = args.isReverseSynchronization ? sourceElementId : targetElementId;
382
+ const elementId = args.isReverseSynchronization
383
+ ? sourceElementId
384
+ : targetElementId;
260
385
  const version = args.isReverseSynchronization
261
386
  ? args.targetDb.elements.queryLastModifiedTime(targetElementId)
262
387
  : args.sourceDb.elements.queryLastModifiedTime(sourceElementId);
263
- const aspectIdentifier = args.isReverseSynchronization ? targetElementId : sourceElementId;
388
+ const aspectIdentifier = args.isReverseSynchronization
389
+ ? targetElementId
390
+ : sourceElementId;
264
391
  const aspectProps = {
265
392
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
266
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
393
+ element: {
394
+ id: elementId,
395
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
396
+ },
267
397
  scope: { id: args.targetScopeElementId },
268
398
  identifier: aspectIdentifier,
269
399
  kind: core_backend_1.ExternalSourceAspect.Kind.Element,
@@ -272,9 +402,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
272
402
  return aspectProps;
273
403
  }
274
404
  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;
405
+ const provenanceDb = args.isReverseSynchronization
406
+ ? args.sourceDb
407
+ : args.targetDb;
408
+ const aspectIdentifier = args.isReverseSynchronization
409
+ ? targetRelInstanceId
410
+ : sourceRelInstanceId;
411
+ const provenanceRelInstanceId = args.isReverseSynchronization
412
+ ? sourceRelInstanceId
413
+ : targetRelInstanceId;
278
414
  const elementId = provenanceDb.withPreparedStatement("SELECT SourceECInstanceId FROM bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
279
415
  stmt.bindId(1, provenanceRelInstanceId);
280
416
  nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
@@ -285,7 +421,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
285
421
  : { provenanceRelInstanceId };
286
422
  const aspectProps = {
287
423
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
288
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
424
+ element: {
425
+ id: elementId,
426
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
427
+ },
289
428
  scope: { id: args.targetScopeElementId },
290
429
  identifier: aspectIdentifier,
291
430
  kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
@@ -296,7 +435,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
296
435
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
297
436
  initElementProvenance(sourceElementId, targetElementId) {
298
437
  return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
299
- isReverseSynchronization: !!this._options.isReverseSynchronization,
438
+ isReverseSynchronization: this.isReverseSynchronization,
300
439
  targetScopeElementId: this.targetScopeElementId,
301
440
  sourceDb: this.sourceDb,
302
441
  targetDb: this.targetDb,
@@ -311,7 +450,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
311
450
  return IModelTransformer.initRelationshipProvenanceOptions(sourceRelationship.id, targetRelInstanceId, {
312
451
  sourceDb: this.sourceDb,
313
452
  targetDb: this.targetDb,
314
- isReverseSynchronization: !!this._options.isReverseSynchronization,
453
+ isReverseSynchronization: this.isReverseSynchronization,
315
454
  targetScopeElementId: this.targetScopeElementId,
316
455
  forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
317
456
  });
@@ -325,13 +464,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
325
464
  get _synchronizationVersion() {
326
465
  if (!this._cachedSynchronizationVersion) {
327
466
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
328
- const version = this._options.isReverseSynchronization
329
- ? this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion
467
+ const version = this.isReverseSynchronization
468
+ ? this._targetScopeProvenanceProps.jsonProperties?.reverseSyncVersion
330
469
  : this._targetScopeProvenanceProps.version;
331
470
  nodeAssert(version !== undefined, "no version contained in target scope");
332
- const [id, index] = version === ""
333
- ? ["", -1]
334
- : version.split(";");
471
+ const [id, index] = version === "" ? ["", -1] : version.split(";");
335
472
  this._cachedSynchronizationVersion = { index: Number(index), id };
336
473
  nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
337
474
  }
@@ -348,7 +485,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
348
485
  if (!provenanceScopeAspect) {
349
486
  return { index: -1, id: "" }; // first synchronization.
350
487
  }
351
- const version = this._options.isReverseSynchronization
488
+ const version = this.isReverseSynchronization
352
489
  ? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
353
490
  : provenanceScopeAspect.version;
354
491
  if (!version) {
@@ -366,15 +503,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
366
503
  * Provenance scope aspect is created and inserted into provenanceDb when [[initScopeProvenance]] is invoked.
367
504
  */
368
505
  tryGetProvenanceScopeAspect() {
369
- const scopeProvenanceAspectId = this.queryScopeExternalSource({
506
+ const scopeProvenanceAspectProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, {
507
+ id: undefined,
370
508
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
371
509
  scope: { id: core_common_1.IModel.rootSubjectId },
372
510
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
373
511
  element: { id: this.targetScopeElementId ?? core_common_1.IModel.rootSubjectId },
374
512
  identifier: this.provenanceSourceDb.iModelId,
375
513
  });
376
- return scopeProvenanceAspectId.aspectId
377
- ? this.provenanceDb.elements.getAspect(scopeProvenanceAspectId.aspectId)
514
+ return scopeProvenanceAspectProps !== undefined
515
+ ? this.provenanceDb.elements.getAspect(scopeProvenanceAspectProps.aspectId)
378
516
  : undefined;
379
517
  }
380
518
  /**
@@ -388,19 +526,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
388
526
  id: undefined,
389
527
  version: undefined,
390
528
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
391
- element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
529
+ element: {
530
+ id: this.targetScopeElementId,
531
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
532
+ },
392
533
  scope: { id: core_common_1.IModel.rootSubjectId },
393
534
  identifier: this.provenanceSourceDb.iModelId,
394
535
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
395
536
  jsonProperties: undefined,
396
537
  };
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) {
538
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps); // this query includes "identifier"
539
+ if (foundEsaProps === undefined) {
404
540
  aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
405
541
  aspectProps.jsonProperties = {
406
542
  pendingReverseSyncChangesetIndices: [],
@@ -426,44 +562,33 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
426
562
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
427
563
  }
428
564
  if (!this._options.noProvenance) {
429
- this.provenanceDb.elements.insertAspect({
565
+ const id = this.provenanceDb.elements.insertAspect({
430
566
  ...aspectProps,
431
567
  jsonProperties: JSON.stringify(aspectProps.jsonProperties),
432
568
  });
569
+ aspectProps.id = id;
433
570
  }
434
571
  }
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
- });
572
+ else {
573
+ // foundEsaProps is defined.
574
+ aspectProps.id = foundEsaProps.aspectId;
575
+ aspectProps.version =
576
+ foundEsaProps.version ??
577
+ (this._options.branchRelationshipDataBehavior === "unsafe-migrate"
578
+ ? ""
579
+ : undefined);
580
+ aspectProps.jsonProperties = foundEsaProps.jsonProperties
581
+ ? JSON.parse(foundEsaProps.jsonProperties)
582
+ : this._options.branchRelationshipDataBehavior === "unsafe-migrate"
583
+ ? {
584
+ pendingReverseSyncChangesetIndices: [],
585
+ pendingSyncChangesetIndices: [],
586
+ reverseSyncVersion: "",
587
+ }
588
+ : undefined;
589
+ }
590
+ this._targetScopeProvenanceProps =
591
+ aspectProps;
467
592
  }
468
593
  /**
469
594
  * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
@@ -477,13 +602,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
477
602
  if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
478
603
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
479
604
  }
480
- const sourceDb = args.isReverseSynchronization ? args.provenanceDb : args.provenanceSourceDb;
481
- const targetDb = args.isReverseSynchronization ? args.provenanceSourceDb : args.provenanceDb;
605
+ const sourceDb = args.isReverseSynchronization
606
+ ? args.provenanceDb
607
+ : args.provenanceSourceDb;
608
+ const targetDb = args.isReverseSynchronization
609
+ ? args.provenanceSourceDb
610
+ : args.provenanceDb;
482
611
  // query for provenanceDb
483
612
  const elementIdByFedGuidQuery = `
484
613
  SELECT e.ECInstanceId, FederationGuid
485
614
  FROM bis.Element e
486
- WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
615
+ ${args.skipPropagateChangesToRootElements
616
+ ? "WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements"
617
+ : ""}
487
618
  ORDER BY FederationGuid
488
619
  `;
489
620
  // iterate through sorted list of fed guids from both dbs to get the intersection
@@ -501,22 +632,22 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
501
632
  // and the fact that '0' < '9' < a' < 'f' in ascii/utf8
502
633
  while (true) {
503
634
  const currSourceRow = sourceRow, currTargetRow = targetRow;
504
- if (currSourceRow.federationGuid !== undefined
505
- && currTargetRow.federationGuid !== undefined
506
- && currSourceRow.federationGuid === currTargetRow.federationGuid) {
635
+ if (currSourceRow.federationGuid !== undefined &&
636
+ currTargetRow.federationGuid !== undefined &&
637
+ currSourceRow.federationGuid === currTargetRow.federationGuid) {
507
638
  // data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
508
639
  args.fn(sourceRow.id, targetRow.id);
509
640
  }
510
- if (currTargetRow.federationGuid === undefined
511
- || (currSourceRow.federationGuid !== undefined
512
- && currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
641
+ if (currTargetRow.federationGuid === undefined ||
642
+ (currSourceRow.federationGuid !== undefined &&
643
+ currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
513
644
  if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
514
645
  return;
515
646
  targetRow = targetStmt.getRow();
516
647
  }
517
- if (currSourceRow.federationGuid === undefined
518
- || (currTargetRow.federationGuid !== undefined
519
- && currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
648
+ if (currSourceRow.federationGuid === undefined ||
649
+ (currTargetRow.federationGuid !== undefined &&
650
+ currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
520
651
  if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
521
652
  return;
522
653
  sourceRow = sourceStmt.getRow();
@@ -534,7 +665,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
534
665
  // victims of the old provenance method that have both fedguids and an inserted aspect.
535
666
  // But this is a private function with one known caller where that doesn't matter
536
667
  args.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
537
- const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization ? args.fn(sourceId, targetId) : args.fn(targetId, sourceId);
668
+ const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization
669
+ ? args.fn(sourceId, targetId)
670
+ : args.fn(targetId, sourceId);
538
671
  stmt.bindId("scopeId", args.targetScopeElementId);
539
672
  stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
540
673
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
@@ -550,240 +683,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
550
683
  provenanceSourceDb: this.provenanceSourceDb,
551
684
  provenanceDb: this.provenanceDb,
552
685
  targetScopeElementId: this.targetScopeElementId,
553
- isReverseSynchronization: !!this._options.isReverseSynchronization,
686
+ isReverseSynchronization: this.isReverseSynchronization,
554
687
  fn,
688
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
555
689
  });
556
690
  }
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
- /**
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.
573
- */
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
691
  _queryProvenanceForElement(entityInProvenanceSourceId) {
788
692
  return this.provenanceDb.withPreparedStatement(`
789
693
  SELECT esa.Element.Id
@@ -833,7 +737,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
833
737
  sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
834
738
  targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
835
739
  };
836
- if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
740
+ if (targetRelInfo.sourceId === undefined ||
741
+ targetRelInfo.targetId === undefined)
837
742
  return undefined; // couldn't find an element, rel is invalid or deleted
838
743
  return this.targetDb.withPreparedStatement(`
839
744
  SELECT ECInstanceId
@@ -867,7 +772,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
867
772
  JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
868
773
  WHERE s.Name=? AND c.Name=?
869
774
  `, (stmt) => {
870
- const [schemaName, className] = classFullName.split(".");
775
+ const [schemaName, className] = classFullName.indexOf(".") !== -1
776
+ ? classFullName.split(".")
777
+ : classFullName.split(":");
871
778
  stmt.bindString(1, schemaName);
872
779
  stmt.bindString(2, className);
873
780
  if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
@@ -885,26 +792,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
885
792
  });
886
793
  }
887
794
  /** Returns `true` if *brute force* delete detections should be run.
795
+ * @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
888
796
  * @note Not relevant for processChanges when change history is known.
889
797
  */
890
798
  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;
799
+ nodeAssert(this._syncType !== undefined);
800
+ return this._syncType === "not-sync";
901
801
  }
902
802
  /**
903
803
  * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
904
804
  * in the source iModel.
905
805
  * @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
906
806
  * This method is only called during [[processAll]] when the option
907
- * [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
807
+ * [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
908
808
  * necessary when using [[processChanges]] since changeset information is sufficient.
909
809
  * @note you do not need to call this directly unless processing a subset of an iModel.
910
810
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
@@ -916,18 +816,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
916
816
  WHERE Scope.Id=:scopeId
917
817
  AND Kind=:kind
918
818
  `;
919
- nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
819
+ nodeAssert(!this.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
920
820
  this.provenanceDb.withPreparedStatement(sql, (stmt) => {
921
821
  stmt.bindId("scopeId", this.targetScopeElementId);
922
822
  stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
923
823
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
924
824
  // ExternalSourceAspect.Identifier is of type string
925
825
  const aspectIdentifier = stmt.getValue(0).getString();
926
- if (!core_bentley_1.Id64.isId64(aspectIdentifier)) {
826
+ if (!core_bentley_1.Id64.isValidId64(aspectIdentifier)) {
927
827
  continue;
928
828
  }
929
829
  const targetElemId = stmt.getValue(1).getId();
930
- const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
830
+ const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
831
+ entityReference: `e${aspectIdentifier}`,
832
+ });
931
833
  if (wasDeletedInSource)
932
834
  this.importer.deleteElement(targetElemId);
933
835
  }
@@ -937,7 +839,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
937
839
  * @deprecated in 3.x, this no longer has any effect except emitting a warning
938
840
  */
939
841
  skipElement(_sourceElement) {
940
- core_bentley_1.Logger.logWarning(loggerCategory, `Tried to defer/skip an element, which is no longer necessary`);
842
+ core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
941
843
  }
942
844
  /** Transform the specified sourceElement into ElementProps for the target iModel.
943
845
  * @param sourceElement The Element from the source iModel to transform.
@@ -956,39 +858,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
956
858
  }
957
859
  return targetElementProps;
958
860
  }
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
861
  /** Returns true if a change within sourceElement is detected.
993
862
  * @param sourceElement The Element from the source iModel
994
863
  * @param targetElementId The Element from the target iModel to compare against.
@@ -1000,8 +869,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1000
869
  if (this._sourceChangeDataState === "unconnected")
1001
870
  return true;
1002
871
  nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
1003
- if (this._hasElementChangedCache === undefined)
1004
- this._cacheSourceChanges();
872
+ nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
1005
873
  return this._hasElementChangedCache.has(sourceElement.id);
1006
874
  }
1007
875
  static transformCallbackFor(transformer, entity) {
@@ -1028,8 +896,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1028
896
  const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
1029
897
  const targetProps = onEntityTransform.call(this, sourceEntity);
1030
898
  if (sourceEntity instanceof core_backend_1.Relationship) {
1031
- targetProps.sourceId = this.context.findTargetElementId(sourceEntity.sourceId);
1032
- targetProps.targetId = this.context.findTargetElementId(sourceEntity.targetId);
899
+ targetProps.sourceId =
900
+ this.context.findTargetElementId(sourceEntity.sourceId);
901
+ targetProps.targetId =
902
+ this.context.findTargetElementId(sourceEntity.targetId);
1033
903
  }
1034
904
  updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
1035
905
  this._partiallyCommittedEntities.delete(sourceEntity);
@@ -1045,11 +915,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1045
915
  for (const referenceId of entity.getReferenceConcreteIds()) {
1046
916
  // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
1047
917
  const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
1048
- const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) || this._skippedEntities.has(referenceId);
918
+ const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
919
+ this._skippedEntities.has(referenceId);
1049
920
  if (alreadyProcessed)
1050
921
  continue;
1051
922
  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 });
923
+ const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
924
+ entityReference: referenceId,
925
+ });
1053
926
  if (!referencedExistsInSource) {
1054
927
  core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
1055
928
  switch (this._options.danglingReferencesBehavior) {
@@ -1059,7 +932,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1059
932
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
1060
933
  `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
1061
934
  "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",
935
+ "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
1063
936
  "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
1064
937
  "transformer to remove the reference and fix affected elements may be suitable.",
1065
938
  ].join("\n"));
@@ -1097,7 +970,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1097
970
  /** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel.
1098
971
  * @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter.
1099
972
  */
1100
- shouldExportElement(_sourceElement) { return true; }
973
+ shouldExportElement(_sourceElement) {
974
+ return true;
975
+ }
1101
976
  onSkipElement(sourceElementId) {
1102
977
  if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
1103
978
  // element already has provenance
@@ -1128,7 +1003,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1128
1003
  const referenceType = elemClass.requiredReferenceKeyTypeMap[referenceKey];
1129
1004
  // For now we just consider all required references to be elements (as they are in biscore), and do not support
1130
1005
  // 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);
1006
+ (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element ||
1007
+ referenceType === core_common_1.ConcreteEntityTypes.Model);
1132
1008
  return mapId64(idContainer, (id) => {
1133
1009
  if (id === core_bentley_1.Id64.invalid || id === core_common_1.IModel.rootSubjectId)
1134
1010
  return undefined; // not allowed to directly export the root subject
@@ -1137,13 +1013,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1137
1013
  // This is relied upon by the TemplateModelCloner
1138
1014
  // TODO: extract this out to only be in the TemplateModelCloner
1139
1015
  const asDefinitionElem = this.sourceDb.elements.tryGetElement(id, core_backend_1.DefinitionElement);
1140
- if (asDefinitionElem && !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
1016
+ if (asDefinitionElem &&
1017
+ !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
1141
1018
  this.context.remapElement(id, id);
1142
1019
  }
1143
1020
  }
1144
1021
  return id;
1145
- })
1146
- .filter((sourceReferenceId) => {
1022
+ }).filter((sourceReferenceId) => {
1147
1023
  if (sourceReferenceId === undefined)
1148
1024
  return false;
1149
1025
  const referenceInTargetId = this.context.findTargetElementId(sourceReferenceId);
@@ -1171,7 +1047,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1171
1047
  const isSubModeled = dbHasModel(this.sourceDb, elementId);
1172
1048
  const idOfElemInTarget = this.context.findTargetElementId(elementId);
1173
1049
  const isElemInTarget = core_bentley_1.Id64.invalid !== idOfElemInTarget;
1174
- const needsModelImport = isSubModeled && (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
1050
+ const needsModelImport = isSubModeled &&
1051
+ (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
1175
1052
  return { needsElemImport: !isElemInTarget, needsModelImport };
1176
1053
  }
1177
1054
  /** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel.
@@ -1186,27 +1063,33 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1186
1063
  }
1187
1064
  else if (this._options.wasSourceIModelCopiedToTarget) {
1188
1065
  targetElementId = sourceElement.id;
1189
- targetElementProps = this.targetDb.elements.getElementProps(targetElementId);
1066
+ targetElementProps =
1067
+ this.targetDb.elements.getElementProps(targetElementId);
1190
1068
  }
1191
1069
  else {
1192
1070
  targetElementId = this.context.findTargetElementId(sourceElement.id);
1193
1071
  targetElementProps = this.onTransformElement(sourceElement);
1194
1072
  }
1195
1073
  // 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;
1074
+ if (this.context.isBetweenIModels &&
1075
+ !core_bentley_1.Id64.isValid(targetElementId) &&
1076
+ sourceElement.federationGuid !== undefined) {
1077
+ targetElementId =
1078
+ this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1198
1079
  if (core_bentley_1.Id64.isValid(targetElementId))
1199
1080
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
1200
1081
  }
1201
1082
  // 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)) {
1083
+ if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
1084
+ core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1203
1085
  // respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
1204
1086
  // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
1205
1087
  targetElementProps.code.value = targetElementProps.code.value ?? "";
1206
1088
  const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
1207
1089
  if (undefined !== maybeTargetElementId) {
1208
1090
  const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
1209
- if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1091
+ if (maybeTargetElem.classFullName === targetElementProps.classFullName) {
1092
+ // ensure code remapping doesn't change the target class
1210
1093
  targetElementId = maybeTargetElementId;
1211
1094
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
1212
1095
  }
@@ -1215,14 +1098,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1215
1098
  }
1216
1099
  }
1217
1100
  }
1218
- if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
1101
+ if (core_bentley_1.Id64.isValid(targetElementId) &&
1102
+ !this.hasElementChanged(sourceElement, targetElementId))
1219
1103
  return;
1220
1104
  this.collectUnmappedReferences(sourceElement);
1221
1105
  // targetElementId will be valid (indicating update) or undefined (indicating insert)
1222
- targetElementProps.id
1223
- = core_bentley_1.Id64.isValid(targetElementId)
1224
- ? targetElementId
1225
- : undefined;
1106
+ targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
1107
+ ? targetElementId
1108
+ : undefined;
1226
1109
  if (!this._options.wasSourceIModelCopiedToTarget) {
1227
1110
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1228
1111
  }
@@ -1237,17 +1120,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1237
1120
  // FIXME: verify at finalization time that we don't lose provenance on new elements
1238
1121
  // FIXME: make public and improve `initElementProvenance` API for usage by consolidators
1239
1122
  if (!this._options.noProvenance) {
1240
- let provenance = this._options.forceExternalSourceAspectProvenance || this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1123
+ let provenance = this._options.forceExternalSourceAspectProvenance ||
1124
+ this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1241
1125
  ? undefined
1242
1126
  : sourceElement.federationGuid;
1243
1127
  if (!provenance) {
1244
1128
  const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
1245
- const aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
1246
- if (aspectId === undefined) {
1129
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1130
+ if (foundEsaProps === undefined)
1247
1131
  aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1248
- }
1249
1132
  else {
1250
- aspectProps.id = aspectId;
1133
+ // Since initElementProvenance sets a property 'version' on the aspectProps that we wish to persist in the provenanceDb, only grab the id from the foundEsaProps.
1134
+ aspectProps.id = foundEsaProps.aspectId;
1251
1135
  this.provenanceDb.elements.updateAspect(aspectProps);
1252
1136
  }
1253
1137
  provenance = aspectProps;
@@ -1278,10 +1162,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1278
1162
  * This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel.
1279
1163
  */
1280
1164
  onExportModel(sourceModel) {
1281
- if (core_common_1.IModel.repositoryModelId === sourceModel.id) {
1165
+ if (this._options.skipPropagateChangesToRootElements &&
1166
+ core_common_1.IModel.repositoryModelId === sourceModel.id)
1282
1167
  return; // The RepositoryModel should not be directly imported
1283
- }
1284
1168
  const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
1169
+ // there can only be one repositoryModel per database, so ignore the repo model on remapped subjects
1170
+ const isRemappedRootSubject = sourceModel.id === core_common_1.IModel.repositoryModelId &&
1171
+ targetModeledElementId != sourceModel.id;
1172
+ if (isRemappedRootSubject)
1173
+ return;
1285
1174
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
1286
1175
  this.importer.importModel(targetModelProps);
1287
1176
  this.resolvePendingReferences(sourceModel);
@@ -1303,9 +1192,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1303
1192
  stmt.bindId(1, targetModelId);
1304
1193
  const val = stmt.step();
1305
1194
  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}'`);
1195
+ case core_bentley_1.DbResult.BE_SQLITE_ROW:
1196
+ return true;
1197
+ case core_bentley_1.DbResult.BE_SQLITE_DONE:
1198
+ return false;
1199
+ default:
1200
+ (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
1309
1201
  }
1310
1202
  });
1311
1203
  if (isDefinitionPartition) {
@@ -1319,7 +1211,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1319
1211
  this.importer.deleteModel(targetModelId);
1320
1212
  }
1321
1213
  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);
1214
+ const isDeletionProhibitedErr = error instanceof core_common_1.IModelError &&
1215
+ (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited ||
1216
+ error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
1323
1217
  if (!isDeletionProhibitedErr)
1324
1218
  throw error;
1325
1219
  // 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 +1224,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1330
1224
  }
1331
1225
  /** Schedule modeled partition deletion */
1332
1226
  scheduleModeledPartitionDeletion(sourceModelId) {
1333
- const deletedElements = this.exporter.sourceDbChanges?.element.deleteIds;
1227
+ const deletedElements = this.exporter.sourceDbChanges?.element
1228
+ .deleteIds;
1334
1229
  if (!deletedElements.has(sourceModelId)) {
1335
1230
  deletedElements.add(sourceModelId);
1336
1231
  }
@@ -1396,7 +1291,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1396
1291
  onTransformModel(sourceModel, targetModeledElementId) {
1397
1292
  const targetModelProps = sourceModel.toJSON();
1398
1293
  // don't directly edit deep object since toJSON performs a shallow clone
1399
- targetModelProps.modeledElement = { ...targetModelProps.modeledElement, id: targetModeledElementId };
1294
+ targetModelProps.modeledElement = {
1295
+ ...targetModelProps.modeledElement,
1296
+ id: targetModeledElementId,
1297
+ };
1400
1298
  targetModelProps.id = targetModeledElementId;
1401
1299
  targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
1402
1300
  return targetModelProps;
@@ -1408,7 +1306,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1408
1306
  /** called at the end ([[finalizeTransformation]]) of a transformation,
1409
1307
  * updates the target scope element to say that transformation up through the
1410
1308
  * source's changeset has been performed. Also stores all changesets that occurred
1411
- * during the transformation as "pending synchronization changeset indices"
1309
+ * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
1412
1310
  *
1413
1311
  * You generally should not call this function yourself and use [[processChanges]] instead.
1414
1312
  * It is public for unsupported use cases of custom synchronization transforms.
@@ -1416,45 +1314,54 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1416
1314
  * without setting the `force` option to `true`
1417
1315
  */
1418
1316
  updateSynchronizationVersion({ force = false } = {}) {
1419
- if (!force && (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization))
1317
+ const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
1318
+ this._sourceChangeDataState !== "has-changes" &&
1319
+ !this._isProvenanceInitTransform;
1320
+ if (notForcedAndHasNoChangesAndIsntProvenanceInit)
1420
1321
  return;
1421
1322
  nodeAssert(this._targetScopeProvenanceProps);
1422
1323
  const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1423
1324
  const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1424
- if (this._isFirstSynchronization) {
1325
+ if (this._isProvenanceInitTransform) {
1425
1326
  this._targetScopeProvenanceProps.version = sourceVersion;
1426
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = targetVersion;
1327
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1328
+ targetVersion;
1427
1329
  }
1428
- else if (this._options.isReverseSynchronization) {
1330
+ else if (this.isReverseSynchronization) {
1429
1331
  const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1430
1332
  core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1431
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = sourceVersion;
1333
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1334
+ sourceVersion;
1432
1335
  }
1433
- else if (!this._options.isReverseSynchronization) {
1336
+ else if (!this.isReverseSynchronization) {
1434
1337
  core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1435
1338
  this._targetScopeProvenanceProps.version = sourceVersion;
1436
1339
  }
1437
- if (this._isSynchronization) {
1438
- (0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1340
+ if (this._isSynchronization ||
1341
+ (this._startingChangesetIndices && this._isProvenanceInitTransform)) {
1342
+ nodeAssert(this.targetDb.changeset.index !== undefined &&
1343
+ this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1439
1344
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1440
1345
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1441
1346
  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];
1445
- // NOTE that as documented in [[processChanges]], this assumes that right after
1446
- // transformation finalization, the work will be saved immediately, otherwise we've
1447
- // just marked this changeset as a synchronization to ignore, and the user can add other
1448
- // stuff to it which would break future synchronizations
1449
- // FIXME: force save for the user to prevent that
1347
+ const [syncChangesetsToClear, syncChangesetsToUpdate] = this
1348
+ .isReverseSynchronization
1349
+ ? [
1350
+ jsonProps.pendingReverseSyncChangesetIndices,
1351
+ jsonProps.pendingSyncChangesetIndices,
1352
+ ]
1353
+ : [
1354
+ jsonProps.pendingSyncChangesetIndices,
1355
+ jsonProps.pendingReverseSyncChangesetIndices,
1356
+ ];
1450
1357
  for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1451
1358
  syncChangesetsToUpdate.push(i);
1452
1359
  // FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
1453
1360
  // probably wrong, and we should filter it instead
1454
1361
  syncChangesetsToClear.length = 0;
1455
1362
  // 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");
1363
+ if (this.isReverseSynchronization) {
1364
+ nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
1458
1365
  for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
1459
1366
  jsonProps.pendingReverseSyncChangesetIndices.push(i);
1460
1367
  }
@@ -1467,17 +1374,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1467
1374
  });
1468
1375
  }
1469
1376
  // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1470
- finalizeTransformation() {
1377
+ async finalizeTransformation(options) {
1471
1378
  this.importer.finalize();
1472
1379
  this.updateSynchronizationVersion();
1473
1380
  if (this._partiallyCommittedEntities.size > 0) {
1474
- // FIXME: throw in this case if danglingReferenceBehavior === reject
1475
- core_bentley_1.Logger.logWarning(loggerCategory, [
1381
+ const message = [
1476
1382
  "The following elements were never fully resolved:",
1477
1383
  [...this._partiallyCommittedEntities.keys()].join(","),
1478
1384
  "This indicates that either some references were excluded from the transformation",
1479
1385
  "or the source has dangling references.",
1480
- ].join("\n"));
1386
+ ].join("\n");
1387
+ if (this._options.danglingReferencesBehavior === "reject")
1388
+ throw new Error(message);
1389
+ core_bentley_1.Logger.logWarning(loggerCategory, message);
1481
1390
  for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
1482
1391
  partiallyCommittedElem.forceComplete();
1483
1392
  }
@@ -1489,11 +1398,36 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1489
1398
  }
1490
1399
  // this internal is guaranteed stable for just transformer usage
1491
1400
  /* eslint-disable @itwin/no-internal */
1492
- if ("codeValueBehavior" in this.sourceDb) {
1401
+ if (("codeValueBehavior" in this.sourceDb)) {
1493
1402
  this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
1494
1403
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1495
1404
  }
1496
1405
  /* eslint-enable @itwin/no-internal */
1406
+ const defaultSaveTargetChanges = () => this.targetDb.saveChanges();
1407
+ await (options?.saveTargetChanges ?? defaultSaveTargetChanges)(this);
1408
+ if (this.isReverseSynchronization)
1409
+ this.sourceDb.saveChanges();
1410
+ const description = `${this._isProvenanceInitTransform
1411
+ ? options?.provenanceInitTransformChangesetDescription ??
1412
+ `initialized branch provenance with master iModel: ${this.sourceDb.iModelId}`
1413
+ : this.isForwardSynchronization
1414
+ ? options?.forwardSyncBranchChangesetDescription ??
1415
+ `Forward sync of iModel: ${this.sourceDb.iModelId}`
1416
+ : options?.reverseSyncMasterChangesetDescription ??
1417
+ `Reverse sync of iModel: ${this.sourceDb.iModelId}`}`;
1418
+ if (this.targetDb.isBriefcaseDb()) {
1419
+ // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1420
+ await this.targetDb.pushChanges({
1421
+ description,
1422
+ });
1423
+ }
1424
+ if (this.isReverseSynchronization && this.sourceDb.isBriefcaseDb()) {
1425
+ // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1426
+ await this.sourceDb.pushChanges({
1427
+ description: options?.reverseSyncBranchChangesetDescription ??
1428
+ `Update provenance in response to a reverse sync to iModel: ${this.targetDb.iModelId}`,
1429
+ });
1430
+ }
1497
1431
  }
1498
1432
  /** Imports all relationships that subclass from the specified base class.
1499
1433
  * @param baseRelClassFullName The specified base relationship class.
@@ -1506,7 +1440,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1506
1440
  /** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported.
1507
1441
  * @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer).
1508
1442
  */
1509
- shouldExportRelationship(_sourceRelationship) { return true; }
1443
+ shouldExportRelationship(_sourceRelationship) {
1444
+ return true;
1445
+ }
1510
1446
  /** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel.
1511
1447
  * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
1512
1448
  */
@@ -1515,14 +1451,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1515
1451
  const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
1516
1452
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
1517
1453
  const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
1518
- if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1454
+ if (!this._options.noProvenance &&
1455
+ core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1519
1456
  let provenance = !this._options.forceExternalSourceAspectProvenance
1520
1457
  ? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
1521
1458
  : undefined;
1522
1459
  if (!provenance) {
1523
1460
  const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
1524
- aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
1525
- if (undefined === aspectProps.id) {
1461
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1462
+ // 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).
1463
+ if (undefined === foundEsaProps) {
1526
1464
  aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1527
1465
  }
1528
1466
  provenance = aspectProps;
@@ -1541,10 +1479,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1541
1479
  core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1542
1480
  return;
1543
1481
  }
1544
- const relArg = deletedRelData.relId ?? {
1545
- sourceId: deletedRelData.sourceIdInTarget,
1546
- targetId: deletedRelData.targetIdInTarget,
1547
- };
1482
+ const relArg = deletedRelData.relId ??
1483
+ {
1484
+ sourceId: deletedRelData.sourceIdInTarget,
1485
+ targetId: deletedRelData.targetIdInTarget,
1486
+ };
1548
1487
  // FIXME: make importer.deleteRelationship not need full props
1549
1488
  const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1550
1489
  if (targetRelationship) {
@@ -1569,7 +1508,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1569
1508
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
1570
1509
  */
1571
1510
  async detectRelationshipDeletes() {
1572
- if (this._options.isReverseSynchronization) {
1511
+ if (this.isReverseSynchronization) {
1573
1512
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
1574
1513
  }
1575
1514
  const aspectDeleteIds = [];
@@ -1584,13 +1523,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1584
1523
  statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
1585
1524
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
1586
1525
  const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
1587
- if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1526
+ if (undefined ===
1527
+ this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1588
1528
  // this function exists only to support some in-imodel transformations, which must
1589
1529
  // use the old (external source aspect) provenance method anyway so we don't need to support
1590
1530
  // new provenance
1591
1531
  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);
1532
+ const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
1533
+ if (targetRelInstanceId) {
1534
+ const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
1594
1535
  this.importer.deleteRelationship(targetRelationship.toJSON());
1595
1536
  }
1596
1537
  aspectDeleteIds.push(statement.getValue(0).getId());
@@ -1611,8 +1552,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1611
1552
  targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
1612
1553
  // TODO: move to cloneRelationship in IModelCloneContext
1613
1554
  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]);
1555
+ if (core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType &&
1556
+ "Id" === propertyMetaData.extendedType) {
1557
+ targetRelationshipProps[propertyName] =
1558
+ this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
1616
1559
  }
1617
1560
  });
1618
1561
  return targetRelationshipProps;
@@ -1644,8 +1587,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1644
1587
  sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
1645
1588
  // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
1646
1589
  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;
1590
+ const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
1591
+ a.scope?.id === this.targetScopeElementId;
1592
+ return (!this._options.includeSourceProvenance ||
1593
+ !isExternalSourceAspectFromTransformer);
1649
1594
  });
1650
1595
  for (let i = 0; i < targetIds.length; ++i) {
1651
1596
  this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
@@ -1684,9 +1629,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1684
1629
  let schemaFileName = schema.name + ext;
1685
1630
  // many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
1686
1631
  const systemMaxPathSegmentSize = 255;
1687
- // windows usually has a limit for the total path length of 260
1688
- const windowsMaxPathLimit = 260;
1689
- if (schemaFileName.length > systemMaxPathSegmentSize || path.join(this._schemaExportDir, schemaFileName).length >= windowsMaxPathLimit) {
1632
+ if (schemaFileName.length > systemMaxPathSegmentSize) {
1690
1633
  // this name should be well under 255 bytes
1691
1634
  // ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
1692
1635
  // You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
@@ -1726,7 +1669,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1726
1669
  const maybeLongNameResolvingSchemaCtx = this._longNamedSchemasMap.size > 0
1727
1670
  ? this._makeLongNameResolvingSchemaCtx()
1728
1671
  : undefined;
1729
- return await this.targetDb.importSchemas(schemaFullPaths, { ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx });
1672
+ return await this.targetDb.importSchemas(schemaFullPaths, {
1673
+ ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx,
1674
+ });
1730
1675
  }
1731
1676
  finally {
1732
1677
  core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
@@ -1734,8 +1679,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1734
1679
  }
1735
1680
  }
1736
1681
  /** 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
- */
1682
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1683
+ */
1739
1684
  async processFonts() {
1740
1685
  // we do not need to initialize for this since no entities are exported
1741
1686
  await this.initialize();
@@ -1762,7 +1707,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1762
1707
  /** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel.
1763
1708
  * @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer).
1764
1709
  */
1765
- shouldExportCodeSpec(_sourceCodeSpec) { return true; }
1710
+ shouldExportCodeSpec(_sourceCodeSpec) {
1711
+ return true;
1712
+ }
1766
1713
  /** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */
1767
1714
  onExportCodeSpec(sourceCodeSpec) {
1768
1715
  this.context.importCodeSpec(sourceCodeSpec.id);
@@ -1786,33 +1733,236 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1786
1733
  async initialize(args) {
1787
1734
  if (this._initialized)
1788
1735
  return;
1789
- await this.context.initialize();
1790
1736
  await this._tryInitChangesetData(args);
1737
+ await this.context.initialize();
1738
+ // need exporter initialized to do remapdeletedsourceentities.
1791
1739
  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);
1740
+ // Exporter must be initialized prior to processing changesets in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
1741
+ await this.processChangesets();
1795
1742
  this._initialized = true;
1796
1743
  }
1744
+ /**
1745
+ * Reads all the changeset files in the private member of the transformer: _csFileProps and does two things with these changesets.
1746
+ * Finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
1747
+ * Populates this._hasElementChangedCache with a set of elementIds that have been updated or inserted into the database.
1748
+ * This function returns early if csFileProps is undefined or is of length 0.
1749
+ * @returns void
1750
+ */
1751
+ async processChangesets() {
1752
+ this.forEachTrackedElement((sourceElementId, targetElementId) => {
1753
+ this.context.remapElement(sourceElementId, targetElementId);
1754
+ });
1755
+ if (this._csFileProps === undefined || this._csFileProps.length === 0)
1756
+ return;
1757
+ const hasElementChangedCache = new Set();
1758
+ const relationshipECClassIdsToSkip = new Set();
1759
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
1760
+ relationshipECClassIdsToSkip.add(row.ECInstanceId);
1761
+ }
1762
+ const relationshipECClassIds = new Set();
1763
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)")) {
1764
+ relationshipECClassIds.add(row.ECInstanceId);
1765
+ }
1766
+ // For later use when processing deletes.
1767
+ const alreadyImportedElementInserts = new Set();
1768
+ const alreadyImportedModelInserts = new Set();
1769
+ this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
1770
+ const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
1771
+ if (core_bentley_1.Id64.isValid(targetElementId))
1772
+ alreadyImportedElementInserts.add(targetElementId);
1773
+ });
1774
+ this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
1775
+ const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
1776
+ if (core_bentley_1.Id64.isValid(targetModelId))
1777
+ alreadyImportedModelInserts.add(targetModelId);
1778
+ });
1779
+ this._deletedSourceRelationshipData = new Map();
1780
+ for (const csFile of this._csFileProps) {
1781
+ const csReader = core_backend_1.SqliteChangesetReader.openFile({
1782
+ fileName: csFile.pathname,
1783
+ db: this.sourceDb,
1784
+ disableSchemaCheck: true,
1785
+ });
1786
+ const csAdaptor = new core_backend_1.ChangesetECAdaptor(csReader);
1787
+ const ecChangeUnifier = new core_backend_1.PartialECChangeUnifier();
1788
+ while (csAdaptor.step()) {
1789
+ ecChangeUnifier.appendFrom(csAdaptor);
1790
+ }
1791
+ const changes = [...ecChangeUnifier.instances];
1792
+ /** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
1793
+ const elemIdToScopeEsa = new Map();
1794
+ for (const change of changes) {
1795
+ if (change.ECClassId !== undefined &&
1796
+ relationshipECClassIdsToSkip.has(change.ECClassId))
1797
+ continue;
1798
+ const changeType = change.$meta?.op;
1799
+ if (changeType === "Deleted" &&
1800
+ change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1801
+ change.Scope.Id === this.targetScopeElementId) {
1802
+ elemIdToScopeEsa.set(change.Element.Id, change);
1803
+ }
1804
+ else if (changeType === "Inserted" || changeType === "Updated")
1805
+ hasElementChangedCache.add(change.ECInstanceId);
1806
+ }
1807
+ // Loop to process deletes.
1808
+ for (const change of changes) {
1809
+ const changeType = change.$meta?.op;
1810
+ const ecClassId = change.ECClassId ?? change.$meta?.fallbackClassId;
1811
+ if (ecClassId === undefined)
1812
+ throw new Error(`ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change?.$meta?.tables}`);
1813
+ if (changeType === undefined)
1814
+ throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
1815
+ if (changeType !== "Deleted" ||
1816
+ relationshipECClassIdsToSkip.has(ecClassId))
1817
+ continue;
1818
+ this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1819
+ }
1820
+ csReader.close();
1821
+ }
1822
+ this._hasElementChangedCache = hasElementChangedCache;
1823
+ return;
1824
+ }
1825
+ /**
1826
+ * Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
1827
+ * @param change the change to process, must be of changeType "Deleted"
1828
+ * @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.
1829
+ * 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.
1830
+ * @param isRelationship is relationship or not
1831
+ * @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
1832
+ * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1833
+ * @returns void
1834
+ */
1835
+ processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1836
+ // we need a connected iModel with changes to remap elements with deletions
1837
+ const notConnectedModel = this.sourceDb.iTwinId === undefined;
1838
+ const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1839
+ if (notConnectedModel || noChanges)
1840
+ return;
1841
+ // optimization: if we have provenance, use it to avoid more querying later
1842
+ // eventually when itwin.js supports attaching a second iModelDb in JS,
1843
+ // this won't have to be a conditional part of the query, and we can always have it by attaching
1844
+ const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
1845
+ const instId = change.ECInstanceId;
1846
+ if (!isRelationship) {
1847
+ const sourceElemFedGuid = change.FederationGuid;
1848
+ let identifierValue;
1849
+ if (queryCanAccessProvenance) {
1850
+ const aspects = this.sourceDb.elements.getAspects(instId, core_backend_1.ExternalSourceAspect.classFullName);
1851
+ for (const aspect of aspects) {
1852
+ // look for aspect where the ecInstanceId = the aspect.element.id
1853
+ if (aspect.element.id === instId &&
1854
+ aspect.scope.id === this.targetScopeElementId)
1855
+ identifierValue = aspect.identifier;
1856
+ }
1857
+ // Think I need to query the esas given the instId.. not sure what db to do it on though.. soruce or target.. or provenance?
1858
+ // I need to know the id of the element dpeneding on which db its stored in.
1859
+ }
1860
+ if (queryCanAccessProvenance && !identifierValue) {
1861
+ if (mapOfDeletedElemIdToScopeEsas.get(instId) !== undefined)
1862
+ identifierValue =
1863
+ mapOfDeletedElemIdToScopeEsas.get(instId).Identifier;
1864
+ }
1865
+ const targetId = (queryCanAccessProvenance && identifierValue) ||
1866
+ // maybe batching these queries would perform better but we should
1867
+ // try to attach the second db and query both together anyway
1868
+ (sourceElemFedGuid &&
1869
+ this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid)) ||
1870
+ // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1871
+ this._queryProvenanceForElement(instId);
1872
+ // since we are processing one changeset at a time, we can see local source deletes
1873
+ // of entities that were never synced and can be safely ignored
1874
+ const deletionNotInTarget = !targetId;
1875
+ if (deletionNotInTarget)
1876
+ return;
1877
+ this.context.remapElement(instId, targetId);
1878
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1879
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
1880
+ if (alreadyImportedElementInserts.has(targetId)) {
1881
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
1882
+ }
1883
+ if (alreadyImportedModelInserts.has(targetId)) {
1884
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
1885
+ }
1886
+ }
1887
+ else {
1888
+ // is deleted relationship
1889
+ const classFullName = change.$meta?.classFullName;
1890
+ const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1891
+ const targetIdOfRelationshipInSource = change.TargetECInstanceId;
1892
+ const [sourceIdInTarget, targetIdInTarget] = [
1893
+ sourceIdOfRelationshipInSource,
1894
+ targetIdOfRelationshipInSource,
1895
+ ].map((id) => {
1896
+ let element;
1897
+ try {
1898
+ element = this.sourceDb.elements.getElement(id);
1899
+ }
1900
+ catch (err) {
1901
+ return undefined;
1902
+ }
1903
+ const fedGuid = element.federationGuid;
1904
+ let identifierValue;
1905
+ if (queryCanAccessProvenance) {
1906
+ const aspects = this.sourceDb.elements.getAspects(id, core_backend_1.ExternalSourceAspect.classFullName);
1907
+ for (const aspect of aspects) {
1908
+ if (aspect.element.id === id &&
1909
+ aspect.scope.id === this.targetScopeElementId)
1910
+ identifierValue = aspect.identifier;
1911
+ }
1912
+ if (identifierValue === undefined) {
1913
+ if (mapOfDeletedElemIdToScopeEsas.get(id) !== undefined)
1914
+ identifierValue =
1915
+ mapOfDeletedElemIdToScopeEsas.get(id).Identifier;
1916
+ }
1917
+ }
1918
+ return ((queryCanAccessProvenance && identifierValue) ||
1919
+ // maybe batching these queries would perform better but we should
1920
+ // try to attach the second db and query both together anyway
1921
+ (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
1922
+ });
1923
+ if (sourceIdInTarget && targetIdInTarget) {
1924
+ this._deletedSourceRelationshipData.set(instId, {
1925
+ classFullName: classFullName ?? "",
1926
+ sourceIdInTarget,
1927
+ targetIdInTarget,
1928
+ });
1929
+ }
1930
+ else {
1931
+ // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1932
+ const relProvenance = this._queryProvenanceForRelationship(instId, {
1933
+ classFullName: classFullName ?? "",
1934
+ sourceId: sourceIdOfRelationshipInSource,
1935
+ targetId: targetIdOfRelationshipInSource,
1936
+ });
1937
+ if (relProvenance && relProvenance.relationshipId)
1938
+ this._deletedSourceRelationshipData.set(instId, {
1939
+ classFullName: classFullName ?? "",
1940
+ relId: relProvenance.relationshipId,
1941
+ provenanceAspectId: relProvenance.aspectId,
1942
+ });
1943
+ }
1944
+ }
1945
+ }
1797
1946
  async _tryInitChangesetData(args) {
1798
- if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
1947
+ if (!args ||
1948
+ this.sourceDb.iTwinId === undefined ||
1949
+ this.sourceDb.changeset.index === undefined) {
1799
1950
  this._sourceChangeDataState = "unconnected";
1800
1951
  return;
1801
1952
  }
1802
1953
  const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1803
1954
  if (noChanges) {
1804
1955
  this._sourceChangeDataState = "no-changes";
1805
- this._changeSummaryIds = [];
1956
+ this._csFileProps = [];
1806
1957
  return;
1807
1958
  }
1808
1959
  // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1809
1960
  // to ignore those already processed changes
1810
- const startChangesetIndexOrId = args.startChangeset?.index
1811
- ?? args.startChangeset?.id
1812
- ?? this._synchronizationVersion.index + 1;
1961
+ const startChangesetIndexOrId = args.startChangeset?.index ??
1962
+ args.startChangeset?.id ??
1963
+ this._synchronizationVersion.index + 1;
1813
1964
  const endChangesetId = this.sourceDb.changeset.id;
1814
- const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
1815
- .map(async (indexOrId) => typeof indexOrId === "number"
1965
+ const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
1816
1966
  ? indexOrId
1817
1967
  : core_backend_1.IModelHost.hubAccess
1818
1968
  .queryChangeset({
@@ -1823,70 +1973,82 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1823
1973
  })
1824
1974
  .then((changeset) => changeset.index)));
1825
1975
  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}.`);
1976
+ if (!this._options.ignoreMissingChangesetsInSynchronizations &&
1977
+ startChangesetIndex !== this._synchronizationVersion.index + 1 &&
1978
+ this._synchronizationVersion.index !== -1) {
1979
+ throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
1980
+ " startChangesetId should be" +
1981
+ " exactly the first changeset *after* the previous synchronization to not miss data." +
1982
+ ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}` +
1983
+ ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'` +
1984
+ ` which is changeset #${this._synchronizationVersion.index}. The transformer expected` +
1985
+ ` #${this._synchronizationVersion.index + 1}.`);
1836
1986
  }
1837
1987
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1838
- const changesetsToSkip = this._isReverseSynchronization
1839
- ? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
1840
- : this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
1988
+ const changesetsToSkip = this.isReverseSynchronization
1989
+ ? this._targetScopeProvenanceProps.jsonProperties
1990
+ .pendingReverseSyncChangesetIndices
1991
+ : this._targetScopeProvenanceProps.jsonProperties
1992
+ .pendingSyncChangesetIndices;
1841
1993
  core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1842
1994
  this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1843
1995
  core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
1996
+ const csFileProps = [];
1844
1997
  for (const [first, end] of this._changesetRanges) {
1845
- this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
1846
- accessToken: args.accessToken,
1998
+ // 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.
1999
+ const fileProps = await core_backend_1.IModelHost.hubAccess.downloadChangesets({
1847
2000
  iModelId: this.sourceDb.iModelId,
1848
- iTwinId: this.sourceDb.iTwinId,
2001
+ targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
1849
2002
  range: { first, end },
1850
2003
  });
2004
+ csFileProps.push(...fileProps);
1851
2005
  }
1852
- core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
2006
+ this._csFileProps = csFileProps;
1853
2007
  this._sourceChangeDataState = "has-changes";
1854
2008
  }
1855
2009
  /** 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
- */
1858
- async processAll() {
2010
+ * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2011
+ */
2012
+ async processAll(options) {
1859
2013
  this.logSettings();
1860
2014
  this.initScopeProvenance();
1861
2015
  await this.initialize();
1862
2016
  await this.exporter.exportCodeSpecs();
1863
2017
  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
2018
+ if (this._options.skipPropagateChangesToRootElements) {
2019
+ // FIXME<NICK>: This option in exportAll was a maybe.
2020
+ // The RepositoryModel and root Subject of the target iModel should not be transformed.
2021
+ await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
2022
+ 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
2023
+ await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
2024
+ }
2025
+ else {
2026
+ await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
2027
+ }
1868
2028
  await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1869
2029
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
1870
2030
  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()) {
2031
+ if (this._options.forceExternalSourceAspectProvenance &&
2032
+ this.shouldDetectDeletes()) {
1873
2033
  await this.detectElementDeletes();
1874
2034
  await this.detectRelationshipDeletes();
1875
2035
  }
1876
2036
  if (this._options.optimizeGeometry)
1877
2037
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
1878
2038
  this.importer.computeProjectExtents();
1879
- this.finalizeTransformation();
2039
+ await this.finalizeTransformation(options);
1880
2040
  }
1881
2041
  markLastProvenance(sourceAspect, { isRelationship = false }) {
1882
- this._lastProvenanceEntityInfo
1883
- = typeof sourceAspect === "string"
2042
+ this._lastProvenanceEntityInfo =
2043
+ typeof sourceAspect === "string"
1884
2044
  ? sourceAspect
1885
2045
  : {
1886
2046
  entityId: sourceAspect.element.id,
1887
2047
  aspectId: sourceAspect.id,
1888
2048
  aspectVersion: sourceAspect.version ?? "",
1889
- aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
2049
+ aspectKind: isRelationship
2050
+ ? core_backend_1.ExternalSourceAspect.Kind.Relationship
2051
+ : core_backend_1.ExternalSourceAspect.Kind.Element,
1890
2052
  };
1891
2053
  }
1892
2054
  /**
@@ -1968,7 +2130,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1968
2130
  this.context.loadStateFromDb(db);
1969
2131
  this.importer.loadStateFromJson(state.importerState);
1970
2132
  this.exporter.loadStateFromJson(state.exporterState);
1971
- this._elementsWithExplicitlyTrackedProvenance = core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
2133
+ this._elementsWithExplicitlyTrackedProvenance =
2134
+ core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
1972
2135
  this.loadAdditionalStateJson(state.additionalState);
1973
2136
  }
1974
2137
  /**
@@ -2025,9 +2188,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2025
2188
  additionalState: this.getAdditionalStateJson(),
2026
2189
  };
2027
2190
  this.context.saveStateToDb(db);
2028
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
2191
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2192
+ db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
2029
2193
  throw Error("Failed to create the js state table in the state database");
2030
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
2194
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2195
+ db.executeSQL(`
2031
2196
  CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
2032
2197
  -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
2033
2198
  entityId TEXT,
@@ -2045,8 +2210,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2045
2210
  throw Error("Failed to insert options into the state database");
2046
2211
  });
2047
2212
  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);
2213
+ const lastProvenanceEntityInfo = this
2214
+ ._lastProvenanceEntityInfo;
2215
+ stmt.bindString(1, lastProvenanceEntityInfo?.entityId ??
2216
+ this._lastProvenanceEntityInfo);
2050
2217
  stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
2051
2218
  stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
2052
2219
  stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
@@ -2080,30 +2247,25 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2080
2247
  db.closeDb();
2081
2248
  }
2082
2249
  }
2083
- async processChanges(optionsOrAccessToken, startChangesetId) {
2250
+ /** Export changes from the source iModel and import the transformed entities into the target iModel.
2251
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2252
+ * @note the transformer saves and pushes changes when its work is complete.
2253
+ * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2254
+ * will automatically be determined and used
2255
+ * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired range and open the source iModel as of the end (inclusive) of the desired range.
2256
+ */
2257
+ async processChanges(options) {
2084
2258
  this._isSynchronization = true;
2085
2259
  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
2260
  this.logSettings();
2095
- await this.initialize(args);
2261
+ await this.initialize(options);
2096
2262
  // must wait for initialization of synchronization provenance data
2097
- await this.exporter.exportChanges(this.getExportInitOpts(args));
2263
+ await this.exporter.exportChanges(this.getExportInitOpts(options));
2098
2264
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2099
2265
  if (this._options.optimizeGeometry)
2100
2266
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2101
2267
  this.importer.computeProjectExtents();
2102
- this.finalizeTransformation();
2103
- const defaultSaveTargetChanges = () => {
2104
- this.targetDb.saveChanges();
2105
- };
2106
- await (args.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2268
+ await this.finalizeTransformation(options);
2107
2269
  }
2108
2270
  /** Changeset data must be initialized in order to build correct changeOptions.
2109
2271
  * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
@@ -2112,12 +2274,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2112
2274
  if (!this._isSynchronization)
2113
2275
  return {};
2114
2276
  return {
2277
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
2115
2278
  accessToken: opts.accessToken,
2116
- ...this._changesetRanges
2117
- ? { changesetRanges: this._changesetRanges }
2118
- : opts.startChangeset
2119
- ? { startChangeset: opts.startChangeset }
2120
- : { startChangeset: { index: this._synchronizationVersion.index + 1 } },
2279
+ ...(this._csFileProps
2280
+ ? { csFileProps: this._csFileProps }
2281
+ : this._changesetRanges
2282
+ ? { changesetRanges: this._changesetRanges }
2283
+ : opts.startChangeset
2284
+ ? { startChangeset: opts.startChangeset }
2285
+ : {
2286
+ startChangeset: {
2287
+ index: this._synchronizationVersion.index + 1,
2288
+ },
2289
+ }),
2121
2290
  };
2122
2291
  }
2123
2292
  /** Combine an array of source elements into a single target element.
@@ -2133,6 +2302,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2133
2302
  }
2134
2303
  }
2135
2304
  exports.IModelTransformer = IModelTransformer;
2305
+ 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.";
2136
2306
  /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
2137
2307
  IModelTransformer.jsStateTable = "TransformerJsState";
2138
2308
  /** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
@@ -2207,7 +2377,8 @@ class TemplateModelCloner extends IModelTransformer {
2207
2377
  }
2208
2378
  else {
2209
2379
  const definitionElement = this.sourceDb.elements.tryGetElement(referenceId, core_backend_1.DefinitionElement);
2210
- if (definitionElement && !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
2380
+ if (definitionElement &&
2381
+ !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
2211
2382
  this.context.remapElement(referenceId, referenceId); // when in the same iModel, can use existing DefinitionElements without remapping
2212
2383
  }
2213
2384
  else {
@@ -2222,7 +2393,7 @@ class TemplateModelCloner extends IModelTransformer {
2222
2393
  if (sourceElement instanceof core_backend_1.GeometricElement) {
2223
2394
  const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
2224
2395
  const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
2225
- const placement = (placementClass).fromJSON(targetElementProps.placement);
2396
+ const placement = placementClass.fromJSON(targetElementProps.placement);
2226
2397
  if (placement.isValid) {
2227
2398
  nodeAssert(this._transform3d);
2228
2399
  placement.multiplyTransform(this._transform3d);