import { Result } from "./result.mjs"; import { PawSQLiteError } from "./pawsqlite_error.mjs"; export class Transaction { constructor(dbName, adapter, enqueue, rollbackOnError=false) { this.dbName = dbName; this.adapter = adapter; this._rollbackOnError = rollbackOnError; this._enqueue = enqueue; this._completeCb = null; this._readyWait = null; this._ready = false; this._finalized = false; } async sql(sql, ...args) { if (this._finalized) { throw new PawSQLiteError("Transaction has already completed"); } if (!this._ready) { await this._waitUntilReady(); } try { return await this._executeSQL(sql, ...args); } catch (e) { if (this._rollbackOnError) { await this.rollback(); } throw e; } } // Allow for slightly more complex parameter substitution. // Instances of "???" will be replaced by the same number of comma-separated // question marks as items in the corresponding nested parateter array // eg. buildQuery("SELECT (???) FROM ?", [["col1", "col2"], "table1"]) // would output: ["SELECT (?, ?) FROM ?", ["col1", "col2", "table1"]] buildQuery(sql, ...args) { const parts = sql.split("???"); const subParamLengths = args .filter(Array.isArray) .map((a) => a.length); if (parts.length !== subParamLengths.length + 1) { throw new PawSQLiteError("Unable to build query: sub-" + "paramters do not match sub-paramters in query"); } const newQuery = parts.reduce((p1, p2, i) => { const length = subParamLengths[i - 1]; return p1 + new Array(length).fill("?").join(", ") + p2; }); const flatParams = args.reduce((acc, v) => { if (Array.isArray(v)) { Array.prototype.push.apply(acc, v); } else { acc.push(v); } return acc; }, []); return [newQuery, ...flatParams]; } commit() { return this._complete("COMMIT"); } rollback() { return this._complete("ROLLBACK"); } async _waitUntilReady() { if (!this._readyWait) { this._readyWait = (async () => { this._completeCb = await this._enqueue(); await this._begin(); this._ready = true; })(); } await this._readyWait; } async _executeSQL(sql, ...args) { const reg = /^\s*(BEGIN|END|COMMIT|ROLLBACK)(?:[^A-Z]|$)/i; const match = reg.exec(sql); if (match) { const statement = match[1].toUpperCase(); throw new PawSQLiteError("Manually managing transactions is " + "forbidden. Found: \"" + statement + "\" statement."); } const result = await this.adapter.sql(this.dbName, ...this.buildQuery(sql, ...args)); return new Result(result); } async _begin() { const result = await this.adapter.sql(this.dbName, "BEGIN"); } async _complete(sql) { if (this._finalized) { throw new PawSQLiteError("Transaction has already completed"); } this._finalized = true; if (!this._readyWait) { // Transaction was unused return; } else if (!this._ready) { await this._waitUntilReady(); } let result; let error; try { result = await this.adapter.sql(this.dbName, sql); } catch (e) { error = e; } this._completeCb(); if (error) { throw error; } return new Result(result); } }