1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +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);
}
}
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 { 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<UnleashTransaction>;
constructor(
stores: Pick<IUnleashStores, 'groupStore' | 'eventStore' | 'userStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
db: Knex,
startTransaction: TransactionCreator<UnleashTransaction>,
) {
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<IGroupModel[]> {
@ -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<void> {
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);

View File

@ -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);

View File

@ -1,5 +1,5 @@
import { Knex } from 'knex';
import { UnleashTransaction } from 'lib/db/transactional';
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 { 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({

View File

@ -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<IGroup[]> {
return Promise.resolve(this.data);
}
@ -56,7 +60,7 @@ export default class FakeGroupStore implements IGroupStore {
}
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
throw new Error('Method not implemented.');
return Promise.resolve();
}
update(group: IGroupModel): Promise<IGroup> {
@ -89,7 +93,13 @@ export default class FakeGroupStore implements IGroupStore {
userId: number,
externalGroups: string[],
): Promise<IGroup[]> {
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<void> {
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<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[]> {
throw new Error('Method not implemented.');
}
transactional(transaction: Knex.Transaction<any, any[]>): IGroupStore {
throw new Error('Method not implemented.');
transactional(transaction: UnleashTransaction): IGroupStore {
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 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);
});