@itwin/core-backend
Version:
iTwin.js backend components
312 lines • 14.4 kB
JavaScript
;
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