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 { 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,
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -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 { 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<void> {
 | 
			
		||||
        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<string[]> {
 | 
			
		||||
        return this.dependentFeaturesStore.getParentOptions(feature);
 | 
			
		||||
        return this.dependentFeaturesReadModel.getParentOptions(feature);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,6 @@ import { FeatureDependency, FeatureDependencyId } from './dependent-features';
 | 
			
		||||
 | 
			
		||||
export interface IDependentFeaturesStore {
 | 
			
		||||
    upsert(featureDependency: FeatureDependency): Promise<void>;
 | 
			
		||||
    getChildren(parent: string): Promise<string[]>;
 | 
			
		||||
    delete(dependency: FeatureDependencyId): Promise<void>;
 | 
			
		||||
    deleteAll(child: string): Promise<void>;
 | 
			
		||||
    getParentOptions(child: string): Promise<string[]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -34,32 +34,6 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
 | 
			
		||||
            .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> {
 | 
			
		||||
        await this.db('dependent_features')
 | 
			
		||||
            .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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChildren(): Promise<string[]> {
 | 
			
		||||
        return Promise.resolve([]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getParentOptions(): Promise<string[]> {
 | 
			
		||||
        return Promise.resolve([]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    delete(): Promise<void> {
 | 
			
		||||
        return Promise.resolve();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user