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