1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

implement transaction creator closure and pass that to group service rather than knex directly, add some tests to prove this can be swapped out

This commit is contained in:
sighphyre 2022-10-26 15:44:38 +02:00
parent d213df36dc
commit 3200673f1c
7 changed files with 108 additions and 19 deletions

View File

@ -38,3 +38,35 @@ export abstract class Transactor<T> implements Transactional<T> {
expectTransaction(db); expectTransaction(db);
} }
} }
export type KnexTransaction = Knex.Transaction<any, any[]>;
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>,
) {
return knex.transaction(scope);
}
return transaction;
};
export const createMockTransactionStarter =
(): TransactionCreator<UnleashTransaction> => {
function transaction<T>(
scope: (trx: MockTransaction) => void | Promise<T>,
) {
scope(null);
return null;
}
return transaction;
};

View File

@ -15,7 +15,7 @@ import { IEventStore } from '../types/stores/event-store';
import NameExistsError from '../error/name-exists-error'; import NameExistsError from '../error/name-exists-error';
import { IUserStore } from '../types/stores/user-store'; import { IUserStore } from '../types/stores/user-store';
import { IUser } from '../types/user'; import { IUser } from '../types/user';
import { Knex } from 'knex'; import { TransactionCreator, UnleashTransaction } from 'lib/db/transactional';
export class GroupService { export class GroupService {
private groupStore: IGroupStore; private groupStore: IGroupStore;
@ -26,18 +26,18 @@ export class GroupService {
private logger: Logger; private logger: Logger;
private db: Knex; private startTransaction: TransactionCreator<UnleashTransaction>;
constructor( constructor(
stores: Pick<IUnleashStores, 'groupStore' | 'eventStore' | 'userStore'>, stores: Pick<IUnleashStores, 'groupStore' | 'eventStore' | 'userStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>, { getLogger }: Pick<IUnleashConfig, 'getLogger'>,
db: Knex, startTransaction: TransactionCreator<UnleashTransaction>,
) { ) {
this.logger = getLogger('service/group-service.js'); this.logger = getLogger('service/group-service.js');
this.groupStore = stores.groupStore; this.groupStore = stores.groupStore;
this.eventStore = stores.eventStore; this.eventStore = stores.eventStore;
this.userStore = stores.userStore; this.userStore = stores.userStore;
this.db = db; this.startTransaction = startTransaction;
} }
async getAll(): Promise<IGroupModel[]> { async getAll(): Promise<IGroupModel[]> {
@ -108,7 +108,7 @@ export class GroupService {
await this.validateGroup(group, preData); await this.validateGroup(group, preData);
return this.db.transaction(async (tx) => { return this.startTransaction(async (tx) => {
const newGroup = await this.groupStore const newGroup = await this.groupStore
.transactional(tx) .transactional(tx)
.update(group); .update(group);
@ -232,7 +232,7 @@ export class GroupService {
createdBy?: string, createdBy?: string,
): Promise<void> { ): Promise<void> {
if (Array.isArray(externalGroups)) { if (Array.isArray(externalGroups)) {
await this.db.transaction(async (trx) => { await this.startTransaction(async (trx) => {
let newGroups = await this.groupStore let newGroups = await this.groupStore
.transactional(trx) .transactional(trx)
.getNewGroupsForExternalUser(userId, externalGroups); .getNewGroupsForExternalUser(userId, externalGroups);

View File

@ -37,12 +37,14 @@ import PatService from './pat-service';
import { PublicSignupTokenService } from './public-signup-token-service'; import { PublicSignupTokenService } from './public-signup-token-service';
import { LastSeenService } from './client-metrics/last-seen-service'; import { LastSeenService } from './client-metrics/last-seen-service';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { createKnexTransactionStarter } from '../db/transactional';
export const createServices = ( export const createServices = (
stores: IUnleashStores, stores: IUnleashStores,
config: IUnleashConfig, config: IUnleashConfig,
db: Knex, db: Knex,
): IUnleashServices => { ): 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 accessService = new AccessService(stores, config, groupService);
const apiTokenService = new ApiTokenService(stores, config); const apiTokenService = new ApiTokenService(stores, config);
const clientInstanceService = new ClientInstanceService(stores, config); const clientInstanceService = new ClientInstanceService(stores, config);

View File

@ -1,5 +1,5 @@
import { Knex } from 'knex'; import { UnleashTransaction } from 'lib/db/transactional';
export interface Transactional<T> { export interface Transactional<T> {
transactional(transaction: Knex.Transaction): T; transactional(transaction: UnleashTransaction): T;
} }

View File

@ -3,6 +3,7 @@ import getLogger from '../../fixtures/no-logger';
import { createTestConfig } from '../../config/test-config'; import { createTestConfig } from '../../config/test-config';
import { GroupService } from '../../../lib/services/group-service'; import { GroupService } from '../../../lib/services/group-service';
import GroupStore from '../../../lib/db/group-store'; import GroupStore from '../../../lib/db/group-store';
import { createKnexTransactionStarter } from '../../../lib/db/transactional';
let stores; let stores;
let db: ITestDb; let db: ITestDb;
@ -21,7 +22,8 @@ beforeAll(async () => {
const config = createTestConfig({ const config = createTestConfig({
getLogger, getLogger,
}); });
groupService = new GroupService(stores, config, db.db); let startTransaction = createKnexTransactionStarter(db.db);
groupService = new GroupService(stores, config, startTransaction);
groupStore = stores.groupStore; groupStore = stores.groupStore;
await stores.groupStore.create({ await stores.groupStore.create({

View File

@ -7,11 +7,15 @@ import Group, {
IGroupUser, IGroupUser,
IGroupUserModel, IGroupUserModel,
} from '../../lib/types/group'; } from '../../lib/types/group';
import { Knex } from 'knex'; import { UnleashTransaction } from 'lib/db/transactional';
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
export default class FakeGroupStore implements IGroupStore { export default class FakeGroupStore implements IGroupStore {
data: IGroup[]; data: IGroup[];
constructor() {
this.data = [];
}
async getAll(): Promise<IGroup[]> { async getAll(): Promise<IGroup[]> {
return Promise.resolve(this.data); return Promise.resolve(this.data);
} }
@ -56,7 +60,7 @@ export default class FakeGroupStore implements IGroupStore {
} }
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> { deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
throw new Error('Method not implemented.'); return Promise.resolve();
} }
update(group: IGroupModel): Promise<IGroup> { update(group: IGroupModel): Promise<IGroup> {
@ -89,7 +93,13 @@ export default class FakeGroupStore implements IGroupStore {
userId: number, userId: number,
externalGroups: string[], externalGroups: string[],
): Promise<IGroup[]> { ): Promise<IGroup[]> {
throw new Error('Method not implemented.'); const mockGroups = externalGroups.map((externalGroup, index) => {
return {
id: index,
name: externalGroup,
};
});
return Promise.resolve(mockGroups);
} }
addUserToGroups( addUserToGroups(
@ -97,21 +107,34 @@ export default class FakeGroupStore implements IGroupStore {
groupIds: number[], groupIds: number[],
createdBy?: string, createdBy?: string,
): Promise<void> { ): Promise<void> {
throw new Error('Method not implemented.'); groupIds.forEach((groupId) => {
this.data.push({
id: groupId,
name: `TestGroup-${groupId}`,
});
});
return Promise.resolve();
} }
getOldGroupsForExternalUser( getOldGroupsForExternalUser(
userId: number, userId: number,
externalGroups: string[], externalGroups: string[],
): Promise<IGroupUser[]> { ): Promise<IGroupUser[]> {
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<Group[]> { getGroupsForUser(userId: number): Promise<Group[]> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
transactional(transaction: Knex.Transaction<any, any[]>): IGroupStore { transactional(transaction: UnleashTransaction): IGroupStore {
throw new Error('Method not implemented.'); return this;
} }
} }

View File

@ -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 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 stores;
let db: ITestDb; let db: ITestDb;
let config: IUnleashConfig;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('transactional_serial', getLogger); db = await dbInit('transactional_serial', noLoggerProvider);
config = createTestConfig({
getLogger: noLoggerProvider,
});
stores = db.stores; stores = db.stores;
}); });
@ -141,3 +150,24 @@ test('should fail entire transaction if encountering an error', async () => {
const toggles = await stores.featureToggleStore.getAll(); const toggles = await stores.featureToggleStore.getAll();
expect(toggles.length).toBe(0); 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);
});