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