import { Knex } from 'knex'; import { IUnleashConfig } from 'lib/server-impl'; export type KnexTransaction = Knex.Transaction; export type MockTransaction = null; export type UnleashTransaction = KnexTransaction | MockTransaction; export type TransactionCreator = ( scope: (trx: S) => void | Promise, ) => Promise; export const createKnexTransactionStarter = ( knex: Knex, ): TransactionCreator => { function transaction( scope: (trx: KnexTransaction) => void | Promise, ) { if (!knex) { console.warn( 'It looks like your DB is not provided. Very often it is a test setup problem in setupAppWithCustomConfig', ); } return knex.transaction(scope); } return transaction; }; export type DeferredServiceFactory = (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 = ( config: IUnleashConfig, ) => DeferredServiceFactory; export type WithTransactional = S & { transactional: (fn: (service: S) => R) => Promise; }; /** * @deprecated this is a temporary solution to deal with transactions at the store level. * 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( db: Knex, fn: (db: Knex) => R, ): Promise { if (db.isTransaction) { return fn(db); } return db.transaction(async (tx) => fn(tx)); } export function withTransactional( serviceFactory: (db: Knex) => S, db: Knex, ): WithTransactional { const service = serviceFactory(db) as WithTransactional; service.transactional = async (fn: (service: S) => R) => // 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. db.transaction(async (trx: Knex.Transaction) => { const transactionalService = serviceFactory(trx); return fn(transactionalService); }); return service; } /** Just for testing purposes */ export function withFakeTransactional(service: S): WithTransactional { const serviceWithFakeTransactional = service as WithTransactional; serviceWithFakeTransactional.transactional = async ( fn: (service: S) => R, ) => fn(serviceWithFakeTransactional); return serviceWithFakeTransactional; }