// This contains two TypeORM patches. // Patch 1: // TypeORM Sqlite driver does not support using transactions in async code, if it is possible // for two transactions to get called (one of the whole point of transactions). This // patch adds support for that, based on a monkey patch published in: // https://gist.github.com/aigoncharov/556f8c61d752eff730841170cd2bc3f1 // Explanation at https://github.com/typeorm/typeorm/issues/1884#issuecomment-380767213 // Patch 2: // TypeORM parameters are global, and collisions in setting them are not detected. // We add a patch to throw an exception if a parameter value is ever set and then // changed during construction of a query. import * as sqlite3 from '@gristlabs/sqlite3'; import * as log from 'app/server/lib/log'; import {Mutex, MutexInterface} from 'async-mutex'; import isEqual = require('lodash/isEqual'); import {EntityManager, QueryRunner} from 'typeorm'; import {SqliteDriver} from 'typeorm/driver/sqlite/SqliteDriver'; import {SqliteQueryRunner} from 'typeorm/driver/sqlite/SqliteQueryRunner'; import { QueryRunnerProviderAlreadyReleasedError } from 'typeorm/error/QueryRunnerProviderAlreadyReleasedError'; import {QueryBuilder} from 'typeorm/query-builder/QueryBuilder'; /********************** * Patch 1 **********************/ // A singleton mutex for all sqlite transactions. const mutex = new Mutex(); class SqliteQueryRunnerPatched extends SqliteQueryRunner { private _releaseMutex: MutexInterface.Releaser | null; public async startTransaction(level?: any): Promise { this._releaseMutex = await mutex.acquire(); return super.startTransaction(level); } public async commitTransaction(): Promise { if (!this._releaseMutex) { throw new Error('SqliteQueryRunnerPatched.commitTransaction -> mutex releaser unknown'); } await super.commitTransaction(); this._releaseMutex(); this._releaseMutex = null; } public async rollbackTransaction(): Promise { if (!this._releaseMutex) { throw new Error('SqliteQueryRunnerPatched.rollbackTransaction -> mutex releaser unknown'); } await super.rollbackTransaction(); this._releaseMutex(); this._releaseMutex = null; } public async connect(): Promise { if (!this.isTransactionActive) { const release = await mutex.acquire(); release(); } return super.connect(); } } class SqliteDriverPatched extends SqliteDriver { public createQueryRunner(): QueryRunner { if (!this.queryRunner) { this.queryRunner = new SqliteQueryRunnerPatched(this); } return this.queryRunner; } protected loadDependencies(): void { // Use our own sqlite3 module, which is a fork of the original. this.sqlite = sqlite3; } } // Patch the underlying SqliteDriver, since it's impossible to convince typeorm to use only our // patched classes. (Previously we patched DriverFactory and Connection, but those would still // create an unpatched SqliteDriver and then overwrite it.) SqliteDriver.prototype.createQueryRunner = SqliteDriverPatched.prototype.createQueryRunner; (SqliteDriver.prototype as any).loadDependencies = (SqliteDriverPatched.prototype as any).loadDependencies; export function applyPatch() { // tslint: disable-next-line EntityManager.prototype.transaction = async function (arg1: any, arg2?: any): Promise { if (this.queryRunner && this.queryRunner.isReleased) { throw new QueryRunnerProviderAlreadyReleasedError(); } if (this.queryRunner && this.queryRunner.isTransactionActive) { throw new Error(`Cannot start transaction because its already started`); } const queryRunner = this.connection.createQueryRunner(); const runInTransaction = typeof arg1 === "function" ? arg1 : arg2; try { await queryRunner.startTransaction(); const result = await runInTransaction(queryRunner.manager); await queryRunner.commitTransaction(); return result; } catch (err) { log.debug(`SQLite transaction error [${arg1} ${arg2}] - ${err}`); try { // we throw original error even if rollback thrown an error await queryRunner.rollbackTransaction(); // tslint: disable-next-line } catch (rollbackError) { // tslint: disable-next-line } throw err; } finally { await queryRunner.release(); } }; } /********************** * Patch 2 **********************/ abstract class QueryBuilderPatched extends QueryBuilder { public setParameter(key: string, value: any): this { const prev = this.expressionMap.parameters[key]; if (prev !== undefined && !isEqual(prev, value)) { throw new Error(`TypeORM parameter collision for key '${key}' ('${prev}' vs '${value}')`); } this.expressionMap.parameters[key] = value; return this; } } (QueryBuilder.prototype as any).setParameter = (QueryBuilderPatched.prototype as any).setParameter;