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