2024-03-18 13:58:05 +01:00
|
|
|
import type { Knex } from 'knex';
|
|
|
|
import type { IUnleashConfig } from '../server-impl';
|
2023-02-16 08:08:51 +01:00
|
|
|
|
|
|
|
export type KnexTransaction = Knex.Transaction;
|
|
|
|
|
|
|
|
export type MockTransaction = null;
|
|
|
|
|
|
|
|
export type UnleashTransaction = KnexTransaction | MockTransaction;
|
|
|
|
|
|
|
|
export type TransactionCreator<S> = <T>(
|
|
|
|
scope: (trx: S) => void | Promise<T>,
|
|
|
|
) => Promise<T>;
|
|
|
|
|
|
|
|
export const createKnexTransactionStarter = (
|
|
|
|
knex: Knex,
|
|
|
|
): TransactionCreator<UnleashTransaction> => {
|
|
|
|
function transaction<T>(
|
|
|
|
scope: (trx: KnexTransaction) => void | Promise<T>,
|
|
|
|
) {
|
2023-10-13 12:09:46 +02:00
|
|
|
if (!knex) {
|
|
|
|
console.warn(
|
|
|
|
'It looks like your DB is not provided. Very often it is a test setup problem in setupAppWithCustomConfig',
|
|
|
|
);
|
|
|
|
}
|
2023-02-16 08:08:51 +01:00
|
|
|
return knex.transaction(scope);
|
|
|
|
}
|
|
|
|
return transaction;
|
|
|
|
};
|
2023-10-04 15:16:37 +02:00
|
|
|
|
2023-10-17 12:30:44 +02:00
|
|
|
export type DeferredServiceFactory<S> = (db: Knex) => S;
|
|
|
|
/**
|
|
|
|
* Services need to be instantiated with a knex instance on a per-transaction basis.
|
|
|
|
* Limiting the input parameters, makes sure we don't inject already instantiated services
|
|
|
|
* that might be bound to a different transaction.
|
|
|
|
*/
|
|
|
|
export type ServiceFactory<S> = (
|
|
|
|
config: IUnleashConfig,
|
|
|
|
) => DeferredServiceFactory<S>;
|
2024-07-31 10:22:05 +02:00
|
|
|
|
2023-10-04 15:16:37 +02:00
|
|
|
export type WithTransactional<S> = S & {
|
|
|
|
transactional: <R>(fn: (service: S) => R) => Promise<R>;
|
|
|
|
};
|
|
|
|
|
2024-07-31 10:22:05 +02:00
|
|
|
export type WithRollback<S> = S & {
|
|
|
|
rollback: <R>(fn: (service: S) => R) => Promise<R>;
|
|
|
|
};
|
|
|
|
|
2023-10-06 13:38:32 +02:00
|
|
|
/**
|
2023-10-06 16:31:39 +02:00
|
|
|
* @deprecated this is a temporary solution to deal with transactions at the store level.
|
2023-10-06 13:38:32 +02:00
|
|
|
* Ideally, we should handle transactions at the service level (each service method should be transactional).
|
|
|
|
* The controller should define the transactional scope as follows:
|
|
|
|
* https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208
|
|
|
|
*
|
|
|
|
* To be able to use .transactional method, services should be instantiated like this:
|
|
|
|
* https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/services/index.ts#L282-L284
|
|
|
|
*
|
|
|
|
* This function makes sure that `fn` is executed in a transaction.
|
|
|
|
* If the db is already in a transaction, it will execute `fn` in that transactional scope.
|
|
|
|
*
|
|
|
|
* https://github.com/knex/knex/blob/bbbe4d4637b3838e4a297a457460cd2c76a700d5/lib/knex-builder/make-knex.js#L143C5-L144C88
|
|
|
|
*/
|
|
|
|
export async function inTransaction<R>(
|
|
|
|
db: Knex,
|
|
|
|
fn: (db: Knex) => R,
|
|
|
|
): Promise<R> {
|
|
|
|
if (db.isTransaction) {
|
|
|
|
return fn(db);
|
|
|
|
}
|
|
|
|
return db.transaction(async (tx) => fn(tx));
|
|
|
|
}
|
|
|
|
|
2023-10-04 15:16:37 +02:00
|
|
|
export function withTransactional<S>(
|
|
|
|
serviceFactory: (db: Knex) => S,
|
|
|
|
db: Knex,
|
|
|
|
): WithTransactional<S> {
|
|
|
|
const service = serviceFactory(db) as WithTransactional<S>;
|
|
|
|
|
|
|
|
service.transactional = async <R>(fn: (service: S) => R) =>
|
2023-10-06 13:38:32 +02:00
|
|
|
// Maybe: inTransaction(db, async (trx: Knex.Transaction) => fn(serviceFactory(trx)));
|
|
|
|
// this assumes that the caller didn't start a transaction already and opens a new one.
|
2023-10-04 15:16:37 +02:00
|
|
|
db.transaction(async (trx: Knex.Transaction) => {
|
|
|
|
const transactionalService = serviceFactory(trx);
|
|
|
|
return fn(transactionalService);
|
|
|
|
});
|
|
|
|
|
|
|
|
return service;
|
|
|
|
}
|
|
|
|
|
2024-07-31 10:22:05 +02:00
|
|
|
export function withRollback<S>(
|
|
|
|
serviceFactory: (db: Knex) => S,
|
|
|
|
db: Knex,
|
|
|
|
): WithRollback<S> {
|
|
|
|
const service = serviceFactory(db) as WithRollback<S>;
|
|
|
|
|
|
|
|
service.rollback = async <R>(fn: (service: S) => R) => {
|
2024-07-31 16:46:15 +02:00
|
|
|
const trx = await db.transaction();
|
2024-07-31 10:22:05 +02:00
|
|
|
try {
|
|
|
|
const transactionService = serviceFactory(trx);
|
2024-07-31 16:46:15 +02:00
|
|
|
const result = await fn(transactionService);
|
|
|
|
return result;
|
2024-07-31 10:22:05 +02:00
|
|
|
} finally {
|
|
|
|
await trx.rollback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return service;
|
|
|
|
}
|
|
|
|
|
2023-10-04 15:16:37 +02:00
|
|
|
/** Just for testing purposes */
|
|
|
|
export function withFakeTransactional<S>(service: S): WithTransactional<S> {
|
|
|
|
const serviceWithFakeTransactional = service as WithTransactional<S>;
|
|
|
|
|
|
|
|
serviceWithFakeTransactional.transactional = async <R>(
|
|
|
|
fn: (service: S) => R,
|
|
|
|
) => fn(serviceWithFakeTransactional);
|
|
|
|
|
|
|
|
return serviceWithFakeTransactional;
|
|
|
|
}
|