@itwin/imodel-transformer 0.0.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +33 -0
  3. package/lib/cjs/ECReferenceTypesCache.d.ts +37 -0
  4. package/lib/cjs/ECReferenceTypesCache.d.ts.map +1 -0
  5. package/lib/cjs/ECReferenceTypesCache.js +180 -0
  6. package/lib/cjs/ECReferenceTypesCache.js.map +1 -0
  7. package/lib/cjs/EntityMap.d.ts +26 -0
  8. package/lib/cjs/EntityMap.d.ts.map +1 -0
  9. package/lib/cjs/EntityMap.js +55 -0
  10. package/lib/cjs/EntityMap.js.map +1 -0
  11. package/lib/cjs/EntityUnifier.d.ts +16 -0
  12. package/lib/cjs/EntityUnifier.d.ts.map +1 -0
  13. package/lib/cjs/EntityUnifier.js +72 -0
  14. package/lib/cjs/EntityUnifier.js.map +1 -0
  15. package/lib/cjs/IModelCloneContext.d.ts +32 -0
  16. package/lib/cjs/IModelCloneContext.d.ts.map +1 -0
  17. package/lib/cjs/IModelCloneContext.js +195 -0
  18. package/lib/cjs/IModelCloneContext.js.map +1 -0
  19. package/lib/cjs/IModelExporter.d.ts +353 -0
  20. package/lib/cjs/IModelExporter.d.ts.map +1 -0
  21. package/lib/cjs/IModelExporter.js +804 -0
  22. package/lib/cjs/IModelExporter.js.map +1 -0
  23. package/lib/cjs/IModelImporter.d.ts +230 -0
  24. package/lib/cjs/IModelImporter.d.ts.map +1 -0
  25. package/lib/cjs/IModelImporter.js +591 -0
  26. package/lib/cjs/IModelImporter.js.map +1 -0
  27. package/lib/cjs/IModelTransformer.d.ts +499 -0
  28. package/lib/cjs/IModelTransformer.d.ts.map +1 -0
  29. package/lib/cjs/IModelTransformer.js +1357 -0
  30. package/lib/cjs/IModelTransformer.js.map +1 -0
  31. package/lib/cjs/PendingReferenceMap.d.ts +35 -0
  32. package/lib/cjs/PendingReferenceMap.d.ts.map +1 -0
  33. package/lib/cjs/PendingReferenceMap.js +81 -0
  34. package/lib/cjs/PendingReferenceMap.js.map +1 -0
  35. package/lib/cjs/TransformerLoggerCategory.d.ts +23 -0
  36. package/lib/cjs/TransformerLoggerCategory.d.ts.map +1 -0
  37. package/lib/cjs/TransformerLoggerCategory.js +31 -0
  38. package/lib/cjs/TransformerLoggerCategory.js.map +1 -0
  39. package/lib/cjs/transformer.d.ts +25 -0
  40. package/lib/cjs/transformer.d.ts.map +1 -0
  41. package/lib/cjs/transformer.js +77 -0
  42. package/lib/cjs/transformer.js.map +1 -0
  43. package/package.json +120 -0
@@ -0,0 +1,1357 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TemplateModelCloner = exports.IModelTransformer = void 0;
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
+ *--------------------------------------------------------------------------------------------*/
8
+ /** @packageDocumentation
9
+ * @module iModels
10
+ */
11
+ const path = require("path");
12
+ const Semver = require("semver");
13
+ const nodeAssert = require("assert");
14
+ const core_bentley_1 = require("@itwin/core-bentley");
15
+ const core_geometry_1 = require("@itwin/core-geometry");
16
+ const core_backend_1 = require("@itwin/core-backend");
17
+ const core_common_1 = require("@itwin/core-common");
18
+ const IModelExporter_1 = require("./IModelExporter");
19
+ const IModelImporter_1 = require("./IModelImporter");
20
+ const TransformerLoggerCategory_1 = require("./TransformerLoggerCategory");
21
+ const PendingReferenceMap_1 = require("./PendingReferenceMap");
22
+ const EntityMap_1 = require("./EntityMap");
23
+ const IModelCloneContext_1 = require("./IModelCloneContext");
24
+ const EntityUnifier_1 = require("./EntityUnifier");
25
+ const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
26
+ const nullLastProvenanceEntityInfo = {
27
+ entityId: core_bentley_1.Id64.invalid,
28
+ aspectId: core_bentley_1.Id64.invalid,
29
+ aspectVersion: "",
30
+ aspectKind: core_backend_1.ExternalSourceAspect.Kind.Element,
31
+ };
32
+ /**
33
+ * A container for tracking the state of a partially committed entity and finalizing it when it's ready to be fully committed
34
+ * @internal
35
+ */
36
+ class PartiallyCommittedEntity {
37
+ constructor(
38
+ /**
39
+ * A set of "model|element ++ ID64" pairs, (e.g. `model0x11` or `element0x12`)
40
+ * It is possible for the submodel of an element to be separately resolved from the actual element,
41
+ * so its resolution must be tracked separately
42
+ */
43
+ _missingReferences, _onComplete) {
44
+ this._missingReferences = _missingReferences;
45
+ this._onComplete = _onComplete;
46
+ }
47
+ resolveReference(id) {
48
+ this._missingReferences.delete(id);
49
+ if (this._missingReferences.size === 0)
50
+ this._onComplete();
51
+ }
52
+ forceComplete() {
53
+ this._onComplete();
54
+ }
55
+ }
56
+ /**
57
+ * Apply a function to each Id64 in a supported container type of Id64s.
58
+ * Currently only supports raw Id64String or RelatedElement-like objects containing an `id` property that is a Id64String,
59
+ * which matches the possible containers of references in [Element.requiredReferenceKeys]($backend).
60
+ * @internal
61
+ */
62
+ function mapId64(idContainer, func) {
63
+ const isId64String = (arg) => {
64
+ const isString = typeof arg === "string";
65
+ (0, core_bentley_1.assert)(() => !isString || core_bentley_1.Id64.isValidId64(arg));
66
+ return isString;
67
+ };
68
+ const isRelatedElem = (arg) => arg && typeof arg === "object" && "id" in arg;
69
+ const results = [];
70
+ // is a string if compressed or singular id64, but check for singular just checks if it's a string so do this test first
71
+ if (idContainer === undefined) {
72
+ // nothing
73
+ }
74
+ else if (isId64String(idContainer)) {
75
+ results.push(func(idContainer));
76
+ }
77
+ else if (isRelatedElem(idContainer)) {
78
+ results.push(func(idContainer.id));
79
+ }
80
+ else {
81
+ throw Error([
82
+ `Id64 container '${idContainer}' is unsupported.`,
83
+ "Currently only singular Id64 strings or prop-like objects containing an 'id' property are supported.",
84
+ ].join("\n"));
85
+ }
86
+ return results;
87
+ }
88
+ /** Base class used to transform a source iModel into a different target iModel.
89
+ * @see [iModel Transformation and Data Exchange]($docs/learning/transformer/index.md), [IModelExporter]($transformer), [IModelImporter]($transformer)
90
+ * @beta
91
+ */
92
+ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
93
+ /** Construct a new IModelTransformer
94
+ * @param source Specifies the source IModelExporter or the source IModelDb that will be used to construct the source IModelExporter.
95
+ * @param target Specifies the target IModelImporter or the target IModelDb that will be used to construct the target IModelImporter.
96
+ * @param options The options that specify how the transformation should be done.
97
+ */
98
+ constructor(source, target, options) {
99
+ var _a, _b, _c, _d, _e;
100
+ super();
101
+ /** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
102
+ * and have some helper methods below for now */
103
+ this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
104
+ /** map of partially committed entities to their partial commit progress */
105
+ this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
106
+ this._yieldManager = new core_bentley_1.YieldManager();
107
+ /** The directory where schemas will be exported, a random temporary directory */
108
+ this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
109
+ this._longNamedSchemasMap = new Map();
110
+ /** state to prevent reinitialization, @see [[initialize]] */
111
+ this._initialized = false;
112
+ this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
113
+ // initialize IModelTransformOptions
114
+ this._options = {
115
+ ...options,
116
+ // non-falsy defaults
117
+ cloneUsingBinaryGeometry: (_a = options === null || options === void 0 ? void 0 : options.cloneUsingBinaryGeometry) !== null && _a !== void 0 ? _a : true,
118
+ targetScopeElementId: (_b = options === null || options === void 0 ? void 0 : options.targetScopeElementId) !== null && _b !== void 0 ? _b : core_common_1.IModel.rootSubjectId,
119
+ // eslint-disable-next-line deprecation/deprecation
120
+ danglingReferencesBehavior: (_d = (_c = options === null || options === void 0 ? void 0 : options.danglingReferencesBehavior) !== null && _c !== void 0 ? _c : options === null || options === void 0 ? void 0 : options.danglingPredecessorsBehavior) !== null && _d !== void 0 ? _d : "reject",
121
+ };
122
+ this._isFirstSynchronization = this._options.wasSourceIModelCopiedToTarget ? true : undefined;
123
+ // initialize exporter and sourceDb
124
+ if (source instanceof core_backend_1.IModelDb) {
125
+ this.exporter = new IModelExporter_1.IModelExporter(source);
126
+ }
127
+ else {
128
+ this.exporter = source;
129
+ }
130
+ this.sourceDb = this.exporter.sourceDb;
131
+ this.exporter.registerHandler(this);
132
+ this.exporter.wantGeometry = (_e = options === null || options === void 0 ? void 0 : options.loadSourceGeometry) !== null && _e !== void 0 ? _e : false; // optimization to not load source GeometryStreams by default
133
+ if (!this._options.includeSourceProvenance) { // clone provenance from the source iModel into the target iModel?
134
+ IModelTransformer.provenanceElementClasses.forEach((cls) => this.exporter.excludeElementClass(cls.classFullName));
135
+ IModelTransformer.provenanceElementAspectClasses.forEach((cls) => this.exporter.excludeElementAspectClass(cls.classFullName));
136
+ }
137
+ this.exporter.excludeElementAspectClass(core_backend_1.ChannelRootAspect.classFullName); // Channel boundaries within the source iModel are not relevant to the target iModel
138
+ this.exporter.excludeElementAspectClass("BisCore:TextAnnotationData"); // This ElementAspect is auto-created by the BisCore:TextAnnotation2d/3d element handlers
139
+ // initialize importer and targetDb
140
+ if (target instanceof core_backend_1.IModelDb) {
141
+ this.importer = new IModelImporter_1.IModelImporter(target, { preserveElementIdsForFiltering: this._options.preserveElementIdsForFiltering });
142
+ }
143
+ else {
144
+ this.importer = target;
145
+ /* eslint-disable deprecation/deprecation */
146
+ if (Boolean(this._options.preserveElementIdsForFiltering) !== this.importer.preserveElementIdsForFiltering) {
147
+ core_bentley_1.Logger.logWarning(loggerCategory, [
148
+ "A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.",
149
+ "The custom importer target's option will be force updated to use the transformer's value.",
150
+ "This behavior is deprecated and will be removed in a future version, throwing an error if they are out of sync.",
151
+ ].join("\n"));
152
+ this.importer.preserveElementIdsForFiltering = Boolean(this._options.preserveElementIdsForFiltering);
153
+ }
154
+ /* eslint-enable deprecation/deprecation */
155
+ }
156
+ this.targetDb = this.importer.targetDb;
157
+ // create the IModelCloneContext, it must be initialized later
158
+ this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
159
+ }
160
+ /** The Id of the Element in the **target** iModel that represents the **source** repository as a whole and scopes its [ExternalSourceAspect]($backend) instances. */
161
+ get targetScopeElementId() {
162
+ return this._options.targetScopeElementId;
163
+ }
164
+ /** The element classes that are considered to define provenance in the iModel */
165
+ static get provenanceElementClasses() {
166
+ return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
167
+ }
168
+ /** The element aspect classes that are considered to define provenance in the iModel */
169
+ static get provenanceElementAspectClasses() {
170
+ return [core_backend_1.ExternalSourceAspect];
171
+ }
172
+ /** Dispose any native resources associated with this IModelTransformer. */
173
+ dispose() {
174
+ core_bentley_1.Logger.logTrace(loggerCategory, "dispose()");
175
+ this.context.dispose();
176
+ }
177
+ /** Log current settings that affect IModelTransformer's behavior. */
178
+ logSettings() {
179
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter, `this.exporter.visitElements=${this.exporter.visitElements}`);
180
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter, `this.exporter.visitRelationships=${this.exporter.visitRelationships}`);
181
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter, `this.exporter.wantGeometry=${this.exporter.wantGeometry}`);
182
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter, `this.exporter.wantSystemSchemas=${this.exporter.wantSystemSchemas}`);
183
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter, `this.exporter.wantTemplateModels=${this.exporter.wantTemplateModels}`);
184
+ core_bentley_1.Logger.logInfo(loggerCategory, `this.targetScopeElementId=${this.targetScopeElementId}`);
185
+ core_bentley_1.Logger.logInfo(loggerCategory, `this._noProvenance=${this._options.noProvenance}`);
186
+ core_bentley_1.Logger.logInfo(loggerCategory, `this._includeSourceProvenance=${this._options.includeSourceProvenance}`);
187
+ core_bentley_1.Logger.logInfo(loggerCategory, `this._cloneUsingBinaryGeometry=${this._options.cloneUsingBinaryGeometry}`);
188
+ core_bentley_1.Logger.logInfo(loggerCategory, `this._wasSourceIModelCopiedToTarget=${this._options.wasSourceIModelCopiedToTarget}`);
189
+ core_bentley_1.Logger.logInfo(loggerCategory, `this._isReverseSynchronization=${this._options.isReverseSynchronization}`);
190
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.autoExtendProjectExtents=${this.importer.options.autoExtendProjectExtents}`);
191
+ core_bentley_1.Logger.logInfo(TransformerLoggerCategory_1.TransformerLoggerCategory.IModelImporter, `this.importer.simplifyElementGeometry=${this.importer.options.simplifyElementGeometry}`);
192
+ }
193
+ /** Return the IModelDb where IModelTransformer will store its provenance.
194
+ * @note This will be [[targetDb]] except when it is a reverse synchronization. In that case it be [[sourceDb]].
195
+ */
196
+ get provenanceDb() {
197
+ return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
198
+ }
199
+ /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
200
+ initElementProvenance(sourceElementId, targetElementId) {
201
+ const elementId = this._options.isReverseSynchronization ? sourceElementId : targetElementId;
202
+ const aspectIdentifier = this._options.isReverseSynchronization ? targetElementId : sourceElementId;
203
+ const aspectProps = {
204
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
205
+ element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
206
+ scope: { id: this.targetScopeElementId },
207
+ identifier: aspectIdentifier,
208
+ kind: core_backend_1.ExternalSourceAspect.Kind.Element,
209
+ version: this.sourceDb.elements.queryLastModifiedTime(sourceElementId),
210
+ };
211
+ return aspectProps;
212
+ }
213
+ /** Create an ExternalSourceAspectProps in a standard way for a Relationship in an iModel --> iModel transformations.
214
+ * The ExternalSourceAspect is meant to be owned by the Element in the target iModel that is the `sourceId` of transformed relationship.
215
+ * The `identifier` property of the ExternalSourceAspect will be the ECInstanceId of the relationship in the source iModel.
216
+ * The ECInstanceId of the relationship in the target iModel will be stored in the JsonProperties of the ExternalSourceAspect.
217
+ */
218
+ initRelationshipProvenance(sourceRelationship, targetRelInstanceId) {
219
+ const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
220
+ const elementId = this._options.isReverseSynchronization ? sourceRelationship.sourceId : targetRelationship.sourceId;
221
+ const aspectIdentifier = this._options.isReverseSynchronization ? targetRelInstanceId : sourceRelationship.id;
222
+ const aspectProps = {
223
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
224
+ element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
225
+ scope: { id: this.targetScopeElementId },
226
+ identifier: aspectIdentifier,
227
+ kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
228
+ jsonProperties: JSON.stringify({ targetRelInstanceId }),
229
+ };
230
+ aspectProps.id = this.queryExternalSourceAspectId(aspectProps);
231
+ return aspectProps;
232
+ }
233
+ validateScopeProvenance() {
234
+ const aspectProps = {
235
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
236
+ element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
237
+ scope: { id: core_common_1.IModel.rootSubjectId },
238
+ identifier: this._options.isReverseSynchronization ? this.targetDb.iModelId : this.sourceDb.iModelId,
239
+ kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
240
+ };
241
+ aspectProps.id = this.queryExternalSourceAspectId(aspectProps); // this query includes "identifier"
242
+ if (undefined === aspectProps.id) {
243
+ // this query does not include "identifier" to find possible conflicts
244
+ const sql = `SELECT ECInstanceId FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Element.Id=:elementId AND Scope.Id=:scopeId AND Kind=:kind LIMIT 1`;
245
+ const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
246
+ statement.bindId("elementId", aspectProps.element.id);
247
+ statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
248
+ statement.bindString("kind", aspectProps.kind);
249
+ return core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step();
250
+ });
251
+ if (hasConflictingScope) {
252
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
253
+ }
254
+ if (!this._options.noProvenance) {
255
+ this.provenanceDb.elements.insertAspect(aspectProps);
256
+ this._isFirstSynchronization = true; // couldn't tell this is the first time without provenance
257
+ }
258
+ }
259
+ }
260
+ queryExternalSourceAspectId(aspectProps) {
261
+ const sql = `SELECT ECInstanceId FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Element.Id=:elementId AND Scope.Id=:scopeId AND Kind=:kind AND Identifier=:identifier LIMIT 1`;
262
+ return this.provenanceDb.withPreparedStatement(sql, (statement) => {
263
+ statement.bindId("elementId", aspectProps.element.id);
264
+ if (aspectProps.scope === undefined)
265
+ return undefined; // return undefined instead of binding an invalid id
266
+ statement.bindId("scopeId", aspectProps.scope.id);
267
+ statement.bindString("kind", aspectProps.kind);
268
+ statement.bindString("identifier", aspectProps.identifier);
269
+ return (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) ? statement.getValue(0).getId() : undefined;
270
+ });
271
+ }
272
+ /** Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one. */
273
+ forEachTrackedElement(fn) {
274
+ if (!this.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
275
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
276
+ }
277
+ const sql = `SELECT Identifier,Element.Id FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Scope.Id=:scopeId AND Kind=:kind`;
278
+ this.provenanceDb.withPreparedStatement(sql, (statement) => {
279
+ statement.bindId("scopeId", this.targetScopeElementId);
280
+ statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
281
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
282
+ const aspectIdentifier = statement.getValue(0).getString(); // ExternalSourceAspect.Identifier is of type string
283
+ const elementId = statement.getValue(1).getId();
284
+ if (this._options.isReverseSynchronization) {
285
+ fn(elementId, aspectIdentifier); // provenance coming from the sourceDb
286
+ }
287
+ else {
288
+ fn(aspectIdentifier, elementId); // provenance coming from the targetDb
289
+ }
290
+ }
291
+ });
292
+ }
293
+ /** Initialize the source to target Element mapping from ExternalSourceAspects in the target iModel.
294
+ * @note This method is called from all `process*` functions and should never need to be called directly.
295
+ * @deprecated in 3.x. call [[initialize]] instead, it does the same thing among other initialization
296
+ * @note Passing an [[InitFromExternalSourceAspectsArgs]] is required when processing changes, to remap any elements that may have been deleted.
297
+ * 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.
298
+ */
299
+ initFromExternalSourceAspects(args) {
300
+ this.forEachTrackedElement((sourceElementId, targetElementId) => {
301
+ this.context.remapElement(sourceElementId, targetElementId);
302
+ });
303
+ if (args)
304
+ return this.remapDeletedSourceElements(args);
305
+ }
306
+ /** When processing deleted elements in a reverse synchronization, the [[provenanceDb]] (usually a branch iModel) has already
307
+ * deleted the [ExternalSourceAspect]($backend)s that tell us which elements in the reverse synchronization target (usually
308
+ * a master iModel) should be deleted. We must use the changesets to get the values of those before they were deleted.
309
+ */
310
+ async remapDeletedSourceElements(args) {
311
+ var _a;
312
+ // we need a connected iModel with changes to remap elements with deletions
313
+ if (this.sourceDb.iTwinId === undefined)
314
+ return;
315
+ try {
316
+ const startChangesetId = (_a = args.startChangesetId) !== null && _a !== void 0 ? _a : this.sourceDb.changeset.id;
317
+ const endChangesetId = this.sourceDb.changeset.id;
318
+ const [firstChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetId, endChangesetId]
319
+ .map(async (id) => core_backend_1.IModelHost.hubAccess
320
+ .queryChangeset({
321
+ iModelId: this.sourceDb.iModelId,
322
+ changeset: { id },
323
+ accessToken: args.accessToken,
324
+ })
325
+ .then((changeset) => changeset.index)));
326
+ const changesetIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
327
+ accessToken: args.accessToken,
328
+ iModelId: this.sourceDb.iModelId,
329
+ iTwinId: this.sourceDb.iTwinId,
330
+ range: { first: firstChangesetIndex, end: endChangesetIndex },
331
+ });
332
+ core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
333
+ for (const changesetId of changesetIds) {
334
+ this.sourceDb.withPreparedStatement(`
335
+ SELECT esac.Element.Id, esac.Identifier
336
+ FROM ecchange.change.InstanceChange ic
337
+ JOIN BisCore.ExternalSourceAspect.Changes(:changesetId, 'BeforeDelete') esac
338
+ ON ic.ChangedInstance.Id=esac.ECInstanceId
339
+ WHERE ic.OpCode=:opcode
340
+ AND ic.Summary.Id=:changesetId
341
+ AND esac.Scope.Id=:targetScopeElementId
342
+ -- not yet documented ecsql feature to check class id
343
+ AND ic.ChangedInstance.ClassId IS (ONLY BisCore.ExternalSourceAspect)
344
+ `, (stmt) => {
345
+ stmt.bindInteger("opcode", core_common_1.ChangeOpCode.Delete);
346
+ stmt.bindInteger("changesetId", changesetId);
347
+ stmt.bindInteger("targetScopeElementId", this.targetScopeElementId);
348
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
349
+ const targetId = stmt.getValue(0).getId();
350
+ const sourceId = stmt.getValue(1).getString(); // BisCore.ExternalSourceAspect.Identifier stores a hex Id64String
351
+ // TODO: maybe delete and don't just remap
352
+ this.context.remapElement(targetId, sourceId);
353
+ }
354
+ });
355
+ }
356
+ }
357
+ finally {
358
+ if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
359
+ core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
360
+ }
361
+ }
362
+ /** Returns `true` if *brute force* delete detections should be run.
363
+ * @note Not relevant for processChanges when change history is known.
364
+ */
365
+ shouldDetectDeletes() {
366
+ if (this._isFirstSynchronization)
367
+ return false; // not necessary the first time since there are no deletes to detect
368
+ if (this._options.isReverseSynchronization)
369
+ return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
370
+ return true;
371
+ }
372
+ /** Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements in the source iModel.
373
+ * @see processChanges
374
+ * @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
375
+ * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
376
+ */
377
+ async detectElementDeletes() {
378
+ if (this._options.isReverseSynchronization) {
379
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
380
+ }
381
+ const targetElementsToDelete = [];
382
+ this.forEachTrackedElement((sourceElementId, targetElementId) => {
383
+ if (undefined === this.sourceDb.elements.tryGetElementProps(sourceElementId)) {
384
+ // if the sourceElement is not found, then it must have been deleted, so propagate the delete to the target iModel
385
+ targetElementsToDelete.push(targetElementId);
386
+ }
387
+ });
388
+ targetElementsToDelete.forEach((targetElementId) => {
389
+ this.importer.deleteElement(targetElementId);
390
+ });
391
+ }
392
+ /**
393
+ * @deprecated in 3.x, this no longer has any effect except emitting a warning
394
+ */
395
+ skipElement(_sourceElement) {
396
+ core_bentley_1.Logger.logWarning(loggerCategory, `Tried to defer/skip an element, which is no longer necessary`);
397
+ }
398
+ /** Transform the specified sourceElement into ElementProps for the target iModel.
399
+ * @param sourceElement The Element from the source iModel to transform.
400
+ * @returns ElementProps for the target iModel.
401
+ * @note A subclass can override this method to provide custom transform behavior.
402
+ * @note This can be called more than once for an element in arbitrary order, so it should not have side-effects.
403
+ */
404
+ onTransformElement(sourceElement) {
405
+ var _a, _b;
406
+ core_bentley_1.Logger.logTrace(loggerCategory, `onTransformElement(${sourceElement.id}) "${sourceElement.getDisplayLabel()}"`);
407
+ const targetElementProps = this.context.cloneElement(sourceElement, { binaryGeometry: this._options.cloneUsingBinaryGeometry });
408
+ if (sourceElement instanceof core_backend_1.Subject) {
409
+ if ((_b = (_a = targetElementProps.jsonProperties) === null || _a === void 0 ? void 0 : _a.Subject) === null || _b === void 0 ? void 0 : _b.Job) {
410
+ // don't propagate source channels into target (legacy bridge case)
411
+ targetElementProps.jsonProperties.Subject.Job = undefined;
412
+ }
413
+ }
414
+ return targetElementProps;
415
+ }
416
+ /** Returns true if a change within sourceElement is detected.
417
+ * @param sourceElement The Element from the source iModel
418
+ * @param targetElementId The Element from the target iModel to compare against.
419
+ * @note A subclass can override this method to provide custom change detection behavior.
420
+ */
421
+ hasElementChanged(sourceElement, targetElementId) {
422
+ const sourceAspects = this.targetDb.elements.getAspects(targetElementId, core_backend_1.ExternalSourceAspect.classFullName);
423
+ for (const sourceAspect of sourceAspects) {
424
+ if (sourceAspect.scope === undefined) // if the scope was lost, we can't correlate so assume it changed
425
+ return true;
426
+ if (sourceAspect.identifier === sourceElement.id &&
427
+ sourceAspect.scope.id === this.targetScopeElementId &&
428
+ sourceAspect.kind === core_backend_1.ExternalSourceAspect.Kind.Element) {
429
+ const lastModifiedTime = sourceElement.iModel.elements.queryLastModifiedTime(sourceElement.id);
430
+ return lastModifiedTime !== sourceAspect.version;
431
+ }
432
+ }
433
+ return true;
434
+ }
435
+ static transformCallbackFor(transformer, entity) {
436
+ if (entity instanceof core_backend_1.Element)
437
+ return transformer.onTransformElement; // eslint-disable-line @typescript-eslint/unbound-method
438
+ else if (entity instanceof core_backend_1.Element)
439
+ return transformer.onTransformModel; // eslint-disable-line @typescript-eslint/unbound-method
440
+ else if (entity instanceof core_backend_1.Relationship)
441
+ return transformer.onTransformRelationship; // eslint-disable-line @typescript-eslint/unbound-method
442
+ else if (entity instanceof core_backend_1.ElementAspect)
443
+ return transformer.onTransformElementAspect; // eslint-disable-line @typescript-eslint/unbound-method
444
+ else
445
+ (0, core_bentley_1.assert)(false, `unreachable; entity was '${entity.constructor.name}' not an Element, Relationship, or ElementAspect`);
446
+ }
447
+ /** callback to perform when a partial element says it's ready to be completed
448
+ * transforms the source element with all references now valid, then updates the partial element with the results
449
+ */
450
+ makePartialEntityCompleter(sourceEntity) {
451
+ return () => {
452
+ const targetId = this.context.findTargetEntityId(core_backend_1.EntityReferences.from(sourceEntity));
453
+ if (!core_backend_1.EntityReferences.isValid(targetId))
454
+ throw Error(`${sourceEntity.id} has not been inserted into the target yet, the completer is invalid. This is a bug.`);
455
+ const onEntityTransform = IModelTransformer.transformCallbackFor(this, sourceEntity);
456
+ const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
457
+ const targetProps = onEntityTransform.call(this, sourceEntity);
458
+ if (sourceEntity instanceof core_backend_1.Relationship) {
459
+ targetProps.sourceId = this.context.findTargetElementId(sourceEntity.sourceId);
460
+ targetProps.targetId = this.context.findTargetElementId(sourceEntity.targetId);
461
+ }
462
+ updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
463
+ this._partiallyCommittedEntities.delete(sourceEntity);
464
+ };
465
+ }
466
+ /** collect references this entity has that are yet to be mapped, and if there are any
467
+ * create a [[PartiallyCommittedEntity]] to track resolution of those references
468
+ */
469
+ collectUnmappedReferences(entity) {
470
+ const missingReferences = new core_common_1.EntityReferenceSet();
471
+ let thisPartialElem;
472
+ for (const referenceId of entity.getReferenceConcreteIds()) {
473
+ // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
474
+ const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
475
+ const alreadyImported = core_backend_1.EntityReferences.isValid(referenceIdInTarget);
476
+ if (alreadyImported)
477
+ continue;
478
+ core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
479
+ const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: referenceId });
480
+ if (!referencedExistsInSource) {
481
+ core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
482
+ switch (this._options.danglingReferencesBehavior) {
483
+ case "ignore":
484
+ continue;
485
+ case "reject":
486
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
487
+ `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
488
+ "This must have been caused by an upstream application that changed the iModel.",
489
+ "You can set the IModelTransformerOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
490
+ "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
491
+ "transformer to remove the reference and fix affected elements may be suitable.",
492
+ ].join("\n"));
493
+ }
494
+ }
495
+ if (thisPartialElem === undefined) {
496
+ thisPartialElem = new PartiallyCommittedEntity(missingReferences, this.makePartialEntityCompleter(entity));
497
+ if (!this._partiallyCommittedEntities.has(entity))
498
+ this._partiallyCommittedEntities.set(entity, thisPartialElem);
499
+ }
500
+ missingReferences.add(referenceId);
501
+ const entityReference = core_backend_1.EntityReferences.from(entity);
502
+ this._pendingReferences.set({ referenced: referenceId, referencer: entityReference }, thisPartialElem);
503
+ }
504
+ }
505
+ /** Cause the specified Element and its child Elements (if applicable) to be exported from the source iModel and imported into the target iModel.
506
+ * @param sourceElementId Identifies the Element from the source iModel to import.
507
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
508
+ */
509
+ async processElement(sourceElementId) {
510
+ await this.initialize();
511
+ if (sourceElementId === core_common_1.IModel.rootSubjectId) {
512
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "The root Subject should not be directly imported");
513
+ }
514
+ return this.exporter.exportElement(sourceElementId);
515
+ }
516
+ /** Import child elements into the target IModelDb
517
+ * @param sourceElementId Import the child elements of this element in the source IModelDb.
518
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
519
+ */
520
+ async processChildElements(sourceElementId) {
521
+ await this.initialize();
522
+ return this.exporter.exportChildElements(sourceElementId);
523
+ }
524
+ /** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel.
525
+ * @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter.
526
+ */
527
+ shouldExportElement(_sourceElement) { return true; }
528
+ /**
529
+ * If they haven't been already, import all of the required references
530
+ * @internal do not call, override or implement this, it will be removed
531
+ */
532
+ async preExportElement(sourceElement) {
533
+ const elemClass = sourceElement.constructor;
534
+ const unresolvedReferences = elemClass.requiredReferenceKeys
535
+ .map((referenceKey) => {
536
+ const idContainer = sourceElement[referenceKey];
537
+ const referenceType = elemClass.requiredReferenceKeyTypeMap[referenceKey];
538
+ // For now we just consider all required references to be elements (as they are in biscore), and do not support
539
+ // entities that refuse to be inserted without a different kind of entity (e.g. aspect or relationship) first being inserted
540
+ (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element || referenceType === core_common_1.ConcreteEntityTypes.Model);
541
+ return mapId64(idContainer, (id) => {
542
+ if (id === core_bentley_1.Id64.invalid || id === core_common_1.IModel.rootSubjectId)
543
+ return undefined; // not allowed to directly export the root subject
544
+ if (!this.context.isBetweenIModels) {
545
+ // Within the same iModel, can use existing DefinitionElements without remapping
546
+ // This is relied upon by the TemplateModelCloner
547
+ // TODO: extract this out to only be in the TemplateModelCloner
548
+ const asDefinitionElem = this.sourceDb.elements.tryGetElement(id, core_backend_1.DefinitionElement);
549
+ if (asDefinitionElem && !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
550
+ this.context.remapElement(id, id);
551
+ }
552
+ }
553
+ return id;
554
+ })
555
+ .filter((sourceReferenceId) => {
556
+ if (sourceReferenceId === undefined)
557
+ return false;
558
+ const referenceInTargetId = this.context.findTargetElementId(sourceReferenceId);
559
+ const isInTarget = core_bentley_1.Id64.isValid(referenceInTargetId);
560
+ return !isInTarget;
561
+ });
562
+ })
563
+ .flat();
564
+ if (unresolvedReferences.length > 0) {
565
+ for (const reference of unresolvedReferences) {
566
+ const processState = this.getElemTransformState(reference);
567
+ // must export element first
568
+ if (processState.needsElemImport)
569
+ await this.exporter.exportElement(reference);
570
+ if (processState.needsModelImport)
571
+ await this.exporter.exportModel(reference);
572
+ }
573
+ }
574
+ }
575
+ getElemTransformState(elementId) {
576
+ const dbHasModel = (db, id) => {
577
+ const maybeModelId = core_backend_1.EntityReferences.fromEntityType(id, core_common_1.ConcreteEntityTypes.Model);
578
+ return EntityUnifier_1.EntityUnifier.exists(db, { entityReference: maybeModelId });
579
+ };
580
+ const isSubModeled = dbHasModel(this.sourceDb, elementId);
581
+ const idOfElemInTarget = this.context.findTargetElementId(elementId);
582
+ const isElemInTarget = core_bentley_1.Id64.invalid !== idOfElemInTarget;
583
+ const needsModelImport = isSubModeled && (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
584
+ return { needsElemImport: !isElemInTarget, needsModelImport };
585
+ }
586
+ /** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel.
587
+ * This override calls [[onTransformElement]] and then [IModelImporter.importElement]($transformer) to update the target iModel.
588
+ */
589
+ onExportElement(sourceElement) {
590
+ var _a;
591
+ let targetElementId;
592
+ let targetElementProps;
593
+ if (this._options.preserveElementIdsForFiltering) {
594
+ targetElementId = sourceElement.id;
595
+ targetElementProps = this.onTransformElement(sourceElement);
596
+ }
597
+ else if (this._options.wasSourceIModelCopiedToTarget) {
598
+ targetElementId = sourceElement.id;
599
+ targetElementProps = this.targetDb.elements.getElementProps(targetElementId);
600
+ }
601
+ else {
602
+ targetElementId = this.context.findTargetElementId(sourceElement.id);
603
+ targetElementProps = this.onTransformElement(sourceElement);
604
+ }
605
+ // 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)
606
+ if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
607
+ // respond the same way to undefined code value as the @see Code class, but don't use that class because is trims
608
+ // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
609
+ targetElementProps.code.value = (_a = targetElementProps.code.value) !== null && _a !== void 0 ? _a : "";
610
+ targetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
611
+ if (undefined !== targetElementId) {
612
+ const targetElement = this.targetDb.elements.getElement(targetElementId);
613
+ if (targetElement.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
614
+ this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
615
+ }
616
+ else {
617
+ targetElementId = undefined;
618
+ targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
619
+ }
620
+ }
621
+ }
622
+ if (undefined !== targetElementId && core_bentley_1.Id64.isValidId64(targetElementId)) {
623
+ // compare LastMod of sourceElement to ExternalSourceAspect of targetElement to see there are changes to import
624
+ if (!this.hasElementChanged(sourceElement, targetElementId)) {
625
+ return;
626
+ }
627
+ }
628
+ this.collectUnmappedReferences(sourceElement);
629
+ // TODO: untangle targetElementId state...
630
+ if (targetElementId === core_bentley_1.Id64.invalid)
631
+ targetElementId = undefined;
632
+ targetElementProps.id = targetElementId; // targetElementId will be valid (indicating update) or undefined (indicating insert)
633
+ if (!this._options.wasSourceIModelCopiedToTarget) {
634
+ this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
635
+ }
636
+ this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
637
+ // now that we've mapped this elem we can fix unmapped references to it
638
+ this.resolvePendingReferences(sourceElement);
639
+ if (!this._options.noProvenance) {
640
+ const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
641
+ let aspectId = this.queryExternalSourceAspectId(aspectProps);
642
+ if (aspectId === undefined) {
643
+ aspectId = this.provenanceDb.elements.insertAspect(aspectProps);
644
+ }
645
+ else {
646
+ this.provenanceDb.elements.updateAspect(aspectProps);
647
+ }
648
+ aspectProps.id = aspectId;
649
+ this.markLastProvenance(aspectProps, { isRelationship: false });
650
+ }
651
+ }
652
+ resolvePendingReferences(entity) {
653
+ for (const referencer of this._pendingReferences.getReferencers(entity)) {
654
+ const key = PendingReferenceMap_1.PendingReference.from(referencer, entity);
655
+ const pendingRef = this._pendingReferences.get(key);
656
+ if (!pendingRef)
657
+ continue;
658
+ pendingRef.resolveReference(core_backend_1.EntityReferences.from(entity));
659
+ this._pendingReferences.delete(key);
660
+ }
661
+ }
662
+ /** Override of [IModelExportHandler.onDeleteElement]($transformer) that is called when [IModelExporter]($transformer) detects that an Element has been deleted from the source iModel.
663
+ * This override propagates the delete to the target iModel via [IModelImporter.deleteElement]($transformer).
664
+ */
665
+ onDeleteElement(sourceElementId) {
666
+ const targetElementId = this.context.findTargetElementId(sourceElementId);
667
+ if (core_bentley_1.Id64.isValidId64(targetElementId)) {
668
+ this.importer.deleteElement(targetElementId);
669
+ }
670
+ }
671
+ /** Override of [IModelExportHandler.onExportModel]($transformer) that is called when a Model should be exported from the source iModel.
672
+ * This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel.
673
+ */
674
+ onExportModel(sourceModel) {
675
+ if (core_common_1.IModel.repositoryModelId === sourceModel.id) {
676
+ return; // The RepositoryModel should not be directly imported
677
+ }
678
+ const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
679
+ const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
680
+ this.importer.importModel(targetModelProps);
681
+ this.resolvePendingReferences(sourceModel);
682
+ }
683
+ /** Override of [IModelExportHandler.onDeleteModel]($transformer) that is called when [IModelExporter]($transformer) detects that a [Model]($backend) has been deleted from the source iModel. */
684
+ onDeleteModel(sourceModelId) {
685
+ // It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
686
+ // - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
687
+ // - If both were deleted, [[remapDeletedSourceElements]] will find and remap the deleted element making this operation valid
688
+ const targetModelId = this.context.findTargetElementId(sourceModelId);
689
+ if (core_bentley_1.Id64.isValidId64(targetModelId)) {
690
+ this.importer.deleteModel(targetModelId);
691
+ }
692
+ }
693
+ /** Cause the model container, contents, and sub-models to be exported from the source iModel and imported into the target iModel.
694
+ * @param sourceModeledElementId Import this [Model]($backend) from the source IModelDb.
695
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
696
+ */
697
+ async processModel(sourceModeledElementId) {
698
+ await this.initialize();
699
+ return this.exporter.exportModel(sourceModeledElementId);
700
+ }
701
+ /** Cause the model contents to be exported from the source iModel and imported into the target iModel.
702
+ * @param sourceModelId Import the contents of this model from the source IModelDb.
703
+ * @param targetModelId Import into this model in the target IModelDb. The target model must exist prior to this call.
704
+ * @param elementClassFullName Optional classFullName of an element subclass to limit import query against the source model.
705
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
706
+ */
707
+ async processModelContents(sourceModelId, targetModelId, elementClassFullName = core_backend_1.Element.classFullName) {
708
+ await this.initialize();
709
+ this.targetDb.models.getModel(targetModelId); // throws if Model does not exist
710
+ this.context.remapElement(sourceModelId, targetModelId); // set remapping in case importModelContents is called directly
711
+ return this.exporter.exportModelContents(sourceModelId, elementClassFullName);
712
+ }
713
+ /** Cause all sub-models that recursively descend from the specified Subject to be exported from the source iModel and imported into the target iModel. */
714
+ async processSubjectSubModels(sourceSubjectId) {
715
+ await this.initialize();
716
+ // import DefinitionModels first
717
+ const childDefinitionPartitionSql = `SELECT ECInstanceId FROM ${core_backend_1.DefinitionPartition.classFullName} WHERE Parent.Id=:subjectId`;
718
+ await this.sourceDb.withPreparedStatement(childDefinitionPartitionSql, async (statement) => {
719
+ statement.bindId("subjectId", sourceSubjectId);
720
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
721
+ await this.processModel(statement.getValue(0).getId());
722
+ }
723
+ });
724
+ // import other partitions next
725
+ const childPartitionSql = `SELECT ECInstanceId FROM ${core_backend_1.InformationPartitionElement.classFullName} WHERE Parent.Id=:subjectId`;
726
+ await this.sourceDb.withPreparedStatement(childPartitionSql, async (statement) => {
727
+ statement.bindId("subjectId", sourceSubjectId);
728
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
729
+ const modelId = statement.getValue(0).getId();
730
+ const model = this.sourceDb.models.getModel(modelId);
731
+ if (!(model instanceof core_backend_1.DefinitionModel)) {
732
+ await this.processModel(modelId);
733
+ }
734
+ }
735
+ });
736
+ // recurse into child Subjects
737
+ const childSubjectSql = `SELECT ECInstanceId FROM ${core_backend_1.Subject.classFullName} WHERE Parent.Id=:subjectId`;
738
+ await this.sourceDb.withPreparedStatement(childSubjectSql, async (statement) => {
739
+ statement.bindId("subjectId", sourceSubjectId);
740
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
741
+ await this.processSubjectSubModels(statement.getValue(0).getId());
742
+ }
743
+ });
744
+ }
745
+ /** Transform the specified sourceModel into ModelProps for the target iModel.
746
+ * @param sourceModel The Model from the source iModel to be transformed.
747
+ * @param targetModeledElementId The transformed Model will *break down* or *detail* this Element in the target iModel.
748
+ * @returns ModelProps for the target iModel.
749
+ * @note A subclass can override this method to provide custom transform behavior.
750
+ */
751
+ onTransformModel(sourceModel, targetModeledElementId) {
752
+ const targetModelProps = sourceModel.toJSON();
753
+ // don't directly edit deep object since toJSON performs a shallow clone
754
+ targetModelProps.modeledElement = { ...targetModelProps.modeledElement, id: targetModeledElementId };
755
+ targetModelProps.id = targetModeledElementId;
756
+ targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
757
+ return targetModelProps;
758
+ }
759
+ /** Import elements that were deferred in a prior pass.
760
+ * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
761
+ */
762
+ async processDeferredElements(_numRetries = 3) { }
763
+ finalizeTransformation() {
764
+ if (this._partiallyCommittedEntities.size > 0) {
765
+ core_bentley_1.Logger.logWarning(loggerCategory, [
766
+ "The following elements were never fully resolved:",
767
+ [...this._partiallyCommittedEntities.keys()].join(","),
768
+ "This indicates that either some references were excluded from the transformation",
769
+ "or the source has dangling references.",
770
+ ].join("\n"));
771
+ for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
772
+ partiallyCommittedElem.forceComplete();
773
+ }
774
+ }
775
+ }
776
+ /** Imports all relationships that subclass from the specified base class.
777
+ * @param baseRelClassFullName The specified base relationship class.
778
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
779
+ */
780
+ async processRelationships(baseRelClassFullName) {
781
+ await this.initialize();
782
+ return this.exporter.exportRelationships(baseRelClassFullName);
783
+ }
784
+ /** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported.
785
+ * @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer).
786
+ */
787
+ shouldExportRelationship(_sourceRelationship) { return true; }
788
+ /** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel.
789
+ * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
790
+ */
791
+ onExportRelationship(sourceRelationship) {
792
+ const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
793
+ const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
794
+ if (!this._options.noProvenance && core_bentley_1.Id64.isValidId64(targetRelationshipInstanceId)) {
795
+ const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
796
+ if (undefined === aspectProps.id) {
797
+ aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
798
+ }
799
+ (0, core_bentley_1.assert)(aspectProps.id !== undefined);
800
+ this.markLastProvenance(aspectProps, { isRelationship: true });
801
+ }
802
+ }
803
+ /** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
804
+ * This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
805
+ */
806
+ onDeleteRelationship(sourceRelInstanceId) {
807
+ const sql = `SELECT ECInstanceId,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect` +
808
+ ` WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind AND aspect.Identifier=:identifier LIMIT 1`;
809
+ this.targetDb.withPreparedStatement(sql, (statement) => {
810
+ statement.bindId("scopeId", this.targetScopeElementId);
811
+ statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
812
+ statement.bindString("identifier", sourceRelInstanceId);
813
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
814
+ const json = JSON.parse(statement.getValue(1).getString());
815
+ if (undefined !== json.targetRelInstanceId) {
816
+ const targetRelationship = this.targetDb.relationships.tryGetInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
817
+ if (targetRelationship) {
818
+ this.importer.deleteRelationship(targetRelationship.toJSON());
819
+ }
820
+ this.targetDb.elements.deleteAspect(statement.getValue(0).getId());
821
+ }
822
+ }
823
+ });
824
+ }
825
+ /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
826
+ * @see processChanges
827
+ * @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
828
+ * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
829
+ */
830
+ async detectRelationshipDeletes() {
831
+ if (this._options.isReverseSynchronization) {
832
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
833
+ }
834
+ const aspectDeleteIds = [];
835
+ const sql = `SELECT ECInstanceId,Identifier,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind`;
836
+ await this.targetDb.withPreparedStatement(sql, async (statement) => {
837
+ statement.bindId("scopeId", this.targetScopeElementId);
838
+ statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
839
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
840
+ const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
841
+ if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
842
+ const json = JSON.parse(statement.getValue(2).getString());
843
+ if (undefined !== json.targetRelInstanceId) {
844
+ const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
845
+ this.importer.deleteRelationship(targetRelationship.toJSON());
846
+ }
847
+ aspectDeleteIds.push(statement.getValue(0).getId());
848
+ }
849
+ await this._yieldManager.allowYield();
850
+ }
851
+ });
852
+ this.targetDb.elements.deleteAspect(aspectDeleteIds);
853
+ }
854
+ /** Transform the specified sourceRelationship into RelationshipProps for the target iModel.
855
+ * @param sourceRelationship The Relationship from the source iModel to be transformed.
856
+ * @returns RelationshipProps for the target iModel.
857
+ * @note A subclass can override this method to provide custom transform behavior.
858
+ */
859
+ onTransformRelationship(sourceRelationship) {
860
+ const targetRelationshipProps = sourceRelationship.toJSON();
861
+ targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
862
+ targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
863
+ sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
864
+ if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
865
+ targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
866
+ }
867
+ });
868
+ return targetRelationshipProps;
869
+ }
870
+ /** Override of [IModelExportHandler.onExportElementUniqueAspect]($transformer) that imports an ElementUniqueAspect into the target iModel when it is exported from the source iModel.
871
+ * This override calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel.
872
+ */
873
+ onExportElementUniqueAspect(sourceAspect) {
874
+ const targetElementId = this.context.findTargetElementId(sourceAspect.element.id);
875
+ const targetAspectProps = this.onTransformElementAspect(sourceAspect, targetElementId);
876
+ this.collectUnmappedReferences(sourceAspect);
877
+ const targetId = this.importer.importElementUniqueAspect(targetAspectProps);
878
+ this.context.remapElementAspect(sourceAspect.id, targetId);
879
+ this.resolvePendingReferences(sourceAspect);
880
+ }
881
+ /** Override of [IModelExportHandler.onExportElementMultiAspects]($transformer) that imports ElementMultiAspects into the target iModel when they are exported from the source iModel.
882
+ * This override calls [[onTransformElementAspect]] for each ElementMultiAspect and then [IModelImporter.importElementMultiAspects]($transformer) to update the target iModel.
883
+ * @note ElementMultiAspects are handled as a group to make it easier to differentiate between insert, update, and delete.
884
+ */
885
+ onExportElementMultiAspects(sourceAspects) {
886
+ const targetElementId = this.context.findTargetElementId(sourceAspects[0].element.id);
887
+ // Transform source ElementMultiAspects into target ElementAspectProps
888
+ const targetAspectPropsArray = sourceAspects.map((srcA) => this.onTransformElementAspect(srcA, targetElementId));
889
+ sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
890
+ // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
891
+ const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
892
+ var _a;
893
+ const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect && ((_a = a.scope) === null || _a === void 0 ? void 0 : _a.id) === this.targetScopeElementId;
894
+ return !this._options.includeSourceProvenance || !isExternalSourceAspectFromTransformer;
895
+ });
896
+ for (let i = 0; i < targetIds.length; ++i) {
897
+ this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
898
+ this.resolvePendingReferences(sourceAspects[i]);
899
+ }
900
+ }
901
+ /** Transform the specified sourceElementAspect into ElementAspectProps for the target iModel.
902
+ * @param sourceElementAspect The ElementAspect from the source iModel to be transformed.
903
+ * @param _targetElementId The ElementId of the target Element that will own the ElementAspects after transformation.
904
+ * @returns ElementAspectProps for the target iModel.
905
+ * @note A subclass can override this method to provide custom transform behavior.
906
+ */
907
+ onTransformElementAspect(sourceElementAspect, _targetElementId) {
908
+ const targetElementAspectProps = this.context.cloneElementAspect(sourceElementAspect);
909
+ return targetElementAspectProps;
910
+ }
911
+ /** Override of [IModelExportHandler.shouldExportSchema]($transformer) that is called to determine if a schema should be exported
912
+ * @note the default behavior doesn't import schemas older than those already in the target
913
+ */
914
+ shouldExportSchema(schemaKey) {
915
+ const versionInTarget = this.targetDb.querySchemaVersion(schemaKey.name);
916
+ if (versionInTarget === undefined)
917
+ return true;
918
+ return Semver.gt(`${schemaKey.version.read}.${schemaKey.version.write}.${schemaKey.version.minor}`, core_backend_1.Schema.toSemverString(versionInTarget));
919
+ }
920
+ /** Override of [IModelExportHandler.onExportSchema]($transformer) that serializes a schema to disk for [[processSchemas]] to import into
921
+ * the target iModel when it is exported from the source iModel.
922
+ * @returns {Promise<ExportSchemaResult>} Although the type is possibly void for backwards compatibility of subclasses,
923
+ * `IModelTransformer.onExportSchema` always returns an[[IModelExportHandler.ExportSchemaResult]]
924
+ * with a defined `schemaPath` property, for subclasses to know where the schema was written.
925
+ * Schemas are *not* guaranteed to be written to [[IModelTransformer._schemaExportDir]] by a
926
+ * known pattern derivable from the schema's name, so you must use this to find it.
927
+ */
928
+ async onExportSchema(schema) {
929
+ const ext = ".ecschema.xml";
930
+ let schemaFileName = schema.name + ext;
931
+ // many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
932
+ const systemMaxPathSegmentSize = 255;
933
+ if (schemaFileName.length > systemMaxPathSegmentSize) {
934
+ // this name should be well under 255 bytes
935
+ // ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
936
+ // You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
937
+ // and that's on the scale of at least petabytes. `Map.prototype.size` shouldn't return floating points, and even
938
+ // if they do they're in scientific notation, size bound and contain no invalid windows path chars
939
+ schemaFileName = `${schema.name.slice(0, 100)}${this._longNamedSchemasMap.size}${ext}`;
940
+ nodeAssert(schemaFileName.length <= systemMaxPathSegmentSize, "Schema name was still long. This is a bug.");
941
+ this._longNamedSchemasMap.set(schema.name, schemaFileName);
942
+ }
943
+ this.sourceDb.nativeDb.exportSchema(schema.name, this._schemaExportDir, schemaFileName);
944
+ return { schemaPath: path.join(this._schemaExportDir, schemaFileName) };
945
+ }
946
+ _makeLongNameResolvingSchemaCtx() {
947
+ const result = new core_backend_1.ECSchemaXmlContext();
948
+ result.setSchemaLocater((key) => {
949
+ const match = this._longNamedSchemasMap.get(key.name);
950
+ if (match !== undefined)
951
+ return path.join(this._schemaExportDir, match);
952
+ return undefined;
953
+ });
954
+ return result;
955
+ }
956
+ /** Cause all schemas to be exported from the source iModel and imported into the target iModel.
957
+ * @note For performance reasons, it is recommended that [IModelDb.saveChanges]($backend) be called after `processSchemas` is complete.
958
+ * It is more efficient to process *data* changes after the schema changes have been saved.
959
+ */
960
+ async processSchemas() {
961
+ // we do not need to initialize for this since no entities are exported
962
+ try {
963
+ core_backend_1.IModelJsFs.mkdirSync(this._schemaExportDir);
964
+ this._longNamedSchemasMap.clear();
965
+ await this.exporter.exportSchemas();
966
+ const exportedSchemaFiles = core_backend_1.IModelJsFs.readdirSync(this._schemaExportDir);
967
+ if (exportedSchemaFiles.length === 0)
968
+ return;
969
+ const schemaFullPaths = exportedSchemaFiles.map((s) => path.join(this._schemaExportDir, s));
970
+ const maybeLongNameResolvingSchemaCtx = this._longNamedSchemasMap.size > 0
971
+ ? this._makeLongNameResolvingSchemaCtx()
972
+ : undefined;
973
+ return await this.targetDb.importSchemas(schemaFullPaths, { ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx });
974
+ }
975
+ finally {
976
+ core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
977
+ this._longNamedSchemasMap.clear();
978
+ }
979
+ }
980
+ /** Cause all fonts to be exported from the source iModel and imported into the target iModel.
981
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
982
+ */
983
+ async processFonts() {
984
+ // we do not need to initialize for this since no entities are exported
985
+ await this.initialize();
986
+ return this.exporter.exportFonts();
987
+ }
988
+ /** Override of [IModelExportHandler.onExportFont]($transformer) that imports a font into the target iModel when it is exported from the source iModel. */
989
+ onExportFont(font, _isUpdate) {
990
+ this.context.importFont(font.id);
991
+ }
992
+ /** Cause all CodeSpecs to be exported from the source iModel and imported into the target iModel.
993
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
994
+ */
995
+ async processCodeSpecs() {
996
+ await this.initialize();
997
+ return this.exporter.exportCodeSpecs();
998
+ }
999
+ /** Cause a single CodeSpec to be exported from the source iModel and imported into the target iModel.
1000
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1001
+ */
1002
+ async processCodeSpec(codeSpecName) {
1003
+ await this.initialize();
1004
+ return this.exporter.exportCodeSpecByName(codeSpecName);
1005
+ }
1006
+ /** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel.
1007
+ * @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer).
1008
+ */
1009
+ shouldExportCodeSpec(_sourceCodeSpec) { return true; }
1010
+ /** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */
1011
+ onExportCodeSpec(sourceCodeSpec) {
1012
+ this.context.importCodeSpec(sourceCodeSpec.id);
1013
+ }
1014
+ /** Recursively import all Elements and sub-Models that descend from the specified Subject */
1015
+ async processSubject(sourceSubjectId, targetSubjectId) {
1016
+ await this.initialize();
1017
+ this.sourceDb.elements.getElement(sourceSubjectId, core_backend_1.Subject); // throws if sourceSubjectId is not a Subject
1018
+ this.targetDb.elements.getElement(targetSubjectId, core_backend_1.Subject); // throws if targetSubjectId is not a Subject
1019
+ this.context.remapElement(sourceSubjectId, targetSubjectId);
1020
+ await this.processChildElements(sourceSubjectId);
1021
+ await this.processSubjectSubModels(sourceSubjectId);
1022
+ return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1023
+ }
1024
+ /**
1025
+ * Initialize prerequisites of processing, you must initialize with an [[InitFromExternalSourceAspectsArgs]] if you
1026
+ * are intending process changes, but prefer using [[processChanges]]
1027
+ * Called by all `process*` functions implicitly.
1028
+ * Overriders must call `super.initialize()` first
1029
+ */
1030
+ async initialize(args) {
1031
+ if (this._initialized)
1032
+ return;
1033
+ await this.context.initialize();
1034
+ // eslint-disable-next-line deprecation/deprecation
1035
+ await this.initFromExternalSourceAspects(args);
1036
+ this._initialized = true;
1037
+ }
1038
+ /** Export everything from the source iModel and import the transformed entities into the target iModel.
1039
+ * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
1040
+ */
1041
+ async processAll() {
1042
+ core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
1043
+ this.logSettings();
1044
+ this.validateScopeProvenance();
1045
+ await this.initialize();
1046
+ await this.exporter.exportCodeSpecs();
1047
+ await this.exporter.exportFonts();
1048
+ // The RepositoryModel and root Subject of the target iModel should not be transformed.
1049
+ await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
1050
+ 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
1051
+ await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
1052
+ await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
1053
+ await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1054
+ if (this.shouldDetectDeletes()) {
1055
+ await this.detectElementDeletes();
1056
+ await this.detectRelationshipDeletes();
1057
+ }
1058
+ if (this._options.optimizeGeometry)
1059
+ this.importer.optimizeGeometry(this._options.optimizeGeometry);
1060
+ this.importer.computeProjectExtents();
1061
+ this.finalizeTransformation();
1062
+ }
1063
+ markLastProvenance(sourceAspect, { isRelationship = false }) {
1064
+ var _a;
1065
+ this._lastProvenanceEntityInfo = {
1066
+ entityId: sourceAspect.element.id,
1067
+ aspectId: sourceAspect.id,
1068
+ aspectVersion: (_a = sourceAspect.version) !== null && _a !== void 0 ? _a : "",
1069
+ aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1070
+ };
1071
+ }
1072
+ /**
1073
+ * Load the state of the active transformation from an open SQLiteDb
1074
+ * You can override this if you'd like to load from custom tables in the resumable dump state, but you should call
1075
+ * this super implementation
1076
+ * @note the SQLiteDb must be open
1077
+ */
1078
+ loadStateFromDb(db) {
1079
+ const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
1080
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1081
+ throw Error("expected row when getting lastProvenanceEntityId from target state table");
1082
+ return {
1083
+ entityId: stmt.getValueString(0),
1084
+ aspectId: stmt.getValueString(1),
1085
+ aspectVersion: stmt.getValueString(2),
1086
+ aspectKind: stmt.getValueString(3),
1087
+ };
1088
+ });
1089
+ const targetHasCorrectLastProvenance =
1090
+ // ignore provenance check if it's null since we can't bind those ids
1091
+ !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1092
+ !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
1093
+ this.provenanceDb.withPreparedStatement(`
1094
+ SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
1095
+ WHERE Scope.Id=:scopeId
1096
+ AND ECInstanceId=:aspectId
1097
+ AND Kind=:kind
1098
+ AND Element.Id=:entityId
1099
+ `, (statement) => {
1100
+ statement.bindId("scopeId", this.targetScopeElementId);
1101
+ statement.bindId("aspectId", lastProvenanceEntityInfo.aspectId);
1102
+ statement.bindString("kind", lastProvenanceEntityInfo.aspectKind);
1103
+ statement.bindId("entityId", lastProvenanceEntityInfo.entityId);
1104
+ const stepResult = statement.step();
1105
+ switch (stepResult) {
1106
+ case core_bentley_1.DbResult.BE_SQLITE_ROW:
1107
+ const version = statement.getValue(0).getString();
1108
+ return version === lastProvenanceEntityInfo.aspectVersion;
1109
+ case core_bentley_1.DbResult.BE_SQLITE_DONE:
1110
+ return false;
1111
+ default:
1112
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.SQLiteError, `got sql error ${stepResult}`);
1113
+ }
1114
+ });
1115
+ if (!targetHasCorrectLastProvenance)
1116
+ throw Error([
1117
+ "Target for resuming from does not have the expected provenance ",
1118
+ "from the target that the resume state was made with",
1119
+ ].join("\n"));
1120
+ this._lastProvenanceEntityInfo = lastProvenanceEntityInfo;
1121
+ const state = db.withSqliteStatement(`SELECT data FROM ${IModelTransformer.jsStateTable}`, (stmt) => {
1122
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1123
+ throw Error("expected row when getting data from js state table");
1124
+ return JSON.parse(stmt.getValueString(0));
1125
+ });
1126
+ if (state.transformerClass !== this.constructor.name)
1127
+ throw Error("resuming from a differently named transformer class, it is not necessarily valid to resume with a different transformer class");
1128
+ // force assign to readonly options since we do not know how the transformer subclass takes options to pass to the superclass
1129
+ this._options = state.options;
1130
+ this.context.loadStateFromDb(db);
1131
+ this.importer.loadStateFromJson(state.importerState);
1132
+ this.exporter.loadStateFromJson(state.exporterState);
1133
+ this.loadAdditionalStateJson(state.additionalState);
1134
+ }
1135
+ /**
1136
+ * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
1137
+ * This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
1138
+ * again but the remapping state will cause already mapped elements to be skipped.
1139
+ * To "resume" an iModel Transformation you need:
1140
+ * - the sourceDb at the same changeset
1141
+ * - the same targetDb in the state in which it was before
1142
+ * @param statePath the path to the serialized state of the transformer, use [[IModelTransformer.saveStateToFile]] to get this from an existing transformer instance
1143
+ * @param constructorArgs remaining arguments that you would normally pass to the Transformer subclass you are using, usually (sourceDb, targetDb)
1144
+ * @note custom transformers with custom state may need to override this method in order to handle loading their own custom state somewhere
1145
+ */
1146
+ static resumeTransformation(statePath, ...constructorArgs) {
1147
+ const transformer = new this(...constructorArgs);
1148
+ const db = new core_backend_1.SQLiteDb();
1149
+ db.openDb(statePath, core_bentley_1.OpenMode.Readonly);
1150
+ try {
1151
+ transformer.loadStateFromDb(db);
1152
+ }
1153
+ finally {
1154
+ db.closeDb();
1155
+ }
1156
+ return transformer;
1157
+ }
1158
+ /**
1159
+ * You may override this to store arbitrary json state in a transformer state dump, useful for some resumptions
1160
+ * @see [[IModelTransformer.saveStateToFile]]
1161
+ */
1162
+ getAdditionalStateJson() {
1163
+ return {};
1164
+ }
1165
+ /**
1166
+ * You may override this to load arbitrary json state in a transformer state dump, useful for some resumptions
1167
+ * @see [[IModelTransformer.loadStateFromFile]]
1168
+ */
1169
+ loadAdditionalStateJson(_additionalState) { }
1170
+ /**
1171
+ * Save the state of the active transformation to an open SQLiteDb
1172
+ * You can override this if you'd like to write custom tables to the resumable dump state, but you should call
1173
+ * this super implementation
1174
+ * @note the SQLiteDb must be open
1175
+ */
1176
+ saveStateToDb(db) {
1177
+ const jsonState = {
1178
+ transformerClass: this.constructor.name,
1179
+ options: this._options,
1180
+ importerState: this.importer.saveStateToJson(),
1181
+ exporterState: this.exporter.saveStateToJson(),
1182
+ additionalState: this.getAdditionalStateJson(),
1183
+ };
1184
+ this.context.saveStateToDb(db);
1185
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
1186
+ throw Error("Failed to create the js state table in the state database");
1187
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
1188
+ CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
1189
+ -- because we cannot bind the invalid id which we use for our null state, we actually store the id as a hex string
1190
+ entityId TEXT,
1191
+ aspectId TEXT,
1192
+ aspectVersion TEXT,
1193
+ aspectKind TEXT
1194
+ )
1195
+ `))
1196
+ throw Error("Failed to create the target state table in the state database");
1197
+ db.saveChanges();
1198
+ db.withSqliteStatement(`INSERT INTO ${IModelTransformer.jsStateTable} (data) VALUES (?)`, (stmt) => {
1199
+ stmt.bindString(1, JSON.stringify(jsonState));
1200
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
1201
+ throw Error("Failed to insert options into the state database");
1202
+ });
1203
+ db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
1204
+ stmt.bindString(1, this._lastProvenanceEntityInfo.entityId);
1205
+ stmt.bindString(2, this._lastProvenanceEntityInfo.aspectId);
1206
+ stmt.bindString(3, this._lastProvenanceEntityInfo.aspectVersion);
1207
+ stmt.bindString(4, this._lastProvenanceEntityInfo.aspectKind);
1208
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
1209
+ throw Error("Failed to insert options into the state database");
1210
+ });
1211
+ db.saveChanges();
1212
+ }
1213
+ /**
1214
+ * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
1215
+ * This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
1216
+ * The serialization format is a custom sqlite database.
1217
+ * @note custom transformers with custom state may override [[IModelTransformer.saveStateToDb]] or [[IModelTransformer.getAdditionalStateJson]]
1218
+ * and [[IModelTransformer.loadStateFromDb]] (with a super call) or [[IModelTransformer.loadAdditionalStateJson]]
1219
+ * if they have custom state that needs to be stored with
1220
+ * potentially inside the same sqlite file in separate tables
1221
+ */
1222
+ saveStateToFile(nativeStatePath) {
1223
+ const db = new core_backend_1.SQLiteDb();
1224
+ if (core_backend_1.IModelJsFs.existsSync(nativeStatePath))
1225
+ core_backend_1.IModelJsFs.unlinkSync(nativeStatePath);
1226
+ db.createDb(nativeStatePath);
1227
+ try {
1228
+ this.saveStateToDb(db);
1229
+ db.saveChanges();
1230
+ }
1231
+ finally {
1232
+ db.closeDb();
1233
+ }
1234
+ }
1235
+ /** Export changes from the source iModel and import the transformed entities into the target iModel.
1236
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
1237
+ * @param accessToken A valid access token string
1238
+ * @param startChangesetId Include changes from this changeset up through and including the current changeset.
1239
+ * If this parameter is not provided, then just the current changeset will be exported.
1240
+ * @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.
1241
+ */
1242
+ async processChanges(accessToken, startChangesetId) {
1243
+ core_bentley_1.Logger.logTrace(loggerCategory, "processChanges()");
1244
+ this.logSettings();
1245
+ this.validateScopeProvenance();
1246
+ await this.initialize({ accessToken, startChangesetId });
1247
+ await this.exporter.exportChanges(accessToken, startChangesetId);
1248
+ await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1249
+ if (this._options.optimizeGeometry)
1250
+ this.importer.optimizeGeometry(this._options.optimizeGeometry);
1251
+ this.importer.computeProjectExtents();
1252
+ this.finalizeTransformation();
1253
+ }
1254
+ }
1255
+ exports.IModelTransformer = IModelTransformer;
1256
+ /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
1257
+ IModelTransformer.jsStateTable = "TransformerJsState";
1258
+ /** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
1259
+ IModelTransformer.lastProvenanceEntityInfoTable = "LastProvenanceEntityInfo";
1260
+ /** IModelTransformer that clones the contents of a template model.
1261
+ * @beta
1262
+ */
1263
+ class TemplateModelCloner extends IModelTransformer {
1264
+ /** Construct a new TemplateModelCloner
1265
+ * @param sourceDb The source IModelDb that contains the templates to clone
1266
+ * @param targetDb Optionally specify the target IModelDb where the cloned template will be inserted.
1267
+ * Typically this is left unspecified, and the default is to use the sourceDb as the target
1268
+ * @note The expectation is that the template definitions are within the same iModel where instances will be placed.
1269
+ */
1270
+ constructor(sourceDb, targetDb = sourceDb) {
1271
+ const target = new IModelImporter_1.IModelImporter(targetDb, {
1272
+ autoExtendProjectExtents: false, // autoExtendProjectExtents is intended for transformation service use cases, not template --> instance cloning
1273
+ });
1274
+ super(sourceDb, target, { noProvenance: true }); // WIP: need to decide the proper way to handle provenance
1275
+ }
1276
+ /** Place a template from the sourceDb at the specified placement in the target model within the targetDb.
1277
+ * @param sourceTemplateModelId The Id of the template model in the sourceDb
1278
+ * @param targetModelId The Id of the target model (must be a subclass of GeometricModel3d) where the cloned component will be inserted.
1279
+ * @param placement The placement for the cloned component.
1280
+ * @note *Required References* like the SpatialCategory must be remapped before calling this method.
1281
+ * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
1282
+ */
1283
+ async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
1284
+ this.context.remapElement(sourceTemplateModelId, targetModelId);
1285
+ this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
1286
+ this._sourceIdToTargetIdMap = new Map();
1287
+ await this.exporter.exportModelContents(sourceTemplateModelId);
1288
+ // Note: the source --> target mapping was needed during the template model cloning phase (remapping parent/child, for example), but needs to be reset afterwards
1289
+ for (const sourceElementId of this._sourceIdToTargetIdMap.keys()) {
1290
+ const targetElementId = this.context.findTargetElementId(sourceElementId);
1291
+ this._sourceIdToTargetIdMap.set(sourceElementId, targetElementId);
1292
+ this.context.removeElement(sourceElementId); // clear the underlying native remapping context for the next clone operation
1293
+ }
1294
+ return this._sourceIdToTargetIdMap; // return the sourceElementId -> targetElementId Map in case further post-processing is required.
1295
+ }
1296
+ /** Place a template from the sourceDb at the specified placement in the target model within the targetDb.
1297
+ * @param sourceTemplateModelId The Id of the template model in the sourceDb
1298
+ * @param targetModelId The Id of the target model (must be a subclass of GeometricModel2d) where the cloned component will be inserted.
1299
+ * @param placement The placement for the cloned component.
1300
+ * @note *Required References* like the DrawingCategory must be remapped before calling this method.
1301
+ * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
1302
+ */
1303
+ async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
1304
+ this.context.remapElement(sourceTemplateModelId, targetModelId);
1305
+ this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
1306
+ this._sourceIdToTargetIdMap = new Map();
1307
+ await this.exporter.exportModelContents(sourceTemplateModelId);
1308
+ // Note: the source --> target mapping was needed during the template model cloning phase (remapping parent/child, for example), but needs to be reset afterwards
1309
+ for (const sourceElementId of this._sourceIdToTargetIdMap.keys()) {
1310
+ const targetElementId = this.context.findTargetElementId(sourceElementId);
1311
+ this._sourceIdToTargetIdMap.set(sourceElementId, targetElementId);
1312
+ this.context.removeElement(sourceElementId); // clear the underlying native remapping context for the next clone operation
1313
+ }
1314
+ return this._sourceIdToTargetIdMap; // return the sourceElementId -> targetElementId Map in case further post-processing is required.
1315
+ }
1316
+ /** Cloning from a template requires this override of onTransformElement. */
1317
+ onTransformElement(sourceElement) {
1318
+ const referenceIds = sourceElement.getReferenceIds();
1319
+ referenceIds.forEach((referenceId) => {
1320
+ if (core_bentley_1.Id64.invalid === this.context.findTargetElementId(referenceId)) {
1321
+ if (this.context.isBetweenIModels) {
1322
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, `Remapping for source dependency ${referenceId} not found for target iModel`);
1323
+ }
1324
+ else {
1325
+ const definitionElement = this.sourceDb.elements.tryGetElement(referenceId, core_backend_1.DefinitionElement);
1326
+ if (definitionElement && !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
1327
+ this.context.remapElement(referenceId, referenceId); // when in the same iModel, can use existing DefinitionElements without remapping
1328
+ }
1329
+ else {
1330
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, `Remapping for dependency ${referenceId} not found`);
1331
+ }
1332
+ }
1333
+ }
1334
+ });
1335
+ const targetElementProps = super.onTransformElement(sourceElement);
1336
+ targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
1337
+ targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
1338
+ if (sourceElement instanceof core_backend_1.GeometricElement3d) {
1339
+ const placement = core_common_1.Placement3d.fromJSON(targetElementProps.placement);
1340
+ if (placement.isValid) {
1341
+ placement.multiplyTransform(this._transform3d);
1342
+ targetElementProps.placement = placement;
1343
+ }
1344
+ }
1345
+ else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
1346
+ const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
1347
+ if (placement.isValid) {
1348
+ placement.multiplyTransform(this._transform3d);
1349
+ targetElementProps.placement = placement;
1350
+ }
1351
+ }
1352
+ this._sourceIdToTargetIdMap.set(sourceElement.id, core_bentley_1.Id64.invalid); // keep track of (source) elementIds from the template model, but the target hasn't been inserted yet
1353
+ return targetElementProps;
1354
+ }
1355
+ }
1356
+ exports.TemplateModelCloner = TemplateModelCloner;
1357
+ //# sourceMappingURL=IModelTransformer.js.map