UNPKG

@itwin/core-backend

Version:
271 lines • 14.2 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module iModels */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CatalogIModelHandler = exports.CatalogDb = void 0; const fs = require("fs"); const path_1 = require("path"); const CloudSqlite_1 = require("./CloudSqlite"); const IModelHost_1 = require("./IModelHost"); const core_common_1 = require("@itwin/core-common"); const IpcHost_1 = require("./IpcHost"); const IModelDb_1 = require("./IModelDb"); const core_bentley_1 = require("@itwin/core-bentley"); const Symbols_1 = require("./internal/Symbols"); const NativePlatform_1 = require("./internal/NativePlatform"); let readonlyCloudCache; let writeableCloudCache; const catalogManifestName = "CatalogManifest"; // we make a readonly CloudCache and a writeable CloudCache. That way the access token authorizations are distinct. function makeCloudCache(arg, writeable) { const cache = CloudSqlite_1.CloudSqlite.CloudCaches.getCache(arg); // if the cache was just created, add the "catalog" members as hidden if (undefined === cache.catalogContainers) { CloudSqlite_1.CloudSqlite.addHiddenProperty(cache, "catalogContainers", new Map()); CloudSqlite_1.CloudSqlite.addHiddenProperty(cache, "writeable", writeable); } return cache; } // find an existing CloudContainer for accessing a CatalogIModel, or make a new one and connect it async function getCatalogContainerObj(cache, containerId) { const cloudContainer = cache.catalogContainers.get(containerId); if (undefined !== cloudContainer) return cloudContainer; const accessLevel = cache.writeable ? "write" : "read"; const tokenProps = await CloudSqlite_1.CloudSqlite.getBlobService().requestToken({ containerId, accessLevel, userToken: await IModelHost_1.IModelHost.getAccessToken() }); const container = CloudSqlite_1.CloudSqlite.createCloudContainer({ accessLevel, baseUri: tokenProps.baseUri, containerId, storageType: tokenProps.provider, writeable: cache.writeable, accessToken: tokenProps.token }); cache.catalogContainers.set(containerId, container); // save the container in the map of ContainerIds so we can reuse them container.connect(cache); return container; } function getReadonlyCloudCache() { return readonlyCloudCache ??= makeCloudCache({ cacheName: "catalogs", cacheSize: "10G" }, false); } ; function getWritableCloudCache() { return writeableCloudCache ??= makeCloudCache({ cacheName: "writeableCatalogs", cacheSize: "10G" }, true); } ; async function getReadonlyContainer(containerId) { return getCatalogContainerObj(getReadonlyCloudCache(), containerId); } ; async function getWriteableContainer(containerId) { return getCatalogContainerObj(getWritableCloudCache(), containerId); } ; /** Throw an error if the write lock is not held for the supplied container */ function ensureLocked(container, reason) { if (!container.hasWriteLock) core_common_1.CloudSqliteError.throwError("write-lock-not-held", { message: `Write lock must be held to ${reason}` }); } /** update the manifest in a CatalogIModel (calls `saveChanges`) */ function updateManifest(nativeDb, manifest) { nativeDb.saveLocalValue(catalogManifestName, JSON.stringify(manifest)); nativeDb.saveChanges("update manifest"); } function catalogDbNameWithDefault(dbName) { return dbName ?? "catalog-db"; } /** A StandaloneDb that holds a CatalogIModel */ class CatalogDbImpl extends IModelDb_1.StandaloneDb { isEditable() { return false; } getManifest() { const manifestString = this[Symbols_1._nativeDb].queryLocalValue(catalogManifestName); if (undefined === manifestString) return undefined; return JSON.parse(manifestString); } getVersion() { return CloudSqlite_1.CloudSqlite.parseDbFileName(this[Symbols_1._nativeDb].getFilePath()).version; } getInfo() { return { manifest: this.getManifest(), version: this.getVersion() }; } } /** * A CatalogDb that permits editing. * This class ensures that CatalogIModels never have a Txn table when they are published. * It also automatically updates the `lastEditedBy` field in the CatalogManifest. */ class EditableCatalogDbImpl extends CatalogDbImpl { isEditable() { return true; } updateCatalogManifest(manifest) { updateManifest(this[Symbols_1._nativeDb], manifest); } // Make sure the txn table is deleted and update the manifest every time we close. beforeClose() { try { const manifest = this.getManifest(); const container = this.cloudContainer; if (container && manifest) { manifest.lastEditedBy = CloudSqlite_1.CloudSqlite.getWriteLockHeldBy(container); this.updateCatalogManifest(manifest); } // when saved, CatalogIModels should never have any Txns. If we wanted to create a changeset, we'd have to do it here. this[Symbols_1._nativeDb].deleteAllTxns(); } catch { } // ignore errors attempting to update // might also want to vacuum here? super.beforeClose(); } } function findCatalogByKey(key) { return CatalogDbImpl.findByKey(key); } /** @beta */ var CatalogDb; (function (CatalogDb) { /** Create a new [[BlobContainer]] to hold versions of a [[CatalogDb]]. * @returns The properties of the newly created container. * @note creating new containers requires "admin" authorization. */ async function createNewContainer(args) { const dbName = catalogDbNameWithDefault(args.dbName); CloudSqlite_1.CloudSqlite.validateDbName(dbName); CloudSqlite_1.CloudSqlite.validateDbVersion(args.version); const tmpName = (0, path_1.join)(IModelHost_1.KnownLocations.tmpdir, `temp-${dbName}`); try { // make a copy of the file they supplied so we can modify its contents safely fs.copyFileSync(args.localCatalogFile, tmpName); const nativeDb = new NativePlatform_1.IModelNative.platform.DgnDb(); nativeDb.openIModel(tmpName, core_bentley_1.OpenMode.ReadWrite); nativeDb.setITwinId(core_bentley_1.Guid.empty); // catalogs must be a StandaloneDb nativeDb.setIModelId(core_bentley_1.Guid.createValue()); // make sure its iModelId is unique updateManifest(nativeDb, args.manifest); // store the manifest inside the Catalog nativeDb.deleteAllTxns(); // Catalogs should never have Txns (and, this must be empty before resetting BriefcaseId) nativeDb.resetBriefcaseId(core_common_1.BriefcaseIdValue.Unassigned); // catalogs should always be unassigned nativeDb.saveChanges(); // save change to briefcaseId nativeDb.vacuum(); nativeDb.closeFile(); } catch (e) { core_common_1.CatalogError.throwError("invalid-seed-catalog", { message: "Illegal seed catalog", ...args, cause: e }); } const userToken = await IModelHost_1.IModelHost.getAccessToken(); // create tne new container from the blob service, requires "admin" authorization const cloudContainerProps = await CloudSqlite_1.CloudSqlite.getBlobService().create({ scope: { iTwinId: args.iTwinId }, metadata: { ...args.metadata, containerType: "CatalogIModel" }, userToken }); // now create a CloudSqlite container object to access it const container = CloudSqlite_1.CloudSqlite.createCloudContainer({ accessToken: await CloudSqlite_1.CloudSqlite.requestToken(cloudContainerProps), accessLevel: "admin", writeable: true, baseUri: cloudContainerProps.baseUri, containerId: cloudContainerProps.containerId, storageType: cloudContainerProps.provider, }); // initialize the container for use by CloudSqlite container.initializeContainer({ blockSize: 4 * 1024 * 1024 }); container.connect(getWritableCloudCache()); // upload the initial version of the Catalog await CloudSqlite_1.CloudSqlite.withWriteLock({ user: "initialize", container }, async () => { await CloudSqlite_1.CloudSqlite.uploadDb(container, { dbName: CloudSqlite_1.CloudSqlite.makeSemverName(dbName, args.version), localFileName: tmpName }); fs.unlinkSync(tmpName); // delete temporary copy of catalog }); container.disconnect(); return cloudContainerProps; } CatalogDb.createNewContainer = createNewContainer; /** Acquire the write lock for a [CatalogIModel]($common) container. Only one person may obtain the write lock at a time. * You must obtain the lock before attempting to write to the container via functions like [[CatalogDb.openEditable]] and [[CatalogDb.createNewVersion]]. * @note This requires "write" authorization to the container */ async function acquireWriteLock(args) { const container = await getWriteableContainer(args.containerId); return CloudSqlite_1.CloudSqlite.acquireWriteLock({ container, user: args.username }); } CatalogDb.acquireWriteLock = acquireWriteLock; /** Release the write lock on a [CatalogIModel]($common) container. This uploads all changes made while the lock is held, so they become visible to other users. */ async function releaseWriteLock(args) { const container = await getWriteableContainer(args.containerId); if (args.abandon) container.abandonChanges(); CloudSqlite_1.CloudSqlite.releaseWriteLock(container); } CatalogDb.releaseWriteLock = releaseWriteLock; /** Open an [[EditableCatalogDb]] for write access. * @note Once a version of a catalog iModel has been published (i.e. the write lock has been released), it is no longer editable, *unless* it is a prerelease version. * @note The write lock must be held for this operation to succeed */ async function openEditable(args) { const dbName = catalogDbNameWithDefault(args.dbName); if (undefined === args.containerId) // local file? return EditableCatalogDbImpl.openFile(dbName, core_bentley_1.OpenMode.ReadWrite, args); const container = await getWriteableContainer(args.containerId); ensureLocked(container, "open a Catalog for editing"); // editing Catalogs requires the write lock // look up the full name with version const dbFullName = CloudSqlite_1.CloudSqlite.querySemverMatch({ container, dbName, version: args.version ?? "*" }); if (!CloudSqlite_1.CloudSqlite.isSemverEditable(dbFullName, container)) core_common_1.CloudSqliteError.throwError("already-published", { message: "Catalog has already been published and is not editable. Make a new version first.", ...args }); if (args.prefetch) CloudSqlite_1.CloudSqlite.startCloudPrefetch(container, dbFullName); return EditableCatalogDbImpl.openFile(dbFullName, core_bentley_1.OpenMode.ReadWrite, { container, ...args }); } CatalogDb.openEditable = openEditable; /** Open a [[CatalogDb]] for read-only access. */ async function openReadonly(args) { const dbName = catalogDbNameWithDefault(args.dbName); if (undefined === args.containerId) // local file? return CatalogDbImpl.openFile(dbName, core_bentley_1.OpenMode.Readonly, args); const container = await getReadonlyContainer(args.containerId); if (args.syncWithCloud) container.checkForChanges(); const dbFullName = CloudSqlite_1.CloudSqlite.querySemverMatch({ container, ...args, dbName }); if (args.prefetch) CloudSqlite_1.CloudSqlite.startCloudPrefetch(container, dbFullName); return CatalogDbImpl.openFile(dbFullName, core_bentley_1.OpenMode.Readonly, { container, ...args }); } CatalogDb.openReadonly = openReadonly; /** * Create a new version of a [CatalogIModel]($common) as a copy of an existing version. Immediately after this operation, the new version will be an exact copy * of the source CatalogIModel. Then, use [[CatalogDb.openEditable]] to modify the new version with new content. * @note The write lock must be held for this operation to succeed */ async function createNewVersion(args) { const container = await getWriteableContainer(args.containerId); ensureLocked(container, "create a new version"); return CloudSqlite_1.CloudSqlite.createNewDbVersion(container, { ...args, fromDb: { ...args.fromDb, dbName: catalogDbNameWithDefault(args.fromDb.dbName) } }); } CatalogDb.createNewVersion = createNewVersion; })(CatalogDb || (exports.CatalogDb = CatalogDb = {})); /** * Handler for Ipc access to CatalogIModels. Registered by NativeHost. * @internal */ class CatalogIModelHandler extends IpcHost_1.IpcHandler { get channelName() { return "catalogIModel/ipc"; } async createNewContainer(args) { return CatalogDb.createNewContainer(args); } async acquireWriteLock(args) { return CatalogDb.acquireWriteLock(args); } async releaseWriteLock(args) { return CatalogDb.releaseWriteLock(args); } async openReadonly(args) { return ((await CatalogDb.openReadonly(args)).getConnectionProps()); } async openEditable(args) { return (await CatalogDb.openEditable(args)).getConnectionProps(); } async createNewVersion(args) { return CatalogDb.createNewVersion(args); } async getInfo(key) { return findCatalogByKey(key).getInfo(); } async updateCatalogManifest(key, manifest) { findCatalogByKey(key).updateCatalogManifest(manifest); } } exports.CatalogIModelHandler = CatalogIModelHandler; //# sourceMappingURL=CatalogDb.js.map