@itwin/imodel-transformer 2.0.0-dev.33 → 2.0.0-dev.35

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 (48) hide show
  1. package/lib/cjs/ElementAspectCleanup.d.ts +16 -0
  2. package/lib/cjs/ElementAspectCleanup.d.ts.map +1 -0
  3. package/lib/cjs/ElementAspectCleanup.js +83 -0
  4. package/lib/cjs/ElementAspectCleanup.js.map +1 -0
  5. package/lib/cjs/ElementAspectExportCoordinator.d.ts +44 -0
  6. package/lib/cjs/ElementAspectExportCoordinator.d.ts.map +1 -0
  7. package/lib/cjs/ElementAspectExportCoordinator.js +148 -0
  8. package/lib/cjs/ElementAspectExportCoordinator.js.map +1 -0
  9. package/lib/cjs/ElementAspectExportProcessor.d.ts +55 -0
  10. package/lib/cjs/ElementAspectExportProcessor.d.ts.map +1 -0
  11. package/lib/cjs/ElementAspectExportProcessor.js +186 -0
  12. package/lib/cjs/ElementAspectExportProcessor.js.map +1 -0
  13. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  14. package/lib/cjs/IModelCloneContext.js +2 -1
  15. package/lib/cjs/IModelCloneContext.js.map +1 -1
  16. package/lib/cjs/IModelExporter.d.ts +48 -10
  17. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  18. package/lib/cjs/IModelExporter.js +368 -41
  19. package/lib/cjs/IModelExporter.js.map +1 -1
  20. package/lib/cjs/IModelImporter.d.ts +6 -0
  21. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  22. package/lib/cjs/IModelImporter.js +9 -0
  23. package/lib/cjs/IModelImporter.js.map +1 -1
  24. package/lib/cjs/IModelTransformer.d.ts +2 -0
  25. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  26. package/lib/cjs/IModelTransformer.js +40 -19
  27. package/lib/cjs/IModelTransformer.js.map +1 -1
  28. package/lib/cjs/IModelTransformerError.d.ts +2 -0
  29. package/lib/cjs/IModelTransformerError.d.ts.map +1 -1
  30. package/lib/cjs/IModelTransformerError.js +2 -0
  31. package/lib/cjs/IModelTransformerError.js.map +1 -1
  32. package/lib/cjs/imodel-transformer.d.ts +9 -1
  33. package/lib/cjs/imodel-transformer.d.ts.map +1 -1
  34. package/lib/cjs/imodel-transformer.js +9 -1
  35. package/lib/cjs/imodel-transformer.js.map +1 -1
  36. package/package.json +1 -1
  37. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts +0 -16
  38. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +0 -1
  39. package/lib/cjs/DetachedExportElementAspectsStrategy.js +0 -108
  40. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +0 -1
  41. package/lib/cjs/ExportElementAspectsStrategy.d.ts +0 -35
  42. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +0 -1
  43. package/lib/cjs/ExportElementAspectsStrategy.js +0 -52
  44. package/lib/cjs/ExportElementAspectsStrategy.js.map +0 -1
  45. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts +0 -12
  46. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +0 -1
  47. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +0 -48
  48. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +0 -1
@@ -14,9 +14,10 @@ const core_common_1 = require("@itwin/core-common");
14
14
  const ecschema_metadata_1 = require("@itwin/ecschema-metadata");
15
15
  const TransformerLoggerCategory_1 = require("./TransformerLoggerCategory");
16
16
  const node_assert_1 = require("node:assert");
17
- const ExportElementAspectsWithElementsStrategy_1 = require("./ExportElementAspectsWithElementsStrategy");
18
- const ChangesetScanner_1 = require("./ChangesetScanner");
17
+ const ElementAspectExportProcessor_1 = require("./ElementAspectExportProcessor");
18
+ const ElementAspectExportCoordinator_1 = require("./ElementAspectExportCoordinator");
19
19
  const IModelTransformerError_1 = require("./IModelTransformerError");
20
+ const ChangesetScanner_1 = require("./ChangesetScanner");
20
21
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter;
21
22
  /** Handles the events generated by IModelExporter.
22
23
  * @note Change information is available when `IModelExportHandler` methods are invoked via [IModelExporter.exportChanges]($transformer), but not available when invoked via [IModelExporter.exportAll]($transformer).
@@ -128,10 +129,14 @@ class IModelExportHandler {
128
129
  exports.IModelExportHandler = IModelExportHandler;
129
130
  /** Base class for exporting data from an iModel.
130
131
  * @note Most uses cases will not require a custom subclass of `IModelExporter`. Instead, it is more typical to subclass/customize [IModelExportHandler]($transformer).
132
+ * ElementAspects are exported separately from element callbacks in bounded,
133
+ * owner-scoped groups. Aspect callbacks are not guaranteed to be adjacent to
134
+ * callbacks for their owners.
131
135
  * @see [iModel Transformation and Data Exchange]($docs/learning/transformer/index.md), [[registerHandler]], [IModelTransformer]($transformer), [IModelImporter]($transformer)
132
136
  * @beta
133
137
  */
134
138
  class IModelExporter {
139
+ static _elementAspectOwnerBatchSize = 1000;
135
140
  /** The read-only source iModel. */
136
141
  sourceDb;
137
142
  /** A flag that indicates whether element GeometryStreams are loaded or not.
@@ -197,20 +202,37 @@ class IModelExporter {
197
202
  _excludedElementClasses = new Set();
198
203
  /** The set of classes of Relationships that will be excluded (polymorphically) from transformation to the target iModel. */
199
204
  _excludedRelationshipClasses = new Set();
200
- /** Strategy for how ElementAspects are exported */
201
- _exportElementAspectsStrategy;
205
+ /** Exports ElementAspects for accepted owner groups. */
206
+ _elementAspectExportProcessor;
207
+ /** Coordinates accepted-owner scopes and bounded group processing. */
208
+ _elementAspectExportCoordinator;
209
+ /** Coordinates accepted ElementAspect owners and bounded group processing.
210
+ * @internal
211
+ */
212
+ get elementAspectExportCoordinator() {
213
+ return this._elementAspectExportCoordinator;
214
+ }
215
+ /** Whether change traversal skips root-owned entities. */
216
+ _skipPropagateChangesToRootElements = false;
217
+ _rootElementIds = new Set([
218
+ core_common_1.IModel.rootSubjectId,
219
+ core_common_1.IModel.repositoryModelId,
220
+ core_common_1.IModel.dictionaryId,
221
+ "0xe", // The reality-data-sources modelId, skipped with root propagation.
222
+ ]);
202
223
  /** Construct a new IModelExporter
203
224
  * @param sourceDb The source IModelDb
204
225
  * @see registerHandler
205
226
  */
206
- constructor(sourceDb, elementAspectsStrategy = ExportElementAspectsWithElementsStrategy_1.ExportElementAspectsWithElementsStrategy) {
227
+ constructor(sourceDb) {
207
228
  this.sourceDb = sourceDb;
208
- this._exportElementAspectsStrategy = new elementAspectsStrategy(this.sourceDb, {
229
+ this._elementAspectExportProcessor = new ElementAspectExportProcessor_1.ElementAspectExportProcessor(this.sourceDb, {
209
230
  onExportElementMultiAspects: async (aspects) => this.handler.onExportElementMultiAspects(aspects),
210
231
  onExportElementUniqueAspect: async (aspect, isUpdate) => this.handler.onExportElementUniqueAspect(aspect, isUpdate),
211
232
  shouldExportElementAspect: async (aspect) => this.handler.shouldExportElementAspect(aspect),
212
233
  trackProgress: async () => this.trackProgress(),
213
234
  });
235
+ this._elementAspectExportCoordinator = new ElementAspectExportCoordinator_1.ElementAspectExportCoordinator(IModelExporter._elementAspectOwnerBatchSize, () => this._elementAspectExportProcessor.excludedElementAspectClassFullNames, async (elementIds) => this._elementAspectExportProcessor.exportAllElementAspects(elementIds), () => this._elementAspectExportProcessor.resetCaches());
214
236
  }
215
237
  /**
216
238
  * Initialize prerequisites of exporting. This is implicitly done by any `export*` calls that need initialization
@@ -228,7 +250,7 @@ class IModelExporter {
228
250
  });
229
251
  if (this._sourceDbChanges === undefined)
230
252
  return;
231
- this._exportElementAspectsStrategy.setAspectChanges(this._sourceDbChanges.aspect);
253
+ this._elementAspectExportProcessor.setAspectChanges(this._sourceDbChanges.aspect);
232
254
  }
233
255
  /** Register the handler that will be called by IModelExporter. */
234
256
  registerHandler(handler) {
@@ -252,7 +274,7 @@ class IModelExporter {
252
274
  }
253
275
  /** Add a rule to exclude all ElementAspects of a specified class. */
254
276
  excludeElementAspectClass(classFullName) {
255
- this._exportElementAspectsStrategy.excludeElementAspectClass(classFullName);
277
+ this._elementAspectExportProcessor.excludeElementAspectClass(classFullName);
256
278
  }
257
279
  /** Add a rule to exclude all Relationships of a specified class. */
258
280
  excludeRelationshipClass(classFullName) {
@@ -265,12 +287,12 @@ class IModelExporter {
265
287
  await this.initialize({});
266
288
  await this.exportCodeSpecs();
267
289
  await this.exportFonts();
268
- await this.exportModel(core_common_1.IModel.repositoryModelId);
290
+ await this._elementAspectExportCoordinator.run(async () => this.exportModel(core_common_1.IModel.repositoryModelId));
269
291
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
270
292
  }
271
293
  /** Export changes from the source iModel.
272
294
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
273
- * @throws [[ITwinError]] identified by [[IModelTransformerError.NoChangesets]] if the source iModel has no changesets and no custom changes. Call [[exportAll]] to export all content.
295
+ * @throws [[ITwinError]] with scope `@itwin/imodel-transformer` and key `no-changesets` if the source iModel has no changesets and no custom changes. Call [[exportAll]] to export all content.
274
296
  * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired
275
297
  * range and open the source iModel as of the end (inclusive) of the desired range.
276
298
  * @note the changedInstanceIds are just for this call to exportChanges, so you must continue to pass it in
@@ -307,22 +329,33 @@ class IModelExporter {
307
329
  message: "Cannot export changes because the source iModel has no changesets or custom changes. Call exportAll() to export all content.",
308
330
  });
309
331
  }
332
+ this._skipPropagateChangesToRootElements =
333
+ initOpts.skipPropagateChangesToRootElements ?? false;
310
334
  // _sourceDbChanges are initialized in this.initialize
311
335
  (0, node_assert_1.strict)(this._sourceDbChanges !== undefined, "sourceDbChanges must be initialized.");
312
336
  await this.exportCodeSpecs();
313
337
  await this.exportFonts();
314
- if (initOpts.skipPropagateChangesToRootElements) {
315
- // The root Subject is in the RepositoryModel. Traverse its children
316
- // separately, then export other top-level repository elements while
317
- // excluding the root so no element is visited twice.
318
- await this.exportChildElements(core_common_1.IModel.rootSubjectId);
319
- await this.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true);
320
- await this.exportSubModels(core_common_1.IModel.repositoryModelId);
338
+ await this._elementAspectExportCoordinator.run(async () => {
339
+ if (initOpts.skipPropagateChangesToRootElements) {
340
+ // The root Subject is in the RepositoryModel. Traverse its children
341
+ // separately, then export other top-level repository elements while
342
+ // excluding the root so no element is visited twice.
343
+ await this.exportChildElements(core_common_1.IModel.rootSubjectId);
344
+ await this.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true);
345
+ await this.exportSubModels(core_common_1.IModel.repositoryModelId);
346
+ }
347
+ else {
348
+ await this.exportModel(core_common_1.IModel.repositoryModelId);
349
+ }
350
+ });
351
+ const aspectOnlyOwnerElementIds = new Set(this._sourceDbChanges.aspectOwnerElementIds);
352
+ for (const elementId of this._sourceDbChanges.element.insertIds) {
353
+ aspectOnlyOwnerElementIds.delete(elementId);
321
354
  }
322
- else {
323
- await this.exportModel(core_common_1.IModel.repositoryModelId);
355
+ for (const elementId of this._sourceDbChanges.element.updateIds) {
356
+ aspectOnlyOwnerElementIds.delete(elementId);
324
357
  }
325
- await this.exportAllAspects();
358
+ await this.exportAspectsForOwners(await this.filterOwnerElementIdsForAspectExport(aspectOnlyOwnerElementIds));
326
359
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
327
360
  // handle deletes
328
361
  if (this.visitElements) {
@@ -358,8 +391,10 @@ class IModelExporter {
358
391
  // You can counteract the obvious impact of losing this expensive data by always calling
359
392
  // exportChanges with the [[ExportChangesOptions.changedInstanceIds]] option set to
360
393
  // whatever you want
361
- if (this._resetChangeDataOnExport)
394
+ if (this._resetChangeDataOnExport) {
362
395
  this._sourceDbChanges = undefined;
396
+ this._elementAspectExportProcessor.setAspectChanges(undefined);
397
+ }
363
398
  }
364
399
  _resetChangeDataOnExport = true;
365
400
  /** Export schemas from the source iModel.
@@ -518,6 +553,9 @@ class IModelExporter {
518
553
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
519
554
  */
520
555
  async exportModel(modeledElementId) {
556
+ return this.runScopedElementExport(async () => this.exportModelImpl(modeledElementId));
557
+ }
558
+ async exportModelImpl(modeledElementId) {
521
559
  const model = this.sourceDb.models.getModel(modeledElementId);
522
560
  if (model.isTemplate && !this.wantTemplateModels) {
523
561
  return;
@@ -562,6 +600,9 @@ class IModelExporter {
562
600
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
563
601
  */
564
602
  async exportModelContents(modelId, elementClassFullName = core_backend_1.Element.classFullName, skipRootSubject) {
603
+ return this.runScopedElementExport(async () => this.exportModelContentsImpl(modelId, elementClassFullName, skipRootSubject));
604
+ }
605
+ async exportModelContentsImpl(modelId, elementClassFullName, skipRootSubject) {
565
606
  if (skipRootSubject) {
566
607
  // NOTE: IModelTransformer.processAll should skip the root Subject since it is specific to the individual iModel and is not part of the changes that need to be synchronized
567
608
  // NOTE: IModelExporter.exportAll should not skip the root Subject since the goal is to export everything
@@ -601,6 +642,9 @@ class IModelExporter {
601
642
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
602
643
  */
603
644
  async exportSubModels(parentModelId) {
645
+ return this.runScopedElementExport(async () => this.exportSubModelsImpl(parentModelId));
646
+ }
647
+ async exportSubModelsImpl(parentModelId) {
604
648
  core_bentley_1.Logger.logTrace(loggerCategory, `exportSubModels(${parentModelId})`);
605
649
  const definitionModelIds = [];
606
650
  const otherModelIds = [];
@@ -635,11 +679,10 @@ class IModelExporter {
635
679
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Id`);
636
680
  return false;
637
681
  }
638
- if (element instanceof core_backend_1.GeometricElement) {
639
- if (this._excludedElementCategoryIds.has(element.category)) {
640
- core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Category`);
641
- return false;
642
- }
682
+ if (element instanceof core_backend_1.GeometricElement &&
683
+ this._excludedElementCategoryIds.has(element.category)) {
684
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Category`);
685
+ return false;
643
686
  }
644
687
  if (!this.wantTemplateModels &&
645
688
  element instanceof core_backend_1.RecipeDefinitionElement) {
@@ -653,12 +696,19 @@ class IModelExporter {
653
696
  }
654
697
  }
655
698
  // element has passed standard exclusion rules, now give handler a chance to accept/reject
656
- return this.handler.shouldExportElement(element);
699
+ if (!(await this.handler.shouldExportElement(element))) {
700
+ return false;
701
+ }
702
+ return true;
657
703
  }
658
- /** Export the specified element, its child elements (if applicable), and any owned ElementAspects.
704
+ /** Export the specified element and its child elements (if applicable).
659
705
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
706
+ * @note ElementAspects are exported separately in owner-scoped groups, so their callbacks are not guaranteed to follow this element callback.
660
707
  */
661
708
  async exportElement(elementId) {
709
+ return this.runScopedElementExport(async () => this.exportElementImpl(elementId));
710
+ }
711
+ async exportElementImpl(elementId) {
662
712
  if (!this.visitElements) {
663
713
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportElement(${elementId})`);
664
714
  return;
@@ -690,7 +740,7 @@ class IModelExporter {
690
740
  await this.handler.preExportElement(element);
691
741
  await this.handler.onExportElement(element, isUpdate);
692
742
  await this.trackProgress();
693
- await this._exportElementAspectsStrategy.exportElementAspectsForElement(elementId);
743
+ await this._elementAspectExportCoordinator.addAcceptedOwner(elementId);
694
744
  return this.exportChildElements(elementId);
695
745
  }
696
746
  else {
@@ -701,6 +751,9 @@ class IModelExporter {
701
751
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
702
752
  */
703
753
  async exportChildElements(elementId) {
754
+ return this.runScopedElementExport(async () => this.exportChildElementsImpl(elementId));
755
+ }
756
+ async exportChildElementsImpl(elementId) {
704
757
  if (!this.visitElements) {
705
758
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportChildElements(${elementId})`);
706
759
  return;
@@ -713,10 +766,212 @@ class IModelExporter {
713
766
  }
714
767
  }
715
768
  }
716
- /** Exports all aspects present in the iModel.
717
- */
718
- async exportAllAspects() {
719
- return this._exportElementAspectsStrategy.exportAllElementAspects();
769
+ /** Exports all aspects owned by the supplied elements. */
770
+ async exportAspectsForOwners(ownerElementIds) {
771
+ return this._elementAspectExportCoordinator.exportOwners(ownerElementIds);
772
+ }
773
+ async filterOwnerElementIdsForAspectExport(ownerElementIds) {
774
+ if (ownerElementIds.size === 0)
775
+ return new Set();
776
+ const elementFacts = await this.queryAspectElementHierarchyFacts(ownerElementIds);
777
+ const modelFacts = await this.queryAspectModelHierarchyFacts(new Set([...elementFacts.values()].map((elementFact) => elementFact.modelId)));
778
+ await this.queryAspectElementFacts(new Set(modelFacts.keys()), elementFacts);
779
+ const elementIds = new Set();
780
+ for (const elementId of ownerElementIds) {
781
+ if (this._skipPropagateChangesToRootElements &&
782
+ this._rootElementIds.has(elementId)) {
783
+ continue;
784
+ }
785
+ if (await this.shouldExportElementHierarchyForAspect(elementId, elementFacts, modelFacts)) {
786
+ elementIds.add(elementId);
787
+ }
788
+ }
789
+ return elementIds;
790
+ }
791
+ async shouldExportElementHierarchyForAspect(elementId, elementFacts, modelFacts) {
792
+ let elementFact = elementFacts.get(elementId);
793
+ while (elementFact !== undefined) {
794
+ if (!(await this.shouldExportElementForAspect(elementFact.id, elementFact)))
795
+ return false;
796
+ if (!(await this.shouldExportModelForAspect(elementFact.modelId, elementFacts, modelFacts))) {
797
+ return false;
798
+ }
799
+ const parentId = elementFact.parentId;
800
+ if (parentId === undefined ||
801
+ (this._skipPropagateChangesToRootElements &&
802
+ parentId === core_common_1.IModel.rootSubjectId)) {
803
+ return true;
804
+ }
805
+ elementFact = elementFacts.get(parentId);
806
+ }
807
+ return false;
808
+ }
809
+ async shouldExportModelForAspect(modelId, elementFacts, modelFacts) {
810
+ const modelFact = modelFacts.get(modelId);
811
+ if (modelFact === undefined) {
812
+ this.sourceDb.models.getModel(modelId);
813
+ return false;
814
+ }
815
+ if (modelFact.isTemplate && !this.wantTemplateModels)
816
+ return false;
817
+ if (modelFact.id !== core_common_1.IModel.repositoryModelId &&
818
+ modelFact.id !== core_common_1.IModel.dictionaryId &&
819
+ modelFact.id !== "0xe" &&
820
+ !(await this.shouldExportElementForAspect(modelFact.id, elementFacts.get(modelFact.id)))) {
821
+ return false;
822
+ }
823
+ if (modelFact.parentModelId === undefined ||
824
+ modelFact.parentModelId === modelFact.id) {
825
+ return true;
826
+ }
827
+ return this.shouldExportModelForAspect(modelFact.parentModelId, elementFacts, modelFacts);
828
+ }
829
+ async runScopedElementExport(exportElements) {
830
+ await this._elementAspectExportCoordinator.run(exportElements);
831
+ }
832
+ /** Apply the element export filter when deciding whether to process an aspect owner. @internal */
833
+ async shouldExportElementForAspect(elementId, elementFact) {
834
+ if (!this.visitElements)
835
+ return false;
836
+ if (this.shouldExportElement ===
837
+ IModelExporter.prototype.shouldExportElement &&
838
+ elementFact !== undefined &&
839
+ !this.shouldExportElementFact(elementFact)) {
840
+ return false;
841
+ }
842
+ const element = this.sourceDb.elements.getElement({
843
+ id: elementId,
844
+ wantGeometry: false,
845
+ wantBRepData: false,
846
+ });
847
+ return this.shouldExportElement(element);
848
+ }
849
+ shouldExportElementFact(elementFact) {
850
+ if (this._excludedElementIds.has(elementFact.id)) {
851
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by Id`);
852
+ return false;
853
+ }
854
+ const elementClass = this.sourceDb.getJsClass(elementFact.classFullName);
855
+ if ((elementFact.classFullName === core_backend_1.GeometricElement.classFullName ||
856
+ elementClass.prototype instanceof core_backend_1.GeometricElement) &&
857
+ elementFact.categoryId !== undefined &&
858
+ this._excludedElementCategoryIds.has(elementFact.categoryId)) {
859
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by Category`);
860
+ return false;
861
+ }
862
+ if (!this.wantTemplateModels &&
863
+ (elementFact.classFullName === core_backend_1.RecipeDefinitionElement.classFullName ||
864
+ elementClass.prototype instanceof core_backend_1.RecipeDefinitionElement)) {
865
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded RecipeDefinitionElement ${elementFact.id} because wantTemplate=false`);
866
+ return false;
867
+ }
868
+ for (const excludedElementClass of this._excludedElementClasses) {
869
+ if (elementFact.classFullName === excludedElementClass.classFullName ||
870
+ elementClass.prototype instanceof excludedElementClass) {
871
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by class: ${excludedElementClass.classFullName}`);
872
+ return false;
873
+ }
874
+ }
875
+ return true;
876
+ }
877
+ async queryAspectElementHierarchyFacts(elementIds) {
878
+ const elementFacts = new Map();
879
+ const queriedElementIds = new Set();
880
+ let pendingElementIds = new Set(elementIds);
881
+ while (pendingElementIds.size > 0) {
882
+ const currentElementIds = pendingElementIds;
883
+ pendingElementIds = new Set();
884
+ await this.queryAspectElementFacts(currentElementIds, elementFacts);
885
+ for (const elementId of currentElementIds)
886
+ queriedElementIds.add(elementId);
887
+ for (const elementId of currentElementIds) {
888
+ const parentId = elementFacts.get(elementId)?.parentId;
889
+ if (parentId !== undefined &&
890
+ !queriedElementIds.has(parentId) &&
891
+ !elementFacts.has(parentId)) {
892
+ pendingElementIds.add(parentId);
893
+ }
894
+ }
895
+ }
896
+ return elementFacts;
897
+ }
898
+ async queryAspectElementFacts(elementIds, elementFacts) {
899
+ const unqueriedElementIds = new Set([...elementIds].filter((elementId) => !elementFacts.has(elementId)));
900
+ if (unqueriedElementIds.size === 0)
901
+ return;
902
+ for (const elementIdBatch of this.getAspectOwnerIdBatches(unqueriedElementIds)) {
903
+ const queryParams = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIdBatch);
904
+ for await (const row of this.sourceDb.createQueryReader(`SELECT e.ECInstanceId id, e.Parent.Id parentId, e.Model.Id modelId,
905
+ ec_className(e.ECClassId, 's') schemaName,
906
+ ec_className(e.ECClassId, 'c') className
907
+ FROM bis.Element e
908
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = e.ECInstanceId
909
+ OPTIONS ENABLE_EXPERIMENTAL_FEATURES`, queryParams, { usePrimaryConn: true })) {
910
+ elementFacts.set(row.id, {
911
+ id: row.id,
912
+ parentId: row.parentId ?? undefined,
913
+ modelId: row.modelId,
914
+ classFullName: `${row.schemaName}:${row.className}`,
915
+ });
916
+ }
917
+ if (this._excludedElementCategoryIds.size > 0) {
918
+ const categoryQueryParams = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIdBatch);
919
+ for await (const row of this.sourceDb.createQueryReader(`SELECT g.ECInstanceId id, g.Category.Id categoryId
920
+ FROM bis.GeometricElement g
921
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = g.ECInstanceId
922
+ OPTIONS ENABLE_EXPERIMENTAL_FEATURES`, categoryQueryParams, { usePrimaryConn: true })) {
923
+ const elementFact = elementFacts.get(row.id);
924
+ if (elementFact !== undefined) {
925
+ elementFact.categoryId = row.categoryId;
926
+ }
927
+ }
928
+ }
929
+ }
930
+ }
931
+ async queryAspectModelHierarchyFacts(modelIds) {
932
+ const modelFacts = new Map();
933
+ const queriedModelIds = new Set();
934
+ let pendingModelIds = new Set(modelIds);
935
+ while (pendingModelIds.size > 0) {
936
+ const currentModelIds = pendingModelIds;
937
+ pendingModelIds = new Set();
938
+ for (const modelIdBatch of this.getAspectOwnerIdBatches(currentModelIds)) {
939
+ const queryParams = new core_common_1.QueryBinder().bindIdSet("modelIds", modelIdBatch);
940
+ for await (const row of this.sourceDb.createQueryReader(`SELECT model.ECInstanceId id, model.ParentModel.Id parentModelId, model.IsTemplate isTemplate
941
+ FROM bis.Model model
942
+ INNER JOIN IdSet(:modelIds) ids ON ids.id = model.ECInstanceId
943
+ OPTIONS ENABLE_EXPERIMENTAL_FEATURES`, queryParams, { usePrimaryConn: true })) {
944
+ modelFacts.set(row.id, {
945
+ id: row.id,
946
+ parentModelId: row.parentModelId ?? undefined,
947
+ isTemplate: row.isTemplate === true || row.isTemplate === 1,
948
+ });
949
+ }
950
+ }
951
+ for (const modelId of currentModelIds)
952
+ queriedModelIds.add(modelId);
953
+ for (const modelId of currentModelIds) {
954
+ const parentModelId = modelFacts.get(modelId)?.parentModelId;
955
+ if (parentModelId !== undefined &&
956
+ !queriedModelIds.has(parentModelId) &&
957
+ !modelFacts.has(parentModelId)) {
958
+ pendingModelIds.add(parentModelId);
959
+ }
960
+ }
961
+ }
962
+ return modelFacts;
963
+ }
964
+ *getAspectOwnerIdBatches(ids) {
965
+ let batch = new Set();
966
+ for (const id of ids) {
967
+ batch.add(id);
968
+ if (batch.size === IModelExporter._elementAspectOwnerBatchSize) {
969
+ yield batch;
970
+ batch = new Set();
971
+ }
972
+ }
973
+ if (batch.size > 0)
974
+ yield batch;
720
975
  }
721
976
  /** Exports all relationships that subclass from the specified base class.
722
977
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
@@ -829,6 +1084,13 @@ class ChangedInstanceIds {
829
1084
  _aspectSubclassIds;
830
1085
  _relationshipSubclassIds;
831
1086
  _relationshipSubclassIdsToSkip;
1087
+ _aspectOwnerElementIds = new Set();
1088
+ /** Element IDs that own the aspects represented by `aspect` changes.
1089
+ * @internal
1090
+ */
1091
+ get aspectOwnerElementIds() {
1092
+ return this._aspectOwnerElementIds;
1093
+ }
832
1094
  _db;
833
1095
  constructor(db) {
834
1096
  this._db = db;
@@ -923,13 +1185,31 @@ class ChangedInstanceIds {
923
1185
  this.handleChange(this.relationship, changeType, change.ECInstanceId);
924
1186
  else if (this.isCodeSpec(ecClassId))
925
1187
  this.handleChange(this.codeSpec, changeType, change.ECInstanceId);
926
- else if (this.isAspect(ecClassId))
1188
+ else if (this.isAspect(ecClassId)) {
1189
+ const ownerElementId = change.Element?.Id ??
1190
+ this.tryGetAspectOwnerElementId(change.ECInstanceId);
1191
+ if (ownerElementId !== undefined) {
1192
+ this._aspectOwnerElementIds.add(ownerElementId);
1193
+ }
927
1194
  this.handleChange(this.aspect, changeType, change.ECInstanceId);
1195
+ }
928
1196
  else if (this.isModel(ecClassId))
929
1197
  this.handleChange(this.model, changeType, change.ECInstanceId);
930
1198
  else if (this.isElement(ecClassId))
931
1199
  this.handleChange(this.element, changeType, change.ECInstanceId);
932
1200
  }
1201
+ tryGetAspectOwnerElementId(aspectId) {
1202
+ try {
1203
+ return this._db.elements.getAspect(aspectId).element.id;
1204
+ }
1205
+ catch (error) {
1206
+ if (error instanceof core_common_1.IModelError &&
1207
+ error.errorNumber === core_bentley_1.IModelStatus.NotFound) {
1208
+ return undefined;
1209
+ }
1210
+ throw error;
1211
+ }
1212
+ }
933
1213
  /**
934
1214
  * This method should only be called inside [[IModelTransformer.addCustomChanges]].
935
1215
  * It adds the provided change to the element changes maintained by this instance of ChangedInstanceIds.
@@ -991,14 +1271,47 @@ class ChangedInstanceIds {
991
1271
  * If the same ECInstanceId is seen multiple times, the changedInstanceIds will be modified accordingly, i.e. if an id 'x' was updated but now we see 'x' was deleted, we will remove 'x'
992
1272
  * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
993
1273
  * @note It is the responsibility of the caller to ensure that the provided id is, in fact an aspect.
1274
+ * @param elementIds Owning element IDs. Required when `changeType` is
1275
+ * `Deleted` or the source aspect row is unavailable.
994
1276
  * @note In most cases, this method does not need to be called. Its only for consumers to mimic changes as if they were found in a changeset, which should only be useful in certain cases such as the changing of filter criteria for a preexisting master branch relationship.
995
1277
  * @beta
996
1278
  */
997
- addCustomAspectChange(changeType, ids) {
1279
+ addCustomAspectChange(changeType, ids, elementIds) {
1280
+ if (elementIds !== undefined) {
1281
+ for (const elementId of core_bentley_1.Id64.iterable(elementIds)) {
1282
+ this._aspectOwnerElementIds.add(elementId);
1283
+ }
1284
+ }
998
1285
  for (const id of core_bentley_1.Id64.iterable(ids)) {
1286
+ if (elementIds === undefined && changeType === "Deleted") {
1287
+ core_bentley_1.ITwinError.throwError({
1288
+ iTwinErrorId: {
1289
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1290
+ key: IModelTransformerError_1.IModelTransformerError.AspectOwnerRequired,
1291
+ },
1292
+ message: "Custom deleted ElementAspect changes require the owning element ID.",
1293
+ });
1294
+ }
1295
+ if (elementIds === undefined && changeType !== "Deleted") {
1296
+ const ownerElementId = this.tryGetAspectOwnerElementId(id);
1297
+ if (ownerElementId === undefined) {
1298
+ core_bentley_1.ITwinError.throwError({
1299
+ iTwinErrorId: {
1300
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1301
+ key: IModelTransformerError_1.IModelTransformerError.AspectOwnerRequired,
1302
+ },
1303
+ message: "Custom ElementAspect changes require the owning element ID when the source aspect is unavailable.",
1304
+ });
1305
+ }
1306
+ this._aspectOwnerElementIds.add(ownerElementId);
1307
+ }
999
1308
  this.handleChange(this.aspect, changeType, id);
1000
1309
  }
1001
1310
  }
1311
+ recordCustomAspectChange(changeType, aspectId, ownerElementId) {
1312
+ this._aspectOwnerElementIds.add(ownerElementId);
1313
+ this.handleChange(this.aspect, changeType, aspectId);
1314
+ }
1002
1315
  /**
1003
1316
  * There is an optimization in [IModelExporter.exportModelContents] which doesn't try to export elements within a model unless the model itself is marked as `Updated` or 'Inserted' in sourceDbChanges. This method is used in [[addCustomElementChange]] and [[addCustomModelChange]] to add the parent model hierarchy to the 'updatedIds' so that the custom element changes are exported.
1004
1317
  * Transformer will insert 'Updated' model to target if it does not exist there already. To handle such case, modeled elements of parent models are also marked as updated. This is done, because model can not be inserted without it's modeled element.
@@ -1007,13 +1320,16 @@ class ChangedInstanceIds {
1007
1320
  const params = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
1008
1321
  const ecQuery = `
1009
1322
  WITH RECURSIVE hierarchy (parentId) AS (
1010
- SELECT Model.Id FROM bis.Element WHERE InVirtualSet(:elementIds, ECInstanceId)
1323
+ SELECT element.Model.Id
1324
+ FROM bis.Element element
1325
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = element.ECInstanceId
1011
1326
  UNION
1012
1327
  SELECT ParentModel.id
1013
1328
  FROM bis.Model e
1014
1329
  INNER JOIN hierarchy h ON h.parentId = e.ECInstanceId
1015
1330
  )
1016
1331
  SELECT parentId FROM hierarchy where parentId is not null
1332
+ OPTIONS ENABLE_EXPERIMENTAL_FEATURES
1017
1333
  `;
1018
1334
  const parentModelIds = new Set();
1019
1335
  for await (const row of this._db.createQueryReader(ecQuery, params, {
@@ -1032,9 +1348,16 @@ class ChangedInstanceIds {
1032
1348
  return parentModelIds;
1033
1349
  }
1034
1350
  async markElementRelationshipsAsInserted(relationshipClassName, elementIds) {
1035
- const ecQuery = `SELECT ECInstanceId FROM ${relationshipClassName}
1036
- WHERE InVirtualSet(:elementIds, TargetECInstanceId)
1037
- OR InVirtualSet(:elementIds, SourceECInstanceId)`;
1351
+ const ecQuery = `SELECT relationship.ECInstanceId
1352
+ FROM ${relationshipClassName} relationship
1353
+ INNER JOIN IdSet(:elementIds) ids
1354
+ ON ids.id = relationship.TargetECInstanceId
1355
+ UNION
1356
+ SELECT relationship.ECInstanceId
1357
+ FROM ${relationshipClassName} relationship
1358
+ INNER JOIN IdSet(:elementIds) ids
1359
+ ON ids.id = relationship.SourceECInstanceId
1360
+ OPTIONS ENABLE_EXPERIMENTAL_FEATURES`;
1038
1361
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
1039
1362
  const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
1040
1363
  usePrimaryConn: true,
@@ -1048,13 +1371,17 @@ class ChangedInstanceIds {
1048
1371
  core_backend_1.ElementUniqueAspect.classFullName,
1049
1372
  core_backend_1.ElementMultiAspect.classFullName,
1050
1373
  ]) {
1051
- const ecQuery = `Select ECInstanceId from ${aspectClassName} where InVirtualSet(:elementIds, Element.Id)`;
1374
+ const ecQuery = `SELECT aspect.ECInstanceId, aspect.Element.Id
1375
+ FROM ${aspectClassName} aspect
1376
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = aspect.Element.Id
1377
+ OPTIONS ENABLE_EXPERIMENTAL_FEATURES`;
1052
1378
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
1053
1379
  const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
1054
1380
  usePrimaryConn: true,
1055
1381
  });
1056
1382
  for await (const row of queryReader) {
1057
- this.addCustomAspectChange("Inserted", row.toArray()[0]);
1383
+ const [aspectId, elementId] = row.toArray();
1384
+ this.recordCustomAspectChange("Inserted", aspectId, elementId);
1058
1385
  }
1059
1386
  }
1060
1387
  }