From 8c82e4d0a0c8eaba263c46dfd92b300ced36086c Mon Sep 17 00:00:00 2001 From: sighphyre Date: Thu, 20 Oct 2022 08:35:40 +0200 Subject: [PATCH] small spike for service level transactions, implementing this at the store level for groups --- src/lib/db/group-store.ts | 7 ++- src/lib/db/transactional.ts | 13 ++++++ src/lib/server-impl.ts | 1 + src/lib/types/core.ts | 2 + src/lib/types/stores/group-store.ts | 5 ++- src/lib/types/stores/transactional.ts | 5 +++ src/test/e2e/helpers/database-init.ts | 3 ++ src/test/fixtures/fake-group-store.ts | 5 +++ src/test/transactional.test.ts | 63 +++++++++++++++++++++++++++ 9 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/lib/db/transactional.ts create mode 100644 src/lib/types/stores/transactional.ts create mode 100644 src/test/transactional.test.ts diff --git a/src/lib/db/group-store.ts b/src/lib/db/group-store.ts index 732ebd113d..b91ed844b6 100644 --- a/src/lib/db/group-store.ts +++ b/src/lib/db/group-store.ts @@ -10,6 +10,7 @@ import Group, { IGroupUserModel, } from '../types/group'; import Transaction = Knex.Transaction; +import { AbstractTransactional } from './transactional'; const T = { GROUPS: 'groups', @@ -60,10 +61,14 @@ const groupToRow = (group: IStoreGroup) => ({ mappings_sso: JSON.stringify(group.mappingsSSO), }); -export default class GroupStore implements IGroupStore { +export default class GroupStore + extends AbstractTransactional + implements IGroupStore +{ private db: Knex; constructor(db: Knex) { + super(); this.db = db; } diff --git a/src/lib/db/transactional.ts b/src/lib/db/transactional.ts new file mode 100644 index 0000000000..45db097479 --- /dev/null +++ b/src/lib/db/transactional.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; +import { Transactional } from 'lib/types/stores/transactional'; + +export abstract class AbstractTransactional implements Transactional { + transactional(transaction: Knex.Transaction): T { + let clone = new (this.constructor as { new (): any })(); + for (const attribute in this) { + clone[attribute] = this[attribute]; + } + clone.db = transaction; + return clone as T; + } +} diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index 379389346c..04c5422ca1 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -76,6 +76,7 @@ async function createApp( app, config, version: serverVersion, + db, }; if (config.import.file) { diff --git a/src/lib/types/core.ts b/src/lib/types/core.ts index 70d1b07697..872be09256 100644 --- a/src/lib/types/core.ts +++ b/src/lib/types/core.ts @@ -6,6 +6,7 @@ import User from './user'; import { IUnleashConfig } from './option'; import { IUnleashStores } from './stores'; import { IUnleashServices } from './services'; +import { Knex } from 'knex'; export interface AuthedRequest extends Request { user: User; @@ -20,4 +21,5 @@ export interface IUnleash { services: IUnleashServices; stop: () => Promise; version: string; + db: Knex; } diff --git a/src/lib/types/stores/group-store.ts b/src/lib/types/stores/group-store.ts index 060f357c0e..07f9161fd8 100644 --- a/src/lib/types/stores/group-store.ts +++ b/src/lib/types/stores/group-store.ts @@ -7,6 +7,7 @@ import Group, { IGroupUser, IGroupUserModel, } from '../group'; +import { Transactional } from './transactional'; export interface IStoreGroup { name: string; @@ -14,7 +15,9 @@ export interface IStoreGroup { mappingsSSO?: string[]; } -export interface IGroupStore extends Store { +export interface IGroupStore + extends Store, + Transactional { getGroupsForUser(userId: number): Promise; getOldGroupsForExternalUser( userId: number, diff --git a/src/lib/types/stores/transactional.ts b/src/lib/types/stores/transactional.ts new file mode 100644 index 0000000000..fc44176d02 --- /dev/null +++ b/src/lib/types/stores/transactional.ts @@ -0,0 +1,5 @@ +import { Knex } from 'knex'; + +export interface Transactional { + transactional(transaction: Knex.Transaction): T; +} diff --git a/src/test/e2e/helpers/database-init.ts b/src/test/e2e/helpers/database-init.ts index 2947ff8869..982b07a0fc 100644 --- a/src/test/e2e/helpers/database-init.ts +++ b/src/test/e2e/helpers/database-init.ts @@ -11,6 +11,7 @@ import { IUnleashStores } from '../../../lib/types'; import { IFeatureEnvironmentStore } from '../../../lib/types/stores/feature-environment-store'; import { DEFAULT_ENV } from '../../../lib/util/constants'; import { IUnleashOptions } from 'lib/server-impl'; +import { Knex } from 'knex'; // require('db-migrate-shared').log.silence(false); @@ -73,6 +74,7 @@ async function setupDatabase(stores) { export interface ITestDb { stores: IUnleashStores; + db: Knex; reset: () => Promise; destroy: () => Promise; } @@ -108,6 +110,7 @@ export default async function init( return { stores, + db: testDb, reset: async () => { await resetDatabase(testDb); await setupDatabase(stores); diff --git a/src/test/fixtures/fake-group-store.ts b/src/test/fixtures/fake-group-store.ts index 3395e6d0b6..5d307fa126 100644 --- a/src/test/fixtures/fake-group-store.ts +++ b/src/test/fixtures/fake-group-store.ts @@ -7,6 +7,7 @@ import Group, { IGroupUser, IGroupUserModel, } from '../../lib/types/group'; +import { Knex } from 'knex'; /* eslint-disable @typescript-eslint/no-unused-vars */ export default class FakeGroupStore implements IGroupStore { data: IGroup[]; @@ -109,4 +110,8 @@ export default class FakeGroupStore implements IGroupStore { getGroupsForUser(userId: number): Promise { throw new Error('Method not implemented.'); } + + transactional(transaction: Knex.Transaction): IGroupStore { + throw new Error('Method not implemented.'); + } } diff --git a/src/test/transactional.test.ts b/src/test/transactional.test.ts new file mode 100644 index 0000000000..944da64ff3 --- /dev/null +++ b/src/test/transactional.test.ts @@ -0,0 +1,63 @@ +import dbInit, { ITestDb } from './/e2e/helpers/database-init'; +import getLogger from './fixtures/no-logger'; + +let stores; +let db: ITestDb; + +beforeAll(async () => { + db = await dbInit('group_service_serial', getLogger); + stores = db.stores; + + await stores.groupStore.create({ + name: 'dev_group', + description: 'dev_group', + mappingsSSO: ['dev'], + }); + await stores.groupStore.create({ + name: 'maintainer_group', + description: 'maintainer_group', + mappingsSSO: ['maintainer'], + }); + + await stores.groupStore.create({ + name: 'admin_group', + description: 'admin_group', + mappingsSSO: ['admin'], + }); +}); + +afterAll(async () => { + await db.destroy(); +}); + +test('should actually do something transactional mode', async () => { + await db.db.transaction(async (trx) => { + await stores.groupStore.transactional(trx).create({ + name: 'some_other_group', + description: 'admin_group', + mappingsSSO: ['admin'], + }); + }); + + const groups = await stores.groupStore.getAll(); + const createdGroup = groups.find((group) => { + return group.name === 'some_other_group'; + }); + expect(createdGroup).toBeDefined(); +}); + +test('should actually do something transactional mode', async () => { + await db.db.transaction(async (trx) => { + await stores.groupStore.transactional(trx).create({ + name: 'some_other_group', + description: 'admin_group', + mappingsSSO: ['admin'], + }); + }); + + const groups = await stores.groupStore.getAll(); + const createdGroup = groups.find((group) => { + return group.name === 'some_other_group'; + }); + expect(createdGroup).toBeDefined(); +});