@itwin/imodel-transformer 0.4.4-dev.0 → 0.4.18-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/CHANGELOG.md +1 -9
- package/lib/cjs/Algo.d.ts +7 -0
- package/lib/cjs/Algo.d.ts.map +1 -0
- package/lib/cjs/Algo.js +65 -0
- package/lib/cjs/Algo.js.map +1 -0
- package/lib/cjs/BigMap.d.ts +1 -6
- package/lib/cjs/BigMap.d.ts.map +1 -1
- package/lib/cjs/BigMap.js +3 -29
- package/lib/cjs/BigMap.js.map +1 -1
- package/lib/cjs/BranchProvenanceInitializer.d.ts +10 -3
- package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
- package/lib/cjs/BranchProvenanceInitializer.js +101 -10
- 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 -14
- package/lib/cjs/IModelCloneContext.js.map +1 -1
- package/lib/cjs/IModelExporter.d.ts +87 -21
- package/lib/cjs/IModelExporter.d.ts.map +1 -1
- package/lib/cjs/IModelExporter.js +279 -114
- package/lib/cjs/IModelExporter.js.map +1 -1
- package/lib/cjs/IModelImporter.d.ts +29 -21
- package/lib/cjs/IModelImporter.d.ts.map +1 -1
- package/lib/cjs/IModelImporter.js +115 -62
- package/lib/cjs/IModelImporter.js.map +1 -1
- package/lib/cjs/IModelTransformer.d.ts +285 -48
- package/lib/cjs/IModelTransformer.d.ts.map +1 -1
- package/lib/cjs/IModelTransformer.js +1273 -337
- 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
|
*/
|
|
@@ -22,6 +22,7 @@ const PendingReferenceMap_1 = require("./PendingReferenceMap");
|
|
|
22
22
|
const EntityMap_1 = require("./EntityMap");
|
|
23
23
|
const IModelCloneContext_1 = require("./IModelCloneContext");
|
|
24
24
|
const EntityUnifier_1 = require("./EntityUnifier");
|
|
25
|
+
const Algo_1 = require("./Algo");
|
|
25
26
|
const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
|
|
26
27
|
const nullLastProvenanceEntityInfo = {
|
|
27
28
|
entityId: core_bentley_1.Id64.invalid,
|
|
@@ -94,9 +95,115 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
94
95
|
get targetScopeElementId() {
|
|
95
96
|
return this._options.targetScopeElementId;
|
|
96
97
|
}
|
|
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
|
+
}
|
|
188
|
+
}
|
|
189
|
+
get isReverseSynchronization() {
|
|
190
|
+
if (this._syncType === undefined)
|
|
191
|
+
this._syncType = this.determineSyncType();
|
|
192
|
+
return this._syncType === "reverse";
|
|
193
|
+
}
|
|
194
|
+
get isForwardSynchronization() {
|
|
195
|
+
if (this._syncType === undefined)
|
|
196
|
+
this._syncType = this.determineSyncType();
|
|
197
|
+
return this._syncType === "forward";
|
|
198
|
+
}
|
|
97
199
|
/** The element classes that are considered to define provenance in the iModel */
|
|
98
200
|
static get provenanceElementClasses() {
|
|
99
|
-
return [
|
|
201
|
+
return [
|
|
202
|
+
core_backend_1.FolderLink,
|
|
203
|
+
core_backend_1.SynchronizationConfigLink,
|
|
204
|
+
core_backend_1.ExternalSource,
|
|
205
|
+
core_backend_1.ExternalSourceAttachment,
|
|
206
|
+
];
|
|
100
207
|
}
|
|
101
208
|
/** The element aspect classes that are considered to define provenance in the iModel */
|
|
102
209
|
static get provenanceElementAspectClasses() {
|
|
@@ -112,18 +219,52 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
112
219
|
/** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
|
|
113
220
|
* and have some helper methods below for now */
|
|
114
221
|
this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
|
|
222
|
+
/** a set of elements for which source provenance will be explicitly tracked by ExternalSourceAspects */
|
|
223
|
+
this._elementsWithExplicitlyTrackedProvenance = new Set();
|
|
115
224
|
/** map of partially committed entities to their partial commit progress */
|
|
116
225
|
this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
|
|
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;
|
|
234
|
+
this._changesetRanges = undefined;
|
|
117
235
|
/** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
|
|
118
236
|
* @note Currently only tracks elements which were not exported.
|
|
119
237
|
*/
|
|
120
238
|
this._skippedEntities = new Set();
|
|
239
|
+
/**
|
|
240
|
+
* Previously the transformer would insert provenance always pointing to the "target" relationship.
|
|
241
|
+
* It should (and now by default does) instead insert provenance pointing to the provenanceSource
|
|
242
|
+
* SEE: https://github.com/iTwin/imodel-transformer/issues/54
|
|
243
|
+
* This exists only to facilitate testing that the transformer can handle the older, flawed method
|
|
244
|
+
*/
|
|
245
|
+
this._forceOldRelationshipProvenanceMethod = false;
|
|
246
|
+
/** NOTE: the json properties must be converted to string before insertion */
|
|
247
|
+
this._targetScopeProvenanceProps = undefined;
|
|
248
|
+
/**
|
|
249
|
+
* Index of the changeset that the transformer was at when the transformation begins (was constructed).
|
|
250
|
+
* Used to determine at the end which changesets were part of a synchronization.
|
|
251
|
+
*/
|
|
252
|
+
this._startingChangesetIndices = undefined;
|
|
253
|
+
this._cachedSynchronizationVersion = undefined;
|
|
254
|
+
this._targetClassNameToClassIdCache = new Map();
|
|
255
|
+
// if undefined, it can be initialized by calling [[this.processChangesets]]
|
|
256
|
+
this._hasElementChangedCache = undefined;
|
|
257
|
+
this._deletedSourceRelationshipData = undefined;
|
|
121
258
|
this._yieldManager = new core_bentley_1.YieldManager();
|
|
122
259
|
/** The directory where schemas will be exported, a random temporary directory */
|
|
123
260
|
this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
|
|
124
261
|
this._longNamedSchemasMap = new Map();
|
|
125
262
|
/** state to prevent reinitialization, @see [[initialize]] */
|
|
126
263
|
this._initialized = false;
|
|
264
|
+
this._sourceChangeDataState = "uninited";
|
|
265
|
+
/** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
|
|
266
|
+
this._csFileProps = undefined;
|
|
267
|
+
/** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
|
|
127
268
|
this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
|
|
128
269
|
// initialize IModelTransformOptions
|
|
129
270
|
this._options = {
|
|
@@ -132,9 +273,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
132
273
|
cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
|
|
133
274
|
targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
|
|
134
275
|
// eslint-disable-next-line deprecation/deprecation
|
|
135
|
-
danglingReferencesBehavior: options?.danglingReferencesBehavior ??
|
|
276
|
+
danglingReferencesBehavior: options?.danglingReferencesBehavior ??
|
|
277
|
+
options?.danglingPredecessorsBehavior ??
|
|
278
|
+
"reject",
|
|
279
|
+
branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
|
|
136
280
|
};
|
|
137
|
-
this.
|
|
281
|
+
this._isProvenanceInitTransform = this._options
|
|
282
|
+
.wasSourceIModelCopiedToTarget
|
|
283
|
+
? true
|
|
284
|
+
: undefined;
|
|
138
285
|
// initialize exporter and sourceDb
|
|
139
286
|
if (source instanceof core_backend_1.IModelDb) {
|
|
140
287
|
this.exporter = new IModelExporter_1.IModelExporter(source);
|
|
@@ -145,7 +292,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
145
292
|
this.sourceDb = this.exporter.sourceDb;
|
|
146
293
|
this.exporter.registerHandler(this);
|
|
147
294
|
this.exporter.wantGeometry = options?.loadSourceGeometry ?? false; // optimization to not load source GeometryStreams by default
|
|
148
|
-
if (!this._options.includeSourceProvenance) {
|
|
295
|
+
if (!this._options.includeSourceProvenance) {
|
|
296
|
+
// clone provenance from the source iModel into the target iModel?
|
|
149
297
|
IModelTransformer.provenanceElementClasses.forEach((cls) => this.exporter.excludeElementClass(cls.classFullName));
|
|
150
298
|
IModelTransformer.provenanceElementAspectClasses.forEach((cls) => this.exporter.excludeElementAspectClass(cls.classFullName));
|
|
151
299
|
}
|
|
@@ -153,32 +301,49 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
153
301
|
this.exporter.excludeElementAspectClass("BisCore:TextAnnotationData"); // This ElementAspect is auto-created by the BisCore:TextAnnotation2d/3d element handlers
|
|
154
302
|
// initialize importer and targetDb
|
|
155
303
|
if (target instanceof core_backend_1.IModelDb) {
|
|
156
|
-
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
|
+
});
|
|
157
308
|
}
|
|
158
309
|
else {
|
|
159
310
|
this.importer = target;
|
|
160
|
-
|
|
161
|
-
if (Boolean(this._options.preserveElementIdsForFiltering) !== this.importer.preserveElementIdsForFiltering) {
|
|
162
|
-
core_bentley_1.Logger.logWarning(loggerCategory, [
|
|
163
|
-
"A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.",
|
|
164
|
-
"The custom importer target's option will be force updated to use the transformer's value.",
|
|
165
|
-
"This behavior is deprecated and will be removed in a future version, throwing an error if they are out of sync.",
|
|
166
|
-
].join("\n"));
|
|
167
|
-
this.importer.preserveElementIdsForFiltering = Boolean(this._options.preserveElementIdsForFiltering);
|
|
168
|
-
}
|
|
169
|
-
/* eslint-enable deprecation/deprecation */
|
|
311
|
+
this.validateSharedOptionsMatch();
|
|
170
312
|
}
|
|
171
313
|
this.targetDb = this.importer.targetDb;
|
|
172
314
|
// create the IModelCloneContext, it must be initialized later
|
|
173
315
|
this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
|
|
316
|
+
if (this.sourceDb.isBriefcase && this.targetDb.isBriefcase) {
|
|
317
|
+
nodeAssert(this.sourceDb.changeset.index !== undefined &&
|
|
318
|
+
this.targetDb.changeset.index !== undefined, "database has no changeset index");
|
|
319
|
+
this._startingChangesetIndices = {
|
|
320
|
+
target: this.targetDb.changeset.index,
|
|
321
|
+
source: this.sourceDb.changeset.index,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
174
324
|
// this internal is guaranteed stable for just transformer usage
|
|
175
325
|
/* eslint-disable @itwin/no-internal */
|
|
176
|
-
if ("codeValueBehavior" in this.sourceDb) {
|
|
326
|
+
if (("codeValueBehavior" in this.sourceDb)) {
|
|
177
327
|
this.sourceDb.codeValueBehavior = "exact";
|
|
178
328
|
this.targetDb.codeValueBehavior = "exact";
|
|
179
329
|
}
|
|
180
330
|
/* eslint-enable @itwin/no-internal */
|
|
181
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
|
+
}
|
|
182
347
|
/** Dispose any native resources associated with this IModelTransformer. */
|
|
183
348
|
dispose() {
|
|
184
349
|
core_bentley_1.Logger.logTrace(loggerCategory, "dispose()");
|
|
@@ -204,24 +369,31 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
204
369
|
* @note This will be [[targetDb]] except when it is a reverse synchronization. In that case it be [[sourceDb]].
|
|
205
370
|
*/
|
|
206
371
|
get provenanceDb() {
|
|
207
|
-
return this.
|
|
372
|
+
return this.isReverseSynchronization ? this.sourceDb : this.targetDb;
|
|
208
373
|
}
|
|
209
374
|
/** Return the IModelDb where IModelTransformer looks for entities referred to by stored provenance.
|
|
210
375
|
* @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
|
|
211
376
|
*/
|
|
212
377
|
get provenanceSourceDb() {
|
|
213
|
-
return this.
|
|
378
|
+
return this.isReverseSynchronization ? this.targetDb : this.sourceDb;
|
|
214
379
|
}
|
|
215
380
|
/** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
|
|
216
381
|
static initElementProvenanceOptions(sourceElementId, targetElementId, args) {
|
|
217
|
-
const elementId = args.isReverseSynchronization
|
|
382
|
+
const elementId = args.isReverseSynchronization
|
|
383
|
+
? sourceElementId
|
|
384
|
+
: targetElementId;
|
|
218
385
|
const version = args.isReverseSynchronization
|
|
219
386
|
? args.targetDb.elements.queryLastModifiedTime(targetElementId)
|
|
220
387
|
: args.sourceDb.elements.queryLastModifiedTime(sourceElementId);
|
|
221
|
-
const aspectIdentifier = args.isReverseSynchronization
|
|
388
|
+
const aspectIdentifier = args.isReverseSynchronization
|
|
389
|
+
? targetElementId
|
|
390
|
+
: sourceElementId;
|
|
222
391
|
const aspectProps = {
|
|
223
392
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
224
|
-
element: {
|
|
393
|
+
element: {
|
|
394
|
+
id: elementId,
|
|
395
|
+
relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
|
|
396
|
+
},
|
|
225
397
|
scope: { id: args.targetScopeElementId },
|
|
226
398
|
identifier: aspectIdentifier,
|
|
227
399
|
kind: core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
@@ -229,10 +401,41 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
229
401
|
};
|
|
230
402
|
return aspectProps;
|
|
231
403
|
}
|
|
404
|
+
static initRelationshipProvenanceOptions(sourceRelInstanceId, targetRelInstanceId, args) {
|
|
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;
|
|
414
|
+
const elementId = provenanceDb.withPreparedStatement("SELECT SourceECInstanceId FROM bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
|
|
415
|
+
stmt.bindId(1, provenanceRelInstanceId);
|
|
416
|
+
nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
|
|
417
|
+
return stmt.getValue(0).getId();
|
|
418
|
+
});
|
|
419
|
+
const jsonProperties = args.forceOldRelationshipProvenanceMethod
|
|
420
|
+
? { targetRelInstanceId }
|
|
421
|
+
: { provenanceRelInstanceId };
|
|
422
|
+
const aspectProps = {
|
|
423
|
+
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
424
|
+
element: {
|
|
425
|
+
id: elementId,
|
|
426
|
+
relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
|
|
427
|
+
},
|
|
428
|
+
scope: { id: args.targetScopeElementId },
|
|
429
|
+
identifier: aspectIdentifier,
|
|
430
|
+
kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
|
|
431
|
+
jsonProperties: JSON.stringify(jsonProperties),
|
|
432
|
+
};
|
|
433
|
+
return aspectProps;
|
|
434
|
+
}
|
|
232
435
|
/** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
|
|
233
436
|
initElementProvenance(sourceElementId, targetElementId) {
|
|
234
437
|
return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
|
|
235
|
-
isReverseSynchronization:
|
|
438
|
+
isReverseSynchronization: this.isReverseSynchronization,
|
|
236
439
|
targetScopeElementId: this.targetScopeElementId,
|
|
237
440
|
sourceDb: this.sourceDb,
|
|
238
441
|
targetDb: this.targetDb,
|
|
@@ -240,36 +443,115 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
240
443
|
}
|
|
241
444
|
/** Create an ExternalSourceAspectProps in a standard way for a Relationship in an iModel --> iModel transformations.
|
|
242
445
|
* The ExternalSourceAspect is meant to be owned by the Element in the target iModel that is the `sourceId` of transformed relationship.
|
|
243
|
-
* The `identifier` property of the ExternalSourceAspect will be the ECInstanceId of the relationship in the
|
|
244
|
-
* The ECInstanceId of the relationship in the
|
|
446
|
+
* The `identifier` property of the ExternalSourceAspect will be the ECInstanceId of the relationship in the master iModel.
|
|
447
|
+
* The ECInstanceId of the relationship in the branch iModel will be stored in the JsonProperties of the ExternalSourceAspect.
|
|
245
448
|
*/
|
|
246
449
|
initRelationshipProvenance(sourceRelationship, targetRelInstanceId) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
450
|
+
return IModelTransformer.initRelationshipProvenanceOptions(sourceRelationship.id, targetRelInstanceId, {
|
|
451
|
+
sourceDb: this.sourceDb,
|
|
452
|
+
targetDb: this.targetDb,
|
|
453
|
+
isReverseSynchronization: this.isReverseSynchronization,
|
|
454
|
+
targetScopeElementId: this.targetScopeElementId,
|
|
455
|
+
forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
/** the changeset in the scoping element's source version found for this transformation
|
|
459
|
+
* @note: the version depends on whether this is a reverse synchronization or not, as
|
|
460
|
+
* it is stored separately for both synchronization directions.
|
|
461
|
+
* @note: must call [[initScopeProvenance]] before using this property.
|
|
462
|
+
* @note: empty string and -1 for changeset and index if it has never been transformed or was transformed before federation guid update (pre 1.x).
|
|
463
|
+
*/
|
|
464
|
+
get _synchronizationVersion() {
|
|
465
|
+
if (!this._cachedSynchronizationVersion) {
|
|
466
|
+
nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
|
|
467
|
+
const version = this.isReverseSynchronization
|
|
468
|
+
? this._targetScopeProvenanceProps.jsonProperties?.reverseSyncVersion
|
|
469
|
+
: this._targetScopeProvenanceProps.version;
|
|
470
|
+
nodeAssert(version !== undefined, "no version contained in target scope");
|
|
471
|
+
const [id, index] = version === "" ? ["", -1] : version.split(";");
|
|
472
|
+
this._cachedSynchronizationVersion = { index: Number(index), id };
|
|
473
|
+
nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
|
|
474
|
+
}
|
|
475
|
+
return this._cachedSynchronizationVersion;
|
|
476
|
+
}
|
|
477
|
+
/** the changeset in the scoping element's source version found for this transformation
|
|
478
|
+
* @note: the version depends on whether this is a reverse synchronization or not, as
|
|
479
|
+
* it is stored separately for both synchronization directions.
|
|
480
|
+
* @note: empty string and -1 for changeset and index if it has never been transformed, or was transformed before federation guid update (pre 1.x).
|
|
481
|
+
*/
|
|
482
|
+
get synchronizationVersion() {
|
|
483
|
+
if (this._cachedSynchronizationVersion === undefined) {
|
|
484
|
+
const provenanceScopeAspect = this.tryGetProvenanceScopeAspect();
|
|
485
|
+
if (!provenanceScopeAspect) {
|
|
486
|
+
return { index: -1, id: "" }; // first synchronization.
|
|
487
|
+
}
|
|
488
|
+
const version = this.isReverseSynchronization
|
|
489
|
+
? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
|
|
490
|
+
: provenanceScopeAspect.version;
|
|
491
|
+
if (!version) {
|
|
492
|
+
return { index: -1, id: "" }; // previous synchronization was done before fed guid update.
|
|
493
|
+
}
|
|
494
|
+
const [id, index] = version.split(";");
|
|
495
|
+
if (Number.isNaN(Number(index)))
|
|
496
|
+
throw new Error("Could not parse version data from scope aspect");
|
|
497
|
+
this._cachedSynchronizationVersion = { index: Number(index), id }; // synchronization version found and cached.
|
|
498
|
+
}
|
|
499
|
+
return this._cachedSynchronizationVersion;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* @returns provenance scope aspect if it exists in the provenanceDb.
|
|
503
|
+
* Provenance scope aspect is created and inserted into provenanceDb when [[initScopeProvenance]] is invoked.
|
|
504
|
+
*/
|
|
505
|
+
tryGetProvenanceScopeAspect() {
|
|
506
|
+
const scopeProvenanceAspectProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, {
|
|
507
|
+
id: undefined,
|
|
251
508
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
509
|
+
scope: { id: core_common_1.IModel.rootSubjectId },
|
|
510
|
+
kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
|
|
511
|
+
element: { id: this.targetScopeElementId ?? core_common_1.IModel.rootSubjectId },
|
|
512
|
+
identifier: this.provenanceSourceDb.iModelId,
|
|
513
|
+
});
|
|
514
|
+
return scopeProvenanceAspectProps !== undefined
|
|
515
|
+
? this.provenanceDb.elements.getAspect(scopeProvenanceAspectProps.aspectId)
|
|
516
|
+
: undefined;
|
|
260
517
|
}
|
|
261
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Make sure there are no conflicting other scope-type external source aspects on the *target scope element*,
|
|
520
|
+
* If there are none at all, insert one, then this must be a first synchronization.
|
|
521
|
+
* @returns the last synced version (changesetId) on the target scope's external source aspect,
|
|
522
|
+
* if this was a [BriefcaseDb]($backend)
|
|
523
|
+
*/
|
|
524
|
+
initScopeProvenance() {
|
|
262
525
|
const aspectProps = {
|
|
526
|
+
id: undefined,
|
|
527
|
+
version: undefined,
|
|
263
528
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
264
|
-
element: {
|
|
529
|
+
element: {
|
|
530
|
+
id: this.targetScopeElementId,
|
|
531
|
+
relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
|
|
532
|
+
},
|
|
265
533
|
scope: { id: core_common_1.IModel.rootSubjectId },
|
|
266
|
-
identifier: this.
|
|
534
|
+
identifier: this.provenanceSourceDb.iModelId,
|
|
267
535
|
kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
|
|
536
|
+
jsonProperties: undefined,
|
|
268
537
|
};
|
|
269
|
-
|
|
270
|
-
if (
|
|
538
|
+
const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps); // this query includes "identifier"
|
|
539
|
+
if (foundEsaProps === undefined) {
|
|
540
|
+
aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
|
|
541
|
+
aspectProps.jsonProperties = {
|
|
542
|
+
pendingReverseSyncChangesetIndices: [],
|
|
543
|
+
pendingSyncChangesetIndices: [],
|
|
544
|
+
reverseSyncVersion: "", // empty since never before transformed. Will be updated in first reverse sync
|
|
545
|
+
};
|
|
271
546
|
// this query does not include "identifier" to find possible conflicts
|
|
272
|
-
const sql = `
|
|
547
|
+
const sql = `
|
|
548
|
+
SELECT ECInstanceId
|
|
549
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
550
|
+
WHERE Element.Id=:elementId
|
|
551
|
+
AND Scope.Id=:scopeId
|
|
552
|
+
AND Kind=:kind
|
|
553
|
+
LIMIT 1
|
|
554
|
+
`;
|
|
273
555
|
const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
|
|
274
556
|
statement.bindId("elementId", aspectProps.element.id);
|
|
275
557
|
statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
|
|
@@ -280,162 +562,276 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
280
562
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
|
|
281
563
|
}
|
|
282
564
|
if (!this._options.noProvenance) {
|
|
283
|
-
this.provenanceDb.elements.insertAspect(
|
|
565
|
+
const id = this.provenanceDb.elements.insertAspect({
|
|
566
|
+
...aspectProps,
|
|
567
|
+
jsonProperties: JSON.stringify(aspectProps.jsonProperties),
|
|
568
|
+
});
|
|
569
|
+
aspectProps.id = id;
|
|
284
570
|
}
|
|
285
571
|
}
|
|
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;
|
|
286
592
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
statement.bindId("scopeId", aspectProps.scope.id);
|
|
294
|
-
statement.bindString("kind", aspectProps.kind);
|
|
295
|
-
statement.bindString("identifier", aspectProps.identifier);
|
|
296
|
-
return (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) ? statement.getValue(0).getId() : undefined;
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
/** Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one. */
|
|
593
|
+
/**
|
|
594
|
+
* Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
|
|
595
|
+
* and call a function for each one.
|
|
596
|
+
* @note provenance is done by federation guids where possible
|
|
597
|
+
* @note this may execute on each element more than once! Only use in cases where that is handled
|
|
598
|
+
*/
|
|
300
599
|
static forEachTrackedElement(args) {
|
|
600
|
+
if (args.provenanceDb === args.provenanceSourceDb)
|
|
601
|
+
return;
|
|
301
602
|
if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
|
|
302
603
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
|
|
303
604
|
}
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
605
|
+
const sourceDb = args.isReverseSynchronization
|
|
606
|
+
? args.provenanceDb
|
|
607
|
+
: args.provenanceSourceDb;
|
|
608
|
+
const targetDb = args.isReverseSynchronization
|
|
609
|
+
? args.provenanceSourceDb
|
|
610
|
+
: args.provenanceDb;
|
|
611
|
+
// query for provenanceDb
|
|
612
|
+
const elementIdByFedGuidQuery = `
|
|
613
|
+
SELECT e.ECInstanceId, FederationGuid
|
|
614
|
+
FROM bis.Element e
|
|
615
|
+
${args.skipPropagateChangesToRootElements
|
|
616
|
+
? "WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements"
|
|
617
|
+
: ""}
|
|
618
|
+
ORDER BY FederationGuid
|
|
619
|
+
`;
|
|
620
|
+
// iterate through sorted list of fed guids from both dbs to get the intersection
|
|
621
|
+
// NOTE: if we exposed the native attach database support,
|
|
622
|
+
// we could get the intersection of fed guids in one query, not sure if it would be faster
|
|
623
|
+
// OR we could do a raw sqlite query...
|
|
624
|
+
sourceDb.withStatement(elementIdByFedGuidQuery, (sourceStmt) => targetDb.withStatement(elementIdByFedGuidQuery, (targetStmt) => {
|
|
625
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
626
|
+
return;
|
|
627
|
+
let sourceRow = sourceStmt.getRow();
|
|
628
|
+
if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
629
|
+
return;
|
|
630
|
+
let targetRow = targetStmt.getRow();
|
|
631
|
+
// NOTE: these comparisons rely upon the lowercase of the guid,
|
|
632
|
+
// and the fact that '0' < '9' < a' < 'f' in ascii/utf8
|
|
633
|
+
while (true) {
|
|
634
|
+
const currSourceRow = sourceRow, currTargetRow = targetRow;
|
|
635
|
+
if (currSourceRow.federationGuid !== undefined &&
|
|
636
|
+
currTargetRow.federationGuid !== undefined &&
|
|
637
|
+
currSourceRow.federationGuid === currTargetRow.federationGuid) {
|
|
638
|
+
// data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
|
|
639
|
+
args.fn(sourceRow.id, targetRow.id);
|
|
313
640
|
}
|
|
314
|
-
|
|
315
|
-
|
|
641
|
+
if (currTargetRow.federationGuid === undefined ||
|
|
642
|
+
(currSourceRow.federationGuid !== undefined &&
|
|
643
|
+
currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
|
|
644
|
+
if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
645
|
+
return;
|
|
646
|
+
targetRow = targetStmt.getRow();
|
|
647
|
+
}
|
|
648
|
+
if (currSourceRow.federationGuid === undefined ||
|
|
649
|
+
(currTargetRow.federationGuid !== undefined &&
|
|
650
|
+
currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
|
|
651
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
652
|
+
return;
|
|
653
|
+
sourceRow = sourceStmt.getRow();
|
|
316
654
|
}
|
|
317
655
|
}
|
|
656
|
+
}));
|
|
657
|
+
// query for provenanceDb
|
|
658
|
+
const provenanceAspectsQuery = `
|
|
659
|
+
SELECT esa.Identifier, Element.Id
|
|
660
|
+
FROM bis.ExternalSourceAspect esa
|
|
661
|
+
WHERE Scope.Id=:scopeId
|
|
662
|
+
AND Kind=:kind
|
|
663
|
+
`;
|
|
664
|
+
// Technically this will a second time call the function (as documented) on
|
|
665
|
+
// victims of the old provenance method that have both fedguids and an inserted aspect.
|
|
666
|
+
// But this is a private function with one known caller where that doesn't matter
|
|
667
|
+
args.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
|
|
668
|
+
const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization
|
|
669
|
+
? args.fn(sourceId, targetId)
|
|
670
|
+
: args.fn(targetId, sourceId);
|
|
671
|
+
stmt.bindId("scopeId", args.targetScopeElementId);
|
|
672
|
+
stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
673
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
674
|
+
// ExternalSourceAspect.Identifier is of type string
|
|
675
|
+
const aspectIdentifier = stmt.getValue(0).getString();
|
|
676
|
+
const elementId = stmt.getValue(1).getId();
|
|
677
|
+
runFnInDataFlowDirection(elementId, aspectIdentifier);
|
|
678
|
+
}
|
|
318
679
|
});
|
|
319
680
|
}
|
|
320
681
|
forEachTrackedElement(fn) {
|
|
321
682
|
return IModelTransformer.forEachTrackedElement({
|
|
322
|
-
provenanceSourceDb: this.
|
|
683
|
+
provenanceSourceDb: this.provenanceSourceDb,
|
|
323
684
|
provenanceDb: this.provenanceDb,
|
|
324
685
|
targetScopeElementId: this.targetScopeElementId,
|
|
325
|
-
isReverseSynchronization:
|
|
686
|
+
isReverseSynchronization: this.isReverseSynchronization,
|
|
326
687
|
fn,
|
|
688
|
+
skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
|
|
327
689
|
});
|
|
328
690
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
691
|
+
_queryProvenanceForElement(entityInProvenanceSourceId) {
|
|
692
|
+
return this.provenanceDb.withPreparedStatement(`
|
|
693
|
+
SELECT esa.Element.Id
|
|
694
|
+
FROM Bis.ExternalSourceAspect esa
|
|
695
|
+
WHERE esa.Kind=?
|
|
696
|
+
AND esa.Scope.Id=?
|
|
697
|
+
AND esa.Identifier=?
|
|
698
|
+
`, (stmt) => {
|
|
699
|
+
stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
700
|
+
stmt.bindId(2, this.targetScopeElementId);
|
|
701
|
+
stmt.bindString(3, entityInProvenanceSourceId);
|
|
702
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
703
|
+
return stmt.getValue(0).getId();
|
|
704
|
+
else
|
|
705
|
+
return undefined;
|
|
338
706
|
});
|
|
339
|
-
if (args)
|
|
340
|
-
return this.remapDeletedSourceElements(args);
|
|
341
707
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
finally {
|
|
399
|
-
if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
|
|
400
|
-
core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
|
|
708
|
+
_queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
|
|
709
|
+
return this.provenanceDb.withPreparedStatement(`
|
|
710
|
+
SELECT
|
|
711
|
+
ECInstanceId,
|
|
712
|
+
JSON_EXTRACT(JsonProperties, '$.targetRelInstanceId'),
|
|
713
|
+
JSON_EXTRACT(JsonProperties, '$.provenanceRelInstanceId')
|
|
714
|
+
FROM Bis.ExternalSourceAspect
|
|
715
|
+
WHERE Kind=?
|
|
716
|
+
AND Scope.Id=?
|
|
717
|
+
AND Identifier=?
|
|
718
|
+
`, (stmt) => {
|
|
719
|
+
stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
720
|
+
stmt.bindId(2, this.targetScopeElementId);
|
|
721
|
+
stmt.bindString(3, entityInProvenanceSourceId);
|
|
722
|
+
if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
723
|
+
return undefined;
|
|
724
|
+
const aspectId = stmt.getValue(0).getId();
|
|
725
|
+
const provenanceRelInstIdVal = stmt.getValue(2);
|
|
726
|
+
const provenanceRelInstanceId = !provenanceRelInstIdVal.isNull
|
|
727
|
+
? provenanceRelInstIdVal.getString()
|
|
728
|
+
: this._queryTargetRelId(sourceRelInfo);
|
|
729
|
+
return {
|
|
730
|
+
aspectId,
|
|
731
|
+
relationshipId: provenanceRelInstanceId,
|
|
732
|
+
};
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
_queryTargetRelId(sourceRelInfo) {
|
|
736
|
+
const targetRelInfo = {
|
|
737
|
+
sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
|
|
738
|
+
targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
|
|
739
|
+
};
|
|
740
|
+
if (targetRelInfo.sourceId === undefined ||
|
|
741
|
+
targetRelInfo.targetId === undefined)
|
|
742
|
+
return undefined; // couldn't find an element, rel is invalid or deleted
|
|
743
|
+
return this.targetDb.withPreparedStatement(`
|
|
744
|
+
SELECT ECInstanceId
|
|
745
|
+
FROM bis.ElementRefersToElements
|
|
746
|
+
WHERE SourceECInstanceId=?
|
|
747
|
+
AND TargetECInstanceId=?
|
|
748
|
+
AND ECClassId=?
|
|
749
|
+
`, (stmt) => {
|
|
750
|
+
stmt.bindId(1, targetRelInfo.sourceId);
|
|
751
|
+
stmt.bindId(2, targetRelInfo.targetId);
|
|
752
|
+
stmt.bindId(3, this._targetClassNameToClassId(sourceRelInfo.classFullName));
|
|
753
|
+
if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
754
|
+
return undefined;
|
|
755
|
+
return stmt.getValue(0).getId();
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
_targetClassNameToClassId(classFullName) {
|
|
759
|
+
let classId = this._targetClassNameToClassIdCache.get(classFullName);
|
|
760
|
+
if (classId === undefined) {
|
|
761
|
+
classId = this._getRelClassId(this.targetDb, classFullName);
|
|
762
|
+
this._targetClassNameToClassIdCache.set(classFullName, classId);
|
|
401
763
|
}
|
|
764
|
+
return classId;
|
|
765
|
+
}
|
|
766
|
+
// NOTE: this doesn't handle remapped element classes,
|
|
767
|
+
// but is only used for relationships rn
|
|
768
|
+
_getRelClassId(db, classFullName) {
|
|
769
|
+
return db.withPreparedStatement(`
|
|
770
|
+
SELECT c.ECInstanceId
|
|
771
|
+
FROM ECDbMeta.ECClassDef c
|
|
772
|
+
JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
|
|
773
|
+
WHERE s.Name=? AND c.Name=?
|
|
774
|
+
`, (stmt) => {
|
|
775
|
+
const [schemaName, className] = classFullName.indexOf(".") !== -1
|
|
776
|
+
? classFullName.split(".")
|
|
777
|
+
: classFullName.split(":");
|
|
778
|
+
stmt.bindString(1, schemaName);
|
|
779
|
+
stmt.bindString(2, className);
|
|
780
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
781
|
+
return stmt.getValue(0).getId();
|
|
782
|
+
(0, core_bentley_1.assert)(false, "relationship was not found");
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
_queryElemIdByFedGuid(db, fedGuid) {
|
|
786
|
+
return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
|
|
787
|
+
stmt.bindGuid(1, fedGuid);
|
|
788
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
789
|
+
return stmt.getValue(0).getId();
|
|
790
|
+
else
|
|
791
|
+
return undefined;
|
|
792
|
+
});
|
|
402
793
|
}
|
|
403
794
|
/** Returns `true` if *brute force* delete detections should be run.
|
|
795
|
+
* @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
|
|
404
796
|
* @note Not relevant for processChanges when change history is known.
|
|
405
797
|
*/
|
|
406
798
|
shouldDetectDeletes() {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (this._options.isReverseSynchronization)
|
|
410
|
-
return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
|
|
411
|
-
return true;
|
|
799
|
+
nodeAssert(this._syncType !== undefined);
|
|
800
|
+
return this._syncType === "not-sync";
|
|
412
801
|
}
|
|
413
|
-
/**
|
|
414
|
-
*
|
|
415
|
-
*
|
|
802
|
+
/**
|
|
803
|
+
* Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
|
|
804
|
+
* in the source iModel.
|
|
805
|
+
* @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
|
|
806
|
+
* This method is only called during [[processAll]] when the option
|
|
807
|
+
* [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
|
|
808
|
+
* necessary when using [[processChanges]] since changeset information is sufficient.
|
|
809
|
+
* @note you do not need to call this directly unless processing a subset of an iModel.
|
|
416
810
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
417
811
|
*/
|
|
418
812
|
async detectElementDeletes() {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
813
|
+
const sql = `
|
|
814
|
+
SELECT Identifier, Element.Id
|
|
815
|
+
FROM BisCore.ExternalSourceAspect
|
|
816
|
+
WHERE Scope.Id=:scopeId
|
|
817
|
+
AND Kind=:kind
|
|
818
|
+
`;
|
|
819
|
+
nodeAssert(!this.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
|
|
820
|
+
this.provenanceDb.withPreparedStatement(sql, (stmt) => {
|
|
821
|
+
stmt.bindId("scopeId", this.targetScopeElementId);
|
|
822
|
+
stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
823
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
824
|
+
// ExternalSourceAspect.Identifier is of type string
|
|
825
|
+
const aspectIdentifier = stmt.getValue(0).getString();
|
|
826
|
+
if (!core_bentley_1.Id64.isValidId64(aspectIdentifier)) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const targetElemId = stmt.getValue(1).getId();
|
|
830
|
+
const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
|
|
831
|
+
entityReference: `e${aspectIdentifier}`,
|
|
832
|
+
});
|
|
833
|
+
if (wasDeletedInSource)
|
|
834
|
+
this.importer.deleteElement(targetElemId);
|
|
439
835
|
}
|
|
440
836
|
});
|
|
441
837
|
}
|
|
@@ -443,7 +839,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
443
839
|
* @deprecated in 3.x, this no longer has any effect except emitting a warning
|
|
444
840
|
*/
|
|
445
841
|
skipElement(_sourceElement) {
|
|
446
|
-
core_bentley_1.Logger.logWarning(loggerCategory,
|
|
842
|
+
core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
|
|
447
843
|
}
|
|
448
844
|
/** Transform the specified sourceElement into ElementProps for the target iModel.
|
|
449
845
|
* @param sourceElement The Element from the source iModel to transform.
|
|
@@ -467,19 +863,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
467
863
|
* @param targetElementId The Element from the target iModel to compare against.
|
|
468
864
|
* @note A subclass can override this method to provide custom change detection behavior.
|
|
469
865
|
*/
|
|
470
|
-
hasElementChanged(sourceElement,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const lastModifiedTime = sourceElement.iModel.elements.queryLastModifiedTime(sourceElement.id);
|
|
479
|
-
return lastModifiedTime !== sourceAspect.version;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return true;
|
|
866
|
+
hasElementChanged(sourceElement, _targetElementId) {
|
|
867
|
+
if (this._sourceChangeDataState === "no-changes")
|
|
868
|
+
return false;
|
|
869
|
+
if (this._sourceChangeDataState === "unconnected")
|
|
870
|
+
return true;
|
|
871
|
+
nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
|
|
872
|
+
nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
|
|
873
|
+
return this._hasElementChangedCache.has(sourceElement.id);
|
|
483
874
|
}
|
|
484
875
|
static transformCallbackFor(transformer, entity) {
|
|
485
876
|
if (entity instanceof core_backend_1.Element)
|
|
@@ -505,8 +896,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
505
896
|
const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
|
|
506
897
|
const targetProps = onEntityTransform.call(this, sourceEntity);
|
|
507
898
|
if (sourceEntity instanceof core_backend_1.Relationship) {
|
|
508
|
-
targetProps.sourceId =
|
|
509
|
-
|
|
899
|
+
targetProps.sourceId =
|
|
900
|
+
this.context.findTargetElementId(sourceEntity.sourceId);
|
|
901
|
+
targetProps.targetId =
|
|
902
|
+
this.context.findTargetElementId(sourceEntity.targetId);
|
|
510
903
|
}
|
|
511
904
|
updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
|
|
512
905
|
this._partiallyCommittedEntities.delete(sourceEntity);
|
|
@@ -522,11 +915,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
522
915
|
for (const referenceId of entity.getReferenceConcreteIds()) {
|
|
523
916
|
// TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
|
|
524
917
|
const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
|
|
525
|
-
const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
|
|
918
|
+
const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
|
|
919
|
+
this._skippedEntities.has(referenceId);
|
|
526
920
|
if (alreadyProcessed)
|
|
527
921
|
continue;
|
|
528
922
|
core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
|
|
529
|
-
const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
|
|
923
|
+
const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
|
|
924
|
+
entityReference: referenceId,
|
|
925
|
+
});
|
|
530
926
|
if (!referencedExistsInSource) {
|
|
531
927
|
core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
|
|
532
928
|
switch (this._options.danglingReferencesBehavior) {
|
|
@@ -536,7 +932,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
536
932
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
|
|
537
933
|
`Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
|
|
538
934
|
"This must have been caused by an upstream application that changed the iModel.",
|
|
539
|
-
"You can set the
|
|
935
|
+
"You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
|
|
540
936
|
"in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
|
|
541
937
|
"transformer to remove the reference and fix affected elements may be suitable.",
|
|
542
938
|
].join("\n"));
|
|
@@ -574,7 +970,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
574
970
|
/** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel.
|
|
575
971
|
* @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter.
|
|
576
972
|
*/
|
|
577
|
-
shouldExportElement(_sourceElement) {
|
|
973
|
+
shouldExportElement(_sourceElement) {
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
578
976
|
onSkipElement(sourceElementId) {
|
|
579
977
|
if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
|
|
580
978
|
// element already has provenance
|
|
@@ -605,7 +1003,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
605
1003
|
const referenceType = elemClass.requiredReferenceKeyTypeMap[referenceKey];
|
|
606
1004
|
// For now we just consider all required references to be elements (as they are in biscore), and do not support
|
|
607
1005
|
// entities that refuse to be inserted without a different kind of entity (e.g. aspect or relationship) first being inserted
|
|
608
|
-
(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);
|
|
609
1008
|
return mapId64(idContainer, (id) => {
|
|
610
1009
|
if (id === core_bentley_1.Id64.invalid || id === core_common_1.IModel.rootSubjectId)
|
|
611
1010
|
return undefined; // not allowed to directly export the root subject
|
|
@@ -614,13 +1013,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
614
1013
|
// This is relied upon by the TemplateModelCloner
|
|
615
1014
|
// TODO: extract this out to only be in the TemplateModelCloner
|
|
616
1015
|
const asDefinitionElem = this.sourceDb.elements.tryGetElement(id, core_backend_1.DefinitionElement);
|
|
617
|
-
if (asDefinitionElem &&
|
|
1016
|
+
if (asDefinitionElem &&
|
|
1017
|
+
!(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
|
|
618
1018
|
this.context.remapElement(id, id);
|
|
619
1019
|
}
|
|
620
1020
|
}
|
|
621
1021
|
return id;
|
|
622
|
-
})
|
|
623
|
-
.filter((sourceReferenceId) => {
|
|
1022
|
+
}).filter((sourceReferenceId) => {
|
|
624
1023
|
if (sourceReferenceId === undefined)
|
|
625
1024
|
return false;
|
|
626
1025
|
const referenceInTargetId = this.context.findTargetElementId(sourceReferenceId);
|
|
@@ -648,7 +1047,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
648
1047
|
const isSubModeled = dbHasModel(this.sourceDb, elementId);
|
|
649
1048
|
const idOfElemInTarget = this.context.findTargetElementId(elementId);
|
|
650
1049
|
const isElemInTarget = core_bentley_1.Id64.invalid !== idOfElemInTarget;
|
|
651
|
-
const needsModelImport = isSubModeled &&
|
|
1050
|
+
const needsModelImport = isSubModeled &&
|
|
1051
|
+
(!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
|
|
652
1052
|
return { needsElemImport: !isElemInTarget, needsModelImport };
|
|
653
1053
|
}
|
|
654
1054
|
/** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel.
|
|
@@ -663,57 +1063,80 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
663
1063
|
}
|
|
664
1064
|
else if (this._options.wasSourceIModelCopiedToTarget) {
|
|
665
1065
|
targetElementId = sourceElement.id;
|
|
666
|
-
targetElementProps =
|
|
1066
|
+
targetElementProps =
|
|
1067
|
+
this.targetDb.elements.getElementProps(targetElementId);
|
|
667
1068
|
}
|
|
668
1069
|
else {
|
|
669
1070
|
targetElementId = this.context.findTargetElementId(sourceElement.id);
|
|
670
1071
|
targetElementProps = this.onTransformElement(sourceElement);
|
|
671
1072
|
}
|
|
1073
|
+
// if an existing remapping was not yet found, check by FederationGuid
|
|
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;
|
|
1079
|
+
if (core_bentley_1.Id64.isValid(targetElementId))
|
|
1080
|
+
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
|
|
1081
|
+
}
|
|
672
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)
|
|
673
|
-
if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
|
|
1083
|
+
if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
|
|
1084
|
+
core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
|
|
674
1085
|
// respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
|
|
675
1086
|
// whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
|
|
676
1087
|
targetElementProps.code.value = targetElementProps.code.value ?? "";
|
|
677
|
-
|
|
678
|
-
if (undefined !==
|
|
679
|
-
const
|
|
680
|
-
if (
|
|
1088
|
+
const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
|
|
1089
|
+
if (undefined !== maybeTargetElementId) {
|
|
1090
|
+
const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
|
|
1091
|
+
if (maybeTargetElem.classFullName === targetElementProps.classFullName) {
|
|
1092
|
+
// ensure code remapping doesn't change the target class
|
|
1093
|
+
targetElementId = maybeTargetElementId;
|
|
681
1094
|
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
|
|
682
1095
|
}
|
|
683
1096
|
else {
|
|
684
|
-
targetElementId = undefined;
|
|
685
1097
|
targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
|
|
686
1098
|
}
|
|
687
1099
|
}
|
|
688
1100
|
}
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
1101
|
+
if (core_bentley_1.Id64.isValid(targetElementId) &&
|
|
1102
|
+
!this.hasElementChanged(sourceElement, targetElementId))
|
|
1103
|
+
return;
|
|
695
1104
|
this.collectUnmappedReferences(sourceElement);
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
targetElementId
|
|
699
|
-
|
|
1105
|
+
// targetElementId will be valid (indicating update) or undefined (indicating insert)
|
|
1106
|
+
targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
|
|
1107
|
+
? targetElementId
|
|
1108
|
+
: undefined;
|
|
700
1109
|
if (!this._options.wasSourceIModelCopiedToTarget) {
|
|
701
1110
|
this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
|
|
702
1111
|
}
|
|
703
1112
|
this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
|
|
704
1113
|
// now that we've mapped this elem we can fix unmapped references to it
|
|
705
1114
|
this.resolvePendingReferences(sourceElement);
|
|
1115
|
+
// the transformer does not currently 'split' or 'join' any elements, therefore, it does not
|
|
1116
|
+
// insert external source aspects because federation guids are sufficient for this.
|
|
1117
|
+
// Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
|
|
1118
|
+
// when splitting/joining elements
|
|
1119
|
+
// physical consolidation is an example of a 'joining' transform
|
|
1120
|
+
// FIXME: verify at finalization time that we don't lose provenance on new elements
|
|
1121
|
+
// FIXME: make public and improve `initElementProvenance` API for usage by consolidators
|
|
706
1122
|
if (!this._options.noProvenance) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.provenanceDb
|
|
1123
|
+
let provenance = this._options.forceExternalSourceAspectProvenance ||
|
|
1124
|
+
this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
|
|
1125
|
+
? undefined
|
|
1126
|
+
: sourceElement.federationGuid;
|
|
1127
|
+
if (!provenance) {
|
|
1128
|
+
const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
|
|
1129
|
+
const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
|
|
1130
|
+
if (foundEsaProps === undefined)
|
|
1131
|
+
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1132
|
+
else {
|
|
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;
|
|
1135
|
+
this.provenanceDb.elements.updateAspect(aspectProps);
|
|
1136
|
+
}
|
|
1137
|
+
provenance = aspectProps;
|
|
714
1138
|
}
|
|
715
|
-
|
|
716
|
-
this.markLastProvenance(aspectProps, { isRelationship: false });
|
|
1139
|
+
this.markLastProvenance(provenance, { isRelationship: false });
|
|
717
1140
|
}
|
|
718
1141
|
}
|
|
719
1142
|
resolvePendingReferences(entity) {
|
|
@@ -739,10 +1162,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
739
1162
|
* This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel.
|
|
740
1163
|
*/
|
|
741
1164
|
onExportModel(sourceModel) {
|
|
742
|
-
if (
|
|
1165
|
+
if (this._options.skipPropagateChangesToRootElements &&
|
|
1166
|
+
core_common_1.IModel.repositoryModelId === sourceModel.id)
|
|
743
1167
|
return; // The RepositoryModel should not be directly imported
|
|
744
|
-
}
|
|
745
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;
|
|
746
1174
|
const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
|
|
747
1175
|
this.importer.importModel(targetModelProps);
|
|
748
1176
|
this.resolvePendingReferences(sourceModel);
|
|
@@ -751,7 +1179,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
751
1179
|
onDeleteModel(sourceModelId) {
|
|
752
1180
|
// It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
|
|
753
1181
|
// - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
|
|
754
|
-
// - If both were deleted, [[
|
|
1182
|
+
// - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
|
|
755
1183
|
const targetModelId = this.context.findTargetElementId(sourceModelId);
|
|
756
1184
|
if (!core_bentley_1.Id64.isValidId64(targetModelId))
|
|
757
1185
|
return;
|
|
@@ -764,9 +1192,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
764
1192
|
stmt.bindId(1, targetModelId);
|
|
765
1193
|
const val = stmt.step();
|
|
766
1194
|
switch (val) {
|
|
767
|
-
case core_bentley_1.DbResult.BE_SQLITE_ROW:
|
|
768
|
-
|
|
769
|
-
|
|
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}'`);
|
|
770
1201
|
}
|
|
771
1202
|
});
|
|
772
1203
|
if (isDefinitionPartition) {
|
|
@@ -780,7 +1211,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
780
1211
|
this.importer.deleteModel(targetModelId);
|
|
781
1212
|
}
|
|
782
1213
|
catch (error) {
|
|
783
|
-
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);
|
|
784
1217
|
if (!isDeletionProhibitedErr)
|
|
785
1218
|
throw error;
|
|
786
1219
|
// Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
|
|
@@ -791,7 +1224,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
791
1224
|
}
|
|
792
1225
|
/** Schedule modeled partition deletion */
|
|
793
1226
|
scheduleModeledPartitionDeletion(sourceModelId) {
|
|
794
|
-
const deletedElements = this.exporter.sourceDbChanges?.element
|
|
1227
|
+
const deletedElements = this.exporter.sourceDbChanges?.element
|
|
1228
|
+
.deleteIds;
|
|
795
1229
|
if (!deletedElements.has(sourceModelId)) {
|
|
796
1230
|
deletedElements.add(sourceModelId);
|
|
797
1231
|
}
|
|
@@ -857,7 +1291,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
857
1291
|
onTransformModel(sourceModel, targetModeledElementId) {
|
|
858
1292
|
const targetModelProps = sourceModel.toJSON();
|
|
859
1293
|
// don't directly edit deep object since toJSON performs a shallow clone
|
|
860
|
-
targetModelProps.modeledElement = {
|
|
1294
|
+
targetModelProps.modeledElement = {
|
|
1295
|
+
...targetModelProps.modeledElement,
|
|
1296
|
+
id: targetModeledElementId,
|
|
1297
|
+
};
|
|
861
1298
|
targetModelProps.id = targetModeledElementId;
|
|
862
1299
|
targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
|
|
863
1300
|
return targetModelProps;
|
|
@@ -866,25 +1303,131 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
866
1303
|
* @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
|
|
867
1304
|
*/
|
|
868
1305
|
async processDeferredElements(_numRetries = 3) { }
|
|
869
|
-
finalizeTransformation
|
|
1306
|
+
/** called at the end ([[finalizeTransformation]]) of a transformation,
|
|
1307
|
+
* updates the target scope element to say that transformation up through the
|
|
1308
|
+
* source's changeset has been performed. Also stores all changesets that occurred
|
|
1309
|
+
* during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
|
|
1310
|
+
*
|
|
1311
|
+
* You generally should not call this function yourself and use [[processChanges]] instead.
|
|
1312
|
+
* It is public for unsupported use cases of custom synchronization transforms.
|
|
1313
|
+
* @note if you are not running processChanges in this transformation, this will fail
|
|
1314
|
+
* without setting the `force` option to `true`
|
|
1315
|
+
*/
|
|
1316
|
+
updateSynchronizationVersion({ force = false } = {}) {
|
|
1317
|
+
const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
|
|
1318
|
+
this._sourceChangeDataState !== "has-changes" &&
|
|
1319
|
+
!this._isProvenanceInitTransform;
|
|
1320
|
+
if (notForcedAndHasNoChangesAndIsntProvenanceInit)
|
|
1321
|
+
return;
|
|
1322
|
+
nodeAssert(this._targetScopeProvenanceProps);
|
|
1323
|
+
const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
|
|
1324
|
+
const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
|
|
1325
|
+
if (this._isProvenanceInitTransform) {
|
|
1326
|
+
this._targetScopeProvenanceProps.version = sourceVersion;
|
|
1327
|
+
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
|
|
1328
|
+
targetVersion;
|
|
1329
|
+
}
|
|
1330
|
+
else if (this.isReverseSynchronization) {
|
|
1331
|
+
const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
|
|
1332
|
+
core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
|
|
1333
|
+
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
|
|
1334
|
+
sourceVersion;
|
|
1335
|
+
}
|
|
1336
|
+
else if (!this.isReverseSynchronization) {
|
|
1337
|
+
core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
|
|
1338
|
+
this._targetScopeProvenanceProps.version = sourceVersion;
|
|
1339
|
+
}
|
|
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");
|
|
1344
|
+
const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
|
|
1345
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
|
|
1346
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
|
|
1347
|
+
const [syncChangesetsToClear, syncChangesetsToUpdate] = this
|
|
1348
|
+
.isReverseSynchronization
|
|
1349
|
+
? [
|
|
1350
|
+
jsonProps.pendingReverseSyncChangesetIndices,
|
|
1351
|
+
jsonProps.pendingSyncChangesetIndices,
|
|
1352
|
+
]
|
|
1353
|
+
: [
|
|
1354
|
+
jsonProps.pendingSyncChangesetIndices,
|
|
1355
|
+
jsonProps.pendingReverseSyncChangesetIndices,
|
|
1356
|
+
];
|
|
1357
|
+
for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
|
|
1358
|
+
syncChangesetsToUpdate.push(i);
|
|
1359
|
+
// FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
|
|
1360
|
+
// probably wrong, and we should filter it instead
|
|
1361
|
+
syncChangesetsToClear.length = 0;
|
|
1362
|
+
// if reverse sync then we may have received provenance changes which should be marked as sync changes
|
|
1363
|
+
if (this.isReverseSynchronization) {
|
|
1364
|
+
nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
|
|
1365
|
+
for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
|
|
1366
|
+
jsonProps.pendingReverseSyncChangesetIndices.push(i);
|
|
1367
|
+
}
|
|
1368
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `new pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
|
|
1369
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `new pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
|
|
1370
|
+
}
|
|
1371
|
+
this.provenanceDb.elements.updateAspect({
|
|
1372
|
+
...this._targetScopeProvenanceProps,
|
|
1373
|
+
jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
// FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
|
|
1377
|
+
async finalizeTransformation(options) {
|
|
1378
|
+
this.importer.finalize();
|
|
1379
|
+
this.updateSynchronizationVersion();
|
|
870
1380
|
if (this._partiallyCommittedEntities.size > 0) {
|
|
871
|
-
|
|
1381
|
+
const message = [
|
|
872
1382
|
"The following elements were never fully resolved:",
|
|
873
1383
|
[...this._partiallyCommittedEntities.keys()].join(","),
|
|
874
1384
|
"This indicates that either some references were excluded from the transformation",
|
|
875
1385
|
"or the source has dangling references.",
|
|
876
|
-
].join("\n")
|
|
1386
|
+
].join("\n");
|
|
1387
|
+
if (this._options.danglingReferencesBehavior === "reject")
|
|
1388
|
+
throw new Error(message);
|
|
1389
|
+
core_bentley_1.Logger.logWarning(loggerCategory, message);
|
|
877
1390
|
for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
|
|
878
1391
|
partiallyCommittedElem.forceComplete();
|
|
879
1392
|
}
|
|
880
1393
|
}
|
|
1394
|
+
// TODO: ignore if we remove change cache usage
|
|
1395
|
+
if (!this._options.noDetachChangeCache) {
|
|
1396
|
+
if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
|
|
1397
|
+
core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
|
|
1398
|
+
}
|
|
881
1399
|
// this internal is guaranteed stable for just transformer usage
|
|
882
1400
|
/* eslint-disable @itwin/no-internal */
|
|
883
|
-
if ("codeValueBehavior" in this.sourceDb) {
|
|
1401
|
+
if (("codeValueBehavior" in this.sourceDb)) {
|
|
884
1402
|
this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
|
|
885
1403
|
this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
|
|
886
1404
|
}
|
|
887
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
|
+
}
|
|
888
1431
|
}
|
|
889
1432
|
/** Imports all relationships that subclass from the specified base class.
|
|
890
1433
|
* @param baseRelClassFullName The specified base relationship class.
|
|
@@ -897,64 +1440,98 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
897
1440
|
/** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported.
|
|
898
1441
|
* @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer).
|
|
899
1442
|
*/
|
|
900
|
-
shouldExportRelationship(_sourceRelationship) {
|
|
1443
|
+
shouldExportRelationship(_sourceRelationship) {
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
901
1446
|
/** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel.
|
|
902
1447
|
* This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
|
|
903
1448
|
*/
|
|
904
1449
|
onExportRelationship(sourceRelationship) {
|
|
1450
|
+
const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
|
|
1451
|
+
const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
|
|
905
1452
|
const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
|
|
906
1453
|
const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
|
|
907
|
-
if (!this._options.noProvenance &&
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1454
|
+
if (!this._options.noProvenance &&
|
|
1455
|
+
core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
|
|
1456
|
+
let provenance = !this._options.forceExternalSourceAspectProvenance
|
|
1457
|
+
? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
|
|
1458
|
+
: undefined;
|
|
1459
|
+
if (!provenance) {
|
|
1460
|
+
const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
|
|
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) {
|
|
1464
|
+
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1465
|
+
}
|
|
1466
|
+
provenance = aspectProps;
|
|
911
1467
|
}
|
|
912
|
-
(
|
|
913
|
-
this.markLastProvenance(aspectProps, { isRelationship: true });
|
|
1468
|
+
this.markLastProvenance(provenance, { isRelationship: true });
|
|
914
1469
|
}
|
|
915
1470
|
}
|
|
916
1471
|
/** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
|
|
917
1472
|
* This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
|
|
918
1473
|
*/
|
|
919
1474
|
onDeleteRelationship(sourceRelInstanceId) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1475
|
+
nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
|
|
1476
|
+
const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
|
|
1477
|
+
if (!deletedRelData) {
|
|
1478
|
+
// this can occur if both the source and target deleted it
|
|
1479
|
+
core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const relArg = deletedRelData.relId ??
|
|
1483
|
+
{
|
|
1484
|
+
sourceId: deletedRelData.sourceIdInTarget,
|
|
1485
|
+
targetId: deletedRelData.targetIdInTarget,
|
|
1486
|
+
};
|
|
1487
|
+
// FIXME: make importer.deleteRelationship not need full props
|
|
1488
|
+
const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
|
|
1489
|
+
if (targetRelationship) {
|
|
1490
|
+
this.importer.deleteRelationship(targetRelationship.toJSON());
|
|
1491
|
+
}
|
|
1492
|
+
if (deletedRelData.provenanceAspectId) {
|
|
1493
|
+
try {
|
|
1494
|
+
this.provenanceDb.elements.deleteAspect(deletedRelData.provenanceAspectId);
|
|
935
1495
|
}
|
|
936
|
-
|
|
1496
|
+
catch (error) {
|
|
1497
|
+
// This aspect may no longer exist if it was deleted at some other point during the transformation. This is fine.
|
|
1498
|
+
if (error.errorNumber === core_bentley_1.IModelStatus.NotFound)
|
|
1499
|
+
return;
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
937
1503
|
}
|
|
938
1504
|
/** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
|
|
1505
|
+
* @deprecated in 1.x. Don't use this anymore
|
|
939
1506
|
* @see processChanges
|
|
940
1507
|
* @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
|
|
941
1508
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
942
1509
|
*/
|
|
943
1510
|
async detectRelationshipDeletes() {
|
|
944
|
-
if (this.
|
|
1511
|
+
if (this.isReverseSynchronization) {
|
|
945
1512
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
|
|
946
1513
|
}
|
|
947
1514
|
const aspectDeleteIds = [];
|
|
948
|
-
const sql = `
|
|
1515
|
+
const sql = `
|
|
1516
|
+
SELECT ECInstanceId, Identifier, JsonProperties
|
|
1517
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
|
|
1518
|
+
WHERE aspect.Scope.Id=:scopeId
|
|
1519
|
+
AND aspect.Kind=:kind
|
|
1520
|
+
`;
|
|
949
1521
|
await this.targetDb.withPreparedStatement(sql, async (statement) => {
|
|
950
1522
|
statement.bindId("scopeId", this.targetScopeElementId);
|
|
951
1523
|
statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
952
1524
|
while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
|
|
953
1525
|
const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
|
|
954
|
-
if (undefined ===
|
|
1526
|
+
if (undefined ===
|
|
1527
|
+
this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
|
|
1528
|
+
// this function exists only to support some in-imodel transformations, which must
|
|
1529
|
+
// use the old (external source aspect) provenance method anyway so we don't need to support
|
|
1530
|
+
// new provenance
|
|
955
1531
|
const json = JSON.parse(statement.getValue(2).getString());
|
|
956
|
-
|
|
957
|
-
|
|
1532
|
+
const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
|
|
1533
|
+
if (targetRelInstanceId) {
|
|
1534
|
+
const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
|
|
958
1535
|
this.importer.deleteRelationship(targetRelationship.toJSON());
|
|
959
1536
|
}
|
|
960
1537
|
aspectDeleteIds.push(statement.getValue(0).getId());
|
|
@@ -973,9 +1550,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
973
1550
|
const targetRelationshipProps = sourceRelationship.toJSON();
|
|
974
1551
|
targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
|
|
975
1552
|
targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
|
|
1553
|
+
// TODO: move to cloneRelationship in IModelCloneContext
|
|
976
1554
|
sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
|
|
977
|
-
if (
|
|
978
|
-
|
|
1555
|
+
if (core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType &&
|
|
1556
|
+
"Id" === propertyMetaData.extendedType) {
|
|
1557
|
+
targetRelationshipProps[propertyName] =
|
|
1558
|
+
this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
|
|
979
1559
|
}
|
|
980
1560
|
});
|
|
981
1561
|
return targetRelationshipProps;
|
|
@@ -1007,8 +1587,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1007
1587
|
sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
|
|
1008
1588
|
// const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
|
|
1009
1589
|
const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
|
|
1010
|
-
const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
|
|
1011
|
-
|
|
1590
|
+
const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
|
|
1591
|
+
a.scope?.id === this.targetScopeElementId;
|
|
1592
|
+
return (!this._options.includeSourceProvenance ||
|
|
1593
|
+
!isExternalSourceAspectFromTransformer);
|
|
1012
1594
|
});
|
|
1013
1595
|
for (let i = 0; i < targetIds.length; ++i) {
|
|
1014
1596
|
this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
|
|
@@ -1047,9 +1629,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1047
1629
|
let schemaFileName = schema.name + ext;
|
|
1048
1630
|
// many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
|
|
1049
1631
|
const systemMaxPathSegmentSize = 255;
|
|
1050
|
-
|
|
1051
|
-
const windowsMaxPathLimit = 260;
|
|
1052
|
-
if (schemaFileName.length > systemMaxPathSegmentSize || path.join(this._schemaExportDir, schemaFileName).length >= windowsMaxPathLimit) {
|
|
1632
|
+
if (schemaFileName.length > systemMaxPathSegmentSize) {
|
|
1053
1633
|
// this name should be well under 255 bytes
|
|
1054
1634
|
// ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
|
|
1055
1635
|
// You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
|
|
@@ -1089,7 +1669,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1089
1669
|
const maybeLongNameResolvingSchemaCtx = this._longNamedSchemasMap.size > 0
|
|
1090
1670
|
? this._makeLongNameResolvingSchemaCtx()
|
|
1091
1671
|
: undefined;
|
|
1092
|
-
return await this.targetDb.importSchemas(schemaFullPaths, {
|
|
1672
|
+
return await this.targetDb.importSchemas(schemaFullPaths, {
|
|
1673
|
+
ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx,
|
|
1674
|
+
});
|
|
1093
1675
|
}
|
|
1094
1676
|
finally {
|
|
1095
1677
|
core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
|
|
@@ -1097,8 +1679,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1097
1679
|
}
|
|
1098
1680
|
}
|
|
1099
1681
|
/** Cause all fonts to be exported from the source iModel and imported into the target iModel.
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
+
*/
|
|
1102
1684
|
async processFonts() {
|
|
1103
1685
|
// we do not need to initialize for this since no entities are exported
|
|
1104
1686
|
await this.initialize();
|
|
@@ -1125,7 +1707,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1125
1707
|
/** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel.
|
|
1126
1708
|
* @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer).
|
|
1127
1709
|
*/
|
|
1128
|
-
shouldExportCodeSpec(_sourceCodeSpec) {
|
|
1710
|
+
shouldExportCodeSpec(_sourceCodeSpec) {
|
|
1711
|
+
return true;
|
|
1712
|
+
}
|
|
1129
1713
|
/** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */
|
|
1130
1714
|
onExportCodeSpec(sourceCodeSpec) {
|
|
1131
1715
|
this.context.importCodeSpec(sourceCodeSpec.id);
|
|
@@ -1141,52 +1725,331 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1141
1725
|
return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1142
1726
|
}
|
|
1143
1727
|
/**
|
|
1144
|
-
* Initialize prerequisites of processing, you must initialize with an [[
|
|
1145
|
-
* are intending process changes, but prefer using [[processChanges]]
|
|
1146
|
-
* Called by all `process*` functions implicitly.
|
|
1728
|
+
* Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
|
|
1729
|
+
* are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
|
|
1730
|
+
* @note Called by all `process*` functions implicitly.
|
|
1147
1731
|
* Overriders must call `super.initialize()` first
|
|
1148
1732
|
*/
|
|
1149
1733
|
async initialize(args) {
|
|
1150
1734
|
if (this._initialized)
|
|
1151
1735
|
return;
|
|
1736
|
+
await this._tryInitChangesetData(args);
|
|
1152
1737
|
await this.context.initialize();
|
|
1153
|
-
//
|
|
1154
|
-
await this.
|
|
1738
|
+
// need exporter initialized to do remapdeletedsourceentities.
|
|
1739
|
+
await this.exporter.initialize(this.getExportInitOpts(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();
|
|
1155
1742
|
this._initialized = true;
|
|
1156
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
|
+
}
|
|
1946
|
+
async _tryInitChangesetData(args) {
|
|
1947
|
+
if (!args ||
|
|
1948
|
+
this.sourceDb.iTwinId === undefined ||
|
|
1949
|
+
this.sourceDb.changeset.index === undefined) {
|
|
1950
|
+
this._sourceChangeDataState = "unconnected";
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
1954
|
+
if (noChanges) {
|
|
1955
|
+
this._sourceChangeDataState = "no-changes";
|
|
1956
|
+
this._csFileProps = [];
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
// NOTE: that we do NOT download the changesummary for the last transformed version, we want
|
|
1960
|
+
// to ignore those already processed changes
|
|
1961
|
+
const startChangesetIndexOrId = args.startChangeset?.index ??
|
|
1962
|
+
args.startChangeset?.id ??
|
|
1963
|
+
this._synchronizationVersion.index + 1;
|
|
1964
|
+
const endChangesetId = this.sourceDb.changeset.id;
|
|
1965
|
+
const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
|
|
1966
|
+
? indexOrId
|
|
1967
|
+
: core_backend_1.IModelHost.hubAccess
|
|
1968
|
+
.queryChangeset({
|
|
1969
|
+
iModelId: this.sourceDb.iModelId,
|
|
1970
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
1971
|
+
changeset: { id: indexOrId },
|
|
1972
|
+
accessToken: args.accessToken,
|
|
1973
|
+
})
|
|
1974
|
+
.then((changeset) => changeset.index)));
|
|
1975
|
+
const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
|
|
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}.`);
|
|
1986
|
+
}
|
|
1987
|
+
nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
|
|
1988
|
+
const changesetsToSkip = this.isReverseSynchronization
|
|
1989
|
+
? this._targetScopeProvenanceProps.jsonProperties
|
|
1990
|
+
.pendingReverseSyncChangesetIndices
|
|
1991
|
+
: this._targetScopeProvenanceProps.jsonProperties
|
|
1992
|
+
.pendingSyncChangesetIndices;
|
|
1993
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
|
|
1994
|
+
this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
|
|
1995
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
|
|
1996
|
+
const csFileProps = [];
|
|
1997
|
+
for (const [first, end] of this._changesetRanges) {
|
|
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({
|
|
2000
|
+
iModelId: this.sourceDb.iModelId,
|
|
2001
|
+
targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
|
|
2002
|
+
range: { first, end },
|
|
2003
|
+
});
|
|
2004
|
+
csFileProps.push(...fileProps);
|
|
2005
|
+
}
|
|
2006
|
+
this._csFileProps = csFileProps;
|
|
2007
|
+
this._sourceChangeDataState = "has-changes";
|
|
2008
|
+
}
|
|
1157
2009
|
/** Export everything from the source iModel and import the transformed entities into the target iModel.
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
async processAll() {
|
|
1161
|
-
core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
|
|
2010
|
+
* @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
|
|
2011
|
+
*/
|
|
2012
|
+
async processAll(options) {
|
|
1162
2013
|
this.logSettings();
|
|
1163
|
-
this.
|
|
2014
|
+
this.initScopeProvenance();
|
|
1164
2015
|
await this.initialize();
|
|
1165
2016
|
await this.exporter.exportCodeSpecs();
|
|
1166
2017
|
await this.exporter.exportFonts();
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
+
}
|
|
1171
2028
|
await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
|
|
1172
2029
|
await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
|
|
1173
2030
|
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1174
|
-
if (this.
|
|
2031
|
+
if (this._options.forceExternalSourceAspectProvenance &&
|
|
2032
|
+
this.shouldDetectDeletes()) {
|
|
1175
2033
|
await this.detectElementDeletes();
|
|
1176
2034
|
await this.detectRelationshipDeletes();
|
|
1177
2035
|
}
|
|
1178
2036
|
if (this._options.optimizeGeometry)
|
|
1179
2037
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
1180
2038
|
this.importer.computeProjectExtents();
|
|
1181
|
-
this.finalizeTransformation();
|
|
2039
|
+
await this.finalizeTransformation(options);
|
|
1182
2040
|
}
|
|
1183
2041
|
markLastProvenance(sourceAspect, { isRelationship = false }) {
|
|
1184
|
-
this._lastProvenanceEntityInfo =
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
2042
|
+
this._lastProvenanceEntityInfo =
|
|
2043
|
+
typeof sourceAspect === "string"
|
|
2044
|
+
? sourceAspect
|
|
2045
|
+
: {
|
|
2046
|
+
entityId: sourceAspect.element.id,
|
|
2047
|
+
aspectId: sourceAspect.id,
|
|
2048
|
+
aspectVersion: sourceAspect.version ?? "",
|
|
2049
|
+
aspectKind: isRelationship
|
|
2050
|
+
? core_backend_1.ExternalSourceAspect.Kind.Relationship
|
|
2051
|
+
: core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
2052
|
+
};
|
|
1190
2053
|
}
|
|
1191
2054
|
/**
|
|
1192
2055
|
* Load the state of the active transformation from an open SQLiteDb
|
|
@@ -1198,17 +2061,35 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1198
2061
|
const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
|
|
1199
2062
|
if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
|
|
1200
2063
|
throw Error("expected row when getting lastProvenanceEntityId from target state table");
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
2064
|
+
const entityId = stmt.getValueString(0);
|
|
2065
|
+
const isGuidOrGuidPair = entityId.includes("-");
|
|
2066
|
+
return isGuidOrGuidPair
|
|
2067
|
+
? entityId
|
|
2068
|
+
: {
|
|
2069
|
+
entityId,
|
|
2070
|
+
aspectId: stmt.getValueString(1),
|
|
2071
|
+
aspectVersion: stmt.getValueString(2),
|
|
2072
|
+
aspectKind: stmt.getValueString(3),
|
|
2073
|
+
};
|
|
1207
2074
|
});
|
|
1208
|
-
|
|
1209
|
-
//
|
|
1210
|
-
|
|
2075
|
+
/*
|
|
2076
|
+
// TODO: maybe save transformer state resumption state based on target changset and require calls
|
|
2077
|
+
// to saveChanges
|
|
2078
|
+
if () {
|
|
2079
|
+
const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
|
|
2080
|
+
const isRelProvenance = targetFedGuid !== undefined;
|
|
2081
|
+
const instanceId = isRelProvenance
|
|
2082
|
+
? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
|
|
2083
|
+
: "";
|
|
2084
|
+
//const classId =
|
|
2085
|
+
if (isRelProvenance) {
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
*/
|
|
2089
|
+
const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
|
|
2090
|
+
// ignore provenance check if it's null since we can't bind those ids
|
|
1211
2091
|
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
|
|
2092
|
+
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
|
|
1212
2093
|
this.provenanceDb.withPreparedStatement(`
|
|
1213
2094
|
SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
1214
2095
|
WHERE Scope.Id=:scopeId
|
|
@@ -1249,10 +2130,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1249
2130
|
this.context.loadStateFromDb(db);
|
|
1250
2131
|
this.importer.loadStateFromJson(state.importerState);
|
|
1251
2132
|
this.exporter.loadStateFromJson(state.exporterState);
|
|
2133
|
+
this._elementsWithExplicitlyTrackedProvenance =
|
|
2134
|
+
core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
|
|
1252
2135
|
this.loadAdditionalStateJson(state.additionalState);
|
|
1253
2136
|
}
|
|
1254
2137
|
/**
|
|
1255
|
-
* @deprecated in 0.1.x, this is buggy, and
|
|
2138
|
+
* @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
|
|
1256
2139
|
* from the original changeset
|
|
1257
2140
|
*
|
|
1258
2141
|
* Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
|
|
@@ -1299,17 +2182,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1299
2182
|
const jsonState = {
|
|
1300
2183
|
transformerClass: this.constructor.name,
|
|
1301
2184
|
options: this._options,
|
|
2185
|
+
explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
|
|
1302
2186
|
importerState: this.importer.saveStateToJson(),
|
|
1303
2187
|
exporterState: this.exporter.saveStateToJson(),
|
|
1304
2188
|
additionalState: this.getAdditionalStateJson(),
|
|
1305
2189
|
};
|
|
1306
2190
|
this.context.saveStateToDb(db);
|
|
1307
|
-
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)`))
|
|
1308
2193
|
throw Error("Failed to create the js state table in the state database");
|
|
1309
|
-
if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
|
|
2194
|
+
if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
|
|
2195
|
+
db.executeSQL(`
|
|
1310
2196
|
CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
|
|
1311
|
-
--
|
|
2197
|
+
-- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
|
|
1312
2198
|
entityId TEXT,
|
|
2199
|
+
-- the following are only valid if the above entityId is a hex id representation
|
|
1313
2200
|
aspectId TEXT,
|
|
1314
2201
|
aspectVersion TEXT,
|
|
1315
2202
|
aspectKind TEXT
|
|
@@ -1323,17 +2210,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1323
2210
|
throw Error("Failed to insert options into the state database");
|
|
1324
2211
|
});
|
|
1325
2212
|
db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
stmt.bindString(
|
|
1329
|
-
|
|
2213
|
+
const lastProvenanceEntityInfo = this
|
|
2214
|
+
._lastProvenanceEntityInfo;
|
|
2215
|
+
stmt.bindString(1, lastProvenanceEntityInfo?.entityId ??
|
|
2216
|
+
this._lastProvenanceEntityInfo);
|
|
2217
|
+
stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
|
|
2218
|
+
stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
|
|
2219
|
+
stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
|
|
1330
2220
|
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
|
|
1331
2221
|
throw Error("Failed to insert options into the state database");
|
|
1332
2222
|
});
|
|
1333
2223
|
db.saveChanges();
|
|
1334
2224
|
}
|
|
1335
2225
|
/**
|
|
1336
|
-
* @deprecated in 0.1.x, this is buggy, and
|
|
2226
|
+
* @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
|
|
1337
2227
|
* from the original changeset
|
|
1338
2228
|
*
|
|
1339
2229
|
* Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
|
|
@@ -1357,28 +2247,62 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1357
2247
|
db.closeDb();
|
|
1358
2248
|
}
|
|
1359
2249
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
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) {
|
|
2258
|
+
this._isSynchronization = true;
|
|
2259
|
+
this.initScopeProvenance();
|
|
1362
2260
|
this.logSettings();
|
|
1363
|
-
this.validateScopeProvenance();
|
|
1364
|
-
const options = typeof accessTokenOrArgs === "string"
|
|
1365
|
-
? {
|
|
1366
|
-
accessToken: accessTokenOrArgs,
|
|
1367
|
-
startChangeset: startChangesetId ? { id: startChangesetId } : this.sourceDb.changeset,
|
|
1368
|
-
changedInstanceIds: undefined,
|
|
1369
|
-
}
|
|
1370
|
-
: accessTokenOrArgs;
|
|
1371
2261
|
await this.initialize(options);
|
|
1372
|
-
|
|
2262
|
+
// must wait for initialization of synchronization provenance data
|
|
2263
|
+
await this.exporter.exportChanges(this.getExportInitOpts(options));
|
|
1373
2264
|
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1374
|
-
await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
|
|
1375
2265
|
if (this._options.optimizeGeometry)
|
|
1376
2266
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
1377
2267
|
this.importer.computeProjectExtents();
|
|
1378
|
-
this.finalizeTransformation();
|
|
2268
|
+
await this.finalizeTransformation(options);
|
|
2269
|
+
}
|
|
2270
|
+
/** Changeset data must be initialized in order to build correct changeOptions.
|
|
2271
|
+
* Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
|
|
2272
|
+
*/
|
|
2273
|
+
getExportInitOpts(opts) {
|
|
2274
|
+
if (!this._isSynchronization)
|
|
2275
|
+
return {};
|
|
2276
|
+
return {
|
|
2277
|
+
skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
|
|
2278
|
+
accessToken: opts.accessToken,
|
|
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
|
+
}),
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
/** Combine an array of source elements into a single target element.
|
|
2293
|
+
* All source and target elements must be created before calling this method.
|
|
2294
|
+
* The "combine" operation is a remap and no properties from the source elements will be exported into the target
|
|
2295
|
+
* and provenance will be explicitly tracked by ExternalSourceAspects
|
|
2296
|
+
*/
|
|
2297
|
+
combineElements(sourceElementIds, targetElementId) {
|
|
2298
|
+
for (const elementId of sourceElementIds) {
|
|
2299
|
+
this.context.remapElement(elementId, targetElementId);
|
|
2300
|
+
this._elementsWithExplicitlyTrackedProvenance.add(elementId);
|
|
2301
|
+
}
|
|
1379
2302
|
}
|
|
1380
2303
|
}
|
|
1381
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.";
|
|
1382
2306
|
/** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
|
|
1383
2307
|
IModelTransformer.jsStateTable = "TransformerJsState";
|
|
1384
2308
|
/** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
|
|
@@ -1407,6 +2331,7 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1407
2331
|
* @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
|
|
1408
2332
|
*/
|
|
1409
2333
|
async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
|
|
2334
|
+
await this.initialize();
|
|
1410
2335
|
this.context.remapElement(sourceTemplateModelId, targetModelId);
|
|
1411
2336
|
this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
|
|
1412
2337
|
this._sourceIdToTargetIdMap = new Map();
|
|
@@ -1427,6 +2352,7 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1427
2352
|
* @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
|
|
1428
2353
|
*/
|
|
1429
2354
|
async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
|
|
2355
|
+
await this.initialize();
|
|
1430
2356
|
this.context.remapElement(sourceTemplateModelId, targetModelId);
|
|
1431
2357
|
this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
|
|
1432
2358
|
this._sourceIdToTargetIdMap = new Map();
|
|
@@ -1444,14 +2370,15 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1444
2370
|
// eslint-disable-next-line deprecation/deprecation
|
|
1445
2371
|
const referenceIds = sourceElement.getReferenceConcreteIds();
|
|
1446
2372
|
referenceIds.forEach((referenceId) => {
|
|
1447
|
-
//
|
|
2373
|
+
// TODO: consider going through all definition elements at once and remapping them to themselves
|
|
1448
2374
|
if (!core_backend_1.EntityReferences.isValid(this.context.findTargetEntityId(referenceId))) {
|
|
1449
2375
|
if (this.context.isBetweenIModels) {
|
|
1450
2376
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, `Remapping for source dependency ${referenceId} not found for target iModel`);
|
|
1451
2377
|
}
|
|
1452
2378
|
else {
|
|
1453
2379
|
const definitionElement = this.sourceDb.elements.tryGetElement(referenceId, core_backend_1.DefinitionElement);
|
|
1454
|
-
if (definitionElement &&
|
|
2380
|
+
if (definitionElement &&
|
|
2381
|
+
!(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
|
|
1455
2382
|
this.context.remapElement(referenceId, referenceId); // when in the same iModel, can use existing DefinitionElements without remapping
|
|
1456
2383
|
}
|
|
1457
2384
|
else {
|
|
@@ -1463,16 +2390,12 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1463
2390
|
const targetElementProps = super.onTransformElement(sourceElement);
|
|
1464
2391
|
targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
|
|
1465
2392
|
targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
|
|
1466
|
-
if (sourceElement instanceof core_backend_1.
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
targetElementProps.placement = placement;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
|
|
1474
|
-
const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
|
|
2393
|
+
if (sourceElement instanceof core_backend_1.GeometricElement) {
|
|
2394
|
+
const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
|
|
2395
|
+
const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
|
|
2396
|
+
const placement = placementClass.fromJSON(targetElementProps.placement);
|
|
1475
2397
|
if (placement.isValid) {
|
|
2398
|
+
nodeAssert(this._transform3d);
|
|
1476
2399
|
placement.multiplyTransform(this._transform3d);
|
|
1477
2400
|
targetElementProps.placement = placement;
|
|
1478
2401
|
}
|
|
@@ -1482,4 +2405,17 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1482
2405
|
}
|
|
1483
2406
|
}
|
|
1484
2407
|
exports.TemplateModelCloner = TemplateModelCloner;
|
|
2408
|
+
function queryElemFedGuid(db, elemId) {
|
|
2409
|
+
return db.withPreparedStatement(`
|
|
2410
|
+
SELECT FederationGuid
|
|
2411
|
+
FROM bis.Element
|
|
2412
|
+
WHERE ECInstanceId=?
|
|
2413
|
+
`, (stmt) => {
|
|
2414
|
+
stmt.bindId(1, elemId);
|
|
2415
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
|
|
2416
|
+
const result = stmt.getValue(0).getGuid();
|
|
2417
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
|
|
2418
|
+
return result;
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
1485
2421
|
//# sourceMappingURL=IModelTransformer.js.map
|