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:
parent
d213df36dc
commit
3200673f1c
@ -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;
|
||||||
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
37
src/test/fixtures/fake-group-store.ts
vendored
37
src/test/fixtures/fake-group-store.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user