UNPKG

@itwin/core-backend

Version:
312 lines • 14.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StashManager = exports.StashError = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const node_fs_1 = require("node:fs"); const path = require("node:path"); const BriefcaseManager_1 = require("./BriefcaseManager"); const Symbols_1 = require("./internal/Symbols"); const SQLiteDb_1 = require("./SQLiteDb"); const IModelHost_1 = require("./IModelHost"); const BackendLoggerCategory_1 = require("./BackendLoggerCategory"); const loggerCategory = BackendLoggerCategory_1.BackendLoggerCategory.StashManager; var LockOrigin; (function (LockOrigin) { LockOrigin[LockOrigin["Acquired"] = 0] = "Acquired"; LockOrigin[LockOrigin["NewElement"] = 1] = "NewElement"; LockOrigin[LockOrigin["Discovered"] = 2] = "Discovered"; })(LockOrigin || (LockOrigin = {})); ; /** * An error originating from the StashManager API. * @internal */ var StashError; (function (StashError) { StashError.scope = "itwin-StashManager"; /** Instantiate and throw a StashError */ function throwError(key, message) { core_bentley_1.ITwinError.throwError({ iTwinErrorId: { scope: StashError.scope, key }, message }); } StashError.throwError = throwError; /** Determine whether an error object is a StashError */ function isError(error, key) { return core_bentley_1.ITwinError.isError(error, StashError.scope, key); } StashError.isError = isError; })(StashError || (exports.StashError = StashError = {})); /** * Stash manager allow stash, drop, apply and merge stashes * @internal */ class StashManager { static STASHES_ROOT_DIR_NAME = ".stashes"; /** * Retrieves the root folder path for stash files associated with the specified BriefcaseDb. * * @param db - The BriefcaseDb instance for which to determine the stash root folder. * @param ensureExists - If true, the stash root directory will be created if it does not already exist. * @returns The absolute path to the stash root directory. */ static getStashRootFolder(db, ensureExists) { if (!db.isOpen || db.isReadonly) StashError.throwError("readonly", "Database is not open or is readonly"); if (!(0, node_fs_1.existsSync)(db[Symbols_1._nativeDb].getFilePath())) { StashError.throwError("no-file", "Database file does not exist"); } const stashDir = path.join(path.dirname(db[Symbols_1._nativeDb].getFilePath()), this.STASHES_ROOT_DIR_NAME, `${db.briefcaseId}`); if (ensureExists && !(0, node_fs_1.existsSync)(stashDir)) { (0, node_fs_1.mkdirSync)(stashDir, { recursive: true }); } return stashDir; } /** * Retrieves the stash ID from the provided arguments. * * If the `stash` property of `args` is a string, it returns the string in lowercase. * If the `stash` property is an object, it returns the `id` property of the object in lowercase. * * @param args - The arguments containing the stash information, which can be either a string or an object with an `id` property. * @returns The stash ID as a lowercase string. */ static getStashId(args) { return (typeof args.stash === "string" ? args.stash : args.stash.id).toLowerCase(); } /** * Retrieves the file path to the stash file associated with the provided arguments. * * @param args - The arguments required to identify the stash, including the database reference. * @returns The absolute path to the stash file. */ static getStashFilePath(args) { const stashRoot = this.getStashRootFolder(args.db, false); if (!(0, node_fs_1.existsSync)(stashRoot)) { StashError.throwError("no-stashes", "No stashes exist for this briefcase"); } const stashFilePath = path.join(stashRoot, `${this.getStashId(args)}.stash`); if (!(0, node_fs_1.existsSync)(stashFilePath)) { StashError.throwError("invalid-stash", "Invalid stash"); } return stashFilePath; } /** * Queries the stash database for lock IDs matching the specified state and origin. * * @param args - The arguments required to access the stash database. * @param state - The lock state to filter by. * @param origin - The lock origin to filter by. * @returns An array of lock IDs (`Id64Array`) that match the given state and origin. */ static queryLocks(args, state, origin) { return this.withStash(args, (stashDb) => { const query = `SELECT JSON_GROUP_ARRAY(FORMAT('0x%x', Id)) FROM [locks] WHERE State = ${state} AND origin = ${origin}`; return stashDb.withPreparedSqliteStatement(query, (stmt) => { if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) { return JSON.parse(stmt.getValueString(0)); } return []; }); }); } /** * Acquire locks for the specified stash. If this fail then stash should not be applied. * @param args The stash arguments. */ static async acquireLocks(args) { const shared = this.queryLocks(args, core_common_1.LockState.Shared, LockOrigin.Acquired); await args.db.locks.acquireLocks({ shared }); const exclusive = this.queryLocks(args, core_common_1.LockState.Exclusive, LockOrigin.Acquired); await args.db.locks.acquireLocks({ exclusive }); const newElements = this.queryLocks(args, core_common_1.LockState.Shared, LockOrigin.NewElement); for (const id of newElements) { if (!args.db.locks.holdsExclusiveLock(id)) { args.db.locks[Symbols_1._elementWasCreated](id); } } } /** * Creates a stash of changes for the specified briefcase. * * This method generates a stash in the stash root directory for the given briefcase, using the provided description and iModelId. * Optionally, it can reset the briefcase by releasing all locks after stashing. * * @param args - The properties required to create a stash, including the briefcase, description, iModelId, and an optional resetBriefcase flag. * @returns A promise that resolves to the properties of the created stash. */ static async stash(args) { if (!args.db.txns.hasPendingTxns) { StashError.throwError("nothing-to-stash", "Nothing to stash"); } if (args.db.txns.hasUnsavedChanges) { StashError.throwError("unsaved-changes", "Unsaved changes exist"); } if (args.db.txns.hasPendingSchemaChanges) { StashError.throwError("pending-schema-changes", "Pending schema changeset. Stashing is not currently supported for schema changes"); } const stashRootDir = this.getStashRootFolder(args.db, true); const iModelId = args.db.iModelId; const stash = args.db[Symbols_1._nativeDb].stashChanges({ stashRootDir, description: args.description, iModelId }); if (args.discardLocalChanges) { await args.db.discardChanges({ retainLocks: args.retainLocks }); } core_bentley_1.Logger.logInfo(loggerCategory, `Stashed changes`, () => stash); return stash; } /** * Retrieves the stash properties from the database for the given arguments. * * @param args - The arguments required to locate and access the stash. * @returns The stash file properties if found; otherwise, `undefined`. */ static tryGetStash(args) { try { return this.getStash(args); } catch (error) { core_bentley_1.Logger.logError(loggerCategory, `Error getting stash with ${this.getStashId(args)}: ${error.message}`); } return undefined; } /** * Retrieves the stash properties from the database using the provided arguments. * * @param args - The arguments required to access the stash. * @returns The stash properties parsed from the database. */ static getStash(args) { return this.withStash(args, (stashDb) => { const stashProps = stashDb.withPreparedSqliteStatement("SELECT [val] FROM [be_Local] WHERE [name]='$stash_info'", (stmt) => { if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW) StashError.throwError("invalid-stash", "Invalid stash"); return JSON.parse(stmt.getValueString(0)); }); return stashProps; }); } /** * Executes a callback function with a read-only SQLite database connection to a stash file. * * @typeParam T - The return type of the callback function. * @param args - Arguments required to determine the stash file path. * @param callback - A function that receives an open {@link SQLiteDb} instance connected to the stash file. * @returns The value returned by the callback function. */ static withStash(args, callback) { const stashFile = this.getStashFilePath(args); if (!(0, node_fs_1.existsSync)(stashFile)) { StashError.throwError("invalid-stash", "Invalid stash"); } const stashDb = new SQLiteDb_1.SQLiteDb(); stashDb.openDb(stashFile, core_bentley_1.OpenMode.Readonly); try { return callback(stashDb); } finally { stashDb.closeDb(); } } /** * Retrieves all stash files associated with the specified {@link BriefcaseDb}. * @param db - The {@link BriefcaseDb} instance for which to retrieve stash files. * @returns An array of `StashProps` representing the found stash files, sorted by timestamp. */ static getStashes(db) { const stashes = []; const stashDir = this.getStashRootFolder(db, false); if (!(0, node_fs_1.existsSync)(stashDir)) { return stashes; } (0, node_fs_1.readdirSync)(stashDir).filter((file) => { const filePath = path.join(stashDir, file); if ((0, node_fs_1.existsSync)(filePath) && (0, node_fs_1.statSync)(filePath).isFile() && file.endsWith(".stash")) { const id = file.slice(0, -path.extname(file).length); const stash = this.tryGetStash({ db, stash: id }); if (stash) { stashes.push(stash); } } }); stashes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp)); return stashes; } /** * Deletes the stash file associated with the specified stash ID or properties from the given {@link BriefcaseDb}. * * @param db - The {@link BriefcaseDb} instance from which the stash should be dropped. * @param stashId - The unique identifier (GuidString) or properties (StashProps) of the stash to be deleted. * @returns Returns `true` if the stash file was successfully deleted, otherwise returns `false`. */ static dropStash(args) { try { const stashFile = this.getStashFilePath(args); (0, node_fs_1.unlinkSync)(stashFile); return true; } catch (error) { core_bentley_1.Logger.logError(loggerCategory, `Error dropping stash: ${error}`); } return false; } /** * Removes all stashes associated with the specified {@link BriefcaseDb}. * * @param db - The {@link BriefcaseDb} instance from which all stashes will be removed. */ static dropAllStashes(db) { this.getStashes(db).forEach((stash) => { this.dropStash({ db, stash }); }); } /** * Queries the hub for the changeset information associated with the given stash. * * @param args - The arguments including the stash properties. * @returns A promise resolving to the changeset ID and index. */ static async queryChangeset(args) { return IModelHost_1.IModelHost[Symbols_1._hubAccess].queryChangeset({ iModelId: args.stash.iModelId, changeset: args.stash.parentChangeset, accessToken: await IModelHost_1.IModelHost.getAccessToken() }); } /** * Restores the specified stash to the given {@link BriefcaseDb}. This operation will discard any local changes made to db and reverse the tip to the state of the stash and then apply stash. This will restore the undo stack. * * @param args - The arguments including the target database and stash properties. */ static async restore(args) { const { db } = args; core_bentley_1.Logger.logInfo(loggerCategory, `Restoring stash: ${this.getStashId(args)}`); const stash = this.tryGetStash(args); if (!stash) { StashError.throwError("stash-not-found", `Stash not found ${this.getStashId(args)}`); } if (db.txns.hasUnsavedChanges) { StashError.throwError("unsaved-changes", `Unsaved changes are present.`); } if (db.iModelId !== stash.iModelId) { StashError.throwError("invalid-stash", `Stash does not belong to this iModel`); } if (db.briefcaseId !== stash.briefcaseId) { StashError.throwError("invalid-stash", `Stash does not belong to this briefcase`); } const stashFile = this.getStashFilePath({ db, stash }); // we need to retain lock that overlapped with stash locks instead of all locks await db.discardChanges({ retainLocks: true }); await this.acquireLocks(args); if (db.changeset.id !== stash.parentChangeset.id) { // Changeset ID mismatch core_bentley_1.Logger.logWarning(loggerCategory, "Changeset ID mismatch"); const stashChangeset = await this.queryChangeset({ db, stash }); await BriefcaseManager_1.BriefcaseManager.pullAndApplyChangesets(db, { toIndex: stashChangeset.index }); } db[Symbols_1._nativeDb].stashRestore(stashFile); db[Symbols_1._resetIModelDb](); db.saveChanges(); core_bentley_1.Logger.logInfo(loggerCategory, `Restored stash: ${this.getStashId(args)}`); } } exports.StashManager = StashManager; //# sourceMappingURL=StashManager.js.map