@itwin/core-backend 4.2.0-dev.9 → 4.3.0-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.
@@ -8,8 +8,9 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.StandaloneDb = exports.SnapshotDb = exports.BriefcaseDb = exports.IModelDb = exports.BriefcaseLocalValue = void 0;
11
- const path_1 = require("path");
12
11
  const fs = require("fs");
12
+ const path_1 = require("path");
13
+ const touch = require("touch");
13
14
  const core_bentley_1 = require("@itwin/core-bentley");
14
15
  const core_common_1 = require("@itwin/core-common");
15
16
  const core_geometry_1 = require("@itwin/core-geometry");
@@ -24,7 +25,6 @@ const CodeSpecs_1 = require("./CodeSpecs");
24
25
  const ConcurrentQuery_1 = require("./ConcurrentQuery");
25
26
  const ECSqlStatement_1 = require("./ECSqlStatement");
26
27
  const Element_1 = require("./Element");
27
- const ElementAspect_1 = require("./ElementAspect");
28
28
  const ElementGraphics_1 = require("./ElementGraphics");
29
29
  const Entity_1 = require("./Entity");
30
30
  const GeoCoordConfig_1 = require("./GeoCoordConfig");
@@ -33,6 +33,7 @@ const IModelJsFs_1 = require("./IModelJsFs");
33
33
  const IpcHost_1 = require("./IpcHost");
34
34
  const Model_1 = require("./Model");
35
35
  const Relationship_1 = require("./Relationship");
36
+ const SchemaSync_1 = require("./SchemaSync");
36
37
  const ServerBasedLocks_1 = require("./ServerBasedLocks");
37
38
  const SqliteStatement_1 = require("./SqliteStatement");
38
39
  const TxnManager_1 = require("./TxnManager");
@@ -154,12 +155,18 @@ class IModelDb extends core_common_1.IModel {
154
155
  (0, core_bentley_1.assert)(undefined !== super.iModelId);
155
156
  return super.iModelId;
156
157
  } // GuidString | undefined for the IModel superclass, but required for all IModelDb subclasses
157
- /** @internal*/
158
- get nativeDb() { return this._nativeDb; } // eslint-disable-line @typescript-eslint/no-non-null-assertion
159
158
  /** Get the full path fileName of this iModelDb
160
159
  * @note this member is only valid while the iModel is opened.
161
160
  */
162
161
  get pathName() { return this.nativeDb.getFilePath(); }
162
+ /** Get the full path to this iModel's "watch file".
163
+ * A read-only briefcase opened with `watchForChanges: true` creates this file next to the briefcase file on open, if it doesn't already exist.
164
+ * A writable briefcase "touches" this file if it exists whenever it commits changes to the briefcase.
165
+ * The read-only briefcase can use a file watcher to react when the writable briefcase makes changes to the briefcase.
166
+ * This is more reliable than watching the sqlite WAL file.
167
+ * @internal
168
+ */
169
+ get watchFilePathName() { return `${this.pathName}-watch`; }
163
170
  /** @internal */
164
171
  constructor(args) {
165
172
  super({ ...args, iTwinId: args.nativeDb.getITwinId(), iModelId: args.nativeDb.getIModelId() });
@@ -179,7 +186,7 @@ class IModelDb extends core_common_1.IModel {
179
186
  this.onChangesetApplied = new core_bentley_1.BeEvent();
180
187
  /** Event called when the iModel is about to be closed. */
181
188
  this.onBeforeClose = new core_bentley_1.BeEvent();
182
- this._nativeDb = args.nativeDb;
189
+ this.nativeDb = args.nativeDb;
183
190
  this.nativeDb.setIModelDb(this);
184
191
  this.loadSettingDictionaries();
185
192
  GeoCoordConfig_1.GeoCoordConfig.loadForImodel(this.workspace.settings); // load gcs data specified by iModel's settings dictionaries, must be done before calling initializeIModelDb
@@ -209,7 +216,6 @@ class IModelDb extends core_common_1.IModel {
209
216
  this._codeService?.close();
210
217
  this._codeService = undefined;
211
218
  this.nativeDb.closeIModel();
212
- this._nativeDb = undefined; // the underlying nativeDb has been freed by closeIModel
213
219
  }
214
220
  /** @internal */
215
221
  async refreshContainer(_userAccessToken) { }
@@ -260,7 +266,7 @@ class IModelDb extends core_common_1.IModel {
260
266
  /** Return `true` if the underlying nativeDb is open and valid.
261
267
  * @internal
262
268
  */
263
- get isOpen() { return undefined !== this.nativeDb; }
269
+ get isOpen() { return this.nativeDb.isOpen(); }
264
270
  /** Get the briefcase Id of this iModel */
265
271
  getBriefcaseId() { return this.isOpen ? this.nativeDb.getBriefcaseId() : core_common_1.BriefcaseIdValue.Illegal; }
266
272
  /**
@@ -336,9 +342,8 @@ class IModelDb extends core_common_1.IModel {
336
342
  * @public
337
343
  * */
338
344
  createQueryReader(ecsql, params, config) {
339
- if (!this._nativeDb || !this._nativeDb.isOpen()) {
345
+ if (!this.nativeDb.isOpen())
340
346
  throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "db not open");
341
- }
342
347
  const executor = {
343
348
  execute: async (request) => {
344
349
  return ConcurrentQuery_1.ConcurrentQuery.executeQueryRequest(this.nativeDb, request);
@@ -541,6 +546,7 @@ class IModelDb extends core_common_1.IModel {
541
546
  clearCaches() {
542
547
  this._statementCache.clear();
543
548
  this._sqliteStatementCache.clear();
549
+ this._classMetaDataRegistry = undefined;
544
550
  }
545
551
  /** Update the project extents for this iModel.
546
552
  * <p><em>Example:</em>
@@ -630,16 +636,34 @@ class IModelDb extends core_common_1.IModel {
630
636
  * @see querySchemaVersion
631
637
  */
632
638
  async importSchemas(schemaFileNames, options) {
633
- if (this.nativeDb.getITwinId() !== core_bentley_1.Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
634
- await this.acquireSchemaLock();
639
+ if (schemaFileNames.length === 0)
640
+ return;
635
641
  const maybeCustomNativeContext = options?.ecSchemaXmlContext?.nativeContext;
636
- const nativeImportOptions = {
637
- schemaLockHeld: true,
638
- ecSchemaXmlContext: maybeCustomNativeContext,
639
- };
640
- const stat = this.nativeDb.importSchemas(schemaFileNames, nativeImportOptions);
641
- if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat) {
642
- throw new core_common_1.IModelError(stat, "Error importing schema");
642
+ if (this.nativeDb.schemaSyncEnabled()) {
643
+ await SchemaSync_1.SchemaSync.withLockedAccess(this, { openMode: core_bentley_1.OpenMode.Readonly, operationName: "schema sync" }, async (syncAccess) => {
644
+ const schemaSyncDbUri = syncAccess.getUri();
645
+ this.saveChanges();
646
+ let stat = this.nativeDb.importSchemas(schemaFileNames, { schemaLockHeld: false, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
647
+ if (core_bentley_1.DbResult.BE_SQLITE_ERROR_SchemaLockFailed === stat) {
648
+ this.abandonChanges();
649
+ if (this.nativeDb.getITwinId() !== core_bentley_1.Guid.empty)
650
+ await this.acquireSchemaLock();
651
+ stat = this.nativeDb.importSchemas(schemaFileNames, { schemaLockHeld: true, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
652
+ }
653
+ if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat)
654
+ throw new core_common_1.IModelError(stat, "Error importing schema");
655
+ });
656
+ }
657
+ else {
658
+ const nativeImportOptions = {
659
+ schemaLockHeld: true,
660
+ ecSchemaXmlContext: maybeCustomNativeContext,
661
+ };
662
+ if (this.nativeDb.getITwinId() !== core_bentley_1.Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
663
+ await this.acquireSchemaLock();
664
+ const stat = this.nativeDb.importSchemas(schemaFileNames, nativeImportOptions);
665
+ if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat)
666
+ throw new core_common_1.IModelError(stat, "Error importing schema");
643
667
  }
644
668
  this.clearCaches();
645
669
  }
@@ -653,11 +677,30 @@ class IModelDb extends core_common_1.IModel {
653
677
  * @alpha
654
678
  */
655
679
  async importSchemaStrings(serializedXmlSchemas) {
656
- if (this.iTwinId && this.iTwinId !== core_bentley_1.Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
657
- await this.acquireSchemaLock();
658
- const stat = this.nativeDb.importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: true });
659
- if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat)
660
- throw new core_common_1.IModelError(stat, "Error importing schema");
680
+ if (serializedXmlSchemas.length === 0)
681
+ return;
682
+ if (this.nativeDb.schemaSyncEnabled()) {
683
+ await SchemaSync_1.SchemaSync.withLockedAccess(this, { openMode: core_bentley_1.OpenMode.Readonly, operationName: "schemaSync" }, async (syncAccess) => {
684
+ const schemaSyncDbUri = syncAccess.getUri();
685
+ this.saveChanges();
686
+ let stat = this.nativeDb.importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: false, schemaSyncDbUri });
687
+ if (core_bentley_1.DbResult.BE_SQLITE_ERROR_SchemaLockFailed === stat) {
688
+ this.abandonChanges();
689
+ if (this.nativeDb.getITwinId() !== core_bentley_1.Guid.empty)
690
+ await this.acquireSchemaLock();
691
+ stat = this.nativeDb.importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: true, schemaSyncDbUri });
692
+ }
693
+ if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat)
694
+ throw new core_common_1.IModelError(stat, "Error importing schema");
695
+ });
696
+ }
697
+ else {
698
+ if (this.iTwinId && this.iTwinId !== core_bentley_1.Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
699
+ await this.acquireSchemaLock();
700
+ const stat = this.nativeDb.importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: true });
701
+ if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat)
702
+ throw new core_common_1.IModelError(stat, "Error importing schema");
703
+ }
661
704
  this.clearCaches();
662
705
  }
663
706
  /** Find an opened instance of any subclass of IModelDb, by filename
@@ -1706,18 +1749,77 @@ exports.IModelDb = IModelDb;
1706
1749
  }
1707
1750
  return this._queryAspect(aspectInstanceId, aspectClassFullName);
1708
1751
  }
1752
+ runInstanceQuery(sql, elementId, excludedClassFullNames) {
1753
+ return this._iModel.withPreparedStatement(sql, (statement) => {
1754
+ statement.bindId("elementId", elementId);
1755
+ const aspects = [];
1756
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
1757
+ const row = {};
1758
+ const parsedRow = JSON.parse(statement.getValue(0).getString());
1759
+ // eslint-disable-next-line guard-for-in
1760
+ for (const key in parsedRow) {
1761
+ const jsName = core_common_1.ECJsNames.toJsName(key[0].toUpperCase() + key.substring(1));
1762
+ Object.defineProperty(row, jsName, { enumerable: true, configurable: true, writable: true, value: parsedRow[key] });
1763
+ }
1764
+ const aspectProps = row;
1765
+ aspectProps.classFullName = aspectProps.className.replace(".", ":"); // add in property required by EntityProps
1766
+ aspectProps.className = undefined; // clear property from SELECT $ that we don't want in the final instance
1767
+ if ((undefined === excludedClassFullNames) || !excludedClassFullNames.has(aspectProps.classFullName))
1768
+ aspects.push(this._iModel.constructEntity(aspectProps));
1769
+ }
1770
+ return aspects;
1771
+ });
1772
+ }
1709
1773
  /** Get the ElementAspect instances that are owned by the specified element.
1710
1774
  * @param elementId Get ElementAspects associated with this Element
1711
1775
  * @param aspectClassFullName Optionally filter ElementAspects polymorphically by this class name
1776
+ * @param excludedClassFullNames Optional filter to exclude aspects from classes in the given set.
1712
1777
  * @throws [[IModelError]]
1713
1778
  */
1714
- getAspects(elementId, aspectClassFullName) {
1715
- if (undefined === aspectClassFullName) {
1716
- const uniqueAspects = this._queryAspects(elementId, ElementAspect_1.ElementUniqueAspect.classFullName);
1717
- const multiAspects = this._queryAspects(elementId, ElementAspect_1.ElementMultiAspect.classFullName);
1718
- return uniqueAspects.concat(multiAspects);
1779
+ getAspects(elementId, aspectClassFullName, excludedClassFullNames) {
1780
+ if (aspectClassFullName === undefined) {
1781
+ const allAspects = this.runInstanceQuery(`SELECT $ FROM (
1782
+ SELECT ECInstanceId, ECClassId FROM Bis.ElementMultiAspect WHERE Element.Id = :elementId
1783
+ UNION ALL
1784
+ SELECT ECInstanceId, ECClassId FROM Bis.ElementUniqueAspect WHERE Element.Id = :elementId) OPTIONS USE_JS_PROP_NAMES DO_NOT_TRUNCATE_BLOB`, elementId, excludedClassFullNames);
1785
+ if (allAspects.length === 0)
1786
+ core_bentley_1.Logger.logError(BackendLoggerCategory_1.BackendLoggerCategory.ECDb, `No aspects found for class ${aspectClassFullName} and element ${elementId}`);
1787
+ return allAspects;
1788
+ }
1789
+ // Check if class is abstract
1790
+ const fullClassName = aspectClassFullName.split(":");
1791
+ const val = this._iModel.nativeDb.getECClassMetaData(fullClassName[0], fullClassName[1]);
1792
+ if (val.result !== undefined) {
1793
+ const metaData = new core_common_1.EntityMetaData(JSON.parse(val.result));
1794
+ if (metaData.modifier !== "Abstract") // Class is not abstract, use normal query to retrieve aspects
1795
+ return this._queryAspects(elementId, aspectClassFullName, excludedClassFullNames);
1719
1796
  }
1720
- const aspects = this._queryAspects(elementId, aspectClassFullName);
1797
+ // If class specified is abstract, get the list of all classes derived from it
1798
+ let classIdList = IModelDb.Elements.classMap.get(aspectClassFullName);
1799
+ if (classIdList === undefined) {
1800
+ const classIds = [];
1801
+ this._iModel.withPreparedStatement(`select SourceECInstanceId from meta.ClassHasAllBaseClasses where TargetECInstanceId = (select ECInstanceId from meta.ECClassDef where Name='${fullClassName[1]}'
1802
+ and Schema.Id = (select ECInstanceId from meta.ECSchemaDef where Name='${fullClassName[0]}')) and SourceECInstanceId != TargetECInstanceId`, (statement) => {
1803
+ while (statement.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
1804
+ classIds.push(statement.getValue(0).getId());
1805
+ });
1806
+ if (classIds.length > 0) {
1807
+ classIdList = classIds.join(",");
1808
+ IModelDb.Elements.classMap.set(aspectClassFullName, classIdList);
1809
+ }
1810
+ }
1811
+ if (classIdList === undefined) {
1812
+ core_bentley_1.Logger.logError(BackendLoggerCategory_1.BackendLoggerCategory.ECDb, `No aspects found for the class ${aspectClassFullName}`);
1813
+ return [];
1814
+ }
1815
+ // Execute an instance query to retrieve all aspects from all the derived classes
1816
+ const aspects = this.runInstanceQuery(`SELECT $ FROM (
1817
+ SELECT ECInstanceId, ECClassId FROM Bis.ElementMultiAspect WHERE Element.Id = :elementId AND ECClassId IN (${classIdList})
1818
+ UNION ALL
1819
+ SELECT ECInstanceId, ECClassId FROM Bis.ElementUniqueAspect WHERE Element.Id = :elementId AND ECClassId IN (${classIdList})
1820
+ ) OPTIONS USE_JS_PROP_NAMES DO_NOT_TRUNCATE_BLOB`, elementId, excludedClassFullNames);
1821
+ if (aspects.length === 0)
1822
+ core_bentley_1.Logger.logError(BackendLoggerCategory_1.BackendLoggerCategory.ECDb, `No aspects found for class ${aspectClassFullName} and element ${elementId}`);
1721
1823
  return aspects;
1722
1824
  }
1723
1825
  /** Insert a new ElementAspect into the iModel.
@@ -1763,6 +1865,7 @@ exports.IModelDb = IModelDb;
1763
1865
  });
1764
1866
  }
1765
1867
  }
1868
+ Elements.classMap = new Map();
1766
1869
  IModelDb.Elements = Elements;
1767
1870
  /** The collection of views in an [[IModelDb]].
1768
1871
  * @public
@@ -1942,8 +2045,10 @@ exports.IModelDb = IModelDb;
1942
2045
  this._iModel.nativeDb.saveFileProperty(viewArg, JSON.stringify(props), thumbnail.image);
1943
2046
  return 0;
1944
2047
  }
1945
- /** Set the default view property the iModel
2048
+ /** Set the default view property the iModel.
1946
2049
  * @param viewId The Id of the ViewDefinition to use as the default
2050
+ * @deprecated in 4.2.x. Avoid setting this property - it is not practical for one single view to serve the needs of the many applications
2051
+ * that might wish to view the contents of the iModel.
1947
2052
  */
1948
2053
  setDefaultViewId(viewId) {
1949
2054
  const spec = { namespace: "dgn_View", name: "DefaultView" };
@@ -2124,12 +2229,20 @@ class BriefcaseDb extends IModelDb {
2124
2229
  const openMode = (args.readonly || args.watchForChanges) ? core_bentley_1.OpenMode.Readonly : core_bentley_1.OpenMode.ReadWrite;
2125
2230
  const nativeDb = this.openDgnDb(file, openMode, undefined, args);
2126
2231
  const briefcaseDb = new BriefcaseDb({ nativeDb, key: file.key ?? core_bentley_1.Guid.createValue(), openMode, briefcaseId: nativeDb.getBriefcaseId() });
2127
- // If they asked to watch for changes, set an fs.watch on the "-wal" file (only it is modified while we hold this connection.)
2232
+ // If they asked to watch for changes, set an fs.watch on the "-watch" file (only it is modified while we hold this connection.)
2128
2233
  // Whenever there are changes, restart our defaultTxn. That loads the changes from the other connection and sends
2129
2234
  // notifications as if they happened on this connection. Note: the watcher is called only when the backend event loop cycles.
2130
2235
  if (args.watchForChanges && undefined === args.container) {
2131
- const watcher = fs.watch(`${file.path}-wal`, { persistent: false }, () => nativeDb.restartDefaultTxn());
2132
- briefcaseDb.onBeforeClose.addOnce(() => watcher.close()); // Stop the watcher when we close this connection.
2236
+ // Must touch the file synchronously - cannot watch a file until it exists.
2237
+ touch.sync(briefcaseDb.watchFilePathName);
2238
+ // Restart default txn to trigger events when watch file is changed by some other process.
2239
+ const watcher = fs.watch(briefcaseDb.watchFilePathName, { persistent: false }, () => {
2240
+ nativeDb.restartDefaultTxn();
2241
+ });
2242
+ // Stop the watcher when we close this connection.
2243
+ briefcaseDb.onBeforeClose.addOnce(() => {
2244
+ watcher.close();
2245
+ });
2133
2246
  }
2134
2247
  if (openMode === core_bentley_1.OpenMode.ReadWrite && CodeService_1.CodeService.createForIModel) {
2135
2248
  try {
@@ -2144,23 +2257,39 @@ class BriefcaseDb extends IModelDb {
2144
2257
  this.onOpened.raiseEvent(briefcaseDb, args);
2145
2258
  return briefcaseDb;
2146
2259
  }
2147
- closeAndReopen(openMode) {
2260
+ /** If the briefcase is read-only, reopen the native briefcase for writing.
2261
+ * Execute the supplied function.
2262
+ * If the briefcase was read-only, reopen the native briefcase as read-only.
2263
+ * @note this._openMode is not changed from its initial value.
2264
+ * @internal Exported strictly for tests.
2265
+ */
2266
+ async executeWritable(func) {
2148
2267
  const fileName = this.pathName;
2268
+ try {
2269
+ if (this.isReadonly)
2270
+ this.closeAndReopen(core_bentley_1.OpenMode.ReadWrite, fileName);
2271
+ await func();
2272
+ }
2273
+ finally {
2274
+ if (this.isReadonly)
2275
+ this.closeAndReopen(core_bentley_1.OpenMode.Readonly, fileName);
2276
+ }
2277
+ }
2278
+ closeAndReopen(openMode, fileName) {
2279
+ // Unclosed statements will produce BUSY error when attempting to close.
2280
+ this.clearCaches();
2281
+ // The following resets the native db's pointer to this JavaScript object.
2149
2282
  this.nativeDb.closeIModel();
2150
2283
  this.nativeDb.openIModel(fileName, openMode);
2284
+ // Restore the native db's pointer to this JavaScript object.
2285
+ this.nativeDb.setIModelDb(this);
2151
2286
  }
2152
2287
  /** Pull and apply changesets from iModelHub */
2153
2288
  async pullChanges(arg) {
2154
- if (this.isReadonly) // we allow pulling changes into a briefcase that is readonly - close and reopen it writeable
2155
- this.closeAndReopen(core_bentley_1.OpenMode.ReadWrite);
2156
- try {
2289
+ await this.executeWritable(async () => {
2157
2290
  await BriefcaseManager_1.BriefcaseManager.pullAndApplyChangesets(this, arg ?? {});
2158
2291
  this.initializeIModelDb();
2159
- }
2160
- finally {
2161
- if (this.isReadonly) // if the briefcase was opened readonly - close and reopen it readonly
2162
- this.closeAndReopen(core_bentley_1.OpenMode.Readonly);
2163
- }
2292
+ });
2164
2293
  IpcHost_1.IpcHost.notifyTxns(this, "notifyPulledChanges", this.changeset);
2165
2294
  }
2166
2295
  /** Push changes to iModelHub. */