@itwin/imodel-transformer 2.0.0-dev.67 → 2.0.0-dev.69
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/lib/cjs/ChangesetScanner.d.ts +111 -31
- package/lib/cjs/ChangesetScanner.d.ts.map +1 -1
- package/lib/cjs/ChangesetScanner.js +130 -47
- package/lib/cjs/ChangesetScanner.js.map +1 -1
- package/lib/cjs/IModelExporter.d.ts +28 -13
- package/lib/cjs/IModelExporter.d.ts.map +1 -1
- package/lib/cjs/IModelExporter.js +65 -81
- package/lib/cjs/IModelExporter.js.map +1 -1
- package/lib/cjs/IModelTransformer.d.ts +54 -15
- package/lib/cjs/IModelTransformer.d.ts.map +1 -1
- package/lib/cjs/IModelTransformer.js +197 -194
- package/lib/cjs/IModelTransformer.js.map +1 -1
- package/lib/cjs/SourceClassKinds.d.ts +22 -0
- package/lib/cjs/SourceClassKinds.d.ts.map +1 -0
- package/lib/cjs/SourceClassKinds.js +60 -0
- package/lib/cjs/SourceClassKinds.js.map +1 -0
- package/lib/cjs/imodel-transformer.d.ts +1 -0
- package/lib/cjs/imodel-transformer.d.ts.map +1 -1
- package/lib/cjs/imodel-transformer.js.map +1 -1
- package/package.json +1 -1
|
@@ -61,6 +61,10 @@ const SchemaProcessingCoordinator_1 = require("./schema-processing/SchemaProcess
|
|
|
61
61
|
const ChangesetScanner_1 = require("./ChangesetScanner");
|
|
62
62
|
const IModelTransformerError_1 = require("./IModelTransformerError");
|
|
63
63
|
const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
|
|
64
|
+
/** Key of a relationship's source and target element ids. */
|
|
65
|
+
function relationshipEndpointsKey(sourceId, targetId) {
|
|
66
|
+
return `${sourceId}|${targetId}`;
|
|
67
|
+
}
|
|
64
68
|
/**
|
|
65
69
|
* Apply a function to each Id64 in a supported container type of Id64s.
|
|
66
70
|
* Currently only supports raw Id64String or RelatedElement-like objects containing an `id` property that is a Id64String,
|
|
@@ -537,8 +541,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
537
541
|
const combinedTransform = targetTransformInv.multiplyTransformTransform(srcTransform);
|
|
538
542
|
return combinedTransform;
|
|
539
543
|
}
|
|
540
|
-
// if undefined, it can be initialized by calling [[this.
|
|
544
|
+
// if undefined, it can be initialized by calling [[this.prepareSourceChanges]]
|
|
541
545
|
_deletedSourceRelationshipData = undefined;
|
|
546
|
+
/** Target relationship ids and target endpoint keys of [[_deletedSourceRelationshipData]] entries, for fast matching in [[onExportRelationship]]. */
|
|
547
|
+
_pendingRelationshipDeleteTargets = new Set();
|
|
548
|
+
/** Target relationships that this run imported from a source relationship and that a pending relationship delete also resolves to.
|
|
549
|
+
* A source relationship deleted and recreated with the same endpoints maps to the same target relationship, which must not be deleted.
|
|
550
|
+
*/
|
|
551
|
+
_reimportedTargetRelationshipIds = new Set();
|
|
542
552
|
/** Returns true if a change within sourceElement is detected.
|
|
543
553
|
* @param sourceElement The Element from the source iModel
|
|
544
554
|
* @note A subclass can override this method to provide custom change detection behavior.
|
|
@@ -1152,6 +1162,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1152
1162
|
async onExportRelationship(sourceRelationship, _isUpdate, sourceFedGuid, targetFedGuid) {
|
|
1153
1163
|
const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
|
|
1154
1164
|
const targetRelationshipInstanceId = await this.importer.importRelationship(targetRelationshipProps);
|
|
1165
|
+
if (this._pendingRelationshipDeleteTargets.size > 0 &&
|
|
1166
|
+
core_bentley_1.Id64.isValid(targetRelationshipInstanceId) &&
|
|
1167
|
+
(this._pendingRelationshipDeleteTargets.has(targetRelationshipInstanceId) ||
|
|
1168
|
+
this._pendingRelationshipDeleteTargets.has(relationshipEndpointsKey(targetRelationshipProps.sourceId, targetRelationshipProps.targetId)))) {
|
|
1169
|
+
this._reimportedTargetRelationshipIds.add(targetRelationshipInstanceId);
|
|
1170
|
+
}
|
|
1155
1171
|
const provenanceDb = await this.getProvenanceDb();
|
|
1156
1172
|
const provenanceEditTxn = await this._provenanceManager.getProvenanceEditTxn();
|
|
1157
1173
|
if (!this._options.noProvenance &&
|
|
@@ -1189,7 +1205,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1189
1205
|
sourceId: deletedRelData.sourceIdInTarget,
|
|
1190
1206
|
targetId: deletedRelData.targetIdInTarget,
|
|
1191
1207
|
})?.id;
|
|
1192
|
-
if (id) {
|
|
1208
|
+
if (id && this._reimportedTargetRelationshipIds.has(id)) {
|
|
1209
|
+
// The source relationship was recreated with the same endpoints and already imported into this target relationship.
|
|
1210
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `Skipping delete of target relationship ${id} because source relationship ${sourceRelInstanceId} was recreated`);
|
|
1211
|
+
}
|
|
1212
|
+
else if (id) {
|
|
1193
1213
|
await this.importer.deleteRelationship({
|
|
1194
1214
|
id,
|
|
1195
1215
|
classFullName: deletedRelData.classFullName,
|
|
@@ -1370,9 +1390,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1370
1390
|
/** state to prevent reinitialization, @see [[initialize]] */
|
|
1371
1391
|
_initialized = false;
|
|
1372
1392
|
_sourceChangeDataState = "uninited";
|
|
1373
|
-
|
|
1374
|
-
_csFileProps = undefined;
|
|
1375
|
-
_deletionRecordsByChangeset;
|
|
1393
|
+
_deletionBatches;
|
|
1376
1394
|
/**
|
|
1377
1395
|
* Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
|
|
1378
1396
|
* 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.
|
|
@@ -1388,22 +1406,28 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1388
1406
|
await this._cloneContext.initialize();
|
|
1389
1407
|
const exporterInitOptions = await this.getExportInitOpts(this._options.argsForProcessChanges ?? {});
|
|
1390
1408
|
await this.initializeChangesetScanAndExporter(exporterInitOptions);
|
|
1391
|
-
// Exporter must be initialized prior to
|
|
1392
|
-
await this.
|
|
1409
|
+
// Exporter must be initialized prior to remapping deletions in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
|
|
1410
|
+
await this.prepareSourceChanges();
|
|
1393
1411
|
this._initialized = true;
|
|
1394
1412
|
}
|
|
1395
1413
|
async initializeChangesetScanAndExporter(exporterInitOptions) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1414
|
+
const ranges = this._changesetRanges;
|
|
1415
|
+
if (ranges !== undefined &&
|
|
1416
|
+
ranges.length > 0 &&
|
|
1398
1417
|
this.sourceDb.isBriefcaseDb()) {
|
|
1399
|
-
|
|
1418
|
+
let changedInstanceIds = this.exporter.sourceDbChanges ??
|
|
1400
1419
|
("changedInstanceIds" in exporterInitOptions
|
|
1401
1420
|
? exporterInitOptions.changedInstanceIds
|
|
1402
|
-
:
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
}
|
|
1421
|
+
: undefined);
|
|
1422
|
+
if (changedInstanceIds !== undefined) {
|
|
1423
|
+
// The caller supplied changed IDs, so only deletion metadata is read.
|
|
1424
|
+
this._deletionBatches = await ChangesetScanner_1.ChangesetScanner.scan(this.sourceDb, await ChangesetScanner_1.ChangesetScanner.download(this.sourceDb, ranges), {});
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
const scanResult = await this.collectChanges(ranges);
|
|
1428
|
+
changedInstanceIds = scanResult.changedInstanceIds;
|
|
1429
|
+
this._deletionBatches = scanResult.deletionBatches;
|
|
1430
|
+
}
|
|
1407
1431
|
await this.exporter.initialize({
|
|
1408
1432
|
changedInstanceIds,
|
|
1409
1433
|
skipPropagateChangesToRootElements: exporterInitOptions.skipPropagateChangesToRootElements,
|
|
@@ -1414,19 +1438,42 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1414
1438
|
}
|
|
1415
1439
|
}
|
|
1416
1440
|
/**
|
|
1417
|
-
* Reads
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1441
|
+
* Reads the source changes for the changeset ranges being processed. Override to supply changes from another
|
|
1442
|
+
* source, such as a precomputed cache, instead of downloading and reading changesets.
|
|
1443
|
+
*
|
|
1444
|
+
* An override can supply some ranges itself and pass the rest to `super.collectChanges()`. Pass the same
|
|
1445
|
+
* `changedInstanceIds` to every call and handle the ranges in order, so that changes to the same instance in
|
|
1446
|
+
* different ranges combine correctly. Use [[ChangedInstanceIds.addChangeRecord]] to add each supplied change.
|
|
1447
|
+
* See [Supplying source changes]($docs/learning/transformer/change-scanning.md) for the full contract and an example.
|
|
1448
|
+
* @param ranges Ordered, inclusive `[first, last]` changeset index ranges. They already exclude changesets
|
|
1449
|
+
* that must be skipped, such as those pushed by a previous synchronization in the other direction, so an
|
|
1450
|
+
* override must cover exactly these ranges.
|
|
1451
|
+
* @param changedInstanceIds Receives the changes read from the ranges. Defaults to a new, empty instance.
|
|
1452
|
+
* @returns The changed instance IDs to export and the records used to find the targets of deleted instances.
|
|
1453
|
+
* See [[CollectedChanges]] for what the result must include.
|
|
1454
|
+
* @note The default implementation downloads the changesets, reads them with `ChangesetReader`, and returns one
|
|
1455
|
+
* deletion batch per range.
|
|
1456
|
+
* @note Not called when the exporter already has changed instance IDs, for example when `changedInstanceIds`
|
|
1457
|
+
* is passed in [[IModelTransformOptions.argsForProcessChanges]]. The transformer then still downloads and reads
|
|
1458
|
+
* the changesets to find deletion records.
|
|
1459
|
+
* @beta
|
|
1460
|
+
*/
|
|
1461
|
+
async collectChanges(ranges, changedInstanceIds = new IModelExporter_1.ChangedInstanceIds(this.sourceDb)) {
|
|
1462
|
+
const deletionBatches = await ChangesetScanner_1.ChangesetScanner.scan(this.sourceDb, await ChangesetScanner_1.ChangesetScanner.download(this.sourceDb, ranges), { changedInstanceIds });
|
|
1463
|
+
return { changedInstanceIds, deletionBatches };
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Remaps tracked elements, adds custom changes, and remaps deleted source entities to their targets using the
|
|
1467
|
+
* deletion batches read for the changeset ranges being processed.
|
|
1421
1468
|
*/
|
|
1422
|
-
async
|
|
1469
|
+
async prepareSourceChanges() {
|
|
1423
1470
|
await this._provenanceManager.forEachTrackedElement((sourceElementId, targetElementId) => {
|
|
1424
1471
|
this.context.remapElement(sourceElementId, targetElementId);
|
|
1425
1472
|
});
|
|
1426
1473
|
if (this.exporter.sourceDbChanges)
|
|
1427
1474
|
await this.addCustomChanges(this.exporter.sourceDbChanges);
|
|
1428
|
-
const
|
|
1429
|
-
if (
|
|
1475
|
+
const ranges = this._changesetRanges;
|
|
1476
|
+
if (ranges === undefined || ranges.length === 0) {
|
|
1430
1477
|
if (this.exporter.sourceDbChanges === undefined ||
|
|
1431
1478
|
!this.exporter.sourceDbChanges.hasChanges)
|
|
1432
1479
|
return;
|
|
@@ -1434,21 +1481,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1434
1481
|
if (this._sourceChangeDataState === "no-changes")
|
|
1435
1482
|
this._sourceChangeDataState = "has-changes";
|
|
1436
1483
|
}
|
|
1437
|
-
const
|
|
1438
|
-
if (
|
|
1484
|
+
const deletionBatches = this._deletionBatches;
|
|
1485
|
+
if (deletionBatches === undefined)
|
|
1439
1486
|
return;
|
|
1440
|
-
const relationshipECClassIdsToSkip = new Set();
|
|
1441
|
-
for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)", undefined, { usePrimaryConn: true })) {
|
|
1442
|
-
relationshipECClassIdsToSkip.add(row.ECInstanceId);
|
|
1443
|
-
}
|
|
1444
|
-
const relationshipECClassIds = new Set();
|
|
1445
|
-
for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)", undefined, { usePrimaryConn: true })) {
|
|
1446
|
-
relationshipECClassIds.add(row.ECInstanceId);
|
|
1447
|
-
}
|
|
1448
|
-
const elementAspectECClassIds = new Set();
|
|
1449
|
-
for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementAspect)", undefined, { usePrimaryConn: true })) {
|
|
1450
|
-
elementAspectECClassIds.add(row.ECInstanceId);
|
|
1451
|
-
}
|
|
1452
1487
|
// For later use when processing deletes.
|
|
1453
1488
|
const alreadyImportedElementInserts = new Set();
|
|
1454
1489
|
const alreadyImportedModelInserts = new Set();
|
|
@@ -1463,30 +1498,28 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1463
1498
|
alreadyImportedModelInserts.add(targetModelId);
|
|
1464
1499
|
});
|
|
1465
1500
|
this._deletedSourceRelationshipData = new Map();
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
if (
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
change.elementId !== undefined) {
|
|
1477
|
-
elemIdToScopeEsa.set(change.elementId, change);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
// Loop to process deletes.
|
|
1481
|
-
for (const change of changes) {
|
|
1482
|
-
if (relationshipECClassIdsToSkip.has(change.ecClassId))
|
|
1483
|
-
continue;
|
|
1484
|
-
if (isElementAspectDeletion(change))
|
|
1485
|
-
continue;
|
|
1486
|
-
await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(change.ecClassId), alreadyImportedElementInserts, alreadyImportedModelInserts);
|
|
1501
|
+
this._pendingRelationshipDeleteTargets.clear();
|
|
1502
|
+
this._reimportedTargetRelationshipIds.clear();
|
|
1503
|
+
for (const batch of deletionBatches) {
|
|
1504
|
+
// Deletion records can only resolve targets within their own batch, so these lookups are rebuilt for each batch.
|
|
1505
|
+
// They cover elements and scope ESAs deleted in the source, which can no longer be read from it.
|
|
1506
|
+
const deletedScopeEsaByElementId = new Map();
|
|
1507
|
+
for (const aspect of batch.externalSourceAspects) {
|
|
1508
|
+
if (aspect.scopeId === this.targetScopeElementId &&
|
|
1509
|
+
aspect.kind === core_backend_1.ExternalSourceAspect.Kind.Element)
|
|
1510
|
+
deletedScopeEsaByElementId.set(aspect.elementId, aspect);
|
|
1487
1511
|
}
|
|
1512
|
+
const deletedFederationGuidByElementId = new Map();
|
|
1513
|
+
for (const element of batch.elements)
|
|
1514
|
+
if (element.federationGuid)
|
|
1515
|
+
deletedFederationGuidByElementId.set(element.ecInstanceId, element.federationGuid);
|
|
1516
|
+
for (const relationship of batch.relationships)
|
|
1517
|
+
await this.processDeletedRelationship(relationship, deletedScopeEsaByElementId, deletedFederationGuidByElementId);
|
|
1518
|
+
for (const element of batch.elements)
|
|
1519
|
+
await this.processDeletedElement(element.ecInstanceId, element.federationGuid, deletedScopeEsaByElementId, alreadyImportedElementInserts, alreadyImportedModelInserts);
|
|
1520
|
+
for (const model of batch.models)
|
|
1521
|
+
await this.processDeletedElement(model.ecInstanceId, undefined, deletedScopeEsaByElementId, alreadyImportedElementInserts, alreadyImportedModelInserts);
|
|
1488
1522
|
}
|
|
1489
|
-
return;
|
|
1490
1523
|
}
|
|
1491
1524
|
/**
|
|
1492
1525
|
* This will be called when transformer is called with [[IModelTransformOptions.argsForProcessChanges]] to process changes.
|
|
@@ -1498,127 +1531,110 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1498
1531
|
*/
|
|
1499
1532
|
async addCustomChanges(_sourceDbChanges) { }
|
|
1500
1533
|
/**
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
*
|
|
1505
|
-
* @param
|
|
1506
|
-
* @param
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
*/
|
|
1510
|
-
async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
|
|
1511
|
-
// we need a connected iModel with changes to remap elements with deletions
|
|
1512
|
-
const notConnectedModel = this.sourceDb.iTwinId === undefined;
|
|
1513
|
-
const noChanges = (await this.getSynchronizationVersion()).index ===
|
|
1514
|
-
this.sourceDb.changeset.index &&
|
|
1515
|
-
(this.exporter.sourceDbChanges === undefined ||
|
|
1516
|
-
!this.exporter.sourceDbChanges.hasChanges);
|
|
1517
|
-
if (notConnectedModel || noChanges)
|
|
1518
|
-
return;
|
|
1534
|
+
* Finds the target ID of a source element while processing deletions, first through the transformation's
|
|
1535
|
+
* ExternalSourceAspects when the source holds the provenance, then through the federation GUID. The element may be
|
|
1536
|
+
* deleted itself, or be a surviving endpoint of a deleted relationship.
|
|
1537
|
+
* @param sourceId the source element ID.
|
|
1538
|
+
* @param federationGuid the source element's federation GUID, if known.
|
|
1539
|
+
* @param deletedScopeEsaByElementId this transformation scope's deleted ESAs in the same batch, keyed by the ID of the element that owned each one.
|
|
1540
|
+
*/
|
|
1541
|
+
async findTargetElementIdForDeletion(sourceId, federationGuid, deletedScopeEsaByElementId) {
|
|
1519
1542
|
/**
|
|
1520
1543
|
* If the deleted source entity is in the provenanceDb, then we can use its ids to query for ESAs.
|
|
1521
1544
|
* This is because the ESAs are stored on an element Id thats present in the provenanceDb.
|
|
1522
1545
|
*/
|
|
1523
|
-
|
|
1524
|
-
const getTargetIdFromSourceId = async (id) => {
|
|
1546
|
+
if (this.sourceDb === (await this.getProvenanceDb())) {
|
|
1525
1547
|
let identifierValue;
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
if (changeDataInProvenanceDb) {
|
|
1534
|
-
// TODO: clarify what happens if there are multiple (e.g. elements were merged)
|
|
1535
|
-
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([
|
|
1536
|
-
this.targetScopeElementId,
|
|
1537
|
-
core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
1538
|
-
id,
|
|
1539
|
-
]), { usePrimaryConn: true })) {
|
|
1540
|
-
identifierValue = row.Identifier;
|
|
1541
|
-
}
|
|
1542
|
-
identifierValue =
|
|
1543
|
-
identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.identifier;
|
|
1548
|
+
// TODO: clarify what happens if there are multiple (e.g. elements were merged)
|
|
1549
|
+
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([
|
|
1550
|
+
this.targetScopeElementId,
|
|
1551
|
+
core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
1552
|
+
sourceId,
|
|
1553
|
+
]), { usePrimaryConn: true })) {
|
|
1554
|
+
identifierValue = row.Identifier;
|
|
1544
1555
|
}
|
|
1556
|
+
identifierValue =
|
|
1557
|
+
identifierValue ?? deletedScopeEsaByElementId.get(sourceId)?.identifier;
|
|
1545
1558
|
// Check for targetId by an esa first
|
|
1546
|
-
if (
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1559
|
+
if (identifierValue)
|
|
1560
|
+
return identifierValue;
|
|
1561
|
+
}
|
|
1562
|
+
// Check for targetId using sourceId's fedguid if we didn't find an esa.
|
|
1563
|
+
if (federationGuid)
|
|
1564
|
+
return this.targetDb.elements.getIdFromFederationGuid(federationGuid);
|
|
1565
|
+
return undefined;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Helper function for [[prepareSourceChanges]]. Records how to find the target relationship of a deleted source relationship.
|
|
1569
|
+
* @param deletion the deleted source relationship.
|
|
1570
|
+
* @param deletedScopeEsaByElementId this transformation scope's deleted ESAs in the same batch, keyed by the ID of the element that owned each one.
|
|
1571
|
+
* @param deletedFederationGuidByElementId federation GUIDs of the elements deleted in the same batch, for endpoints that no longer exist in the source.
|
|
1572
|
+
*/
|
|
1573
|
+
async processDeletedRelationship(deletion, deletedScopeEsaByElementId, deletedFederationGuidByElementId) {
|
|
1574
|
+
const { ecInstanceId } = deletion;
|
|
1575
|
+
const classFullName = this.sourceDb.getClassNameFromId(deletion.ecClassId);
|
|
1576
|
+
const getEndpointInTarget = async (sourceId) => this.findTargetElementIdForDeletion(sourceId, this.sourceDb.elements.tryGetElement(sourceId)?.federationGuid ??
|
|
1577
|
+
deletedFederationGuidByElementId.get(sourceId), deletedScopeEsaByElementId);
|
|
1578
|
+
const sourceIdInTarget = await getEndpointInTarget(deletion.sourceECInstanceId);
|
|
1579
|
+
const targetIdInTarget = await getEndpointInTarget(deletion.targetECInstanceId);
|
|
1580
|
+
const isProvenanceSource = this.sourceDb === (await this._provenanceManager.getProvenanceSourceDb());
|
|
1581
|
+
const queryRelationshipProvenance = async () => this._provenanceManager.queryProvenanceForRelationship(ecInstanceId, {
|
|
1582
|
+
classFullName,
|
|
1583
|
+
sourceId: deletion.sourceECInstanceId,
|
|
1584
|
+
targetId: deletion.targetECInstanceId,
|
|
1585
|
+
});
|
|
1586
|
+
if (sourceIdInTarget && targetIdInTarget) {
|
|
1587
|
+
// relationships with ESA provenance must have their aspect deleted too, even when the target relationship is found by endpoints
|
|
1588
|
+
const provenanceAspectId = this._options.forceExternalSourceAspectProvenance && isProvenanceSource
|
|
1589
|
+
? (await queryRelationshipProvenance())?.aspectId
|
|
1590
|
+
: undefined;
|
|
1591
|
+
this._deletedSourceRelationshipData?.set(ecInstanceId, {
|
|
1592
|
+
classFullName,
|
|
1593
|
+
sourceIdInTarget,
|
|
1594
|
+
targetIdInTarget,
|
|
1595
|
+
provenanceAspectId,
|
|
1596
|
+
});
|
|
1597
|
+
this._pendingRelationshipDeleteTargets.add(relationshipEndpointsKey(sourceIdInTarget, targetIdInTarget));
|
|
1598
|
+
}
|
|
1599
|
+
else if (isProvenanceSource) {
|
|
1600
|
+
const relProvenance = await queryRelationshipProvenance();
|
|
1601
|
+
if (relProvenance && relProvenance.relationshipId) {
|
|
1602
|
+
this._deletedSourceRelationshipData?.set(ecInstanceId, {
|
|
1603
|
+
classFullName,
|
|
1604
|
+
relId: relProvenance.relationshipId,
|
|
1605
|
+
provenanceAspectId: relProvenance.aspectId,
|
|
1587
1606
|
});
|
|
1588
|
-
|
|
1589
|
-
this._deletedSourceRelationshipData?.set(changedInstanceId, {
|
|
1590
|
-
classFullName: classFullName ?? "",
|
|
1591
|
-
relId: relProvenance.relationshipId,
|
|
1592
|
-
provenanceAspectId: relProvenance.aspectId,
|
|
1593
|
-
});
|
|
1607
|
+
this._pendingRelationshipDeleteTargets.add(relProvenance.relationshipId);
|
|
1594
1608
|
}
|
|
1595
1609
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
if (
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Helper function for [[prepareSourceChanges]]. Remaps a deleted source element or model to its target.
|
|
1613
|
+
* @param sourceId the deleted source element or model.
|
|
1614
|
+
* @param federationGuid the deleted source element's federation GUID, if it had one. Undefined for models.
|
|
1615
|
+
* @param deletedScopeEsaByElementId this transformation scope's deleted ESAs in the same batch, keyed by the ID of the element that owned each one.
|
|
1616
|
+
* @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
|
|
1617
|
+
* @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
|
|
1618
|
+
*/
|
|
1619
|
+
async processDeletedElement(sourceId, federationGuid, deletedScopeEsaByElementId, alreadyImportedElementInserts, alreadyImportedModelInserts) {
|
|
1620
|
+
let targetId = await this.findTargetElementIdForDeletion(sourceId, federationGuid, deletedScopeEsaByElementId);
|
|
1621
|
+
if (targetId === undefined &&
|
|
1622
|
+
this.sourceDb === (await this._provenanceManager.getProvenanceSourceDb())) {
|
|
1623
|
+
const contextTargetId = this.context.findTargetElementId(sourceId);
|
|
1624
|
+
if (core_bentley_1.Id64.isValidId64(contextTargetId))
|
|
1625
|
+
targetId = contextTargetId;
|
|
1626
|
+
}
|
|
1627
|
+
// Entities inserted and deleted since the last synchronization were never synced and can be safely ignored.
|
|
1628
|
+
if (!targetId)
|
|
1629
|
+
return;
|
|
1630
|
+
this.context.remapElement(sourceId, targetId);
|
|
1631
|
+
// If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
|
|
1632
|
+
// In such case an entity update will be triggered and we no longer need to delete the entity.
|
|
1633
|
+
if (alreadyImportedElementInserts.has(targetId)) {
|
|
1634
|
+
this.exporter.sourceDbChanges?.element.deleteIds.delete(sourceId);
|
|
1635
|
+
}
|
|
1636
|
+
if (alreadyImportedModelInserts.has(targetId)) {
|
|
1637
|
+
this.exporter.sourceDbChanges?.model.deleteIds.delete(sourceId);
|
|
1622
1638
|
}
|
|
1623
1639
|
}
|
|
1624
1640
|
async _tryInitChangesetData(args) {
|
|
@@ -1632,7 +1648,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1632
1648
|
const noChanges = syncVersion.index === this.sourceDb.changeset.index;
|
|
1633
1649
|
if (noChanges) {
|
|
1634
1650
|
this._sourceChangeDataState = "no-changes";
|
|
1635
|
-
this.
|
|
1651
|
+
this._changesetRanges = [];
|
|
1636
1652
|
return;
|
|
1637
1653
|
}
|
|
1638
1654
|
const startChangeset = "startChangeset" in args ? args.startChangeset : undefined;
|
|
@@ -1669,20 +1685,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1669
1685
|
core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
|
|
1670
1686
|
this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
|
|
1671
1687
|
core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
|
|
1672
|
-
|
|
1673
|
-
for (const [first, end] of this._changesetRanges) {
|
|
1674
|
-
// TODO: should the first changeset in a reverse sync really be included even though its 'initialized branch provenance'? The answer is no, its a bug that needs to be fixed.
|
|
1675
|
-
const fileProps = await core_backend_1.BriefcaseManager.downloadChangesets({
|
|
1676
|
-
iModelId: this.sourceDb.iModelId,
|
|
1677
|
-
targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
|
|
1678
|
-
range: { first, end },
|
|
1679
|
-
});
|
|
1680
|
-
csFileProps.push(...fileProps);
|
|
1681
|
-
}
|
|
1682
|
-
this._csFileProps = csFileProps;
|
|
1683
|
-
/** Theres a possibility that our csFileProps length is still 0 here, since we skip cs indices found in the pendingSync and pendingReverseSync indices arrays. */
|
|
1688
|
+
/** Skipping indices found in the pendingSync and pendingReverseSync arrays can leave no ranges to process. */
|
|
1684
1689
|
this._sourceChangeDataState =
|
|
1685
|
-
this.
|
|
1690
|
+
this._changesetRanges.length === 0 ? "no-changes" : "has-changes";
|
|
1686
1691
|
}
|
|
1687
1692
|
/** Asserts that the EditTxn is active before any write operations. */
|
|
1688
1693
|
assertEditTxnActive() {
|
|
@@ -1808,17 +1813,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1808
1813
|
const startChangeset = "startChangeset" in opts ? opts.startChangeset : undefined;
|
|
1809
1814
|
return {
|
|
1810
1815
|
skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
|
|
1811
|
-
...(this.
|
|
1812
|
-
? {
|
|
1813
|
-
:
|
|
1814
|
-
? {
|
|
1815
|
-
:
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
},
|
|
1821
|
-
}),
|
|
1816
|
+
...(this._changesetRanges
|
|
1817
|
+
? { changesetRanges: this._changesetRanges }
|
|
1818
|
+
: startChangeset
|
|
1819
|
+
? { startChangeset }
|
|
1820
|
+
: {
|
|
1821
|
+
startChangeset: {
|
|
1822
|
+
index: (await this.getSynchronizationVersion()).index + 1,
|
|
1823
|
+
},
|
|
1824
|
+
}),
|
|
1822
1825
|
};
|
|
1823
1826
|
}
|
|
1824
1827
|
/** Combine an array of source elements into a single target element.
|