@itwin/imodel-transformer 0.1.3-dev.0 → 0.1.4
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 +9 -1
- package/lib/cjs/ECReferenceTypesCache.js +3 -5
- package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
- package/lib/cjs/EntityMap.d.ts +1 -1
- package/lib/cjs/EntityMap.d.ts.map +1 -1
- package/lib/cjs/IModelCloneContext.js +3 -4
- package/lib/cjs/IModelCloneContext.js.map +1 -1
- package/lib/cjs/IModelExporter.js +16 -17
- package/lib/cjs/IModelExporter.js.map +1 -1
- package/lib/cjs/IModelImporter.js +31 -33
- package/lib/cjs/IModelImporter.js.map +1 -1
- package/lib/cjs/IModelTransformer.d.ts +73 -12
- package/lib/cjs/IModelTransformer.d.ts.map +1 -1
- package/lib/cjs/IModelTransformer.js +513 -174
- package/lib/cjs/IModelTransformer.js.map +1 -1
- package/lib/cjs/transformer.js +6 -2
- package/lib/cjs/transformer.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TemplateModelCloner = exports.IModelTransformer = void 0;
|
|
3
|
+
exports.TemplateModelCloner = exports.IModelTransformer = exports.TransformerEvent = void 0;
|
|
4
4
|
/*---------------------------------------------------------------------------------------------
|
|
5
5
|
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
|
|
6
6
|
* See LICENSE.md in the project root for license terms and full copyright notice.
|
|
@@ -9,6 +9,7 @@ exports.TemplateModelCloner = exports.IModelTransformer = void 0;
|
|
|
9
9
|
* @module iModels
|
|
10
10
|
*/
|
|
11
11
|
const path = require("path");
|
|
12
|
+
const events_1 = require("events");
|
|
12
13
|
const Semver = require("semver");
|
|
13
14
|
const nodeAssert = require("assert");
|
|
14
15
|
const core_bentley_1 = require("@itwin/core-bentley");
|
|
@@ -85,39 +86,74 @@ function mapId64(idContainer, func) {
|
|
|
85
86
|
}
|
|
86
87
|
return results;
|
|
87
88
|
}
|
|
89
|
+
/** events that the transformer emits, e.g. for signaling profilers @internal */
|
|
90
|
+
var TransformerEvent;
|
|
91
|
+
(function (TransformerEvent) {
|
|
92
|
+
TransformerEvent["beginProcessSchemas"] = "beginProcessSchemas";
|
|
93
|
+
TransformerEvent["endProcessSchemas"] = "endProcessSchemas";
|
|
94
|
+
TransformerEvent["beginProcessAll"] = "beginProcessAll";
|
|
95
|
+
TransformerEvent["endProcessAll"] = "endProcessAll";
|
|
96
|
+
TransformerEvent["beginProcessChanges"] = "beginProcessChanges";
|
|
97
|
+
TransformerEvent["endProcessChanges"] = "endProcessChanges";
|
|
98
|
+
})(TransformerEvent = exports.TransformerEvent || (exports.TransformerEvent = {}));
|
|
88
99
|
/** Base class used to transform a source iModel into a different target iModel.
|
|
89
100
|
* @see [iModel Transformation and Data Exchange]($docs/learning/transformer/index.md), [IModelExporter]($transformer), [IModelImporter]($transformer)
|
|
90
101
|
* @beta
|
|
91
102
|
*/
|
|
92
103
|
class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
104
|
+
/** The Id of the Element in the **target** iModel that represents the **source** repository as a whole and scopes its [ExternalSourceAspect]($backend) instances. */
|
|
105
|
+
get targetScopeElementId() {
|
|
106
|
+
return this._options.targetScopeElementId;
|
|
107
|
+
}
|
|
108
|
+
/** The element classes that are considered to define provenance in the iModel */
|
|
109
|
+
static get provenanceElementClasses() {
|
|
110
|
+
return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
|
|
111
|
+
}
|
|
112
|
+
/** The element aspect classes that are considered to define provenance in the iModel */
|
|
113
|
+
static get provenanceElementAspectClasses() {
|
|
114
|
+
return [core_backend_1.ExternalSourceAspect];
|
|
115
|
+
}
|
|
93
116
|
/** Construct a new IModelTransformer
|
|
94
117
|
* @param source Specifies the source IModelExporter or the source IModelDb that will be used to construct the source IModelExporter.
|
|
95
118
|
* @param target Specifies the target IModelImporter or the target IModelDb that will be used to construct the target IModelImporter.
|
|
96
119
|
* @param options The options that specify how the transformation should be done.
|
|
97
120
|
*/
|
|
98
121
|
constructor(source, target, options) {
|
|
99
|
-
var _a, _b, _c, _d, _e;
|
|
100
122
|
super();
|
|
101
123
|
/** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
|
|
102
124
|
* and have some helper methods below for now */
|
|
103
125
|
this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
|
|
104
126
|
/** map of partially committed entities to their partial commit progress */
|
|
105
127
|
this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
|
|
128
|
+
/**
|
|
129
|
+
* Internal event emitter that is used by the transformer to signal events to profilers
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
this.events = new events_1.EventEmitter();
|
|
133
|
+
this._targetScopeProvenanceProps = undefined;
|
|
134
|
+
this._cachedTargetScopeVersion = undefined;
|
|
135
|
+
// if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
|
|
136
|
+
this._hasElementChangedCache = undefined;
|
|
137
|
+
this._deletedSourceRelationshipData = undefined;
|
|
106
138
|
this._yieldManager = new core_bentley_1.YieldManager();
|
|
107
139
|
/** The directory where schemas will be exported, a random temporary directory */
|
|
108
140
|
this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
|
|
109
141
|
this._longNamedSchemasMap = new Map();
|
|
110
142
|
/** state to prevent reinitialization, @see [[initialize]] */
|
|
111
143
|
this._initialized = false;
|
|
144
|
+
/** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
|
|
145
|
+
this._changeSummaryIds = undefined;
|
|
146
|
+
this._changeDataState = "uninited";
|
|
147
|
+
/** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
|
|
112
148
|
this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
|
|
113
149
|
// initialize IModelTransformOptions
|
|
114
150
|
this._options = {
|
|
115
151
|
...options,
|
|
116
152
|
// non-falsy defaults
|
|
117
|
-
cloneUsingBinaryGeometry:
|
|
118
|
-
targetScopeElementId:
|
|
153
|
+
cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
|
|
154
|
+
targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
|
|
119
155
|
// eslint-disable-next-line deprecation/deprecation
|
|
120
|
-
danglingReferencesBehavior:
|
|
156
|
+
danglingReferencesBehavior: options?.danglingReferencesBehavior ?? options?.danglingPredecessorsBehavior ?? "reject",
|
|
121
157
|
};
|
|
122
158
|
this._isFirstSynchronization = this._options.wasSourceIModelCopiedToTarget ? true : undefined;
|
|
123
159
|
// initialize exporter and sourceDb
|
|
@@ -129,7 +165,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
129
165
|
}
|
|
130
166
|
this.sourceDb = this.exporter.sourceDb;
|
|
131
167
|
this.exporter.registerHandler(this);
|
|
132
|
-
this.exporter.wantGeometry =
|
|
168
|
+
this.exporter.wantGeometry = options?.loadSourceGeometry ?? false; // optimization to not load source GeometryStreams by default
|
|
133
169
|
if (!this._options.includeSourceProvenance) { // clone provenance from the source iModel into the target iModel?
|
|
134
170
|
IModelTransformer.provenanceElementClasses.forEach((cls) => this.exporter.excludeElementClass(cls.classFullName));
|
|
135
171
|
IModelTransformer.provenanceElementAspectClasses.forEach((cls) => this.exporter.excludeElementAspectClass(cls.classFullName));
|
|
@@ -156,18 +192,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
156
192
|
this.targetDb = this.importer.targetDb;
|
|
157
193
|
// create the IModelCloneContext, it must be initialized later
|
|
158
194
|
this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
|
|
195
|
+
this._registerEvents();
|
|
159
196
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
/** The element aspect classes that are considered to define provenance in the iModel */
|
|
169
|
-
static get provenanceElementAspectClasses() {
|
|
170
|
-
return [core_backend_1.ExternalSourceAspect];
|
|
197
|
+
/** @internal */
|
|
198
|
+
_registerEvents() {
|
|
199
|
+
this.events.on(TransformerEvent.beginProcessAll, () => {
|
|
200
|
+
core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
|
|
201
|
+
});
|
|
202
|
+
this.events.on(TransformerEvent.beginProcessChanges, () => {
|
|
203
|
+
core_bentley_1.Logger.logTrace(loggerCategory, "processChanges()");
|
|
204
|
+
});
|
|
171
205
|
}
|
|
172
206
|
/** Dispose any native resources associated with this IModelTransformer. */
|
|
173
207
|
dispose() {
|
|
@@ -196,7 +230,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
196
230
|
get provenanceDb() {
|
|
197
231
|
return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
|
|
198
232
|
}
|
|
199
|
-
/**
|
|
233
|
+
/** Return the IModelDb where IModelTransformer will NOT store its provenance.
|
|
234
|
+
* @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
|
|
235
|
+
*/
|
|
236
|
+
get provenanceSourceDb() {
|
|
237
|
+
return this._options.isReverseSynchronization ? this.targetDb : this.sourceDb;
|
|
238
|
+
}
|
|
200
239
|
initElementProvenance(sourceElementId, targetElementId) {
|
|
201
240
|
const elementId = this._options.isReverseSynchronization ? sourceElementId : targetElementId;
|
|
202
241
|
const aspectIdentifier = this._options.isReverseSynchronization ? targetElementId : sourceElementId;
|
|
@@ -227,10 +266,30 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
227
266
|
kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
|
|
228
267
|
jsonProperties: JSON.stringify({ targetRelInstanceId }),
|
|
229
268
|
};
|
|
230
|
-
aspectProps.id = this.
|
|
269
|
+
[aspectProps.id] = this.queryScopeExternalSource(aspectProps);
|
|
231
270
|
return aspectProps;
|
|
232
271
|
}
|
|
233
|
-
|
|
272
|
+
/** the changeset in the scoping element's source version found for this transformation
|
|
273
|
+
* @note: empty string and -1 for changeset and index if it has never been transformed
|
|
274
|
+
*/
|
|
275
|
+
get _targetScopeVersion() {
|
|
276
|
+
if (!this._cachedTargetScopeVersion) {
|
|
277
|
+
nodeAssert(this._targetScopeProvenanceProps?.version !== undefined, "_targetScopeProvenanceProps was not set yet, or contains no version");
|
|
278
|
+
const [id, index] = this._targetScopeProvenanceProps.version === ""
|
|
279
|
+
? ["", -1]
|
|
280
|
+
: this._targetScopeProvenanceProps.version.split(";");
|
|
281
|
+
this._cachedTargetScopeVersion = { index: Number(index), id, };
|
|
282
|
+
nodeAssert(!Number.isNaN(this._cachedTargetScopeVersion.index), "bad parse: invalid index in version");
|
|
283
|
+
}
|
|
284
|
+
return this._cachedTargetScopeVersion;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Make sure there are no conflicting other scope-type external source aspects on the *target scope element*,
|
|
288
|
+
* If there are none at all, insert one, then this must be a first synchronization.
|
|
289
|
+
* @returns the last synced version (changesetId) on the target scope's external source aspect,
|
|
290
|
+
* (if this was a [BriefcaseDb]($backend))
|
|
291
|
+
*/
|
|
292
|
+
initScopeProvenance() {
|
|
234
293
|
const aspectProps = {
|
|
235
294
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
236
295
|
element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
|
|
@@ -238,10 +297,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
238
297
|
identifier: this._options.isReverseSynchronization ? this.targetDb.iModelId : this.sourceDb.iModelId,
|
|
239
298
|
kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
|
|
240
299
|
};
|
|
241
|
-
|
|
300
|
+
// FIXME: handle older transformed iModels
|
|
301
|
+
let version;
|
|
302
|
+
[aspectProps.id, version] = this.queryScopeExternalSource(aspectProps) ?? []; // this query includes "identifier"
|
|
303
|
+
aspectProps.version = version;
|
|
242
304
|
if (undefined === aspectProps.id) {
|
|
305
|
+
aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
|
|
243
306
|
// this query does not include "identifier" to find possible conflicts
|
|
244
|
-
const sql = `
|
|
307
|
+
const sql = `
|
|
308
|
+
SELECT ECInstanceId
|
|
309
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
310
|
+
WHERE Element.Id=:elementId
|
|
311
|
+
AND Scope.Id=:scopeId
|
|
312
|
+
AND Kind=:kind
|
|
313
|
+
LIMIT 1
|
|
314
|
+
`;
|
|
245
315
|
const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
|
|
246
316
|
statement.bindId("elementId", aspectProps.element.id);
|
|
247
317
|
statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
|
|
@@ -256,39 +326,97 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
256
326
|
this._isFirstSynchronization = true; // couldn't tell this is the first time without provenance
|
|
257
327
|
}
|
|
258
328
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
329
|
+
this._targetScopeProvenanceProps = aspectProps;
|
|
330
|
+
}
|
|
331
|
+
queryScopeExternalSource(aspectProps) {
|
|
332
|
+
const sql = `
|
|
333
|
+
SELECT ECInstanceId, Version
|
|
334
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
335
|
+
WHERE Element.Id=:elementId
|
|
336
|
+
AND Scope.Id=:scopeId
|
|
337
|
+
AND Kind=:kind
|
|
338
|
+
AND Identifier=:identifier
|
|
339
|
+
LIMIT 1
|
|
340
|
+
`;
|
|
262
341
|
return this.provenanceDb.withPreparedStatement(sql, (statement) => {
|
|
263
342
|
statement.bindId("elementId", aspectProps.element.id);
|
|
264
343
|
if (aspectProps.scope === undefined)
|
|
265
|
-
return undefined; // return undefined instead of binding an invalid id
|
|
344
|
+
return [undefined, undefined]; // return undefined instead of binding an invalid id
|
|
266
345
|
statement.bindId("scopeId", aspectProps.scope.id);
|
|
267
346
|
statement.bindString("kind", aspectProps.kind);
|
|
268
347
|
statement.bindString("identifier", aspectProps.identifier);
|
|
269
|
-
|
|
348
|
+
if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
|
|
349
|
+
return [undefined, undefined];
|
|
350
|
+
const aspectId = statement.getValue(0).getId();
|
|
351
|
+
const version = statement.getValue(1).getString();
|
|
352
|
+
return [aspectId, version];
|
|
270
353
|
});
|
|
271
354
|
}
|
|
272
|
-
/**
|
|
355
|
+
/**
|
|
356
|
+
* Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one.
|
|
357
|
+
* @note provenance is done by federation guids where possible
|
|
358
|
+
*/
|
|
273
359
|
forEachTrackedElement(fn) {
|
|
274
360
|
if (!this.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
|
|
275
361
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
|
|
276
362
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
363
|
+
// query for provenanceDb
|
|
364
|
+
const provenanceContainerQuery = `
|
|
365
|
+
SELECT e.ECInstanceId, FederationGuid, esa.Identifier as AspectIdentifier
|
|
366
|
+
FROM bis.Element e
|
|
367
|
+
LEFT JOIN bis.ExternalSourceAspect esa ON e.ECInstanceId=esa.Element.Id
|
|
368
|
+
WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special non-federated iModel-local elements
|
|
369
|
+
AND ((Scope.Id IS NULL AND KIND IS NULL) OR (Scope.Id=:scopeId AND Kind=:kind))
|
|
370
|
+
ORDER BY FederationGuid
|
|
371
|
+
`;
|
|
372
|
+
// query for nonProvenanceDb, the source to which the provenance is referring
|
|
373
|
+
const provenanceSourceQuery = `
|
|
374
|
+
SELECT e.ECInstanceId, FederationGuid
|
|
375
|
+
FROM bis.Element e
|
|
376
|
+
WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special non-federated iModel-local elements
|
|
377
|
+
ORDER BY FederationGuid
|
|
378
|
+
`;
|
|
379
|
+
// iterate through sorted list of fed guids from both dbs to get the intersection
|
|
380
|
+
// NOTE: if we exposed the native attach database support,
|
|
381
|
+
// we could get the intersection of fed guids in one query, not sure if it would be faster
|
|
382
|
+
this.provenanceSourceDb.withStatement(provenanceSourceQuery, (sourceStmt) => this.provenanceDb.withStatement(provenanceContainerQuery, (containerStmt) => {
|
|
383
|
+
containerStmt.bindId("scopeId", this.targetScopeElementId);
|
|
384
|
+
containerStmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
385
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
386
|
+
return;
|
|
387
|
+
let sourceRow = sourceStmt.getRow();
|
|
388
|
+
if (containerStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
389
|
+
return;
|
|
390
|
+
let containerRow = containerStmt.getRow();
|
|
391
|
+
const runFnInProvDirection = (sourceId, targetId) => this._options.isReverseSynchronization ? fn(sourceId, targetId) : fn(targetId, sourceId);
|
|
392
|
+
while (true) {
|
|
393
|
+
const currSourceRow = sourceRow, currContainerRow = containerRow;
|
|
394
|
+
if (currSourceRow.federationGuid !== undefined
|
|
395
|
+
&& currContainerRow.federationGuid !== undefined
|
|
396
|
+
&& currSourceRow.federationGuid === currContainerRow.federationGuid) {
|
|
397
|
+
fn(sourceRow.id, containerRow.id);
|
|
286
398
|
}
|
|
287
|
-
|
|
288
|
-
|
|
399
|
+
if (currContainerRow.federationGuid === undefined
|
|
400
|
+
|| (currSourceRow.federationGuid !== undefined
|
|
401
|
+
&& currSourceRow.federationGuid >= currContainerRow.federationGuid)) {
|
|
402
|
+
if (containerStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
403
|
+
return;
|
|
404
|
+
containerRow = containerStmt.getRow();
|
|
289
405
|
}
|
|
406
|
+
if (currSourceRow.federationGuid === undefined
|
|
407
|
+
|| (currContainerRow.federationGuid !== undefined
|
|
408
|
+
&& currSourceRow.federationGuid <= currContainerRow.federationGuid)) {
|
|
409
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
410
|
+
return;
|
|
411
|
+
sourceRow = sourceStmt.getRow();
|
|
412
|
+
}
|
|
413
|
+
// NOTE: needed test cases:
|
|
414
|
+
// - provenance container or provenance source has no fedguids
|
|
415
|
+
// - transforming split and join scenarios
|
|
416
|
+
if (!currContainerRow.federationGuid && currContainerRow.aspectIdentifier)
|
|
417
|
+
runFnInProvDirection(currContainerRow.id, currContainerRow.aspectIdentifier);
|
|
290
418
|
}
|
|
291
|
-
});
|
|
419
|
+
}));
|
|
292
420
|
}
|
|
293
421
|
/** Initialize the source to target Element mapping from ExternalSourceAspects in the target iModel.
|
|
294
422
|
* @note This method is called from all `process*` functions and should never need to be called directly.
|
|
@@ -301,63 +429,62 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
301
429
|
this.context.remapElement(sourceElementId, targetElementId);
|
|
302
430
|
});
|
|
303
431
|
if (args)
|
|
304
|
-
return this.remapDeletedSourceElements(
|
|
432
|
+
return this.remapDeletedSourceElements();
|
|
305
433
|
}
|
|
306
434
|
/** When processing deleted elements in a reverse synchronization, the [[provenanceDb]] (usually a branch iModel) has already
|
|
307
435
|
* deleted the [ExternalSourceAspect]($backend)s that tell us which elements in the reverse synchronization target (usually
|
|
308
436
|
* a master iModel) should be deleted. We must use the changesets to get the values of those before they were deleted.
|
|
309
437
|
*/
|
|
310
|
-
async remapDeletedSourceElements(
|
|
311
|
-
var _a;
|
|
438
|
+
async remapDeletedSourceElements() {
|
|
312
439
|
// we need a connected iModel with changes to remap elements with deletions
|
|
313
|
-
|
|
440
|
+
const notConnectedModel = this.sourceDb.iTwinId === undefined;
|
|
441
|
+
const noChanges = this._targetScopeVersion.index === this.sourceDb.changeset.index;
|
|
442
|
+
if (notConnectedModel || noChanges)
|
|
314
443
|
return;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
349
|
-
const targetId = stmt.getValue(0).getId();
|
|
350
|
-
const sourceId = stmt.getValue(1).getString(); // BisCore.ExternalSourceAspect.Identifier stores a hex Id64String
|
|
351
|
-
// TODO: maybe delete and don't just remap
|
|
352
|
-
this.context.remapElement(targetId, sourceId);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
444
|
+
nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
|
|
445
|
+
nodeAssert(this._changeSummaryIds.length > 0, "change summaries should have at least one");
|
|
446
|
+
const deletedElemSql = `
|
|
447
|
+
SELECT ic.ChangedInstance.Id, ${this._coalesceChangeSummaryJoinedValue((_, i) => `ec${i}.FederationGuid`)}
|
|
448
|
+
FROM ecchange.change.InstanceChange ic
|
|
449
|
+
-- ask affan about whether this is worth it...
|
|
450
|
+
${this._changeSummaryIds.map((id, i) => `
|
|
451
|
+
LEFT JOIN bis.Element.Changes(${id}, 'BeforeDelete') ec${i}
|
|
452
|
+
ON ic.ChangedInstance.Id=ec${i}.ECInstanceId
|
|
453
|
+
`).join('')}
|
|
454
|
+
WHERE ic.OpCode=:opDelete
|
|
455
|
+
AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
|
|
456
|
+
-- not yet documented ecsql feature to check class id
|
|
457
|
+
AND ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
458
|
+
`;
|
|
459
|
+
// must also support old ESA provenance if no fedguids
|
|
460
|
+
this.sourceDb.withStatement(deletedElemSql, (stmt) => {
|
|
461
|
+
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
462
|
+
stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
|
|
463
|
+
// instead of targetScopeElementId, we only operate on elements
|
|
464
|
+
// that had colliding fed guids with the source...
|
|
465
|
+
// currently that is enforced by us checking that the deleted element fedguid is in both
|
|
466
|
+
// before remapping
|
|
467
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
468
|
+
const sourceId = stmt.getValue(0).getId();
|
|
469
|
+
// FIXME: if I could attach the second db, will probably be much faster to get target id
|
|
470
|
+
const sourceFedGuid = stmt.getValue(1).getGuid();
|
|
471
|
+
const targetId = this.queryElemIdByFedGuid(this.targetDb, sourceFedGuid);
|
|
472
|
+
const deletionNotInTarget = !targetId;
|
|
473
|
+
if (deletionNotInTarget)
|
|
474
|
+
return;
|
|
475
|
+
// TODO: maybe delete and don't just remap
|
|
476
|
+
this.context.remapElement(sourceId, targetId);
|
|
355
477
|
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
queryElemIdByFedGuid(db, fedGuid) {
|
|
481
|
+
return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
|
|
482
|
+
stmt.bindGuid(1, fedGuid);
|
|
483
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
484
|
+
return stmt.getValue(0).getId();
|
|
485
|
+
else
|
|
486
|
+
return undefined;
|
|
487
|
+
});
|
|
361
488
|
}
|
|
362
489
|
/** Returns `true` if *brute force* delete detections should be run.
|
|
363
490
|
* @note Not relevant for processChanges when change history is known.
|
|
@@ -375,6 +502,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
375
502
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
376
503
|
*/
|
|
377
504
|
async detectElementDeletes() {
|
|
505
|
+
// FIXME: this is no longer possible to do without change data loading, but I don't think
|
|
506
|
+
// anyone uses this obscure feature, maybe we can remove it?
|
|
507
|
+
// NOTE: can implement this by checking for federation guids in the target that aren't
|
|
378
508
|
if (this._options.isReverseSynchronization) {
|
|
379
509
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
|
|
380
510
|
}
|
|
@@ -402,35 +532,95 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
402
532
|
* @note This can be called more than once for an element in arbitrary order, so it should not have side-effects.
|
|
403
533
|
*/
|
|
404
534
|
onTransformElement(sourceElement) {
|
|
405
|
-
var _a, _b;
|
|
406
535
|
core_bentley_1.Logger.logTrace(loggerCategory, `onTransformElement(${sourceElement.id}) "${sourceElement.getDisplayLabel()}"`);
|
|
407
536
|
const targetElementProps = this.context.cloneElement(sourceElement, { binaryGeometry: this._options.cloneUsingBinaryGeometry });
|
|
408
537
|
if (sourceElement instanceof core_backend_1.Subject) {
|
|
409
|
-
if (
|
|
538
|
+
if (targetElementProps.jsonProperties?.Subject?.Job) {
|
|
410
539
|
// don't propagate source channels into target (legacy bridge case)
|
|
411
540
|
targetElementProps.jsonProperties.Subject.Job = undefined;
|
|
412
541
|
}
|
|
413
542
|
}
|
|
414
543
|
return targetElementProps;
|
|
415
544
|
}
|
|
545
|
+
// handle sqlite coalesce requiring 2 arguments
|
|
546
|
+
_coalesceChangeSummaryJoinedValue(f) {
|
|
547
|
+
nodeAssert(this._changeSummaryIds?.length && this._changeSummaryIds.length > 0, "should have changeset data by now");
|
|
548
|
+
const valueList = this._changeSummaryIds.map(f).join(',');
|
|
549
|
+
return this._changeSummaryIds.length > 1 ? `coalesce(${valueList})` : valueList;
|
|
550
|
+
}
|
|
551
|
+
;
|
|
552
|
+
// FIXME: this is a PoC, don't load this all into memory
|
|
553
|
+
_cacheSourceChanges() {
|
|
554
|
+
nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
|
|
555
|
+
this._hasElementChangedCache = new Set();
|
|
556
|
+
this._deletedSourceRelationshipData = new Map();
|
|
557
|
+
// somewhat complicated query because doing two things at once...
|
|
558
|
+
// (not to mention the multijoin coalescing hack)
|
|
559
|
+
// FIXME: perhaps the coalescing indicates that part should be done manually, not in the query?
|
|
560
|
+
const query = `
|
|
561
|
+
SELECT
|
|
562
|
+
ic.ChangedInstance.Id AS InstId,
|
|
563
|
+
-- NOTE: parse error even with () without iif
|
|
564
|
+
iif(ic.ChangedInstance.ClassId IS (BisCore.Element), TRUE, FALSE) AS IsElemNotDeletedRel,
|
|
565
|
+
coalesce(${
|
|
566
|
+
// HACK: adding "NONE" for empty result seems to prevent a bug where getValue(3) stops working after the NULL columns
|
|
567
|
+
this._changeSummaryIds.map((_, i) => `se${i}.FederationGuid, sec${i}.FederationGuid`).concat("'NONE'").join(',')}) AS SourceFedGuid,
|
|
568
|
+
coalesce(${this._changeSummaryIds.map((_, i) => `te${i}.FederationGuid, tec${i}.FederationGuid`).concat("'NONE'").join(',')}) AS TargetFedGuid,
|
|
569
|
+
ic.ChangedInstance.ClassId AS ClassId
|
|
570
|
+
FROM ecchange.change.InstanceChange ic
|
|
571
|
+
JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
|
|
572
|
+
-- ask affan about whether this is worth it... maybe the ""
|
|
573
|
+
${this._changeSummaryIds.map((id, i) => `
|
|
574
|
+
LEFT JOIN bis.ElementRefersToElements.Changes(${id}, 'BeforeDelete') ertec${i}
|
|
575
|
+
-- NOTE: see how the AND affects performance, it could be dropped
|
|
576
|
+
ON ic.ChangedInstance.Id=ertec${i}.ECInstanceId
|
|
577
|
+
AND NOT ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
578
|
+
-- FIXME: test a deletion of both an element and a relationship at the same time
|
|
579
|
+
LEFT JOIN bis.Element se${i}
|
|
580
|
+
ON se${i}.ECInstanceId=ertec${i}.SourceECInstanceId
|
|
581
|
+
LEFT JOIN bis.Element te${i}
|
|
582
|
+
ON te${i}.ECInstanceId=ertec${i}.TargetECInstanceId
|
|
583
|
+
LEFT JOIN bis.Element.Changes(${id}, 'BeforeDelete') sec${i}
|
|
584
|
+
ON sec${i}.ECInstanceId=ertec${i}.SourceECInstanceId
|
|
585
|
+
LEFT JOIN bis.Element.Changes(${id}, 'BeforeDelete') tec${i}
|
|
586
|
+
ON tec${i}.ECInstanceId=ertec${i}.TargetECInstanceId
|
|
587
|
+
`).join('')}
|
|
588
|
+
-- ignore deleted elems, we take care of those separately
|
|
589
|
+
WHERE ((ic.ChangedInstance.ClassId IS (BisCore.Element) AND ic.OpCode<>:opUpdate)
|
|
590
|
+
OR (ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements) AND ic.OpCode=:opDelete))
|
|
591
|
+
`;
|
|
592
|
+
this.sourceDb.withPreparedStatement(query, (stmt) => {
|
|
593
|
+
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
594
|
+
stmt.bindInteger("opUpdate", core_common_1.ChangeOpCode.Update);
|
|
595
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
596
|
+
// REPORT: stmt.getValue(>3) seems to be bugged but the values survive .getRow so using that for now
|
|
597
|
+
const instId = stmt.getValue(0).getId();
|
|
598
|
+
const isElemNotDeletedRel = stmt.getValue(1).getBoolean();
|
|
599
|
+
if (isElemNotDeletedRel)
|
|
600
|
+
this._hasElementChangedCache.add(instId);
|
|
601
|
+
else {
|
|
602
|
+
const sourceFedGuid = stmt.getValue(2).getGuid();
|
|
603
|
+
const targetFedGuid = stmt.getValue(3).getGuid();
|
|
604
|
+
const classFullName = stmt.getValue(4).getClassNameForClassId();
|
|
605
|
+
this._deletedSourceRelationshipData.set(instId, { classFullName, sourceFedGuid, targetFedGuid });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
416
610
|
/** Returns true if a change within sourceElement is detected.
|
|
417
611
|
* @param sourceElement The Element from the source iModel
|
|
418
612
|
* @param targetElementId The Element from the target iModel to compare against.
|
|
419
613
|
* @note A subclass can override this method to provide custom change detection behavior.
|
|
420
614
|
*/
|
|
421
|
-
hasElementChanged(sourceElement,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return lastModifiedTime !== sourceAspect.version;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return true;
|
|
615
|
+
hasElementChanged(sourceElement, _targetElementId) {
|
|
616
|
+
if (this._changeDataState === "no-changes")
|
|
617
|
+
return false;
|
|
618
|
+
if (this._changeDataState === "unconnected")
|
|
619
|
+
return true;
|
|
620
|
+
nodeAssert(this._changeDataState === "has-changes", "change data should be initialized by now");
|
|
621
|
+
if (this._hasElementChangedCache === undefined)
|
|
622
|
+
this._cacheSourceChanges();
|
|
623
|
+
return this._hasElementChangedCache.has(sourceElement.id);
|
|
434
624
|
}
|
|
435
625
|
static transformCallbackFor(transformer, entity) {
|
|
436
626
|
if (entity instanceof core_backend_1.Element)
|
|
@@ -587,7 +777,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
587
777
|
* This override calls [[onTransformElement]] and then [IModelImporter.importElement]($transformer) to update the target iModel.
|
|
588
778
|
*/
|
|
589
779
|
onExportElement(sourceElement) {
|
|
590
|
-
var _a;
|
|
591
780
|
let targetElementId;
|
|
592
781
|
let targetElementProps;
|
|
593
782
|
if (this._options.preserveElementIdsForFiltering) {
|
|
@@ -606,7 +795,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
606
795
|
if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
|
|
607
796
|
// respond the same way to undefined code value as the @see Code class, but don't use that class because is trims
|
|
608
797
|
// whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
|
|
609
|
-
targetElementProps.code.value =
|
|
798
|
+
targetElementProps.code.value = targetElementProps.code.value ?? "";
|
|
610
799
|
targetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
|
|
611
800
|
if (undefined !== targetElementId) {
|
|
612
801
|
const targetElement = this.targetDb.elements.getElement(targetElementId);
|
|
@@ -619,12 +808,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
619
808
|
}
|
|
620
809
|
}
|
|
621
810
|
}
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
}
|
|
811
|
+
if (targetElementId !== undefined
|
|
812
|
+
&& core_bentley_1.Id64.isValid(targetElementId)
|
|
813
|
+
&& !this.hasElementChanged(sourceElement, targetElementId))
|
|
814
|
+
return;
|
|
628
815
|
this.collectUnmappedReferences(sourceElement);
|
|
629
816
|
// TODO: untangle targetElementId state...
|
|
630
817
|
if (targetElementId === core_bentley_1.Id64.invalid)
|
|
@@ -636,17 +823,29 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
636
823
|
this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
|
|
637
824
|
// now that we've mapped this elem we can fix unmapped references to it
|
|
638
825
|
this.resolvePendingReferences(sourceElement);
|
|
826
|
+
// the transformer does not currently 'split' or 'join' any elements, therefore, it does not
|
|
827
|
+
// insert external source aspects because federation guids are sufficient for this.
|
|
828
|
+
// Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
|
|
829
|
+
// when splitting/joining elements
|
|
830
|
+
// physical consolidation is an example of a 'joining' transform
|
|
831
|
+
// FIXME: document this externally!
|
|
832
|
+
// verify at finalization time that we don't lose provenance on new elements
|
|
833
|
+
// make public and improve `initElementProvenance` API for usage by consolidators
|
|
639
834
|
if (!this._options.noProvenance) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
aspectId = this.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
835
|
+
let provenance = sourceElement.federationGuid;
|
|
836
|
+
if (!provenance) {
|
|
837
|
+
const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
|
|
838
|
+
let [aspectId] = this.queryScopeExternalSource(aspectProps);
|
|
839
|
+
if (aspectId === undefined) {
|
|
840
|
+
aspectId = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
this.provenanceDb.elements.updateAspect(aspectProps);
|
|
844
|
+
}
|
|
845
|
+
aspectProps.id = aspectId;
|
|
846
|
+
provenance = aspectProps;
|
|
647
847
|
}
|
|
648
|
-
|
|
649
|
-
this.markLastProvenance(aspectProps, { isRelationship: false });
|
|
848
|
+
this.markLastProvenance(provenance, { isRelationship: false });
|
|
650
849
|
}
|
|
651
850
|
}
|
|
652
851
|
resolvePendingReferences(entity) {
|
|
@@ -760,7 +959,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
760
959
|
* @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
|
|
761
960
|
*/
|
|
762
961
|
async processDeferredElements(_numRetries = 3) { }
|
|
962
|
+
/** called at the end ([[finalizeTransformation]]) of a transformation,
|
|
963
|
+
* updates the target scope element to say that transformation up through the
|
|
964
|
+
* source's changeset has been performed.
|
|
965
|
+
*/
|
|
966
|
+
_updateTargetScopeVersion() {
|
|
967
|
+
nodeAssert(this._targetScopeProvenanceProps);
|
|
968
|
+
if (this._changeDataState === "has-changes") {
|
|
969
|
+
this._targetScopeProvenanceProps.version = `${this.provenanceSourceDb.changeset.id};${this.provenanceSourceDb.changeset.index}`;
|
|
970
|
+
this.provenanceDb.elements.updateAspect(this._targetScopeProvenanceProps);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// FIXME: is this necessary when manually using lowlevel transform APIs?
|
|
763
974
|
finalizeTransformation() {
|
|
975
|
+
this._updateTargetScopeVersion();
|
|
764
976
|
if (this._partiallyCommittedEntities.size > 0) {
|
|
765
977
|
core_bentley_1.Logger.logWarning(loggerCategory, [
|
|
766
978
|
"The following elements were never fully resolved:",
|
|
@@ -772,6 +984,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
772
984
|
partiallyCommittedElem.forceComplete();
|
|
773
985
|
}
|
|
774
986
|
}
|
|
987
|
+
// FIXME: make processAll have a try {} finally {} that cleans this up
|
|
988
|
+
if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
|
|
989
|
+
core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
|
|
775
990
|
}
|
|
776
991
|
/** Imports all relationships that subclass from the specified base class.
|
|
777
992
|
* @param baseRelClassFullName The specified base relationship class.
|
|
@@ -789,40 +1004,84 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
789
1004
|
* This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
|
|
790
1005
|
*/
|
|
791
1006
|
onExportRelationship(sourceRelationship) {
|
|
1007
|
+
const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
|
|
1008
|
+
const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
|
|
792
1009
|
const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
|
|
793
1010
|
const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
|
|
794
|
-
if (!this._options.noProvenance && core_bentley_1.Id64.
|
|
795
|
-
|
|
796
|
-
if (
|
|
797
|
-
aspectProps
|
|
1011
|
+
if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
|
|
1012
|
+
let provenance = sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`;
|
|
1013
|
+
if (!provenance) {
|
|
1014
|
+
const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
|
|
1015
|
+
if (undefined === aspectProps.id) {
|
|
1016
|
+
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1017
|
+
}
|
|
1018
|
+
(0, core_bentley_1.assert)(aspectProps.id !== undefined);
|
|
1019
|
+
provenance = aspectProps;
|
|
798
1020
|
}
|
|
799
|
-
(
|
|
800
|
-
this.markLastProvenance(aspectProps, { isRelationship: true });
|
|
1021
|
+
this.markLastProvenance(provenance, { isRelationship: true });
|
|
801
1022
|
}
|
|
802
1023
|
}
|
|
1024
|
+
// FIXME: need to check if the element class was remapped and use that id instead
|
|
1025
|
+
// is this really the best way to get class id? shouldn't we cache it somewhere?
|
|
1026
|
+
// NOTE: maybe if we lower remapElementClass into here, we can use that
|
|
1027
|
+
_getRelClassId(db, classFullName) {
|
|
1028
|
+
// is it better to use un-cached `SELECT (ONLY ${classFullName})`?
|
|
1029
|
+
return db.withPreparedStatement(`
|
|
1030
|
+
SELECT c.ECInstanceId
|
|
1031
|
+
FROM ECDbMeta.ECClassDef c
|
|
1032
|
+
JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
|
|
1033
|
+
WHERE s.Name=? AND c.Name=?
|
|
1034
|
+
`, (stmt) => {
|
|
1035
|
+
const [schemaName, className] = classFullName.split(".");
|
|
1036
|
+
stmt.bindString(1, schemaName);
|
|
1037
|
+
stmt.bindString(2, className);
|
|
1038
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
1039
|
+
return stmt.getValue(0).getId();
|
|
1040
|
+
(0, core_bentley_1.assert)(false, "relationship was not found");
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
803
1043
|
/** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
|
|
804
1044
|
* This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
|
|
805
1045
|
*/
|
|
806
1046
|
onDeleteRelationship(sourceRelInstanceId) {
|
|
807
|
-
|
|
808
|
-
|
|
1047
|
+
nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
|
|
1048
|
+
const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
|
|
1049
|
+
if (!deletedRelData) {
|
|
1050
|
+
core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const targetRelClassId = this._getRelClassId(this.targetDb, deletedRelData.classFullName);
|
|
1054
|
+
// NOTE: if no remapping, could store the sourceRel class name earlier and reuse it instead of add to query
|
|
1055
|
+
// TODO: name this query
|
|
1056
|
+
const sql = `
|
|
1057
|
+
SELECT SourceECInstanceId, TargetECInstanceId, erte.ECClassId
|
|
1058
|
+
FROM BisCore.ElementRefersToElements erte
|
|
1059
|
+
JOIN BisCore.Element se ON se.ECInstanceId=SourceECInstanceId
|
|
1060
|
+
JOIN BisCore.Element te ON te.ECInstanceId=TargetECInstanceId
|
|
1061
|
+
WHERE se.FederationGuid=:sourceFedGuid
|
|
1062
|
+
AND te.FederationGuid=:targetFedGuid
|
|
1063
|
+
AND erte.ECClassId=:relClassId
|
|
1064
|
+
`;
|
|
809
1065
|
this.targetDb.withPreparedStatement(sql, (statement) => {
|
|
810
|
-
statement.
|
|
811
|
-
statement.
|
|
812
|
-
statement.
|
|
1066
|
+
statement.bindGuid("sourceFedGuid", deletedRelData.sourceFedGuid);
|
|
1067
|
+
statement.bindGuid("targetFedGuid", deletedRelData.targetFedGuid);
|
|
1068
|
+
statement.bindId("relClassId", targetRelClassId);
|
|
813
1069
|
if (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
this.
|
|
1070
|
+
const sourceId = statement.getValue(0).getId();
|
|
1071
|
+
const targetId = statement.getValue(1).getId();
|
|
1072
|
+
const targetRelClassFullName = statement.getValue(2).getClassNameForClassId();
|
|
1073
|
+
// FIXME: make importer.deleteRelationship not need full props
|
|
1074
|
+
const targetRelationship = this.targetDb.relationships.tryGetInstance(targetRelClassFullName, { sourceId, targetId });
|
|
1075
|
+
if (targetRelationship) {
|
|
1076
|
+
this.importer.deleteRelationship(targetRelationship.toJSON());
|
|
821
1077
|
}
|
|
1078
|
+
// FIXME: restore in ESA compatible method
|
|
1079
|
+
//this.targetDb.elements.deleteAspect(statement.getValue(0).getId());
|
|
822
1080
|
}
|
|
823
1081
|
});
|
|
824
1082
|
}
|
|
825
1083
|
/** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
|
|
1084
|
+
* @deprecated
|
|
826
1085
|
* @see processChanges
|
|
827
1086
|
* @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
|
|
828
1087
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
@@ -832,7 +1091,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
832
1091
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
|
|
833
1092
|
}
|
|
834
1093
|
const aspectDeleteIds = [];
|
|
835
|
-
const sql = `
|
|
1094
|
+
const sql = `
|
|
1095
|
+
SELECT ECInstanceId, Identifier, JsonProperties
|
|
1096
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
|
|
1097
|
+
WHERE aspect.Scope.Id=:scopeId
|
|
1098
|
+
AND aspect.Kind=:kind
|
|
1099
|
+
`;
|
|
836
1100
|
await this.targetDb.withPreparedStatement(sql, async (statement) => {
|
|
837
1101
|
statement.bindId("scopeId", this.targetScopeElementId);
|
|
838
1102
|
statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
@@ -860,6 +1124,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
860
1124
|
const targetRelationshipProps = sourceRelationship.toJSON();
|
|
861
1125
|
targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
|
|
862
1126
|
targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
|
|
1127
|
+
// TODO: move to cloneRelationship in IModelCloneContext
|
|
863
1128
|
sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
|
|
864
1129
|
if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
|
|
865
1130
|
targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
|
|
@@ -889,8 +1154,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
889
1154
|
sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
|
|
890
1155
|
// const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
|
|
891
1156
|
const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
|
|
892
|
-
|
|
893
|
-
const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect && ((_a = a.scope) === null || _a === void 0 ? void 0 : _a.id) === this.targetScopeElementId;
|
|
1157
|
+
const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect && a.scope?.id === this.targetScopeElementId;
|
|
894
1158
|
return !this._options.includeSourceProvenance || !isExternalSourceAspectFromTransformer;
|
|
895
1159
|
});
|
|
896
1160
|
for (let i = 0; i < targetIds.length; ++i) {
|
|
@@ -958,6 +1222,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
958
1222
|
* It is more efficient to process *data* changes after the schema changes have been saved.
|
|
959
1223
|
*/
|
|
960
1224
|
async processSchemas() {
|
|
1225
|
+
this.events.emit(TransformerEvent.beginProcessSchemas);
|
|
961
1226
|
// we do not need to initialize for this since no entities are exported
|
|
962
1227
|
try {
|
|
963
1228
|
core_backend_1.IModelJsFs.mkdirSync(this._schemaExportDir);
|
|
@@ -975,6 +1240,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
975
1240
|
finally {
|
|
976
1241
|
core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
|
|
977
1242
|
this._longNamedSchemasMap.clear();
|
|
1243
|
+
this.events.emit(TransformerEvent.endProcessSchemas);
|
|
978
1244
|
}
|
|
979
1245
|
}
|
|
980
1246
|
/** Cause all fonts to be exported from the source iModel and imported into the target iModel.
|
|
@@ -1031,17 +1297,53 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1031
1297
|
if (this._initialized)
|
|
1032
1298
|
return;
|
|
1033
1299
|
await this.context.initialize();
|
|
1300
|
+
await this._tryInitChangesetData(args);
|
|
1034
1301
|
// eslint-disable-next-line deprecation/deprecation
|
|
1035
1302
|
await this.initFromExternalSourceAspects(args);
|
|
1036
1303
|
this._initialized = true;
|
|
1037
1304
|
}
|
|
1305
|
+
async _tryInitChangesetData(args) {
|
|
1306
|
+
if (!args || this.sourceDb.iTwinId === undefined) {
|
|
1307
|
+
this._changeDataState = "unconnected";
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const noChanges = this._targetScopeVersion.index === this.sourceDb.changeset.index;
|
|
1311
|
+
if (noChanges) {
|
|
1312
|
+
this._changeDataState = "no-changes";
|
|
1313
|
+
this._changeSummaryIds = [];
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
// NOTE: that we do NOT download the changesummary for the last transformed version, we want
|
|
1317
|
+
// to ignore those already processed changes
|
|
1318
|
+
const startChangesetIndexOrId = args?.startChangesetId ?? this._targetScopeVersion.index + 1;
|
|
1319
|
+
const endChangesetId = this.sourceDb.changeset.id;
|
|
1320
|
+
const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
|
|
1321
|
+
.map(async (indexOrId) => typeof indexOrId === "number"
|
|
1322
|
+
? indexOrId
|
|
1323
|
+
: core_backend_1.IModelHost.hubAccess
|
|
1324
|
+
.queryChangeset({
|
|
1325
|
+
iModelId: this.sourceDb.iModelId,
|
|
1326
|
+
changeset: { id: indexOrId },
|
|
1327
|
+
accessToken: args.accessToken,
|
|
1328
|
+
})
|
|
1329
|
+
.then((changeset) => changeset.index)));
|
|
1330
|
+
// FIXME: do we need the startChangesetId?
|
|
1331
|
+
this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
|
|
1332
|
+
accessToken: args.accessToken,
|
|
1333
|
+
iModelId: this.sourceDb.iModelId,
|
|
1334
|
+
iTwinId: this.sourceDb.iTwinId,
|
|
1335
|
+
range: { first: startChangesetIndex, end: endChangesetIndex },
|
|
1336
|
+
});
|
|
1337
|
+
core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
|
|
1338
|
+
this._changeDataState = "has-changes";
|
|
1339
|
+
}
|
|
1038
1340
|
/** Export everything from the source iModel and import the transformed entities into the target iModel.
|
|
1039
1341
|
* @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
|
|
1040
1342
|
*/
|
|
1041
1343
|
async processAll() {
|
|
1042
|
-
|
|
1344
|
+
this.events.emit(TransformerEvent.beginProcessAll);
|
|
1043
1345
|
this.logSettings();
|
|
1044
|
-
this.
|
|
1346
|
+
this.initScopeProvenance();
|
|
1045
1347
|
await this.initialize();
|
|
1046
1348
|
await this.exporter.exportCodeSpecs();
|
|
1047
1349
|
await this.exporter.exportFonts();
|
|
@@ -1059,15 +1361,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1059
1361
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
1060
1362
|
this.importer.computeProjectExtents();
|
|
1061
1363
|
this.finalizeTransformation();
|
|
1364
|
+
this.events.emit(TransformerEvent.endProcessAll);
|
|
1062
1365
|
}
|
|
1063
1366
|
markLastProvenance(sourceAspect, { isRelationship = false }) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1367
|
+
this._lastProvenanceEntityInfo
|
|
1368
|
+
= typeof sourceAspect === "string"
|
|
1369
|
+
? sourceAspect
|
|
1370
|
+
: {
|
|
1371
|
+
entityId: sourceAspect.element.id,
|
|
1372
|
+
aspectId: sourceAspect.id,
|
|
1373
|
+
aspectVersion: sourceAspect.version ?? "",
|
|
1374
|
+
aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
1375
|
+
};
|
|
1071
1376
|
}
|
|
1072
1377
|
/**
|
|
1073
1378
|
* Load the state of the active transformation from an open SQLiteDb
|
|
@@ -1079,17 +1384,35 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1079
1384
|
const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
|
|
1080
1385
|
if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
|
|
1081
1386
|
throw Error("expected row when getting lastProvenanceEntityId from target state table");
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1387
|
+
const entityId = stmt.getValueString(0);
|
|
1388
|
+
const isGuidOrGuidPair = entityId.includes('-');
|
|
1389
|
+
return isGuidOrGuidPair
|
|
1390
|
+
? entityId
|
|
1391
|
+
: {
|
|
1392
|
+
entityId,
|
|
1393
|
+
aspectId: stmt.getValueString(1),
|
|
1394
|
+
aspectVersion: stmt.getValueString(2),
|
|
1395
|
+
aspectKind: stmt.getValueString(3),
|
|
1396
|
+
};
|
|
1088
1397
|
});
|
|
1089
|
-
|
|
1090
|
-
//
|
|
1091
|
-
|
|
1398
|
+
/*
|
|
1399
|
+
// TODO: maybe save transformer state resumption state based on target changset and require calls
|
|
1400
|
+
// to saveChanges
|
|
1401
|
+
if () {
|
|
1402
|
+
const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
|
|
1403
|
+
const isRelProvenance = targetFedGuid !== undefined;
|
|
1404
|
+
const instanceId = isRelProvenance
|
|
1405
|
+
? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
|
|
1406
|
+
: "";
|
|
1407
|
+
//const classId =
|
|
1408
|
+
if (isRelProvenance) {
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
*/
|
|
1412
|
+
const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
|
|
1413
|
+
// ignore provenance check if it's null since we can't bind those ids
|
|
1092
1414
|
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
|
|
1415
|
+
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
|
|
1093
1416
|
this.provenanceDb.withPreparedStatement(`
|
|
1094
1417
|
SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
1095
1418
|
WHERE Scope.Id=:scopeId
|
|
@@ -1186,8 +1509,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1186
1509
|
throw Error("Failed to create the js state table in the state database");
|
|
1187
1510
|
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
|
|
1188
1511
|
CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
|
|
1189
|
-
--
|
|
1512
|
+
-- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
|
|
1190
1513
|
entityId TEXT,
|
|
1514
|
+
-- the following are only valid if the above entityId is a hex id representation
|
|
1191
1515
|
aspectId TEXT,
|
|
1192
1516
|
aspectVersion TEXT,
|
|
1193
1517
|
aspectKind TEXT
|
|
@@ -1201,10 +1525,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1201
1525
|
throw Error("Failed to insert options into the state database");
|
|
1202
1526
|
});
|
|
1203
1527
|
db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
|
|
1204
|
-
|
|
1205
|
-
stmt.bindString(
|
|
1206
|
-
stmt.bindString(
|
|
1207
|
-
stmt.bindString(
|
|
1528
|
+
const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
|
|
1529
|
+
stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
|
|
1530
|
+
stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
|
|
1531
|
+
stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
|
|
1532
|
+
stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
|
|
1208
1533
|
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
|
|
1209
1534
|
throw Error("Failed to insert options into the state database");
|
|
1210
1535
|
});
|
|
@@ -1233,16 +1558,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1233
1558
|
}
|
|
1234
1559
|
}
|
|
1235
1560
|
/** Export changes from the source iModel and import the transformed entities into the target iModel.
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1561
|
+
* Inserts, updates, and deletes are determined by inspecting the changeset(s).
|
|
1562
|
+
* @param accessToken A valid access token string
|
|
1563
|
+
* @param startChangesetId Include changes from this changeset up through and including the current changeset.
|
|
1564
|
+
* If this parameter is not provided, then just the current changeset will be exported.
|
|
1565
|
+
* @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.
|
|
1566
|
+
*/
|
|
1242
1567
|
async processChanges(accessToken, startChangesetId) {
|
|
1243
|
-
|
|
1568
|
+
this.events.emit(TransformerEvent.beginProcessChanges, startChangesetId);
|
|
1244
1569
|
this.logSettings();
|
|
1245
|
-
this.
|
|
1570
|
+
this.initScopeProvenance();
|
|
1246
1571
|
await this.initialize({ accessToken, startChangesetId });
|
|
1247
1572
|
await this.exporter.exportChanges(accessToken, startChangesetId);
|
|
1248
1573
|
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
@@ -1250,13 +1575,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1250
1575
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
1251
1576
|
this.importer.computeProjectExtents();
|
|
1252
1577
|
this.finalizeTransformation();
|
|
1578
|
+
this.events.emit(TransformerEvent.endProcessChanges);
|
|
1253
1579
|
}
|
|
1254
1580
|
}
|
|
1255
|
-
exports.IModelTransformer = IModelTransformer;
|
|
1256
1581
|
/** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
|
|
1257
1582
|
IModelTransformer.jsStateTable = "TransformerJsState";
|
|
1258
1583
|
/** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
|
|
1259
1584
|
IModelTransformer.lastProvenanceEntityInfoTable = "LastProvenanceEntityInfo";
|
|
1585
|
+
exports.IModelTransformer = IModelTransformer;
|
|
1260
1586
|
/** IModelTransformer that clones the contents of a template model.
|
|
1261
1587
|
* @beta
|
|
1262
1588
|
*/
|
|
@@ -1354,4 +1680,17 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1354
1680
|
}
|
|
1355
1681
|
}
|
|
1356
1682
|
exports.TemplateModelCloner = TemplateModelCloner;
|
|
1683
|
+
function queryElemFedGuid(db, elemId) {
|
|
1684
|
+
return db.withPreparedStatement(`
|
|
1685
|
+
SELECT FederationGuid
|
|
1686
|
+
FROM bis.Element
|
|
1687
|
+
WHERE ECInstanceId=?
|
|
1688
|
+
`, (stmt) => {
|
|
1689
|
+
stmt.bindId(1, elemId);
|
|
1690
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
|
|
1691
|
+
const result = stmt.getValue(0).getGuid();
|
|
1692
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
|
|
1693
|
+
return result;
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1357
1696
|
//# sourceMappingURL=IModelTransformer.js.map
|