diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 4c1a08f6bb..8e0ee4e1fb 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -37,6 +37,7 @@ import ProjectStatsStore from './project-stats-store'; import { Db } from './db'; import { ImportTogglesStore } from '../features/export-import-toggles/import-toggles-store'; import PrivateProjectStore from '../features/private-project/privateProjectStore'; +import { DependentFeaturesStore } from '../features/dependent-features/dependent-features-store'; export const createStores = ( config: IUnleashConfig, @@ -130,6 +131,7 @@ export const createStores = ( projectStatsStore: new ProjectStatsStore(db, eventBus, getLogger), importTogglesStore: new ImportTogglesStore(db), privateProjectStore: new PrivateProjectStore(db, getLogger), + dependentFeaturesStore: new DependentFeaturesStore(db), }; }; diff --git a/src/lib/features/dependent-features/dependent-features-service.ts b/src/lib/features/dependent-features/dependent-features-service.ts index bd512c514a..a31fb37445 100644 --- a/src/lib/features/dependent-features/dependent-features-service.ts +++ b/src/lib/features/dependent-features/dependent-features-service.ts @@ -1,4 +1,5 @@ import { CreateDependentFeatureSchema } from '../../openapi'; +import { IDependentFeaturesStore } from './dependent-features-store-type'; export type FeatureDependency = | { @@ -9,6 +10,12 @@ export type FeatureDependency = } | { parent: string; child: string; enabled: false }; export class DependentFeaturesService { + private dependentFeaturesStore: IDependentFeaturesStore; + + constructor(dependentFeaturesStore: IDependentFeaturesStore) { + this.dependentFeaturesStore = dependentFeaturesStore; + } + async upsertFeatureDependency( parentFeature: string, dependentFeature: CreateDependentFeatureSchema, @@ -27,6 +34,6 @@ export class DependentFeaturesService { enabled: true, variants, }; - console.log(featureDependency); + await this.dependentFeaturesStore.upsert(featureDependency); } } diff --git a/src/lib/features/dependent-features/dependent-features-store-type.ts b/src/lib/features/dependent-features/dependent-features-store-type.ts new file mode 100644 index 0000000000..f82fbc056f --- /dev/null +++ b/src/lib/features/dependent-features/dependent-features-store-type.ts @@ -0,0 +1,5 @@ +import { FeatureDependency } from './dependent-features-service'; + +export interface IDependentFeaturesStore { + upsert(featureDependency: FeatureDependency): Promise; +} diff --git a/src/lib/features/dependent-features/dependent-features-store.ts b/src/lib/features/dependent-features/dependent-features-store.ts new file mode 100644 index 0000000000..cb3574e411 --- /dev/null +++ b/src/lib/features/dependent-features/dependent-features-store.ts @@ -0,0 +1,31 @@ +import { FeatureDependency } from './dependent-features-service'; +import { Db } from '../../db/db'; +import { IDependentFeaturesStore } from './dependent-features-store-type'; + +type SerializableFeatureDependency = Omit & { + variants?: string; +}; +export class DependentFeaturesStore implements IDependentFeaturesStore { + private db: Db; + + constructor(db: Db) { + this.db = db; + } + + async upsert(featureDependency: FeatureDependency): Promise { + const serializableFeatureDependency: SerializableFeatureDependency = { + parent: featureDependency.parent, + child: featureDependency.child, + enabled: featureDependency.enabled, + }; + if ('variants' in featureDependency) { + serializableFeatureDependency.variants = JSON.stringify( + featureDependency.variants, + ); + } + await this.db('dependent_features') + .insert(serializableFeatureDependency) + .onConflict(['parent', 'child']) + .merge(); + } +} diff --git a/src/lib/features/dependent-features/features.dependencies.e2e.test.ts b/src/lib/features/dependent-features/dependent.features.e2e.test.ts similarity index 82% rename from src/lib/features/dependent-features/features.dependencies.e2e.test.ts rename to src/lib/features/dependent-features/dependent.features.e2e.test.ts index 7ac45786c1..d1b0503cce 100644 --- a/src/lib/features/dependent-features/features.dependencies.e2e.test.ts +++ b/src/lib/features/dependent-features/dependent.features.e2e.test.ts @@ -11,7 +11,7 @@ let app: IUnleashTest; let db: ITestDb; beforeAll(async () => { - db = await dbInit('feature_dependencies', getLogger); + db = await dbInit('dependent_features', getLogger); app = await setupAppWithCustomConfig( db.stores, { @@ -50,7 +50,14 @@ test('should add feature dependency', async () => { await app.createFeature(parent); await app.createFeature(child); + // save explicit enabled and variants await addFeatureDependency(parent, { feature: child, + enabled: false, + }); + // overwrite with implicit enabled: true and variants + await addFeatureDependency(parent, { + feature: child, + variants: ['variantB'], }); }); diff --git a/src/lib/features/dependent-features/fake-dependent-features-store.ts b/src/lib/features/dependent-features/fake-dependent-features-store.ts new file mode 100644 index 0000000000..27c7eb4a73 --- /dev/null +++ b/src/lib/features/dependent-features/fake-dependent-features-store.ts @@ -0,0 +1,7 @@ +import { IDependentFeaturesStore } from './dependent-features-store-type'; + +export class FakeDependentFeaturesStore implements IDependentFeaturesStore { + async upsert(): Promise { + return Promise.resolve(); + } +} diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 4cd83d2b2f..8f1053a947 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -290,7 +290,9 @@ export const createServices = ( const eventAnnouncerService = new EventAnnouncerService(stores, config); - const dependentFeaturesService = new DependentFeaturesService(); + const dependentFeaturesService = new DependentFeaturesService( + stores.dependentFeaturesStore, + ); return { accessService, diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 7dd9f0c64f..da1f7258fb 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -34,6 +34,7 @@ import { IAccountStore } from './stores/account-store'; import { IProjectStatsStore } from './stores/project-stats-store-type'; import { IImportTogglesStore } from '../features/export-import-toggles/import-toggles-store-type'; import { IPrivateProjectStore } from '../features/private-project/privateProjectStoreType'; +import { IDependentFeaturesStore } from '../features/dependent-features/dependent-features-store-type'; export interface IUnleashStores { accessStore: IAccessStore; @@ -72,6 +73,7 @@ export interface IUnleashStores { projectStatsStore: IProjectStatsStore; importTogglesStore: IImportTogglesStore; privateProjectStore: IPrivateProjectStore; + dependentFeaturesStore: IDependentFeaturesStore; } export { @@ -110,4 +112,5 @@ export { IFavoriteProjectsStore, IImportTogglesStore, IPrivateProjectStore, + IDependentFeaturesStore, }; diff --git a/src/migrations/20230919104006-dependent-features.js b/src/migrations/20230919104006-dependent-features.js new file mode 100644 index 0000000000..7cd1fd333a --- /dev/null +++ b/src/migrations/20230919104006-dependent-features.js @@ -0,0 +1,28 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + CREATE TABLE IF NOT EXISTS dependent_features + ( + parent varchar(255) NOT NULL, + child varchar(255) NOT NULL, + enabled boolean DEFAULT true NOT NULL, + variants JSONB DEFAULT '[]'::jsonb NOT NULL, + PRIMARY KEY (parent, child), + FOREIGN KEY (parent) REFERENCES features (name) ON DELETE RESTRICT, + FOREIGN KEY (child) REFERENCES features (name) ON DELETE CASCADE + ); + `, + cb(), + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + DROP TABLE dependent_features; + `, + cb, + ); +}; diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 4ac4ff4508..8055834420 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -37,6 +37,7 @@ import FakeFavoriteFeaturesStore from './fake-favorite-features-store'; import FakeFavoriteProjectsStore from './fake-favorite-projects-store'; import { FakeAccountStore } from './fake-account-store'; import FakeProjectStatsStore from './fake-project-stats-store'; +import { FakeDependentFeaturesStore } from '../../lib/features/dependent-features/fake-dependent-features-store'; const db = { select: () => ({ @@ -83,6 +84,7 @@ const createStores: () => IUnleashStores = () => { projectStatsStore: new FakeProjectStatsStore(), importTogglesStore: {} as IImportTogglesStore, privateProjectStore: {} as IPrivateProjectStore, + dependentFeaturesStore: new FakeDependentFeaturesStore(), }; };