mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: read model for dependent features (#4846)
This commit is contained in:
		
							parent
							
								
									b9910bf114
								
							
						
					
					
						commit
						fd8775f13d
					
				@ -1,10 +1,27 @@
 | 
				
			|||||||
import { Db } from '../../db/db';
 | 
					import { Db } from '../../db/db';
 | 
				
			||||||
import { DependentFeaturesService } from './dependent-features-service';
 | 
					import { DependentFeaturesService } from './dependent-features-service';
 | 
				
			||||||
import { DependentFeaturesStore } from './dependent-features-store';
 | 
					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 = (
 | 
					export const createDependentFeaturesService = (
 | 
				
			||||||
    db: Db,
 | 
					    db: Db,
 | 
				
			||||||
): DependentFeaturesService => {
 | 
					): DependentFeaturesService => {
 | 
				
			||||||
    const dependentFeaturesStore = new DependentFeaturesStore(db);
 | 
					    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,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					export interface IDependentFeaturesReadModel {
 | 
				
			||||||
 | 
					    getChildren(parent: string): Promise<string[]>;
 | 
				
			||||||
 | 
					    getParents(child: string): Promise<string[]>;
 | 
				
			||||||
 | 
					    getParentOptions(child: string): Promise<string[]>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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<string[]> {
 | 
				
			||||||
 | 
					        const rows = await this.db('dependent_features').where(
 | 
				
			||||||
 | 
					            'parent',
 | 
				
			||||||
 | 
					            parent,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return rows.map((row) => row.child);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getParents(child: string): Promise<string[]> {
 | 
				
			||||||
 | 
					        const rows = await this.db('dependent_features').where('child', child);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return rows.map((row) => row.parent);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getParentOptions(child: string): Promise<string[]> {
 | 
				
			||||||
 | 
					        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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,12 +2,19 @@ import { InvalidOperationError } from '../../error';
 | 
				
			|||||||
import { CreateDependentFeatureSchema } from '../../openapi';
 | 
					import { CreateDependentFeatureSchema } from '../../openapi';
 | 
				
			||||||
import { IDependentFeaturesStore } from './dependent-features-store-type';
 | 
					import { IDependentFeaturesStore } from './dependent-features-store-type';
 | 
				
			||||||
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
 | 
					import { FeatureDependency, FeatureDependencyId } from './dependent-features';
 | 
				
			||||||
 | 
					import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class DependentFeaturesService {
 | 
					export class DependentFeaturesService {
 | 
				
			||||||
    private dependentFeaturesStore: IDependentFeaturesStore;
 | 
					    private dependentFeaturesStore: IDependentFeaturesStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(dependentFeaturesStore: IDependentFeaturesStore) {
 | 
					    private dependentFeaturesReadModel: IDependentFeaturesReadModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(
 | 
				
			||||||
 | 
					        dependentFeaturesStore: IDependentFeaturesStore,
 | 
				
			||||||
 | 
					        dependentFeaturesReadModel: IDependentFeaturesReadModel,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
        this.dependentFeaturesStore = dependentFeaturesStore;
 | 
					        this.dependentFeaturesStore = dependentFeaturesStore;
 | 
				
			||||||
 | 
					        this.dependentFeaturesReadModel = dependentFeaturesReadModel;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async upsertFeatureDependency(
 | 
					    async upsertFeatureDependency(
 | 
				
			||||||
@ -16,7 +23,9 @@ export class DependentFeaturesService {
 | 
				
			|||||||
    ): Promise<void> {
 | 
					    ): Promise<void> {
 | 
				
			||||||
        const { enabled, feature: parent, variants } = dependentFeature;
 | 
					        const { enabled, feature: parent, variants } = dependentFeature;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const children = await this.dependentFeaturesStore.getChildren(child);
 | 
					        const children = await this.dependentFeaturesReadModel.getChildren(
 | 
				
			||||||
 | 
					            child,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        if (children.length > 0) {
 | 
					        if (children.length > 0) {
 | 
				
			||||||
            throw new InvalidOperationError(
 | 
					            throw new InvalidOperationError(
 | 
				
			||||||
                'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.',
 | 
					                '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<string[]> {
 | 
					    async getParentOptions(feature: string): Promise<string[]> {
 | 
				
			||||||
        return this.dependentFeaturesStore.getParentOptions(feature);
 | 
					        return this.dependentFeaturesReadModel.getParentOptions(feature);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,6 @@ import { FeatureDependency, FeatureDependencyId } from './dependent-features';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface IDependentFeaturesStore {
 | 
					export interface IDependentFeaturesStore {
 | 
				
			||||||
    upsert(featureDependency: FeatureDependency): Promise<void>;
 | 
					    upsert(featureDependency: FeatureDependency): Promise<void>;
 | 
				
			||||||
    getChildren(parent: string): Promise<string[]>;
 | 
					 | 
				
			||||||
    delete(dependency: FeatureDependencyId): Promise<void>;
 | 
					    delete(dependency: FeatureDependencyId): Promise<void>;
 | 
				
			||||||
    deleteAll(child: string): Promise<void>;
 | 
					    deleteAll(child: string): Promise<void>;
 | 
				
			||||||
    getParentOptions(child: string): Promise<string[]>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -34,32 +34,6 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
 | 
				
			|||||||
            .merge();
 | 
					            .merge();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getChildren(parent: string): Promise<string[]> {
 | 
					 | 
				
			||||||
        const rows = await this.db('dependent_features').where(
 | 
					 | 
				
			||||||
            'parent',
 | 
					 | 
				
			||||||
            parent,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return rows.map((row) => row.child);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getParentOptions(child: string): Promise<string[]> {
 | 
					 | 
				
			||||||
        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<void> {
 | 
					    async delete(dependency: FeatureDependencyId): Promise<void> {
 | 
				
			||||||
        await this.db('dependent_features')
 | 
					        await this.db('dependent_features')
 | 
				
			||||||
            .where('parent', dependency.parent)
 | 
					            .where('parent', dependency.parent)
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class FakeDependentFeaturesReadModel
 | 
				
			||||||
 | 
					    implements IDependentFeaturesReadModel
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    getChildren(): Promise<string[]> {
 | 
				
			||||||
 | 
					        return Promise.resolve([]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getParents(): Promise<string[]> {
 | 
				
			||||||
 | 
					        return Promise.resolve([]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getParentOptions(): Promise<string[]> {
 | 
				
			||||||
 | 
					        return Promise.resolve([]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -5,14 +5,6 @@ export class FakeDependentFeaturesStore implements IDependentFeaturesStore {
 | 
				
			|||||||
        return Promise.resolve();
 | 
					        return Promise.resolve();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getChildren(): Promise<string[]> {
 | 
					 | 
				
			||||||
        return Promise.resolve([]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    getParentOptions(): Promise<string[]> {
 | 
					 | 
				
			||||||
        return Promise.resolve([]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    delete(): Promise<void> {
 | 
					    delete(): Promise<void> {
 | 
				
			||||||
        return Promise.resolve();
 | 
					        return Promise.resolve();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -69,7 +69,10 @@ import {
 | 
				
			|||||||
    createGetActiveUsers,
 | 
					    createGetActiveUsers,
 | 
				
			||||||
} from '../features/instance-stats/getActiveUsers';
 | 
					} from '../features/instance-stats/getActiveUsers';
 | 
				
			||||||
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
 | 
					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
 | 
					// TODO: will be moved to scheduler feature directory
 | 
				
			||||||
export const scheduleServices = async (
 | 
					export const scheduleServices = async (
 | 
				
			||||||
@ -303,9 +306,9 @@ export const createServices = (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const eventAnnouncerService = new EventAnnouncerService(stores, config);
 | 
					    const eventAnnouncerService = new EventAnnouncerService(stores, config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const dependentFeaturesService = new DependentFeaturesService(
 | 
					    const dependentFeaturesService = db
 | 
				
			||||||
        stores.dependentFeaturesStore,
 | 
					        ? createDependentFeaturesService(db)
 | 
				
			||||||
    );
 | 
					        : createFakeDependentFeaturesService();
 | 
				
			||||||
    const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
 | 
					    const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
 | 
				
			||||||
        createDependentFeaturesService(txDb);
 | 
					        createDependentFeaturesService(txDb);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -13,14 +13,18 @@ beforeAll(async () => {
 | 
				
			|||||||
    db = await dbInit('feature_api_client', getLogger, {
 | 
					    db = await dbInit('feature_api_client', getLogger, {
 | 
				
			||||||
        experimental: { flags: { dependentFeatures: true } },
 | 
					        experimental: { flags: { dependentFeatures: true } },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    app = await setupAppWithCustomConfig(db.stores, {
 | 
					    app = await setupAppWithCustomConfig(
 | 
				
			||||||
        experimental: {
 | 
					        db.stores,
 | 
				
			||||||
            flags: {
 | 
					        {
 | 
				
			||||||
                strictSchemaValidation: true,
 | 
					            experimental: {
 | 
				
			||||||
                featureNamingPattern: true,
 | 
					                flags: {
 | 
				
			||||||
 | 
					                    strictSchemaValidation: true,
 | 
				
			||||||
 | 
					                    featureNamingPattern: true,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    });
 | 
					        db.rawDatabase,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    await app.services.featureToggleServiceV2.createFeatureToggle(
 | 
					    await app.services.featureToggleServiceV2.createFeatureToggle(
 | 
				
			||||||
        'default',
 | 
					        'default',
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user