diff --git a/src/lib/db/transactional.ts b/src/lib/db/transactional.ts index 561d3783c4..535a8a6206 100644 --- a/src/lib/db/transactional.ts +++ b/src/lib/db/transactional.ts @@ -38,3 +38,35 @@ export abstract class Transactor implements Transactional { expectTransaction(db); } } + +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, + ) { + return knex.transaction(scope); + } + return transaction; +}; + +export const createMockTransactionStarter = + (): TransactionCreator => { + function transaction( + scope: (trx: MockTransaction) => void | Promise, + ) { + scope(null); + return null; + } + return transaction; + }; diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts index be76bb0c94..b938d8ca1e 100644 --- a/src/lib/services/group-service.ts +++ b/src/lib/services/group-service.ts @@ -15,7 +15,7 @@ import { IEventStore } from '../types/stores/event-store'; import NameExistsError from '../error/name-exists-error'; import { IUserStore } from '../types/stores/user-store'; import { IUser } from '../types/user'; -import { Knex } from 'knex'; +import { TransactionCreator, UnleashTransaction } from 'lib/db/transactional'; export class GroupService { private groupStore: IGroupStore; @@ -26,18 +26,18 @@ export class GroupService { private logger: Logger; - private db: Knex; + private startTransaction: TransactionCreator; constructor( stores: Pick, { getLogger }: Pick, - db: Knex, + startTransaction: TransactionCreator, ) { this.logger = getLogger('service/group-service.js'); this.groupStore = stores.groupStore; this.eventStore = stores.eventStore; this.userStore = stores.userStore; - this.db = db; + this.startTransaction = startTransaction; } async getAll(): Promise { @@ -108,7 +108,7 @@ export class GroupService { await this.validateGroup(group, preData); - return this.db.transaction(async (tx) => { + return this.startTransaction(async (tx) => { const newGroup = await this.groupStore .transactional(tx) .update(group); @@ -232,7 +232,7 @@ export class GroupService { createdBy?: string, ): Promise { if (Array.isArray(externalGroups)) { - await this.db.transaction(async (trx) => { + await this.startTransaction(async (trx) => { let newGroups = await this.groupStore .transactional(trx) .getNewGroupsForExternalUser(userId, externalGroups); diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index df4ab2ecd7..9f3ef33bc0 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -37,12 +37,14 @@ import PatService from './pat-service'; import { PublicSignupTokenService } from './public-signup-token-service'; import { LastSeenService } from './client-metrics/last-seen-service'; import { Knex } from 'knex'; +import { createKnexTransactionStarter } from '../db/transactional'; export const createServices = ( stores: IUnleashStores, config: IUnleashConfig, db: Knex, ): IUnleashServices => { - const groupService = new GroupService(stores, config, db); + let startTransaction = createKnexTransactionStarter(db); + const groupService = new GroupService(stores, config, startTransaction); const accessService = new AccessService(stores, config, groupService); const apiTokenService = new ApiTokenService(stores, config); const clientInstanceService = new ClientInstanceService(stores, config); diff --git a/src/lib/types/stores/transactional.ts b/src/lib/types/stores/transactional.ts index fc44176d02..81c765d1f1 100644 --- a/src/lib/types/stores/transactional.ts +++ b/src/lib/types/stores/transactional.ts @@ -1,5 +1,5 @@ -import { Knex } from 'knex'; +import { UnleashTransaction } from 'lib/db/transactional'; export interface Transactional { - transactional(transaction: Knex.Transaction): T; + transactional(transaction: UnleashTransaction): T; } diff --git a/src/test/e2e/services/group-service.e2e.test.ts b/src/test/e2e/services/group-service.e2e.test.ts index 14bfd1ca83..473a31a5af 100644 --- a/src/test/e2e/services/group-service.e2e.test.ts +++ b/src/test/e2e/services/group-service.e2e.test.ts @@ -3,6 +3,7 @@ import getLogger from '../../fixtures/no-logger'; import { createTestConfig } from '../../config/test-config'; import { GroupService } from '../../../lib/services/group-service'; import GroupStore from '../../../lib/db/group-store'; +import { createKnexTransactionStarter } from '../../../lib/db/transactional'; let stores; let db: ITestDb; @@ -21,7 +22,8 @@ beforeAll(async () => { const config = createTestConfig({ getLogger, }); - groupService = new GroupService(stores, config, db.db); + let startTransaction = createKnexTransactionStarter(db.db); + groupService = new GroupService(stores, config, startTransaction); groupStore = stores.groupStore; await stores.groupStore.create({ diff --git a/src/test/fixtures/fake-group-store.ts b/src/test/fixtures/fake-group-store.ts index 5d307fa126..7c02d23419 100644 --- a/src/test/fixtures/fake-group-store.ts +++ b/src/test/fixtures/fake-group-store.ts @@ -7,11 +7,15 @@ import Group, { IGroupUser, IGroupUserModel, } from '../../lib/types/group'; -import { Knex } from 'knex'; +import { UnleashTransaction } from 'lib/db/transactional'; /* eslint-disable @typescript-eslint/no-unused-vars */ export default class FakeGroupStore implements IGroupStore { data: IGroup[]; + constructor() { + this.data = []; + } + async getAll(): Promise { return Promise.resolve(this.data); } @@ -56,7 +60,7 @@ export default class FakeGroupStore implements IGroupStore { } deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve(); } update(group: IGroupModel): Promise { @@ -89,7 +93,13 @@ export default class FakeGroupStore implements IGroupStore { userId: number, externalGroups: string[], ): Promise { - throw new Error('Method not implemented.'); + const mockGroups = externalGroups.map((externalGroup, index) => { + return { + id: index, + name: externalGroup, + }; + }); + return Promise.resolve(mockGroups); } addUserToGroups( @@ -97,21 +107,34 @@ export default class FakeGroupStore implements IGroupStore { groupIds: number[], createdBy?: string, ): Promise { - throw new Error('Method not implemented.'); + groupIds.forEach((groupId) => { + this.data.push({ + id: groupId, + name: `TestGroup-${groupId}`, + }); + }); + return Promise.resolve(); } getOldGroupsForExternalUser( userId: number, externalGroups: string[], ): Promise { - throw new Error('Method not implemented.'); + const mockGroups = externalGroups.map((externalGroup, index) => { + return { + groupId: index, + userId: index, + joinedAt: new Date(), + }; + }); + return Promise.resolve(mockGroups); } getGroupsForUser(userId: number): Promise { throw new Error('Method not implemented.'); } - transactional(transaction: Knex.Transaction): IGroupStore { - throw new Error('Method not implemented.'); + transactional(transaction: UnleashTransaction): IGroupStore { + return this; } } diff --git a/src/test/transactional.test.ts b/src/test/transactional.test.ts index 9bad637e8a..0f22ea1e52 100644 --- a/src/test/transactional.test.ts +++ b/src/test/transactional.test.ts @@ -1,11 +1,20 @@ +import { createMockTransactionStarter } from '../lib/db/transactional'; +import { IUnleashConfig } from '../lib/server-impl'; +import { GroupService } from '../lib/services/group-service'; import dbInit, { ITestDb } from './/e2e/helpers/database-init'; -import getLogger from './fixtures/no-logger'; +import { createTestConfig } from './config/test-config'; +import FakeGroupStore from './fixtures/fake-group-store'; +import noLoggerProvider from './fixtures/no-logger'; let stores; let db: ITestDb; +let config: IUnleashConfig; beforeAll(async () => { - db = await dbInit('transactional_serial', getLogger); + db = await dbInit('transactional_serial', noLoggerProvider); + config = createTestConfig({ + getLogger: noLoggerProvider, + }); stores = db.stores; }); @@ -141,3 +150,24 @@ test('should fail entire transaction if encountering an error', async () => { const toggles = await stores.featureToggleStore.getAll(); expect(toggles.length).toBe(0); }); + +test('should allow transactions be swapped for a different implementation', async () => { + const mockStores = { + groupStore: new FakeGroupStore(), + eventStore: null, + userStore: null, + }; + + expect((await mockStores.groupStore.getAll()).length).toBe(0); + + const groupService = new GroupService( + mockStores, + config, + createMockTransactionStarter(), + ); + const externalGroups = ['group-one', 'group-two']; + + await groupService.syncExternalGroups(7, externalGroups, 'David Fincher'); + + expect((await mockStores.groupStore.getAll()).length).toBe(2); +});