@itwin/core-backend
Version:
iTwin.js backend components
267 lines • 13.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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
*/
import * as fs from "fs";
import { join } from "path";
import { CloudSqlite } from "./CloudSqlite";
import { IModelHost, KnownLocations } from "./IModelHost";
import { BriefcaseIdValue, CatalogError, CloudSqliteError } from "@itwin/core-common";
import { IpcHandler } from "./IpcHost";
import { StandaloneDb } from "./IModelDb";
import { Guid, OpenMode } from "@itwin/core-bentley";
import { _nativeDb } from "./internal/Symbols";
import { IModelNative } from "./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.CloudCaches.getCache(arg);
// if the cache was just created, add the "catalog" members as hidden
if (undefined === cache.catalogContainers) {
CloudSqlite.addHiddenProperty(cache, "catalogContainers", new Map());
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.getBlobService().requestToken({ containerId, accessLevel, userToken: await IModelHost.getAccessToken() });
const container = 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)
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 StandaloneDb {
isEditable() {
return false;
}
getManifest() {
const manifestString = this[_nativeDb].queryLocalValue(catalogManifestName);
if (undefined === manifestString)
return undefined;
return JSON.parse(manifestString);
}
getVersion() {
return CloudSqlite.parseDbFileName(this[_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[_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.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[_nativeDb].deleteAllTxns();
}
catch { } // ignore errors attempting to update
// might also want to vacuum here?
super.beforeClose();
}
}
function findCatalogByKey(key) {
return CatalogDbImpl.findByKey(key);
}
/** @beta */
export 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.validateDbName(dbName);
CloudSqlite.validateDbVersion(args.version);
const tmpName = join(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 IModelNative.platform.DgnDb();
nativeDb.openIModel(tmpName, OpenMode.ReadWrite);
nativeDb.setITwinId(Guid.empty); // catalogs must be a StandaloneDb
nativeDb.setIModelId(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(BriefcaseIdValue.Unassigned); // catalogs should always be unassigned
nativeDb.saveChanges(); // save change to briefcaseId
nativeDb.vacuum();
nativeDb.closeFile();
}
catch (e) {
CatalogError.throwError("invalid-seed-catalog", { message: "Illegal seed catalog", ...args, cause: e });
}
const userToken = await IModelHost.getAccessToken();
// create tne new container from the blob service, requires "admin" authorization
const cloudContainerProps = await 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.createCloudContainer({
accessToken: await 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.withWriteLock({ user: "initialize", container }, async () => {
await CloudSqlite.uploadDb(container, { dbName: 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.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.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, 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.querySemverMatch({ container, dbName, version: args.version ?? "*" });
if (!CloudSqlite.isSemverEditable(dbFullName, container))
CloudSqliteError.throwError("already-published", { message: "Catalog has already been published and is not editable. Make a new version first.", ...args });
if (args.prefetch)
CloudSqlite.startCloudPrefetch(container, dbFullName);
return EditableCatalogDbImpl.openFile(dbFullName, 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, OpenMode.Readonly, args);
const container = await getReadonlyContainer(args.containerId);
if (args.syncWithCloud)
container.checkForChanges();
const dbFullName = CloudSqlite.querySemverMatch({ container, ...args, dbName });
if (args.prefetch)
CloudSqlite.startCloudPrefetch(container, dbFullName);
return CatalogDbImpl.openFile(dbFullName, 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.createNewDbVersion(container, { ...args, fromDb: { ...args.fromDb, dbName: catalogDbNameWithDefault(args.fromDb.dbName) } });
}
CatalogDb.createNewVersion = createNewVersion;
})(CatalogDb || (CatalogDb = {}));
/**
* Handler for Ipc access to CatalogIModels. Registered by NativeHost.
* @internal
*/
export class CatalogIModelHandler extends 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);
}
}
//# sourceMappingURL=CatalogDb.js.map