@itwin/imodel-transformer 1.0.0-dev.9 → 1.0.1-customchanges.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.
@@ -55,7 +55,9 @@ class IModelExportHandler {
55
55
  shouldExportElement(_element) {
56
56
  return true;
57
57
  }
58
- /** Called when element is skipped instead of exported. */
58
+ /** Called when element is skipped instead of exported.
59
+ * @note When an element is skipped, exporter will not export any of its child elements. Because of this, [[onSkipElement]] will not be invoked for any children of a "skipped" element.
60
+ */
59
61
  onSkipElement(_elementId) { }
60
62
  /** Called when an element should be exported.
61
63
  * @param element The element to export
@@ -130,7 +132,7 @@ exports.IModelExportHandler = IModelExportHandler;
130
132
  class IModelExporter {
131
133
  /**
132
134
  * Retrieve the cached entity change information.
133
- * @note This will only be initialized after [IModelExporter.exportChanges] is invoked.
135
+ * @note This will only be initialized after [IModelExporter.exportChanges] is invoked or [IModelExporter.initialize] is called.
134
136
  */
135
137
  get sourceDbChanges() {
136
138
  return this._sourceDbChanges;
@@ -214,6 +216,16 @@ class IModelExporter {
214
216
  return;
215
217
  this._exportElementAspectsStrategy.setAspectChanges(this._sourceDbChanges.aspect);
216
218
  }
219
+ /**
220
+ * This function is called by the transformer as it is about to process the changesets passed to it in [[IModelTransformOptions.argsForProcessChanges]].
221
+ * This would be after the exporter has already processed the same set of changesets passed to the transformer in [[IModelTransformOptions.argsForProcessChanges]].
222
+ * This function should be used to modify the exporter's sourceDbChanges, if necessary, using [[ChangedInstanceIds.addCustomChange]]. See [[ChangedInstanceIds.addCustomChange]] for more information.
223
+ * @note [[IModelExporter.sourceDbChanges]] will only be defined if the transformer was called with [[IModelTransformOptions.argsForProcessChanges]].
224
+ * @note If defined, sourceDbChanges will already be populated with the changesets passed to the transformer, if any when this function is called by the transformer.
225
+ * @note The transformer will have built up the remap table between the source and target iModels before calling this function. This means that functions like [[IModelTransformer.context.findTargetElementId]] will return meaningful results.
226
+ * @note Its expected that this function be overridden by a subclass of exporter if it needs to modify sourceDbChanges.
227
+ */
228
+ addCustomChanges() { }
217
229
  /** Register the handler that will be called by IModelExporter. */
218
230
  registerHandler(handler) {
219
231
  this._handler = handler;
@@ -252,19 +264,25 @@ class IModelExporter {
252
264
  await this.exportModel(core_common_1.IModel.repositoryModelId);
253
265
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
254
266
  }
255
- async exportChanges(accessTokenOrOpts, startChangesetId) {
267
+ /** Export changes from the source iModel.
268
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
269
+ * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired
270
+ * range and open the source iModel as of the end (inclusive) of the desired range.
271
+ * @note the changedInstanceIds are just for this call to exportChanges, so you must continue to pass it in
272
+ * for consecutive calls
273
+ */
274
+ async exportChanges(args) {
256
275
  if (!this.sourceDb.isBriefcaseDb())
257
276
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Must be a briefcase to export changes");
258
277
  if ("" === this.sourceDb.changeset.id) {
259
278
  await this.exportAll(); // no changesets, so revert to exportAll
260
279
  return;
261
280
  }
262
- const initOpts = typeof accessTokenOrOpts === "object"
263
- ? accessTokenOrOpts
264
- : {
265
- accessToken: accessTokenOrOpts,
266
- startChangeset: { id: startChangesetId },
267
- };
281
+ const startChangeset = // TODO: This is weird.. why is this needed? I suspect we can remove this and just pass args to initialize?
282
+ args && "startChangeset" in args ? args.startChangeset : undefined;
283
+ const initOpts = {
284
+ startChangeset: { id: startChangeset?.id },
285
+ };
268
286
  await this.initialize(initOpts);
269
287
  // _sourceDbChanges are initialized in this.initialize
270
288
  nodeAssert(this._sourceDbChanges !== undefined, "sourceDbChanges must be initialized.");
@@ -377,7 +395,7 @@ class IModelExporter {
377
395
  async exportCodeSpecByName(codeSpecName) {
378
396
  const codeSpec = this.sourceDb.codeSpecs.getByName(codeSpecName);
379
397
  let isUpdate;
380
- if (undefined !== this._sourceDbChanges) {
398
+ if (this._sourceDbChanges !== undefined) {
381
399
  // is changeset information available?
382
400
  if (this._sourceDbChanges.codeSpec.insertIds.has(codeSpec.id)) {
383
401
  isUpdate = false;
@@ -469,7 +487,7 @@ class IModelExporter {
469
487
  /** Export the model (the container only) from the source iModel. */
470
488
  async exportModelContainer(model) {
471
489
  let isUpdate;
472
- if (undefined !== this._sourceDbChanges) {
490
+ if (this._sourceDbChanges !== undefined) {
473
491
  // is changeset information available?
474
492
  if (this._sourceDbChanges.model.insertIds.has(model.id)) {
475
493
  isUpdate = false;
@@ -500,7 +518,7 @@ class IModelExporter {
500
518
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportModelContents(${modelId})`);
501
519
  return;
502
520
  }
503
- if (undefined !== this._sourceDbChanges) {
521
+ if (this._sourceDbChanges !== undefined) {
504
522
  // is changeset information available?
505
523
  if (!this._sourceDbChanges.model.insertIds.has(modelId) &&
506
524
  !this._sourceDbChanges.model.updateIds.has(modelId)) {
@@ -674,7 +692,7 @@ class IModelExporter {
674
692
  return;
675
693
  }
676
694
  let isUpdate;
677
- if (undefined !== this._sourceDbChanges) {
695
+ if (this._sourceDbChanges !== undefined) {
678
696
  // is changeset information available?
679
697
  if (this._sourceDbChanges.relationship.insertIds.has(relInstanceId)) {
680
698
  isUpdate = false;
@@ -711,7 +729,7 @@ class IModelExporter {
711
729
  }
712
730
  exports.IModelExporter = IModelExporter;
713
731
  /** Class for holding change information.
714
- * @beta
732
+ * @public
715
733
  */
716
734
  class ChangedInstanceOps {
717
735
  constructor() {
@@ -730,11 +748,16 @@ class ChangedInstanceOps {
730
748
  val.delete.forEach((id) => this.deleteIds.add(id));
731
749
  }
732
750
  }
751
+ get isEmpty() {
752
+ return (0 === this.insertIds.size &&
753
+ 0 === this.updateIds.size &&
754
+ 0 === this.deleteIds.size);
755
+ }
733
756
  }
734
757
  exports.ChangedInstanceOps = ChangedInstanceOps;
735
758
  /**
736
759
  * Class for discovering modified elements between 2 versions of an iModel.
737
- * @beta
760
+ * @public
738
761
  */
739
762
  class ChangedInstanceIds {
740
763
  constructor(db) {
@@ -745,6 +768,8 @@ class ChangedInstanceIds {
745
768
  this.relationship = new ChangedInstanceOps();
746
769
  this.font = new ChangedInstanceOps();
747
770
  this._db = db;
771
+ this._hasCustomChanges = false;
772
+ this._entityReferenceToCustomDataMap = new Map();
748
773
  }
749
774
  async setupECClassIds() {
750
775
  this._codeSpecSubclassIds = new Set();
@@ -752,9 +777,12 @@ class ChangedInstanceIds {
752
777
  this._elementSubclassIds = new Set();
753
778
  this._aspectSubclassIds = new Set();
754
779
  this._relationshipSubclassIds = new Set();
780
+ this._relationshipSubclassIdsToSkip = new Set();
781
+ this._ecClassIdsToClassFullNames = new Map();
755
782
  const addECClassIdsToSet = async (setToModify, baseClass) => {
756
- for await (const row of this._db.createQueryReader(`SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (${baseClass})`)) {
757
- setToModify.add(row.ECInstanceId);
783
+ for await (const row of this._db.createQueryReader(`SELECT c.ECInstanceId ECClassId, c.Name className, s.Name schemaName FROM ECDbMeta.ECClassDef c JOIN ECDbMeta.ECSchemaDef s ON s.ECInstanceId = c.Schema.Id WHERE c.ECInstanceId IS (${baseClass})`)) {
784
+ setToModify.add(row.ECClassId);
785
+ this._ecClassIdsToClassFullNames?.set(row.ECClassId, `${row.schemaName}:${row.className}`);
758
786
  }
759
787
  };
760
788
  const promises = [
@@ -764,6 +792,7 @@ class ChangedInstanceIds {
764
792
  addECClassIdsToSet(this._aspectSubclassIds, "BisCore.ElementUniqueAspect"),
765
793
  addECClassIdsToSet(this._aspectSubclassIds, "BisCore.ElementMultiAspect"),
766
794
  addECClassIdsToSet(this._relationshipSubclassIds, "BisCore.ElementRefersToElements"),
795
+ addECClassIdsToSet(this._relationshipSubclassIdsToSkip, "BisCore.ElementDrivesElement"),
767
796
  ];
768
797
  await Promise.all(promises);
769
798
  }
@@ -772,7 +801,8 @@ class ChangedInstanceIds {
772
801
  this._modelSubclassIds &&
773
802
  this._elementSubclassIds &&
774
803
  this._aspectSubclassIds &&
775
- this._relationshipSubclassIds);
804
+ this._relationshipSubclassIds &&
805
+ this._relationshipSubclassIdsToSkip);
776
806
  }
777
807
  isRelationship(ecClassId) {
778
808
  return this._relationshipSubclassIds?.has(ecClassId);
@@ -789,6 +819,17 @@ class ChangedInstanceIds {
789
819
  isElement(ecClassId) {
790
820
  return this._elementSubclassIds?.has(ecClassId);
791
821
  }
822
+ get hasCustomChanges() {
823
+ return this._hasCustomChanges;
824
+ }
825
+ get isEmpty() {
826
+ return (this.codeSpec.isEmpty &&
827
+ this.model.isEmpty &&
828
+ this.element.isEmpty &&
829
+ this.aspect.isEmpty &&
830
+ this.relationship.isEmpty &&
831
+ this.font.isEmpty);
832
+ }
792
833
  /**
793
834
  * Adds the provided [[ChangedECInstance]] to the appropriate set of changes by class type (codeSpec, model, element, aspect, or relationship) maintained by this instance of ChangedInstanceIds.
794
835
  * 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'
@@ -804,6 +845,8 @@ class ChangedInstanceIds {
804
845
  const changeType = change.$meta?.op;
805
846
  if (changeType === undefined)
806
847
  throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
848
+ if (this._relationshipSubclassIdsToSkip?.has(ecClassId))
849
+ return;
807
850
  if (this.isRelationship(ecClassId))
808
851
  this.handleChange(this.relationship, changeType, change.ECInstanceId);
809
852
  else if (this.isCodeSpec(ecClassId))
@@ -815,6 +858,104 @@ class ChangedInstanceIds {
815
858
  else if (this.isElement(ecClassId))
816
859
  this.handleChange(this.element, changeType, change.ECInstanceId);
817
860
  }
861
+ /**
862
+ * Adds the provided change to the element changes maintained by this instance of ChangedInstanceIds
863
+ * 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'
864
+ * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
865
+ * @note element changes will also cause the element's model to be marked as updated in [[ChangedInstanceIds.model]], so that the element does not get skipped by the transformer.
866
+ * @note It is the responsibility of the caller to ensure that the provided id is, in fact an element.
867
+ * @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.
868
+ */
869
+ addCustomElementChange(changeType, id // TODO: Support bulk adds
870
+ ) {
871
+ // if delete unnecessary?
872
+ this.addModelToUpdated(id);
873
+ this.handleChange(this.element, changeType, id);
874
+ }
875
+ /**
876
+ * Adds the provided change to the codespec changes maintained by this instance of ChangedInstanceIds
877
+ * 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'
878
+ * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
879
+ * @note It is the responsibility of the caller to ensure that the provided id is, in fact a codespec.
880
+ * @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.
881
+ */
882
+ addCustomCodeSpecChange(changeType, id) {
883
+ this.handleChange(this.codeSpec, changeType, id);
884
+ }
885
+ /**
886
+ * Adds the provided change to the model changes maintained by this instance of ChangedInstanceIds
887
+ * 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'
888
+ * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
889
+ * @note It is the responsibility of the caller to ensure that the provided id is, in fact a model.
890
+ * @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.
891
+ */
892
+ addCustomModelChange(changeType, id) {
893
+ this.handleChange(this.model, changeType, id);
894
+ }
895
+ /**
896
+ * Adds the provided change to the aspect changes maintained by this instance of ChangedInstanceIds
897
+ * 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'
898
+ * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
899
+ * @note It is the responsibility of the caller to ensure that the provided id is, in fact an aspect.
900
+ * @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.
901
+ */
902
+ addCustomAspectChange(changeType, id) {
903
+ this.handleChange(this.aspect, changeType, id);
904
+ }
905
+ /**
906
+ * TODO: Think more about permutations of model updated / inserted / deleted. Can you delete a model without deleting its elements?
907
+ * What if model delete but custom change si to insert element into target?
908
+ * // It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
909
+ // - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
910
+ // - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
911
+ * TODO: If the element is a custom delete we probably shouldnt be calling this?
912
+ * There is an optimization in [IModelExporter.exportModelContents] which doesn't try to export elements within a model unless the model itself is part of
913
+ * the sourceDbChanges. This method is used in addCustomChange to add the model to the updatedIds set so that the custom element changes are exported.
914
+ */
915
+ addModelToUpdated(elementId) {
916
+ const modelId = this._db.elements.getElement(elementId).model;
917
+ this.handleChange(this.model, "Updated", modelId);
918
+ }
919
+ /** TODO: Maybe relationships only? maybe not. */
920
+ getCustomRelationshipDataFromId(id, type) {
921
+ if (type === "relationship") {
922
+ return this._entityReferenceToCustomDataMap.get(core_backend_1.EntityReferences.fromEntityType(id, core_common_1.ConcreteEntityTypes.Relationship));
923
+ }
924
+ return undefined;
925
+ }
926
+ /**
927
+ * Adds the provided change to the set of relationship changes maintained by this instance of ChangedInstanceIds.
928
+ * 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'
929
+ * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
930
+ * @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.
931
+ * @throws if the ecClassId is NOT a relationship classId
932
+ * @param ecClassId class id of the custom change
933
+ * @param changeType insert, update or delete
934
+ * @param id ECInstanceID of the custom change
935
+ * @param sourceECInstanceId source ECInstanceId of the relationship
936
+ * @param targetECInstanceId target ECInstanceId of the relationship
937
+ */
938
+ async addCustomRelationshipChange(ecClassId, changeType, id, sourceECInstanceId, targetECInstanceId) {
939
+ if (!this._ecClassIdsInitialized)
940
+ await this.setupECClassIds();
941
+ if (this._relationshipSubclassIdsToSkip?.has(ecClassId))
942
+ return;
943
+ if (!this._relationshipSubclassIds?.has(ecClassId))
944
+ throw new Error(`Misuse. id: ${id}, ecClassId: ${ecClassId} is not a relationship class. Use 'addCustomChange' instead.`);
945
+ this._hasCustomChanges = true;
946
+ const classFullName = this._ecClassIdsToClassFullNames?.get(ecClassId);
947
+ (0, core_bentley_1.assert)(classFullName !== undefined); // setupECClassIds adds an entry to the above map for every single ECClassId.
948
+ this._entityReferenceToCustomDataMap.set(core_backend_1.EntityReferences.fromEntityType(id, core_common_1.ConcreteEntityTypes.Relationship), {
949
+ sourceIdOfRelationship: sourceECInstanceId,
950
+ targetIdOfRelationship: targetECInstanceId,
951
+ ecClassId,
952
+ classFullName,
953
+ });
954
+ this.handleChange(this.relationship, changeType, id);
955
+ }
956
+ getClassFullNameFromECClassId(ecClassid) {
957
+ return this._ecClassIdsToClassFullNames?.get(ecClassid);
958
+ }
818
959
  handleChange(changedInstanceOps, changeType, id) {
819
960
  // if changeType is a delete and we already have the id in the inserts then we can remove the id from the inserts.
820
961
  // if changeType is a delete and we already have the id in the updates then we can remove the id from the updates AND add it to the deletes.
@@ -839,12 +980,12 @@ class ChangedInstanceIds {
839
980
  }
840
981
  /**
841
982
  * Initializes a new ChangedInstanceIds object with information taken from a range of changesets.
983
+ * @public
842
984
  */
843
985
  static async initialize(opts) {
844
986
  if ("changedInstanceIds" in opts)
845
987
  return opts.changedInstanceIds;
846
988
  const iModelId = opts.iModel.iModelId;
847
- const accessToken = opts.accessToken;
848
989
  const startChangeset = "startChangeset" in opts ? opts.startChangeset : undefined;
849
990
  const changesetRanges = startChangeset !== undefined
850
991
  ? [
@@ -855,13 +996,11 @@ class ChangedInstanceIds {
855
996
  changeset: {
856
997
  id: startChangeset.id ?? opts.iModel.changeset.id,
857
998
  },
858
- accessToken,
859
999
  })).index,
860
1000
  opts.iModel.changeset.index ??
861
1001
  (await core_backend_1.IModelHost.hubAccess.queryChangeset({
862
1002
  iModelId,
863
1003
  changeset: { id: opts.iModel.changeset.id },
864
- accessToken,
865
1004
  })).index,
866
1005
  ],
867
1006
  ]
@@ -870,7 +1009,6 @@ class ChangedInstanceIds {
870
1009
  : undefined;
871
1010
  const csFileProps = changesetRanges !== undefined
872
1011
  ? (await Promise.all(changesetRanges.map(async ([first, end]) => core_backend_1.IModelHost.hubAccess.downloadChangesets({
873
- accessToken,
874
1012
  iModelId,
875
1013
  range: { first, end },
876
1014
  targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(iModelId),
@@ -881,10 +1019,6 @@ class ChangedInstanceIds {
881
1019
  if (csFileProps === undefined)
882
1020
  return undefined;
883
1021
  const changedInstanceIds = new ChangedInstanceIds(opts.iModel);
884
- const relationshipECClassIdsToSkip = new Set();
885
- for await (const row of opts.iModel.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
886
- relationshipECClassIdsToSkip.add(row.ECInstanceId);
887
- }
888
1022
  for (const csFile of csFileProps) {
889
1023
  const csReader = core_backend_1.SqliteChangesetReader.openFile({
890
1024
  fileName: csFile.pathname,
@@ -898,9 +1032,6 @@ class ChangedInstanceIds {
898
1032
  }
899
1033
  const changes = [...ecChangeUnifier.instances];
900
1034
  for (const change of changes) {
901
- if (change.ECClassId !== undefined &&
902
- relationshipECClassIdsToSkip.has(change.ECClassId))
903
- continue;
904
1035
  await changedInstanceIds.addChange(change);
905
1036
  }
906
1037
  csReader.close();