@itwin/imodel-transformer 0.4.18-fedguidopt.6 → 1.0.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.map +1 -1
- package/lib/cjs/IModelCloneContext.js +23 -12
- package/lib/cjs/IModelCloneContext.js.map +1 -1
- package/lib/cjs/IModelExporter.d.ts +68 -25
- package/lib/cjs/IModelExporter.d.ts.map +1 -1
- package/lib/cjs/IModelExporter.js +241 -123
- package/lib/cjs/IModelExporter.js.map +1 -1
- package/lib/cjs/IModelImporter.d.ts +13 -22
- package/lib/cjs/IModelImporter.d.ts.map +1 -1
- package/lib/cjs/IModelImporter.js +80 -62
- package/lib/cjs/IModelImporter.js.map +1 -1
- package/lib/cjs/IModelTransformer.d.ts +126 -44
- package/lib/cjs/IModelTransformer.d.ts.map +1 -1
- package/lib/cjs/IModelTransformer.js +706 -535
- 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
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Determines the sync direction "forward" or "reverse" of a given sourceDb and targetDb by looking for the scoping ESA.
|
|
136
|
+
* If the sourceDb's iModelId is found as the identifier of the expected scoping ESA in the targetDb, then it is a forward synchronization.
|
|
137
|
+
* If the targetDb's iModelId is found as the identifier of the expected scoping ESA in the sourceDb, then it is a reverse synchronization.
|
|
138
|
+
* @throws if no scoping ESA can be found in either the sourceDb or targetDb which describes a master branch relationship between the two databases.
|
|
139
|
+
* @returns "forward" or "reverse"
|
|
140
|
+
*/
|
|
141
|
+
static determineSyncType(sourceDb, targetDb,
|
|
142
|
+
/** @see [[IModelTransformOptions.targetScopeElementId]] */
|
|
143
|
+
targetScopeElementId) {
|
|
144
|
+
const aspectProps = {
|
|
145
|
+
id: undefined,
|
|
146
|
+
version: undefined,
|
|
147
|
+
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
148
|
+
element: {
|
|
149
|
+
id: targetScopeElementId,
|
|
150
|
+
relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
|
|
151
|
+
},
|
|
152
|
+
scope: { id: core_common_1.IModel.rootSubjectId },
|
|
153
|
+
identifier: sourceDb.iModelId,
|
|
154
|
+
kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
|
|
155
|
+
jsonProperties: undefined,
|
|
156
|
+
};
|
|
157
|
+
/** First check if the targetDb is the branch (branch is the @see provenanceDb) */
|
|
158
|
+
const esaPropsFromTargetDb = this.queryScopeExternalSourceAspect(targetDb, aspectProps);
|
|
159
|
+
if (esaPropsFromTargetDb !== undefined) {
|
|
160
|
+
return "forward"; // we found an esa assuming targetDb is the provenanceDb/branch so this is a forward sync.
|
|
161
|
+
}
|
|
162
|
+
// Now check if the sourceDb is the branch
|
|
163
|
+
aspectProps.identifier = targetDb.iModelId;
|
|
164
|
+
const esaPropsFromSourceDb = this.queryScopeExternalSourceAspect(sourceDb, aspectProps);
|
|
165
|
+
if (esaPropsFromSourceDb !== undefined) {
|
|
166
|
+
return "reverse"; // we found an esa assuming sourceDb is the provenanceDb/branch so this is a reverse sync.
|
|
167
|
+
}
|
|
168
|
+
throw new Error(this.noEsaSyncDirectionErrorMessage);
|
|
169
|
+
}
|
|
170
|
+
determineSyncType() {
|
|
171
|
+
if (this._isProvenanceInitTransform) {
|
|
172
|
+
return "forward";
|
|
173
|
+
}
|
|
174
|
+
if (!this._isSynchronization) {
|
|
175
|
+
return "not-sync";
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
return IModelTransformer.determineSyncType(this.sourceDb, this.targetDb, this.targetScopeElementId);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
if (err instanceof Error &&
|
|
182
|
+
err.message === IModelTransformer.noEsaSyncDirectionErrorMessage &&
|
|
183
|
+
this._allowNoScopingESA) {
|
|
184
|
+
return "forward";
|
|
185
|
+
}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
100
188
|
}
|
|
101
|
-
get
|
|
102
|
-
|
|
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,11 @@ 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
|
-
/**
|
|
571
|
-
* Scan changesets for deleted entities, if in a reverse synchronization, provenance has
|
|
572
|
-
* already been deleted, so we must scan for that as well.
|
|
573
|
-
*/
|
|
574
|
-
async remapDeletedSourceEntities() {
|
|
575
|
-
// we need a connected iModel with changes to remap elements with deletions
|
|
576
|
-
const notConnectedModel = this.sourceDb.iTwinId === undefined;
|
|
577
|
-
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
578
|
-
if (notConnectedModel || noChanges)
|
|
579
|
-
return;
|
|
580
|
-
this._deletedSourceRelationshipData = new Map();
|
|
581
|
-
nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
|
|
582
|
-
if (this._changeSummaryIds.length === 0)
|
|
583
|
-
return;
|
|
584
|
-
const alreadyImportedElementInserts = new Set();
|
|
585
|
-
const alreadyImportedModelInserts = new Set();
|
|
586
|
-
this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
|
|
587
|
-
const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
|
|
588
|
-
if (core_bentley_1.Id64.isValid(targetElementId))
|
|
589
|
-
alreadyImportedElementInserts.add(targetElementId);
|
|
590
|
-
});
|
|
591
|
-
this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
|
|
592
|
-
const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
|
|
593
|
-
if (core_bentley_1.Id64.isValid(targetModelId))
|
|
594
|
-
alreadyImportedModelInserts.add(targetModelId);
|
|
595
|
-
});
|
|
596
|
-
// optimization: if we have provenance, use it to avoid more querying later
|
|
597
|
-
// eventually when itwin.js supports attaching a second iModelDb in JS,
|
|
598
|
-
// this won't have to be a conditional part of the query, and we can always have it by attaching
|
|
599
|
-
const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
|
|
600
|
-
const deletedEntitySql = `
|
|
601
|
-
SELECT
|
|
602
|
-
1 AS IsElemNotRel,
|
|
603
|
-
ic.ChangedInstance.Id AS InstanceId,
|
|
604
|
-
NULL AS InstId2, -- need these columns for relationship ends in the unioned query
|
|
605
|
-
NULL AS InstId3,
|
|
606
|
-
ec.FederationGuid AS FedGuid,
|
|
607
|
-
NULL AS FedGuid2,
|
|
608
|
-
ic.ChangedInstance.ClassId AS ClassId
|
|
609
|
-
${queryCanAccessProvenance ? `
|
|
610
|
-
/*
|
|
611
|
-
-- can't coalesce these due to a bug, so do it in JS
|
|
612
|
-
, coalesce(
|
|
613
|
-
IIF(esa.Scope.Id=:targetScopeElement, esa.Identifier, NULL),
|
|
614
|
-
IIF(esac.Scope.Id=:targetScopeElement, esac.Identifier, NULL)
|
|
615
|
-
) AS Identifier1
|
|
616
|
-
*/
|
|
617
|
-
, CASE WHEN esa.Scope.Id = ${this.targetScopeElementId} THEN esa.Identifier ELSE NULL END AS Identifier1A
|
|
618
|
-
-- FIXME: using :targetScopeElement parameter in this second potential identifier breaks ecsql
|
|
619
|
-
, CASE WHEN esac.Scope.Id = ${this.targetScopeElementId} THEN esac.Identifier ELSE NULL END AS Identifier1B
|
|
620
|
-
, NULL AS Identifier2A
|
|
621
|
-
, NULL AS Identifier2B
|
|
622
|
-
` : ""}
|
|
623
|
-
FROM ecchange.change.InstanceChange ic
|
|
624
|
-
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
|
|
625
|
-
ON ic.ChangedInstance.Id=ec.ECInstanceId
|
|
626
|
-
${queryCanAccessProvenance ? `
|
|
627
|
-
LEFT JOIN bis.ExternalSourceAspect esa
|
|
628
|
-
ON ec.ECInstanceId=esa.Element.Id
|
|
629
|
-
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
|
|
630
|
-
ON ec.ECInstanceId=esac.Element.Id
|
|
631
|
-
` : ""}
|
|
632
|
-
WHERE ic.OpCode=:opDelete
|
|
633
|
-
AND ic.Summary.Id=:changeSummaryId
|
|
634
|
-
AND ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
635
|
-
|
|
636
|
-
UNION ALL
|
|
637
|
-
|
|
638
|
-
SELECT
|
|
639
|
-
0 AS IsElemNotRel,
|
|
640
|
-
ic.ChangedInstance.Id AS InstanceId,
|
|
641
|
-
coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
|
|
642
|
-
coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
|
|
643
|
-
coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
|
|
644
|
-
coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
|
|
645
|
-
ic.ChangedInstance.ClassId AS ClassId
|
|
646
|
-
${queryCanAccessProvenance ? `
|
|
647
|
-
, sesa.Identifier AS Identifier1A
|
|
648
|
-
, sesac.Identifier AS Identifier1B
|
|
649
|
-
, tesa.Identifier AS Identifier2A
|
|
650
|
-
, tesac.Identifier AS Identifier2B
|
|
651
|
-
` : ""}
|
|
652
|
-
FROM ecchange.change.InstanceChange ic
|
|
653
|
-
LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
|
|
654
|
-
ON ic.ChangedInstance.Id=ertec.ECInstanceId
|
|
655
|
-
-- FIXME: test a deletion of both an element and a relationship at the same time
|
|
656
|
-
LEFT JOIN bis.Element se
|
|
657
|
-
ON se.ECInstanceId=ertec.SourceECInstanceId
|
|
658
|
-
LEFT JOIN bis.Element te
|
|
659
|
-
ON te.ECInstanceId=ertec.TargetECInstanceId
|
|
660
|
-
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
|
|
661
|
-
ON sec.ECInstanceId=ertec.SourceECInstanceId
|
|
662
|
-
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
|
|
663
|
-
ON tec.ECInstanceId=ertec.TargetECInstanceId
|
|
664
|
-
${queryCanAccessProvenance ? `
|
|
665
|
-
-- NOTE: need to join on both se/te and sec/tec incase the element was deleted
|
|
666
|
-
LEFT JOIN bis.ExternalSourceAspect sesa
|
|
667
|
-
ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
|
|
668
|
-
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
|
|
669
|
-
ON sec.ECInstanceId=sesac.Element.Id
|
|
670
|
-
LEFT JOIN bis.ExternalSourceAspect tesa
|
|
671
|
-
ON te.ECInstanceId=tesa.Element.Id
|
|
672
|
-
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
|
|
673
|
-
ON tec.ECInstanceId=tesac.Element.Id
|
|
674
|
-
` : ""}
|
|
675
|
-
WHERE ic.OpCode=:opDelete
|
|
676
|
-
AND ic.Summary.Id=:changeSummaryId
|
|
677
|
-
AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
|
|
678
|
-
${queryCanAccessProvenance ? `
|
|
679
|
-
AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
|
|
680
|
-
AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
|
|
681
|
-
AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
|
|
682
|
-
AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
|
|
683
|
-
AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
|
|
684
|
-
AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
|
|
685
|
-
AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
|
|
686
|
-
AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
|
|
687
|
-
` : ""}
|
|
688
|
-
`;
|
|
689
|
-
for (const changeSummaryId of this._changeSummaryIds) {
|
|
690
|
-
this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
|
|
691
|
-
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
692
|
-
if (queryCanAccessProvenance)
|
|
693
|
-
stmt.bindId("targetScopeElement", this.targetScopeElementId);
|
|
694
|
-
stmt.bindId("changeSummaryId", changeSummaryId);
|
|
695
|
-
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
696
|
-
const isElemNotRel = stmt.getValue(0).getBoolean();
|
|
697
|
-
const instId = stmt.getValue(1).getId();
|
|
698
|
-
if (isElemNotRel) {
|
|
699
|
-
const sourceElemFedGuid = stmt.getValue(4).getGuid();
|
|
700
|
-
// "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
|
|
701
|
-
let identifierValue;
|
|
702
|
-
// identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
|
|
703
|
-
if (queryCanAccessProvenance) {
|
|
704
|
-
identifierValue = stmt.getValue(7);
|
|
705
|
-
if (identifierValue.isNull)
|
|
706
|
-
identifierValue = stmt.getValue(8);
|
|
707
|
-
}
|
|
708
|
-
// TODO: if I could attach the second db, will probably be much faster to get target id
|
|
709
|
-
// as part of the whole query rather than with _queryElemIdByFedGuid
|
|
710
|
-
const targetId = (queryCanAccessProvenance && identifierValue
|
|
711
|
-
&& !identifierValue.isNull
|
|
712
|
-
&& identifierValue.getString())
|
|
713
|
-
// maybe batching these queries would perform better but we should
|
|
714
|
-
// try to attach the second db and query both together anyway
|
|
715
|
-
|| (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
|
|
716
|
-
// FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
717
|
-
|| this._queryProvenanceForElement(instId);
|
|
718
|
-
// since we are processing one changeset at a time, we can see local source deletes
|
|
719
|
-
// of entities that were never synced and can be safely ignored
|
|
720
|
-
const deletionNotInTarget = !targetId;
|
|
721
|
-
if (deletionNotInTarget)
|
|
722
|
-
continue;
|
|
723
|
-
this.context.remapElement(instId, targetId);
|
|
724
|
-
// If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
|
|
725
|
-
// In such case an entity update will be triggered and we no longer need to delete the entity.
|
|
726
|
-
if (alreadyImportedElementInserts.has(targetId)) {
|
|
727
|
-
this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
|
|
728
|
-
}
|
|
729
|
-
if (alreadyImportedModelInserts.has(targetId)) {
|
|
730
|
-
this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
else { // is deleted relationship
|
|
734
|
-
const classFullName = stmt.getValue(6).getClassNameForClassId();
|
|
735
|
-
const [sourceIdInTarget, targetIdInTarget] = [
|
|
736
|
-
// identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
|
|
737
|
-
{ guidColumn: 4, identifierColumns: { a: 7, b: 8 }, isTarget: false },
|
|
738
|
-
{ guidColumn: 5, identifierColumns: { a: 9, b: 10 }, isTarget: true },
|
|
739
|
-
].map(({ guidColumn, identifierColumns }) => {
|
|
740
|
-
const fedGuid = stmt.getValue(guidColumn).getGuid();
|
|
741
|
-
let identifierValue;
|
|
742
|
-
// identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
|
|
743
|
-
if (queryCanAccessProvenance) {
|
|
744
|
-
identifierValue = stmt.getValue(identifierColumns.a);
|
|
745
|
-
if (identifierValue.isNull)
|
|
746
|
-
identifierValue = stmt.getValue(identifierColumns.b);
|
|
747
|
-
}
|
|
748
|
-
return ((queryCanAccessProvenance && identifierValue
|
|
749
|
-
// FIXME: this is really far from idiomatic, try to undo that
|
|
750
|
-
&& !identifierValue.isNull
|
|
751
|
-
&& identifierValue.getString())
|
|
752
|
-
// maybe batching these queries would perform better but we should
|
|
753
|
-
// try to attach the second db and query both together anyway
|
|
754
|
-
|| (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
|
|
755
|
-
});
|
|
756
|
-
// since we are processing one changeset at a time, we can see local source deletes
|
|
757
|
-
// of entities that were never synced and can be safely ignored
|
|
758
|
-
if (sourceIdInTarget && targetIdInTarget) {
|
|
759
|
-
this._deletedSourceRelationshipData.set(instId, {
|
|
760
|
-
classFullName,
|
|
761
|
-
sourceIdInTarget,
|
|
762
|
-
targetIdInTarget,
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
// FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
767
|
-
const relProvenance = this._queryProvenanceForRelationship(instId, {
|
|
768
|
-
classFullName,
|
|
769
|
-
sourceId: stmt.getValue(2).getId(),
|
|
770
|
-
targetId: stmt.getValue(3).getId(),
|
|
771
|
-
});
|
|
772
|
-
if (relProvenance && relProvenance.relationshipId)
|
|
773
|
-
this._deletedSourceRelationshipData.set(instId, {
|
|
774
|
-
classFullName,
|
|
775
|
-
relId: relProvenance.relationshipId,
|
|
776
|
-
provenanceAspectId: relProvenance.aspectId,
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
// NEXT: remap sourceId and targetId to target, get provenance there
|
|
782
|
-
// NOTE: it is possible during a forward sync for the target to already have deleted
|
|
783
|
-
// something that the source deleted, in which case we can safely ignore the gone provenance
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
691
|
_queryProvenanceForElement(entityInProvenanceSourceId) {
|
|
788
692
|
return this.provenanceDb.withPreparedStatement(`
|
|
789
693
|
SELECT esa.Element.Id
|
|
@@ -833,7 +737,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
833
737
|
sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
|
|
834
738
|
targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
|
|
835
739
|
};
|
|
836
|
-
if (targetRelInfo.sourceId === undefined ||
|
|
740
|
+
if (targetRelInfo.sourceId === undefined ||
|
|
741
|
+
targetRelInfo.targetId === undefined)
|
|
837
742
|
return undefined; // couldn't find an element, rel is invalid or deleted
|
|
838
743
|
return this.targetDb.withPreparedStatement(`
|
|
839
744
|
SELECT ECInstanceId
|
|
@@ -867,7 +772,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
867
772
|
JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
|
|
868
773
|
WHERE s.Name=? AND c.Name=?
|
|
869
774
|
`, (stmt) => {
|
|
870
|
-
const [schemaName, className] = classFullName.
|
|
775
|
+
const [schemaName, className] = classFullName.indexOf(".") !== -1
|
|
776
|
+
? classFullName.split(".")
|
|
777
|
+
: classFullName.split(":");
|
|
871
778
|
stmt.bindString(1, schemaName);
|
|
872
779
|
stmt.bindString(2, className);
|
|
873
780
|
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
@@ -885,26 +792,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
885
792
|
});
|
|
886
793
|
}
|
|
887
794
|
/** Returns `true` if *brute force* delete detections should be run.
|
|
795
|
+
* @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
|
|
888
796
|
* @note Not relevant for processChanges when change history is known.
|
|
889
797
|
*/
|
|
890
798
|
shouldDetectDeletes() {
|
|
891
|
-
|
|
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;
|
|
799
|
+
nodeAssert(this._syncType !== undefined);
|
|
800
|
+
return this._syncType === "not-sync";
|
|
901
801
|
}
|
|
902
802
|
/**
|
|
903
803
|
* Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
|
|
904
804
|
* in the source iModel.
|
|
905
805
|
* @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
|
|
906
806
|
* This method is only called during [[processAll]] when the option
|
|
907
|
-
* [[
|
|
807
|
+
* [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
|
|
908
808
|
* necessary when using [[processChanges]] since changeset information is sufficient.
|
|
909
809
|
* @note you do not need to call this directly unless processing a subset of an iModel.
|
|
910
810
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
@@ -916,18 +816,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
916
816
|
WHERE Scope.Id=:scopeId
|
|
917
817
|
AND Kind=:kind
|
|
918
818
|
`;
|
|
919
|
-
nodeAssert(!this.
|
|
819
|
+
nodeAssert(!this.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
|
|
920
820
|
this.provenanceDb.withPreparedStatement(sql, (stmt) => {
|
|
921
821
|
stmt.bindId("scopeId", this.targetScopeElementId);
|
|
922
822
|
stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
923
823
|
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
924
824
|
// ExternalSourceAspect.Identifier is of type string
|
|
925
825
|
const aspectIdentifier = stmt.getValue(0).getString();
|
|
926
|
-
if (!core_bentley_1.Id64.
|
|
826
|
+
if (!core_bentley_1.Id64.isValidId64(aspectIdentifier)) {
|
|
927
827
|
continue;
|
|
928
828
|
}
|
|
929
829
|
const targetElemId = stmt.getValue(1).getId();
|
|
930
|
-
const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
|
|
830
|
+
const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
|
|
831
|
+
entityReference: `e${aspectIdentifier}`,
|
|
832
|
+
});
|
|
931
833
|
if (wasDeletedInSource)
|
|
932
834
|
this.importer.deleteElement(targetElemId);
|
|
933
835
|
}
|
|
@@ -937,7 +839,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
937
839
|
* @deprecated in 3.x, this no longer has any effect except emitting a warning
|
|
938
840
|
*/
|
|
939
841
|
skipElement(_sourceElement) {
|
|
940
|
-
core_bentley_1.Logger.logWarning(loggerCategory,
|
|
842
|
+
core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
|
|
941
843
|
}
|
|
942
844
|
/** Transform the specified sourceElement into ElementProps for the target iModel.
|
|
943
845
|
* @param sourceElement The Element from the source iModel to transform.
|
|
@@ -956,39 +858,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
956
858
|
}
|
|
957
859
|
return targetElementProps;
|
|
958
860
|
}
|
|
959
|
-
// TODO: this is a PoC, see if we minimize memory usage
|
|
960
|
-
_cacheSourceChanges() {
|
|
961
|
-
nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
|
|
962
|
-
this._hasElementChangedCache = new Set();
|
|
963
|
-
const query = `
|
|
964
|
-
SELECT
|
|
965
|
-
ic.ChangedInstance.Id AS InstId
|
|
966
|
-
FROM ecchange.change.InstanceChange ic
|
|
967
|
-
JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
|
|
968
|
-
-- TODO: do relationship entities also need this cache optimization?
|
|
969
|
-
WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
970
|
-
AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
|
|
971
|
-
-- ignore deleted, we take care of those in remapDeletedSourceEntities
|
|
972
|
-
-- include inserted since inserted code-colliding elements should be considered
|
|
973
|
-
-- a change so that the colliding element is exported to the target
|
|
974
|
-
AND ic.OpCode<>:opDelete
|
|
975
|
-
`;
|
|
976
|
-
// there is a single mega-query multi-join+coalescing hack that I used originally to get around
|
|
977
|
-
// only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
|
|
978
|
-
// tables in a join. Need to talk to core about .Changes being able to take a set of changesets
|
|
979
|
-
// You can find this version in the `federation-guid-optimization-megaquery` branch
|
|
980
|
-
// I wouldn't use it unless we prove via profiling that it speeds things up significantly
|
|
981
|
-
// And even then let's first try scanning the raw changesets instead of applying them as these queries
|
|
982
|
-
// require
|
|
983
|
-
this.sourceDb.withPreparedStatement(query, (stmt) => {
|
|
984
|
-
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
985
|
-
stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
|
|
986
|
-
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
987
|
-
const instId = stmt.getValue(0).getId();
|
|
988
|
-
this._hasElementChangedCache.add(instId);
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
861
|
/** Returns true if a change within sourceElement is detected.
|
|
993
862
|
* @param sourceElement The Element from the source iModel
|
|
994
863
|
* @param targetElementId The Element from the target iModel to compare against.
|
|
@@ -1000,8 +869,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1000
869
|
if (this._sourceChangeDataState === "unconnected")
|
|
1001
870
|
return true;
|
|
1002
871
|
nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
|
|
1003
|
-
|
|
1004
|
-
this._cacheSourceChanges();
|
|
872
|
+
nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
|
|
1005
873
|
return this._hasElementChangedCache.has(sourceElement.id);
|
|
1006
874
|
}
|
|
1007
875
|
static transformCallbackFor(transformer, entity) {
|
|
@@ -1028,8 +896,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1028
896
|
const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
|
|
1029
897
|
const targetProps = onEntityTransform.call(this, sourceEntity);
|
|
1030
898
|
if (sourceEntity instanceof core_backend_1.Relationship) {
|
|
1031
|
-
targetProps.sourceId =
|
|
1032
|
-
|
|
899
|
+
targetProps.sourceId =
|
|
900
|
+
this.context.findTargetElementId(sourceEntity.sourceId);
|
|
901
|
+
targetProps.targetId =
|
|
902
|
+
this.context.findTargetElementId(sourceEntity.targetId);
|
|
1033
903
|
}
|
|
1034
904
|
updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
|
|
1035
905
|
this._partiallyCommittedEntities.delete(sourceEntity);
|
|
@@ -1045,11 +915,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1045
915
|
for (const referenceId of entity.getReferenceConcreteIds()) {
|
|
1046
916
|
// TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
|
|
1047
917
|
const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
|
|
1048
|
-
const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
|
|
918
|
+
const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
|
|
919
|
+
this._skippedEntities.has(referenceId);
|
|
1049
920
|
if (alreadyProcessed)
|
|
1050
921
|
continue;
|
|
1051
922
|
core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
|
|
1052
|
-
const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
|
|
923
|
+
const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
|
|
924
|
+
entityReference: referenceId,
|
|
925
|
+
});
|
|
1053
926
|
if (!referencedExistsInSource) {
|
|
1054
927
|
core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
|
|
1055
928
|
switch (this._options.danglingReferencesBehavior) {
|
|
@@ -1059,7 +932,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1059
932
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
|
|
1060
933
|
`Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
|
|
1061
934
|
"This must have been caused by an upstream application that changed the iModel.",
|
|
1062
|
-
"You can set the
|
|
935
|
+
"You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
|
|
1063
936
|
"in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
|
|
1064
937
|
"transformer to remove the reference and fix affected elements may be suitable.",
|
|
1065
938
|
].join("\n"));
|
|
@@ -1097,7 +970,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1097
970
|
/** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel.
|
|
1098
971
|
* @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter.
|
|
1099
972
|
*/
|
|
1100
|
-
shouldExportElement(_sourceElement) {
|
|
973
|
+
shouldExportElement(_sourceElement) {
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
1101
976
|
onSkipElement(sourceElementId) {
|
|
1102
977
|
if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
|
|
1103
978
|
// element already has provenance
|
|
@@ -1128,7 +1003,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1128
1003
|
const referenceType = elemClass.requiredReferenceKeyTypeMap[referenceKey];
|
|
1129
1004
|
// For now we just consider all required references to be elements (as they are in biscore), and do not support
|
|
1130
1005
|
// entities that refuse to be inserted without a different kind of entity (e.g. aspect or relationship) first being inserted
|
|
1131
|
-
(0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element ||
|
|
1006
|
+
(0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element ||
|
|
1007
|
+
referenceType === core_common_1.ConcreteEntityTypes.Model);
|
|
1132
1008
|
return mapId64(idContainer, (id) => {
|
|
1133
1009
|
if (id === core_bentley_1.Id64.invalid || id === core_common_1.IModel.rootSubjectId)
|
|
1134
1010
|
return undefined; // not allowed to directly export the root subject
|
|
@@ -1137,13 +1013,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1137
1013
|
// This is relied upon by the TemplateModelCloner
|
|
1138
1014
|
// TODO: extract this out to only be in the TemplateModelCloner
|
|
1139
1015
|
const asDefinitionElem = this.sourceDb.elements.tryGetElement(id, core_backend_1.DefinitionElement);
|
|
1140
|
-
if (asDefinitionElem &&
|
|
1016
|
+
if (asDefinitionElem &&
|
|
1017
|
+
!(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
|
|
1141
1018
|
this.context.remapElement(id, id);
|
|
1142
1019
|
}
|
|
1143
1020
|
}
|
|
1144
1021
|
return id;
|
|
1145
|
-
})
|
|
1146
|
-
.filter((sourceReferenceId) => {
|
|
1022
|
+
}).filter((sourceReferenceId) => {
|
|
1147
1023
|
if (sourceReferenceId === undefined)
|
|
1148
1024
|
return false;
|
|
1149
1025
|
const referenceInTargetId = this.context.findTargetElementId(sourceReferenceId);
|
|
@@ -1171,7 +1047,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1171
1047
|
const isSubModeled = dbHasModel(this.sourceDb, elementId);
|
|
1172
1048
|
const idOfElemInTarget = this.context.findTargetElementId(elementId);
|
|
1173
1049
|
const isElemInTarget = core_bentley_1.Id64.invalid !== idOfElemInTarget;
|
|
1174
|
-
const needsModelImport = isSubModeled &&
|
|
1050
|
+
const needsModelImport = isSubModeled &&
|
|
1051
|
+
(!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
|
|
1175
1052
|
return { needsElemImport: !isElemInTarget, needsModelImport };
|
|
1176
1053
|
}
|
|
1177
1054
|
/** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel.
|
|
@@ -1186,27 +1063,33 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1186
1063
|
}
|
|
1187
1064
|
else if (this._options.wasSourceIModelCopiedToTarget) {
|
|
1188
1065
|
targetElementId = sourceElement.id;
|
|
1189
|
-
targetElementProps =
|
|
1066
|
+
targetElementProps =
|
|
1067
|
+
this.targetDb.elements.getElementProps(targetElementId);
|
|
1190
1068
|
}
|
|
1191
1069
|
else {
|
|
1192
1070
|
targetElementId = this.context.findTargetElementId(sourceElement.id);
|
|
1193
1071
|
targetElementProps = this.onTransformElement(sourceElement);
|
|
1194
1072
|
}
|
|
1195
1073
|
// if an existing remapping was not yet found, check by FederationGuid
|
|
1196
|
-
if (this.context.isBetweenIModels &&
|
|
1197
|
-
|
|
1074
|
+
if (this.context.isBetweenIModels &&
|
|
1075
|
+
!core_bentley_1.Id64.isValid(targetElementId) &&
|
|
1076
|
+
sourceElement.federationGuid !== undefined) {
|
|
1077
|
+
targetElementId =
|
|
1078
|
+
this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
|
|
1198
1079
|
if (core_bentley_1.Id64.isValid(targetElementId))
|
|
1199
1080
|
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
|
|
1200
1081
|
}
|
|
1201
1082
|
// if an existing remapping was not yet found, check by Code as long as the CodeScope is valid (invalid means a missing reference so not worth checking)
|
|
1202
|
-
if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
|
|
1083
|
+
if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
|
|
1084
|
+
core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
|
|
1203
1085
|
// respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
|
|
1204
1086
|
// whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
|
|
1205
1087
|
targetElementProps.code.value = targetElementProps.code.value ?? "";
|
|
1206
1088
|
const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
|
|
1207
1089
|
if (undefined !== maybeTargetElementId) {
|
|
1208
1090
|
const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
|
|
1209
|
-
if (maybeTargetElem.classFullName === targetElementProps.classFullName) {
|
|
1091
|
+
if (maybeTargetElem.classFullName === targetElementProps.classFullName) {
|
|
1092
|
+
// ensure code remapping doesn't change the target class
|
|
1210
1093
|
targetElementId = maybeTargetElementId;
|
|
1211
1094
|
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
|
|
1212
1095
|
}
|
|
@@ -1215,14 +1098,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1215
1098
|
}
|
|
1216
1099
|
}
|
|
1217
1100
|
}
|
|
1218
|
-
if (core_bentley_1.Id64.isValid(targetElementId) &&
|
|
1101
|
+
if (core_bentley_1.Id64.isValid(targetElementId) &&
|
|
1102
|
+
!this.hasElementChanged(sourceElement, targetElementId))
|
|
1219
1103
|
return;
|
|
1220
1104
|
this.collectUnmappedReferences(sourceElement);
|
|
1221
1105
|
// targetElementId will be valid (indicating update) or undefined (indicating insert)
|
|
1222
|
-
targetElementProps.id
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
: undefined;
|
|
1106
|
+
targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
|
|
1107
|
+
? targetElementId
|
|
1108
|
+
: undefined;
|
|
1226
1109
|
if (!this._options.wasSourceIModelCopiedToTarget) {
|
|
1227
1110
|
this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
|
|
1228
1111
|
}
|
|
@@ -1237,17 +1120,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1237
1120
|
// FIXME: verify at finalization time that we don't lose provenance on new elements
|
|
1238
1121
|
// FIXME: make public and improve `initElementProvenance` API for usage by consolidators
|
|
1239
1122
|
if (!this._options.noProvenance) {
|
|
1240
|
-
let provenance = this._options.forceExternalSourceAspectProvenance ||
|
|
1123
|
+
let provenance = this._options.forceExternalSourceAspectProvenance ||
|
|
1124
|
+
this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
|
|
1241
1125
|
? undefined
|
|
1242
1126
|
: sourceElement.federationGuid;
|
|
1243
1127
|
if (!provenance) {
|
|
1244
1128
|
const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
|
|
1245
|
-
const
|
|
1246
|
-
if (
|
|
1129
|
+
const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
|
|
1130
|
+
if (foundEsaProps === undefined)
|
|
1247
1131
|
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1248
|
-
}
|
|
1249
1132
|
else {
|
|
1250
|
-
aspectProps
|
|
1133
|
+
// Since initElementProvenance sets a property 'version' on the aspectProps that we wish to persist in the provenanceDb, only grab the id from the foundEsaProps.
|
|
1134
|
+
aspectProps.id = foundEsaProps.aspectId;
|
|
1251
1135
|
this.provenanceDb.elements.updateAspect(aspectProps);
|
|
1252
1136
|
}
|
|
1253
1137
|
provenance = aspectProps;
|
|
@@ -1278,10 +1162,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1278
1162
|
* This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel.
|
|
1279
1163
|
*/
|
|
1280
1164
|
onExportModel(sourceModel) {
|
|
1281
|
-
if (
|
|
1165
|
+
if (this._options.skipPropagateChangesToRootElements &&
|
|
1166
|
+
core_common_1.IModel.repositoryModelId === sourceModel.id)
|
|
1282
1167
|
return; // The RepositoryModel should not be directly imported
|
|
1283
|
-
}
|
|
1284
1168
|
const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
|
|
1169
|
+
// there can only be one repositoryModel per database, so ignore the repo model on remapped subjects
|
|
1170
|
+
const isRemappedRootSubject = sourceModel.id === core_common_1.IModel.repositoryModelId &&
|
|
1171
|
+
targetModeledElementId != sourceModel.id;
|
|
1172
|
+
if (isRemappedRootSubject)
|
|
1173
|
+
return;
|
|
1285
1174
|
const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
|
|
1286
1175
|
this.importer.importModel(targetModelProps);
|
|
1287
1176
|
this.resolvePendingReferences(sourceModel);
|
|
@@ -1303,9 +1192,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1303
1192
|
stmt.bindId(1, targetModelId);
|
|
1304
1193
|
const val = stmt.step();
|
|
1305
1194
|
switch (val) {
|
|
1306
|
-
case core_bentley_1.DbResult.BE_SQLITE_ROW:
|
|
1307
|
-
|
|
1308
|
-
|
|
1195
|
+
case core_bentley_1.DbResult.BE_SQLITE_ROW:
|
|
1196
|
+
return true;
|
|
1197
|
+
case core_bentley_1.DbResult.BE_SQLITE_DONE:
|
|
1198
|
+
return false;
|
|
1199
|
+
default:
|
|
1200
|
+
(0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
|
|
1309
1201
|
}
|
|
1310
1202
|
});
|
|
1311
1203
|
if (isDefinitionPartition) {
|
|
@@ -1319,7 +1211,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1319
1211
|
this.importer.deleteModel(targetModelId);
|
|
1320
1212
|
}
|
|
1321
1213
|
catch (error) {
|
|
1322
|
-
const isDeletionProhibitedErr = error instanceof core_common_1.IModelError &&
|
|
1214
|
+
const isDeletionProhibitedErr = error instanceof core_common_1.IModelError &&
|
|
1215
|
+
(error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited ||
|
|
1216
|
+
error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
|
|
1323
1217
|
if (!isDeletionProhibitedErr)
|
|
1324
1218
|
throw error;
|
|
1325
1219
|
// Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
|
|
@@ -1330,7 +1224,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1330
1224
|
}
|
|
1331
1225
|
/** Schedule modeled partition deletion */
|
|
1332
1226
|
scheduleModeledPartitionDeletion(sourceModelId) {
|
|
1333
|
-
const deletedElements = this.exporter.sourceDbChanges?.element
|
|
1227
|
+
const deletedElements = this.exporter.sourceDbChanges?.element
|
|
1228
|
+
.deleteIds;
|
|
1334
1229
|
if (!deletedElements.has(sourceModelId)) {
|
|
1335
1230
|
deletedElements.add(sourceModelId);
|
|
1336
1231
|
}
|
|
@@ -1396,7 +1291,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1396
1291
|
onTransformModel(sourceModel, targetModeledElementId) {
|
|
1397
1292
|
const targetModelProps = sourceModel.toJSON();
|
|
1398
1293
|
// don't directly edit deep object since toJSON performs a shallow clone
|
|
1399
|
-
targetModelProps.modeledElement = {
|
|
1294
|
+
targetModelProps.modeledElement = {
|
|
1295
|
+
...targetModelProps.modeledElement,
|
|
1296
|
+
id: targetModeledElementId,
|
|
1297
|
+
};
|
|
1400
1298
|
targetModelProps.id = targetModeledElementId;
|
|
1401
1299
|
targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
|
|
1402
1300
|
return targetModelProps;
|
|
@@ -1408,7 +1306,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1408
1306
|
/** called at the end ([[finalizeTransformation]]) of a transformation,
|
|
1409
1307
|
* updates the target scope element to say that transformation up through the
|
|
1410
1308
|
* source's changeset has been performed. Also stores all changesets that occurred
|
|
1411
|
-
* during the transformation as "pending synchronization changeset indices"
|
|
1309
|
+
* during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
|
|
1412
1310
|
*
|
|
1413
1311
|
* You generally should not call this function yourself and use [[processChanges]] instead.
|
|
1414
1312
|
* It is public for unsupported use cases of custom synchronization transforms.
|
|
@@ -1416,45 +1314,54 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1416
1314
|
* without setting the `force` option to `true`
|
|
1417
1315
|
*/
|
|
1418
1316
|
updateSynchronizationVersion({ force = false } = {}) {
|
|
1419
|
-
|
|
1317
|
+
const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
|
|
1318
|
+
this._sourceChangeDataState !== "has-changes" &&
|
|
1319
|
+
!this._isProvenanceInitTransform;
|
|
1320
|
+
if (notForcedAndHasNoChangesAndIsntProvenanceInit)
|
|
1420
1321
|
return;
|
|
1421
1322
|
nodeAssert(this._targetScopeProvenanceProps);
|
|
1422
1323
|
const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
|
|
1423
1324
|
const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
|
|
1424
|
-
if (this.
|
|
1325
|
+
if (this._isProvenanceInitTransform) {
|
|
1425
1326
|
this._targetScopeProvenanceProps.version = sourceVersion;
|
|
1426
|
-
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
|
|
1327
|
+
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
|
|
1328
|
+
targetVersion;
|
|
1427
1329
|
}
|
|
1428
|
-
else if (this.
|
|
1330
|
+
else if (this.isReverseSynchronization) {
|
|
1429
1331
|
const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
|
|
1430
1332
|
core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
|
|
1431
|
-
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
|
|
1333
|
+
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
|
|
1334
|
+
sourceVersion;
|
|
1432
1335
|
}
|
|
1433
|
-
else if (!this.
|
|
1336
|
+
else if (!this.isReverseSynchronization) {
|
|
1434
1337
|
core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
|
|
1435
1338
|
this._targetScopeProvenanceProps.version = sourceVersion;
|
|
1436
1339
|
}
|
|
1437
|
-
if (this._isSynchronization
|
|
1438
|
-
(
|
|
1340
|
+
if (this._isSynchronization ||
|
|
1341
|
+
(this._startingChangesetIndices && this._isProvenanceInitTransform)) {
|
|
1342
|
+
nodeAssert(this.targetDb.changeset.index !== undefined &&
|
|
1343
|
+
this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
|
|
1439
1344
|
const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
|
|
1440
1345
|
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
|
|
1441
1346
|
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
|
|
1442
|
-
const [syncChangesetsToClear, syncChangesetsToUpdate] = this
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1347
|
+
const [syncChangesetsToClear, syncChangesetsToUpdate] = this
|
|
1348
|
+
.isReverseSynchronization
|
|
1349
|
+
? [
|
|
1350
|
+
jsonProps.pendingReverseSyncChangesetIndices,
|
|
1351
|
+
jsonProps.pendingSyncChangesetIndices,
|
|
1352
|
+
]
|
|
1353
|
+
: [
|
|
1354
|
+
jsonProps.pendingSyncChangesetIndices,
|
|
1355
|
+
jsonProps.pendingReverseSyncChangesetIndices,
|
|
1356
|
+
];
|
|
1450
1357
|
for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
|
|
1451
1358
|
syncChangesetsToUpdate.push(i);
|
|
1452
1359
|
// FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
|
|
1453
1360
|
// probably wrong, and we should filter it instead
|
|
1454
1361
|
syncChangesetsToClear.length = 0;
|
|
1455
1362
|
// if reverse sync then we may have received provenance changes which should be marked as sync changes
|
|
1456
|
-
if (this.
|
|
1457
|
-
nodeAssert(this.sourceDb.changeset.index, "changeset didn't exist");
|
|
1363
|
+
if (this.isReverseSynchronization) {
|
|
1364
|
+
nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
|
|
1458
1365
|
for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
|
|
1459
1366
|
jsonProps.pendingReverseSyncChangesetIndices.push(i);
|
|
1460
1367
|
}
|
|
@@ -1467,17 +1374,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1467
1374
|
});
|
|
1468
1375
|
}
|
|
1469
1376
|
// FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
|
|
1470
|
-
finalizeTransformation() {
|
|
1377
|
+
async finalizeTransformation(options) {
|
|
1471
1378
|
this.importer.finalize();
|
|
1472
1379
|
this.updateSynchronizationVersion();
|
|
1473
1380
|
if (this._partiallyCommittedEntities.size > 0) {
|
|
1474
|
-
|
|
1475
|
-
core_bentley_1.Logger.logWarning(loggerCategory, [
|
|
1381
|
+
const message = [
|
|
1476
1382
|
"The following elements were never fully resolved:",
|
|
1477
1383
|
[...this._partiallyCommittedEntities.keys()].join(","),
|
|
1478
1384
|
"This indicates that either some references were excluded from the transformation",
|
|
1479
1385
|
"or the source has dangling references.",
|
|
1480
|
-
].join("\n")
|
|
1386
|
+
].join("\n");
|
|
1387
|
+
if (this._options.danglingReferencesBehavior === "reject")
|
|
1388
|
+
throw new Error(message);
|
|
1389
|
+
core_bentley_1.Logger.logWarning(loggerCategory, message);
|
|
1481
1390
|
for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
|
|
1482
1391
|
partiallyCommittedElem.forceComplete();
|
|
1483
1392
|
}
|
|
@@ -1489,11 +1398,36 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1489
1398
|
}
|
|
1490
1399
|
// this internal is guaranteed stable for just transformer usage
|
|
1491
1400
|
/* eslint-disable @itwin/no-internal */
|
|
1492
|
-
if ("codeValueBehavior" in this.sourceDb) {
|
|
1401
|
+
if (("codeValueBehavior" in this.sourceDb)) {
|
|
1493
1402
|
this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
|
|
1494
1403
|
this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
|
|
1495
1404
|
}
|
|
1496
1405
|
/* eslint-enable @itwin/no-internal */
|
|
1406
|
+
const defaultSaveTargetChanges = () => this.targetDb.saveChanges();
|
|
1407
|
+
await (options?.saveTargetChanges ?? defaultSaveTargetChanges)(this);
|
|
1408
|
+
if (this.isReverseSynchronization)
|
|
1409
|
+
this.sourceDb.saveChanges();
|
|
1410
|
+
const description = `${this._isProvenanceInitTransform
|
|
1411
|
+
? options?.provenanceInitTransformChangesetDescription ??
|
|
1412
|
+
`initialized branch provenance with master iModel: ${this.sourceDb.iModelId}`
|
|
1413
|
+
: this.isForwardSynchronization
|
|
1414
|
+
? options?.forwardSyncBranchChangesetDescription ??
|
|
1415
|
+
`Forward sync of iModel: ${this.sourceDb.iModelId}`
|
|
1416
|
+
: options?.reverseSyncMasterChangesetDescription ??
|
|
1417
|
+
`Reverse sync of iModel: ${this.sourceDb.iModelId}`}`;
|
|
1418
|
+
if (this.targetDb.isBriefcaseDb()) {
|
|
1419
|
+
// This relies on authorizationClient on iModelHost being defined, otherwise this will fail
|
|
1420
|
+
await this.targetDb.pushChanges({
|
|
1421
|
+
description,
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
if (this.isReverseSynchronization && this.sourceDb.isBriefcaseDb()) {
|
|
1425
|
+
// This relies on authorizationClient on iModelHost being defined, otherwise this will fail
|
|
1426
|
+
await this.sourceDb.pushChanges({
|
|
1427
|
+
description: options?.reverseSyncBranchChangesetDescription ??
|
|
1428
|
+
`Update provenance in response to a reverse sync to iModel: ${this.targetDb.iModelId}`,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1497
1431
|
}
|
|
1498
1432
|
/** Imports all relationships that subclass from the specified base class.
|
|
1499
1433
|
* @param baseRelClassFullName The specified base relationship class.
|
|
@@ -1506,7 +1440,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1506
1440
|
/** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported.
|
|
1507
1441
|
* @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer).
|
|
1508
1442
|
*/
|
|
1509
|
-
shouldExportRelationship(_sourceRelationship) {
|
|
1443
|
+
shouldExportRelationship(_sourceRelationship) {
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1510
1446
|
/** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel.
|
|
1511
1447
|
* This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
|
|
1512
1448
|
*/
|
|
@@ -1515,14 +1451,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1515
1451
|
const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
|
|
1516
1452
|
const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
|
|
1517
1453
|
const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
|
|
1518
|
-
if (!this._options.noProvenance &&
|
|
1454
|
+
if (!this._options.noProvenance &&
|
|
1455
|
+
core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
|
|
1519
1456
|
let provenance = !this._options.forceExternalSourceAspectProvenance
|
|
1520
1457
|
? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
|
|
1521
1458
|
: undefined;
|
|
1522
1459
|
if (!provenance) {
|
|
1523
1460
|
const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
|
|
1524
|
-
|
|
1525
|
-
if (
|
|
1461
|
+
const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
|
|
1462
|
+
// onExportRelationship doesn't need to call updateAspect if esaProps were found, because relationship provenance doesn't have the same concept of a version as element provenance (which uses last mod time on the elements).
|
|
1463
|
+
if (undefined === foundEsaProps) {
|
|
1526
1464
|
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1527
1465
|
}
|
|
1528
1466
|
provenance = aspectProps;
|
|
@@ -1541,10 +1479,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1541
1479
|
core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
|
|
1542
1480
|
return;
|
|
1543
1481
|
}
|
|
1544
|
-
const relArg = deletedRelData.relId ??
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1482
|
+
const relArg = deletedRelData.relId ??
|
|
1483
|
+
{
|
|
1484
|
+
sourceId: deletedRelData.sourceIdInTarget,
|
|
1485
|
+
targetId: deletedRelData.targetIdInTarget,
|
|
1486
|
+
};
|
|
1548
1487
|
// FIXME: make importer.deleteRelationship not need full props
|
|
1549
1488
|
const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
|
|
1550
1489
|
if (targetRelationship) {
|
|
@@ -1569,7 +1508,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1569
1508
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
1570
1509
|
*/
|
|
1571
1510
|
async detectRelationshipDeletes() {
|
|
1572
|
-
if (this.
|
|
1511
|
+
if (this.isReverseSynchronization) {
|
|
1573
1512
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
|
|
1574
1513
|
}
|
|
1575
1514
|
const aspectDeleteIds = [];
|
|
@@ -1584,13 +1523,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1584
1523
|
statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
1585
1524
|
while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
|
|
1586
1525
|
const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
|
|
1587
|
-
if (undefined ===
|
|
1526
|
+
if (undefined ===
|
|
1527
|
+
this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
|
|
1588
1528
|
// this function exists only to support some in-imodel transformations, which must
|
|
1589
1529
|
// use the old (external source aspect) provenance method anyway so we don't need to support
|
|
1590
1530
|
// new provenance
|
|
1591
1531
|
const json = JSON.parse(statement.getValue(2).getString());
|
|
1592
|
-
|
|
1593
|
-
|
|
1532
|
+
const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
|
|
1533
|
+
if (targetRelInstanceId) {
|
|
1534
|
+
const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
|
|
1594
1535
|
this.importer.deleteRelationship(targetRelationship.toJSON());
|
|
1595
1536
|
}
|
|
1596
1537
|
aspectDeleteIds.push(statement.getValue(0).getId());
|
|
@@ -1611,8 +1552,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1611
1552
|
targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
|
|
1612
1553
|
// TODO: move to cloneRelationship in IModelCloneContext
|
|
1613
1554
|
sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
|
|
1614
|
-
if (
|
|
1615
|
-
|
|
1555
|
+
if (core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType &&
|
|
1556
|
+
"Id" === propertyMetaData.extendedType) {
|
|
1557
|
+
targetRelationshipProps[propertyName] =
|
|
1558
|
+
this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
|
|
1616
1559
|
}
|
|
1617
1560
|
});
|
|
1618
1561
|
return targetRelationshipProps;
|
|
@@ -1644,8 +1587,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1644
1587
|
sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
|
|
1645
1588
|
// const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
|
|
1646
1589
|
const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
|
|
1647
|
-
const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
|
|
1648
|
-
|
|
1590
|
+
const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
|
|
1591
|
+
a.scope?.id === this.targetScopeElementId;
|
|
1592
|
+
return (!this._options.includeSourceProvenance ||
|
|
1593
|
+
!isExternalSourceAspectFromTransformer);
|
|
1649
1594
|
});
|
|
1650
1595
|
for (let i = 0; i < targetIds.length; ++i) {
|
|
1651
1596
|
this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
|
|
@@ -1684,9 +1629,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1684
1629
|
let schemaFileName = schema.name + ext;
|
|
1685
1630
|
// many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
|
|
1686
1631
|
const systemMaxPathSegmentSize = 255;
|
|
1687
|
-
|
|
1688
|
-
const windowsMaxPathLimit = 260;
|
|
1689
|
-
if (schemaFileName.length > systemMaxPathSegmentSize || path.join(this._schemaExportDir, schemaFileName).length >= windowsMaxPathLimit) {
|
|
1632
|
+
if (schemaFileName.length > systemMaxPathSegmentSize) {
|
|
1690
1633
|
// this name should be well under 255 bytes
|
|
1691
1634
|
// ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
|
|
1692
1635
|
// You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
|
|
@@ -1726,7 +1669,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1726
1669
|
const maybeLongNameResolvingSchemaCtx = this._longNamedSchemasMap.size > 0
|
|
1727
1670
|
? this._makeLongNameResolvingSchemaCtx()
|
|
1728
1671
|
: undefined;
|
|
1729
|
-
return await this.targetDb.importSchemas(schemaFullPaths, {
|
|
1672
|
+
return await this.targetDb.importSchemas(schemaFullPaths, {
|
|
1673
|
+
ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx,
|
|
1674
|
+
});
|
|
1730
1675
|
}
|
|
1731
1676
|
finally {
|
|
1732
1677
|
core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
|
|
@@ -1734,8 +1679,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1734
1679
|
}
|
|
1735
1680
|
}
|
|
1736
1681
|
/** Cause all fonts to be exported from the source iModel and imported into the target iModel.
|
|
1737
|
-
|
|
1738
|
-
|
|
1682
|
+
* @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
|
|
1683
|
+
*/
|
|
1739
1684
|
async processFonts() {
|
|
1740
1685
|
// we do not need to initialize for this since no entities are exported
|
|
1741
1686
|
await this.initialize();
|
|
@@ -1762,7 +1707,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1762
1707
|
/** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel.
|
|
1763
1708
|
* @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer).
|
|
1764
1709
|
*/
|
|
1765
|
-
shouldExportCodeSpec(_sourceCodeSpec) {
|
|
1710
|
+
shouldExportCodeSpec(_sourceCodeSpec) {
|
|
1711
|
+
return true;
|
|
1712
|
+
}
|
|
1766
1713
|
/** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */
|
|
1767
1714
|
onExportCodeSpec(sourceCodeSpec) {
|
|
1768
1715
|
this.context.importCodeSpec(sourceCodeSpec.id);
|
|
@@ -1786,33 +1733,236 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1786
1733
|
async initialize(args) {
|
|
1787
1734
|
if (this._initialized)
|
|
1788
1735
|
return;
|
|
1789
|
-
await this.context.initialize();
|
|
1790
1736
|
await this._tryInitChangesetData(args);
|
|
1737
|
+
await this.context.initialize();
|
|
1738
|
+
// need exporter initialized to do remapdeletedsourceentities.
|
|
1791
1739
|
await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
|
|
1792
|
-
// Exporter must be initialized prior to
|
|
1793
|
-
|
|
1794
|
-
await this.initFromExternalSourceAspects(args);
|
|
1740
|
+
// Exporter must be initialized prior to processing changesets in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
|
|
1741
|
+
await this.processChangesets();
|
|
1795
1742
|
this._initialized = true;
|
|
1796
1743
|
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Reads all the changeset files in the private member of the transformer: _csFileProps and does two things with these changesets.
|
|
1746
|
+
* Finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
|
|
1747
|
+
* Populates this._hasElementChangedCache with a set of elementIds that have been updated or inserted into the database.
|
|
1748
|
+
* This function returns early if csFileProps is undefined or is of length 0.
|
|
1749
|
+
* @returns void
|
|
1750
|
+
*/
|
|
1751
|
+
async processChangesets() {
|
|
1752
|
+
this.forEachTrackedElement((sourceElementId, targetElementId) => {
|
|
1753
|
+
this.context.remapElement(sourceElementId, targetElementId);
|
|
1754
|
+
});
|
|
1755
|
+
if (this._csFileProps === undefined || this._csFileProps.length === 0)
|
|
1756
|
+
return;
|
|
1757
|
+
const hasElementChangedCache = new Set();
|
|
1758
|
+
const relationshipECClassIdsToSkip = new Set();
|
|
1759
|
+
for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
|
|
1760
|
+
relationshipECClassIdsToSkip.add(row.ECInstanceId);
|
|
1761
|
+
}
|
|
1762
|
+
const relationshipECClassIds = new Set();
|
|
1763
|
+
for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)")) {
|
|
1764
|
+
relationshipECClassIds.add(row.ECInstanceId);
|
|
1765
|
+
}
|
|
1766
|
+
// For later use when processing deletes.
|
|
1767
|
+
const alreadyImportedElementInserts = new Set();
|
|
1768
|
+
const alreadyImportedModelInserts = new Set();
|
|
1769
|
+
this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
|
|
1770
|
+
const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
|
|
1771
|
+
if (core_bentley_1.Id64.isValid(targetElementId))
|
|
1772
|
+
alreadyImportedElementInserts.add(targetElementId);
|
|
1773
|
+
});
|
|
1774
|
+
this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
|
|
1775
|
+
const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
|
|
1776
|
+
if (core_bentley_1.Id64.isValid(targetModelId))
|
|
1777
|
+
alreadyImportedModelInserts.add(targetModelId);
|
|
1778
|
+
});
|
|
1779
|
+
this._deletedSourceRelationshipData = new Map();
|
|
1780
|
+
for (const csFile of this._csFileProps) {
|
|
1781
|
+
const csReader = core_backend_1.SqliteChangesetReader.openFile({
|
|
1782
|
+
fileName: csFile.pathname,
|
|
1783
|
+
db: this.sourceDb,
|
|
1784
|
+
disableSchemaCheck: true,
|
|
1785
|
+
});
|
|
1786
|
+
const csAdaptor = new core_backend_1.ChangesetECAdaptor(csReader);
|
|
1787
|
+
const ecChangeUnifier = new core_backend_1.PartialECChangeUnifier();
|
|
1788
|
+
while (csAdaptor.step()) {
|
|
1789
|
+
ecChangeUnifier.appendFrom(csAdaptor);
|
|
1790
|
+
}
|
|
1791
|
+
const changes = [...ecChangeUnifier.instances];
|
|
1792
|
+
/** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
|
|
1793
|
+
const elemIdToScopeEsa = new Map();
|
|
1794
|
+
for (const change of changes) {
|
|
1795
|
+
if (change.ECClassId !== undefined &&
|
|
1796
|
+
relationshipECClassIdsToSkip.has(change.ECClassId))
|
|
1797
|
+
continue;
|
|
1798
|
+
const changeType = change.$meta?.op;
|
|
1799
|
+
if (changeType === "Deleted" &&
|
|
1800
|
+
change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
|
|
1801
|
+
change.Scope.Id === this.targetScopeElementId) {
|
|
1802
|
+
elemIdToScopeEsa.set(change.Element.Id, change);
|
|
1803
|
+
}
|
|
1804
|
+
else if (changeType === "Inserted" || changeType === "Updated")
|
|
1805
|
+
hasElementChangedCache.add(change.ECInstanceId);
|
|
1806
|
+
}
|
|
1807
|
+
// Loop to process deletes.
|
|
1808
|
+
for (const change of changes) {
|
|
1809
|
+
const changeType = change.$meta?.op;
|
|
1810
|
+
const ecClassId = change.ECClassId ?? change.$meta?.fallbackClassId;
|
|
1811
|
+
if (ecClassId === undefined)
|
|
1812
|
+
throw new Error(`ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change?.$meta?.tables}`);
|
|
1813
|
+
if (changeType === undefined)
|
|
1814
|
+
throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
|
|
1815
|
+
if (changeType !== "Deleted" ||
|
|
1816
|
+
relationshipECClassIdsToSkip.has(ecClassId))
|
|
1817
|
+
continue;
|
|
1818
|
+
this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
|
|
1819
|
+
}
|
|
1820
|
+
csReader.close();
|
|
1821
|
+
}
|
|
1822
|
+
this._hasElementChangedCache = hasElementChangedCache;
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
|
|
1827
|
+
* @param change the change to process, must be of changeType "Deleted"
|
|
1828
|
+
* @param mapOfDeletedElemIdToScopeEsas a map of elementIds to changedECInstances (which are ESAs). the elementId is not the id of the esa itself, but the elementid that the esa was stored on before the esa's deletion.
|
|
1829
|
+
* All ESAs in this map are part of the transformer's scope / ESA data and are tracked in case the ESA is deleted in the target.
|
|
1830
|
+
* @param isRelationship is relationship or not
|
|
1831
|
+
* @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
|
|
1832
|
+
* @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
|
|
1833
|
+
* @returns void
|
|
1834
|
+
*/
|
|
1835
|
+
processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
|
|
1836
|
+
// we need a connected iModel with changes to remap elements with deletions
|
|
1837
|
+
const notConnectedModel = this.sourceDb.iTwinId === undefined;
|
|
1838
|
+
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
1839
|
+
if (notConnectedModel || noChanges)
|
|
1840
|
+
return;
|
|
1841
|
+
// optimization: if we have provenance, use it to avoid more querying later
|
|
1842
|
+
// eventually when itwin.js supports attaching a second iModelDb in JS,
|
|
1843
|
+
// this won't have to be a conditional part of the query, and we can always have it by attaching
|
|
1844
|
+
const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
|
|
1845
|
+
const instId = change.ECInstanceId;
|
|
1846
|
+
if (!isRelationship) {
|
|
1847
|
+
const sourceElemFedGuid = change.FederationGuid;
|
|
1848
|
+
let identifierValue;
|
|
1849
|
+
if (queryCanAccessProvenance) {
|
|
1850
|
+
const aspects = this.sourceDb.elements.getAspects(instId, core_backend_1.ExternalSourceAspect.classFullName);
|
|
1851
|
+
for (const aspect of aspects) {
|
|
1852
|
+
// look for aspect where the ecInstanceId = the aspect.element.id
|
|
1853
|
+
if (aspect.element.id === instId &&
|
|
1854
|
+
aspect.scope.id === this.targetScopeElementId)
|
|
1855
|
+
identifierValue = aspect.identifier;
|
|
1856
|
+
}
|
|
1857
|
+
// Think I need to query the esas given the instId.. not sure what db to do it on though.. soruce or target.. or provenance?
|
|
1858
|
+
// I need to know the id of the element dpeneding on which db its stored in.
|
|
1859
|
+
}
|
|
1860
|
+
if (queryCanAccessProvenance && !identifierValue) {
|
|
1861
|
+
if (mapOfDeletedElemIdToScopeEsas.get(instId) !== undefined)
|
|
1862
|
+
identifierValue =
|
|
1863
|
+
mapOfDeletedElemIdToScopeEsas.get(instId).Identifier;
|
|
1864
|
+
}
|
|
1865
|
+
const targetId = (queryCanAccessProvenance && identifierValue) ||
|
|
1866
|
+
// maybe batching these queries would perform better but we should
|
|
1867
|
+
// try to attach the second db and query both together anyway
|
|
1868
|
+
(sourceElemFedGuid &&
|
|
1869
|
+
this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid)) ||
|
|
1870
|
+
// FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
1871
|
+
this._queryProvenanceForElement(instId);
|
|
1872
|
+
// since we are processing one changeset at a time, we can see local source deletes
|
|
1873
|
+
// of entities that were never synced and can be safely ignored
|
|
1874
|
+
const deletionNotInTarget = !targetId;
|
|
1875
|
+
if (deletionNotInTarget)
|
|
1876
|
+
return;
|
|
1877
|
+
this.context.remapElement(instId, targetId);
|
|
1878
|
+
// If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
|
|
1879
|
+
// In such case an entity update will be triggered and we no longer need to delete the entity.
|
|
1880
|
+
if (alreadyImportedElementInserts.has(targetId)) {
|
|
1881
|
+
this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
|
|
1882
|
+
}
|
|
1883
|
+
if (alreadyImportedModelInserts.has(targetId)) {
|
|
1884
|
+
this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
else {
|
|
1888
|
+
// is deleted relationship
|
|
1889
|
+
const classFullName = change.$meta?.classFullName;
|
|
1890
|
+
const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
|
|
1891
|
+
const targetIdOfRelationshipInSource = change.TargetECInstanceId;
|
|
1892
|
+
const [sourceIdInTarget, targetIdInTarget] = [
|
|
1893
|
+
sourceIdOfRelationshipInSource,
|
|
1894
|
+
targetIdOfRelationshipInSource,
|
|
1895
|
+
].map((id) => {
|
|
1896
|
+
let element;
|
|
1897
|
+
try {
|
|
1898
|
+
element = this.sourceDb.elements.getElement(id);
|
|
1899
|
+
}
|
|
1900
|
+
catch (err) {
|
|
1901
|
+
return undefined;
|
|
1902
|
+
}
|
|
1903
|
+
const fedGuid = element.federationGuid;
|
|
1904
|
+
let identifierValue;
|
|
1905
|
+
if (queryCanAccessProvenance) {
|
|
1906
|
+
const aspects = this.sourceDb.elements.getAspects(id, core_backend_1.ExternalSourceAspect.classFullName);
|
|
1907
|
+
for (const aspect of aspects) {
|
|
1908
|
+
if (aspect.element.id === id &&
|
|
1909
|
+
aspect.scope.id === this.targetScopeElementId)
|
|
1910
|
+
identifierValue = aspect.identifier;
|
|
1911
|
+
}
|
|
1912
|
+
if (identifierValue === undefined) {
|
|
1913
|
+
if (mapOfDeletedElemIdToScopeEsas.get(id) !== undefined)
|
|
1914
|
+
identifierValue =
|
|
1915
|
+
mapOfDeletedElemIdToScopeEsas.get(id).Identifier;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return ((queryCanAccessProvenance && identifierValue) ||
|
|
1919
|
+
// maybe batching these queries would perform better but we should
|
|
1920
|
+
// try to attach the second db and query both together anyway
|
|
1921
|
+
(fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
|
|
1922
|
+
});
|
|
1923
|
+
if (sourceIdInTarget && targetIdInTarget) {
|
|
1924
|
+
this._deletedSourceRelationshipData.set(instId, {
|
|
1925
|
+
classFullName: classFullName ?? "",
|
|
1926
|
+
sourceIdInTarget,
|
|
1927
|
+
targetIdInTarget,
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
else {
|
|
1931
|
+
// FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
1932
|
+
const relProvenance = this._queryProvenanceForRelationship(instId, {
|
|
1933
|
+
classFullName: classFullName ?? "",
|
|
1934
|
+
sourceId: sourceIdOfRelationshipInSource,
|
|
1935
|
+
targetId: targetIdOfRelationshipInSource,
|
|
1936
|
+
});
|
|
1937
|
+
if (relProvenance && relProvenance.relationshipId)
|
|
1938
|
+
this._deletedSourceRelationshipData.set(instId, {
|
|
1939
|
+
classFullName: classFullName ?? "",
|
|
1940
|
+
relId: relProvenance.relationshipId,
|
|
1941
|
+
provenanceAspectId: relProvenance.aspectId,
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1797
1946
|
async _tryInitChangesetData(args) {
|
|
1798
|
-
if (!args ||
|
|
1947
|
+
if (!args ||
|
|
1948
|
+
this.sourceDb.iTwinId === undefined ||
|
|
1949
|
+
this.sourceDb.changeset.index === undefined) {
|
|
1799
1950
|
this._sourceChangeDataState = "unconnected";
|
|
1800
1951
|
return;
|
|
1801
1952
|
}
|
|
1802
1953
|
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
1803
1954
|
if (noChanges) {
|
|
1804
1955
|
this._sourceChangeDataState = "no-changes";
|
|
1805
|
-
this.
|
|
1956
|
+
this._csFileProps = [];
|
|
1806
1957
|
return;
|
|
1807
1958
|
}
|
|
1808
1959
|
// NOTE: that we do NOT download the changesummary for the last transformed version, we want
|
|
1809
1960
|
// to ignore those already processed changes
|
|
1810
|
-
const startChangesetIndexOrId = args.startChangeset?.index
|
|
1811
|
-
|
|
1812
|
-
|
|
1961
|
+
const startChangesetIndexOrId = args.startChangeset?.index ??
|
|
1962
|
+
args.startChangeset?.id ??
|
|
1963
|
+
this._synchronizationVersion.index + 1;
|
|
1813
1964
|
const endChangesetId = this.sourceDb.changeset.id;
|
|
1814
|
-
const [startChangesetIndex, endChangesetIndex] = await Promise.all(
|
|
1815
|
-
.map(async (indexOrId) => typeof indexOrId === "number"
|
|
1965
|
+
const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
|
|
1816
1966
|
? indexOrId
|
|
1817
1967
|
: core_backend_1.IModelHost.hubAccess
|
|
1818
1968
|
.queryChangeset({
|
|
@@ -1823,70 +1973,82 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1823
1973
|
})
|
|
1824
1974
|
.then((changeset) => changeset.index)));
|
|
1825
1975
|
const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
|
|
1826
|
-
if (!this._options.ignoreMissingChangesetsInSynchronizations
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1976
|
+
if (!this._options.ignoreMissingChangesetsInSynchronizations &&
|
|
1977
|
+
startChangesetIndex !== this._synchronizationVersion.index + 1 &&
|
|
1978
|
+
this._synchronizationVersion.index !== -1) {
|
|
1979
|
+
throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
|
|
1980
|
+
" startChangesetId should be" +
|
|
1981
|
+
" exactly the first changeset *after* the previous synchronization to not miss data." +
|
|
1982
|
+
` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}` +
|
|
1983
|
+
` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'` +
|
|
1984
|
+
` which is changeset #${this._synchronizationVersion.index}. The transformer expected` +
|
|
1985
|
+
` #${this._synchronizationVersion.index + 1}.`);
|
|
1836
1986
|
}
|
|
1837
1987
|
nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
|
|
1838
|
-
const changesetsToSkip = this.
|
|
1839
|
-
? this._targetScopeProvenanceProps.jsonProperties
|
|
1840
|
-
|
|
1988
|
+
const changesetsToSkip = this.isReverseSynchronization
|
|
1989
|
+
? this._targetScopeProvenanceProps.jsonProperties
|
|
1990
|
+
.pendingReverseSyncChangesetIndices
|
|
1991
|
+
: this._targetScopeProvenanceProps.jsonProperties
|
|
1992
|
+
.pendingSyncChangesetIndices;
|
|
1841
1993
|
core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
|
|
1842
1994
|
this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
|
|
1843
1995
|
core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
|
|
1996
|
+
const csFileProps = [];
|
|
1844
1997
|
for (const [first, end] of this._changesetRanges) {
|
|
1845
|
-
|
|
1846
|
-
|
|
1998
|
+
// TODO: should the first changeset in a reverse sync really be included even though its 'initialized branch provenance'? The answer is no, its a bug that needs to be fixed.
|
|
1999
|
+
const fileProps = await core_backend_1.IModelHost.hubAccess.downloadChangesets({
|
|
1847
2000
|
iModelId: this.sourceDb.iModelId,
|
|
1848
|
-
|
|
2001
|
+
targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
|
|
1849
2002
|
range: { first, end },
|
|
1850
2003
|
});
|
|
2004
|
+
csFileProps.push(...fileProps);
|
|
1851
2005
|
}
|
|
1852
|
-
|
|
2006
|
+
this._csFileProps = csFileProps;
|
|
1853
2007
|
this._sourceChangeDataState = "has-changes";
|
|
1854
2008
|
}
|
|
1855
2009
|
/** Export everything from the source iModel and import the transformed entities into the target iModel.
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
async processAll() {
|
|
2010
|
+
* @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
|
|
2011
|
+
*/
|
|
2012
|
+
async processAll(options) {
|
|
1859
2013
|
this.logSettings();
|
|
1860
2014
|
this.initScopeProvenance();
|
|
1861
2015
|
await this.initialize();
|
|
1862
2016
|
await this.exporter.exportCodeSpecs();
|
|
1863
2017
|
await this.exporter.exportFonts();
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2018
|
+
if (this._options.skipPropagateChangesToRootElements) {
|
|
2019
|
+
// FIXME<NICK>: This option in exportAll was a maybe.
|
|
2020
|
+
// The RepositoryModel and root Subject of the target iModel should not be transformed.
|
|
2021
|
+
await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
|
|
2022
|
+
await this.exporter.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true); // after the Subject hierarchy, process the other elements of the RepositoryModel
|
|
2023
|
+
await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
|
|
2024
|
+
}
|
|
2025
|
+
else {
|
|
2026
|
+
await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
|
|
2027
|
+
}
|
|
1868
2028
|
await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
|
|
1869
2029
|
await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
|
|
1870
2030
|
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1871
|
-
|
|
1872
|
-
|
|
2031
|
+
if (this._options.forceExternalSourceAspectProvenance &&
|
|
2032
|
+
this.shouldDetectDeletes()) {
|
|
1873
2033
|
await this.detectElementDeletes();
|
|
1874
2034
|
await this.detectRelationshipDeletes();
|
|
1875
2035
|
}
|
|
1876
2036
|
if (this._options.optimizeGeometry)
|
|
1877
2037
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
1878
2038
|
this.importer.computeProjectExtents();
|
|
1879
|
-
this.finalizeTransformation();
|
|
2039
|
+
await this.finalizeTransformation(options);
|
|
1880
2040
|
}
|
|
1881
2041
|
markLastProvenance(sourceAspect, { isRelationship = false }) {
|
|
1882
|
-
this._lastProvenanceEntityInfo
|
|
1883
|
-
|
|
2042
|
+
this._lastProvenanceEntityInfo =
|
|
2043
|
+
typeof sourceAspect === "string"
|
|
1884
2044
|
? sourceAspect
|
|
1885
2045
|
: {
|
|
1886
2046
|
entityId: sourceAspect.element.id,
|
|
1887
2047
|
aspectId: sourceAspect.id,
|
|
1888
2048
|
aspectVersion: sourceAspect.version ?? "",
|
|
1889
|
-
aspectKind: isRelationship
|
|
2049
|
+
aspectKind: isRelationship
|
|
2050
|
+
? core_backend_1.ExternalSourceAspect.Kind.Relationship
|
|
2051
|
+
: core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
1890
2052
|
};
|
|
1891
2053
|
}
|
|
1892
2054
|
/**
|
|
@@ -1968,7 +2130,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1968
2130
|
this.context.loadStateFromDb(db);
|
|
1969
2131
|
this.importer.loadStateFromJson(state.importerState);
|
|
1970
2132
|
this.exporter.loadStateFromJson(state.exporterState);
|
|
1971
|
-
this._elementsWithExplicitlyTrackedProvenance =
|
|
2133
|
+
this._elementsWithExplicitlyTrackedProvenance =
|
|
2134
|
+
core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
|
|
1972
2135
|
this.loadAdditionalStateJson(state.additionalState);
|
|
1973
2136
|
}
|
|
1974
2137
|
/**
|
|
@@ -2025,9 +2188,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
2025
2188
|
additionalState: this.getAdditionalStateJson(),
|
|
2026
2189
|
};
|
|
2027
2190
|
this.context.saveStateToDb(db);
|
|
2028
|
-
if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
|
|
2191
|
+
if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
|
|
2192
|
+
db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
|
|
2029
2193
|
throw Error("Failed to create the js state table in the state database");
|
|
2030
|
-
if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
|
|
2194
|
+
if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
|
|
2195
|
+
db.executeSQL(`
|
|
2031
2196
|
CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
|
|
2032
2197
|
-- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
|
|
2033
2198
|
entityId TEXT,
|
|
@@ -2045,8 +2210,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
2045
2210
|
throw Error("Failed to insert options into the state database");
|
|
2046
2211
|
});
|
|
2047
2212
|
db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
|
|
2048
|
-
const lastProvenanceEntityInfo = this
|
|
2049
|
-
|
|
2213
|
+
const lastProvenanceEntityInfo = this
|
|
2214
|
+
._lastProvenanceEntityInfo;
|
|
2215
|
+
stmt.bindString(1, lastProvenanceEntityInfo?.entityId ??
|
|
2216
|
+
this._lastProvenanceEntityInfo);
|
|
2050
2217
|
stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
|
|
2051
2218
|
stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
|
|
2052
2219
|
stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
|
|
@@ -2080,30 +2247,25 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
2080
2247
|
db.closeDb();
|
|
2081
2248
|
}
|
|
2082
2249
|
}
|
|
2083
|
-
|
|
2250
|
+
/** Export changes from the source iModel and import the transformed entities into the target iModel.
|
|
2251
|
+
* Inserts, updates, and deletes are determined by inspecting the changeset(s).
|
|
2252
|
+
* @note the transformer saves and pushes changes when its work is complete.
|
|
2253
|
+
* @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
|
|
2254
|
+
* will automatically be determined and used
|
|
2255
|
+
* @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired range and open the source iModel as of the end (inclusive) of the desired range.
|
|
2256
|
+
*/
|
|
2257
|
+
async processChanges(options) {
|
|
2084
2258
|
this._isSynchronization = true;
|
|
2085
2259
|
this.initScopeProvenance();
|
|
2086
|
-
const args = typeof optionsOrAccessToken === "string"
|
|
2087
|
-
? {
|
|
2088
|
-
accessToken: optionsOrAccessToken,
|
|
2089
|
-
startChangeset: startChangesetId
|
|
2090
|
-
? { id: startChangesetId }
|
|
2091
|
-
: { index: this._synchronizationVersion.index + 1 },
|
|
2092
|
-
}
|
|
2093
|
-
: optionsOrAccessToken;
|
|
2094
2260
|
this.logSettings();
|
|
2095
|
-
await this.initialize(
|
|
2261
|
+
await this.initialize(options);
|
|
2096
2262
|
// must wait for initialization of synchronization provenance data
|
|
2097
|
-
await this.exporter.exportChanges(this.getExportInitOpts(
|
|
2263
|
+
await this.exporter.exportChanges(this.getExportInitOpts(options));
|
|
2098
2264
|
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
2099
2265
|
if (this._options.optimizeGeometry)
|
|
2100
2266
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
2101
2267
|
this.importer.computeProjectExtents();
|
|
2102
|
-
this.finalizeTransformation();
|
|
2103
|
-
const defaultSaveTargetChanges = () => {
|
|
2104
|
-
this.targetDb.saveChanges();
|
|
2105
|
-
};
|
|
2106
|
-
await (args.saveTargetChanges ?? defaultSaveTargetChanges)(this);
|
|
2268
|
+
await this.finalizeTransformation(options);
|
|
2107
2269
|
}
|
|
2108
2270
|
/** Changeset data must be initialized in order to build correct changeOptions.
|
|
2109
2271
|
* Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
|
|
@@ -2112,12 +2274,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
2112
2274
|
if (!this._isSynchronization)
|
|
2113
2275
|
return {};
|
|
2114
2276
|
return {
|
|
2277
|
+
skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
|
|
2115
2278
|
accessToken: opts.accessToken,
|
|
2116
|
-
...this.
|
|
2117
|
-
? {
|
|
2118
|
-
:
|
|
2119
|
-
? {
|
|
2120
|
-
:
|
|
2279
|
+
...(this._csFileProps
|
|
2280
|
+
? { csFileProps: this._csFileProps }
|
|
2281
|
+
: this._changesetRanges
|
|
2282
|
+
? { changesetRanges: this._changesetRanges }
|
|
2283
|
+
: opts.startChangeset
|
|
2284
|
+
? { startChangeset: opts.startChangeset }
|
|
2285
|
+
: {
|
|
2286
|
+
startChangeset: {
|
|
2287
|
+
index: this._synchronizationVersion.index + 1,
|
|
2288
|
+
},
|
|
2289
|
+
}),
|
|
2121
2290
|
};
|
|
2122
2291
|
}
|
|
2123
2292
|
/** Combine an array of source elements into a single target element.
|
|
@@ -2133,6 +2302,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
2133
2302
|
}
|
|
2134
2303
|
}
|
|
2135
2304
|
exports.IModelTransformer = IModelTransformer;
|
|
2305
|
+
IModelTransformer.noEsaSyncDirectionErrorMessage = "Couldn't find an external source aspect to determine sync direction. This often means that the master->branch relationship has not been established. Consider running the transformer with wasSourceIModelCopiedToTarget set to true.";
|
|
2136
2306
|
/** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
|
|
2137
2307
|
IModelTransformer.jsStateTable = "TransformerJsState";
|
|
2138
2308
|
/** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
|
|
@@ -2207,7 +2377,8 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
2207
2377
|
}
|
|
2208
2378
|
else {
|
|
2209
2379
|
const definitionElement = this.sourceDb.elements.tryGetElement(referenceId, core_backend_1.DefinitionElement);
|
|
2210
|
-
if (definitionElement &&
|
|
2380
|
+
if (definitionElement &&
|
|
2381
|
+
!(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
|
|
2211
2382
|
this.context.remapElement(referenceId, referenceId); // when in the same iModel, can use existing DefinitionElements without remapping
|
|
2212
2383
|
}
|
|
2213
2384
|
else {
|
|
@@ -2222,7 +2393,7 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
2222
2393
|
if (sourceElement instanceof core_backend_1.GeometricElement) {
|
|
2223
2394
|
const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
|
|
2224
2395
|
const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
|
|
2225
|
-
const placement =
|
|
2396
|
+
const placement = placementClass.fromJSON(targetElementProps.placement);
|
|
2226
2397
|
if (placement.isValid) {
|
|
2227
2398
|
nodeAssert(this._transform3d);
|
|
2228
2399
|
placement.multiplyTransform(this._transform3d);
|