diff --git a/src/lib/features/dependent-features/createDependentFeaturesService.ts b/src/lib/features/dependent-features/createDependentFeaturesService.ts index f0b3afb099f3..5855629bd6c6 100644 --- a/src/lib/features/dependent-features/createDependentFeaturesService.ts +++ b/src/lib/features/dependent-features/createDependentFeaturesService.ts @@ -1,10 +1,27 @@ import { Db } from '../../db/db'; import { DependentFeaturesService } from './dependent-features-service'; import { DependentFeaturesStore } from './dependent-features-store'; +import { DependentFeaturesReadModel } from './dependent-features-read-model'; +import { FakeDependentFeaturesStore } from './fake-dependent-features-store'; +import { FakeDependentFeaturesReadModel } from './fake-dependent-features-read-model'; export const createDependentFeaturesService = ( db: Db, ): DependentFeaturesService => { const dependentFeaturesStore = new DependentFeaturesStore(db); - return new DependentFeaturesService(dependentFeaturesStore); + const dependentFeaturesReadModel = new DependentFeaturesReadModel(db); + return new DependentFeaturesService( + dependentFeaturesStore, + dependentFeaturesReadModel, + ); }; + +export const createFakeDependentFeaturesService = + (): DependentFeaturesService => { + const dependentFeaturesStore = new FakeDependentFeaturesStore(); + const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel(); + return new DependentFeaturesService( + dependentFeaturesStore, + dependentFeaturesReadModel, + ); + }; diff --git a/src/lib/features/dependent-features/dependent-features-read-model-type.ts b/src/lib/features/dependent-features/dependent-features-read-model-type.ts new file mode 100644 index 000000000000..171b8c894034 --- /dev/null +++ b/src/lib/features/dependent-features/dependent-features-read-model-type.ts @@ -0,0 +1,5 @@ +export interface IDependentFeaturesReadModel { + getChildren(parent: string): Promise; + getParents(child: string): Promise; + getParentOptions(child: string): Promise; +} diff --git a/src/lib/features/dependent-features/dependent-features-read-model.ts b/src/lib/features/dependent-features/dependent-features-read-model.ts new file mode 100644 index 000000000000..a0a3659631de --- /dev/null +++ b/src/lib/features/dependent-features/dependent-features-read-model.ts @@ -0,0 +1,42 @@ +import { Db } from '../../db/db'; +import { IDependentFeaturesReadModel } from './dependent-features-read-model-type'; + +export class DependentFeaturesReadModel implements IDependentFeaturesReadModel { + private db: Db; + + constructor(db: Db) { + this.db = db; + } + + async getChildren(parent: string): Promise { + const rows = await this.db('dependent_features').where( + 'parent', + parent, + ); + + return rows.map((row) => row.child); + } + + async getParents(child: string): Promise { + const rows = await this.db('dependent_features').where('child', child); + + return rows.map((row) => row.parent); + } + + async getParentOptions(child: string): Promise { + const result = await this.db('features as f') + .where('f.name', child) + .select('f.project'); + if (result.length === 0) { + return []; + } + const rows = await this.db('features as f') + .leftJoin('dependent_features as df', 'f.name', 'df.child') + .where('f.project', result[0].project) + .andWhere('f.name', '!=', child) + .andWhere('df.child', null) + .select('f.name'); + + return rows.map((item) => item.name); + } +} diff --git a/src/lib/features/dependent-features/dependent-features-service.ts b/src/lib/features/dependent-features/dependent-features-service.ts index 91aece1a358d..4ee6ac38486d 100644 --- a/src/lib/features/dependent-features/dependent-features-service.ts +++ b/src/lib/features/dependent-features/dependent-features-service.ts @@ -2,12 +2,19 @@ import { InvalidOperationError } from '../../error'; import { CreateDependentFeatureSchema } from '../../openapi'; import { IDependentFeaturesStore } from './dependent-features-store-type'; import { FeatureDependency, FeatureDependencyId } from './dependent-features'; +import { IDependentFeaturesReadModel } from './dependent-features-read-model-type'; export class DependentFeaturesService { private dependentFeaturesStore: IDependentFeaturesStore; - constructor(dependentFeaturesStore: IDependentFeaturesStore) { + private dependentFeaturesReadModel: IDependentFeaturesReadModel; + + constructor( + dependentFeaturesStore: IDependentFeaturesStore, + dependentFeaturesReadModel: IDependentFeaturesReadModel, + ) { this.dependentFeaturesStore = dependentFeaturesStore; + this.dependentFeaturesReadModel = dependentFeaturesReadModel; } async upsertFeatureDependency( @@ -16,7 +23,9 @@ export class DependentFeaturesService { ): Promise { const { enabled, feature: parent, variants } = dependentFeature; - const children = await this.dependentFeaturesStore.getChildren(child); + const children = await this.dependentFeaturesReadModel.getChildren( + child, + ); if (children.length > 0) { throw new InvalidOperationError( 'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.', @@ -50,6 +59,6 @@ export class DependentFeaturesService { } async getParentOptions(feature: string): Promise { - return this.dependentFeaturesStore.getParentOptions(feature); + return this.dependentFeaturesReadModel.getParentOptions(feature); } } diff --git a/src/lib/features/dependent-features/dependent-features-store-type.ts b/src/lib/features/dependent-features/dependent-features-store-type.ts index 6f841b662a6e..7ea3a5a0f16c 100644 --- a/src/lib/features/dependent-features/dependent-features-store-type.ts +++ b/src/lib/features/dependent-features/dependent-features-store-type.ts @@ -2,8 +2,6 @@ import { FeatureDependency, FeatureDependencyId } from './dependent-features'; export interface IDependentFeaturesStore { upsert(featureDependency: FeatureDependency): Promise; - getChildren(parent: string): Promise; delete(dependency: FeatureDependencyId): Promise; deleteAll(child: string): Promise; - getParentOptions(child: string): Promise; } diff --git a/src/lib/features/dependent-features/dependent-features-store.ts b/src/lib/features/dependent-features/dependent-features-store.ts index e7768a1dcff7..a0ccd3c08535 100644 --- a/src/lib/features/dependent-features/dependent-features-store.ts +++ b/src/lib/features/dependent-features/dependent-features-store.ts @@ -34,32 +34,6 @@ export class DependentFeaturesStore implements IDependentFeaturesStore { .merge(); } - async getChildren(parent: string): Promise { - const rows = await this.db('dependent_features').where( - 'parent', - parent, - ); - - return rows.map((row) => row.child); - } - - async getParentOptions(child: string): Promise { - const result = await this.db('features as f') - .where('f.name', child) - .select('f.project'); - if (result.length === 0) { - return []; - } - const rows = await this.db('features as f') - .leftJoin('dependent_features as df', 'f.name', 'df.child') - .where('f.project', result[0].project) - .andWhere('f.name', '!=', child) - .andWhere('df.child', null) - .select('f.name'); - - return rows.map((item) => item.name); - } - async delete(dependency: FeatureDependencyId): Promise { await this.db('dependent_features') .where('parent', dependency.parent) diff --git a/src/lib/features/dependent-features/fake-dependent-features-read-model.ts b/src/lib/features/dependent-features/fake-dependent-features-read-model.ts new file mode 100644 index 000000000000..86efe5fee9da --- /dev/null +++ b/src/lib/features/dependent-features/fake-dependent-features-read-model.ts @@ -0,0 +1,17 @@ +import { IDependentFeaturesReadModel } from './dependent-features-read-model-type'; + +export class FakeDependentFeaturesReadModel + implements IDependentFeaturesReadModel +{ + getChildren(): Promise { + return Promise.resolve([]); + } + + getParents(): Promise { + return Promise.resolve([]); + } + + getParentOptions(): Promise { + return Promise.resolve([]); + } +} diff --git a/src/lib/features/dependent-features/fake-dependent-features-store.ts b/src/lib/features/dependent-features/fake-dependent-features-store.ts index bc6d4955a3e6..5df0fc5690e8 100644 --- a/src/lib/features/dependent-features/fake-dependent-features-store.ts +++ b/src/lib/features/dependent-features/fake-dependent-features-store.ts @@ -5,14 +5,6 @@ export class FakeDependentFeaturesStore implements IDependentFeaturesStore { return Promise.resolve(); } - getChildren(): Promise { - return Promise.resolve([]); - } - - getParentOptions(): Promise { - return Promise.resolve([]); - } - delete(): Promise { return Promise.resolve(); } diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index d4818a12bceb..af1320ff2581 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -69,7 +69,10 @@ import { createGetActiveUsers, } from '../features/instance-stats/getActiveUsers'; import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service'; -import { createDependentFeaturesService } from '../features/dependent-features/createDependentFeaturesService'; +import { + createDependentFeaturesService, + createFakeDependentFeaturesService, +} from '../features/dependent-features/createDependentFeaturesService'; // TODO: will be moved to scheduler feature directory export const scheduleServices = async ( @@ -303,9 +306,9 @@ export const createServices = ( const eventAnnouncerService = new EventAnnouncerService(stores, config); - const dependentFeaturesService = new DependentFeaturesService( - stores.dependentFeaturesStore, - ); + const dependentFeaturesService = db + ? createDependentFeaturesService(db) + : createFakeDependentFeaturesService(); const transactionalDependentFeaturesService = (txDb: Knex.Transaction) => createDependentFeaturesService(txDb); diff --git a/src/test/e2e/api/client/feature.e2e.test.ts b/src/test/e2e/api/client/feature.e2e.test.ts index 57c9194fa205..4e32d5f3945d 100644 --- a/src/test/e2e/api/client/feature.e2e.test.ts +++ b/src/test/e2e/api/client/feature.e2e.test.ts @@ -13,14 +13,18 @@ beforeAll(async () => { db = await dbInit('feature_api_client', getLogger, { experimental: { flags: { dependentFeatures: true } }, }); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, - featureNamingPattern: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + featureNamingPattern: true, + }, }, }, - }); + db.rawDatabase, + ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', {