mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-19 16:02:01 +00:00
dcaa2b4f29
Summary: grist-ee build was failing since it didn't have a DocUsageBanner implementation available. Made the implementation added to monorepo available, since it will be useful to improve the activation banner. Test Plan: manaul Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: georgegevoian Differential Revision: https://phab.getgrist.com/D3452
138 lines
4.8 KiB
TypeScript
138 lines
4.8 KiB
TypeScript
// 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<void> {
|
|
this._releaseMutex = await mutex.acquire();
|
|
return super.startTransaction(level);
|
|
}
|
|
|
|
public async commitTransaction(): Promise<void> {
|
|
if (!this._releaseMutex) {
|
|
throw new Error('SqliteQueryRunnerPatched.commitTransaction -> mutex releaser unknown');
|
|
}
|
|
await super.commitTransaction();
|
|
this._releaseMutex();
|
|
this._releaseMutex = null;
|
|
}
|
|
|
|
public async rollbackTransaction(): Promise<void> {
|
|
if (!this._releaseMutex) {
|
|
throw new Error('SqliteQueryRunnerPatched.rollbackTransaction -> mutex releaser unknown');
|
|
}
|
|
await super.rollbackTransaction();
|
|
this._releaseMutex();
|
|
this._releaseMutex = null;
|
|
}
|
|
|
|
public async connect(): Promise<any> {
|
|
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 <T>(arg1: any, arg2?: any): Promise<T> {
|
|
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<T> extends QueryBuilder<T> {
|
|
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;
|