1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-17 01:17:29 +02:00

feat: read model for dependent features (#4846)

This commit is contained in:
Mateusz Kwasniewski 2023-09-27 14:33:51 +02:00 committed by GitHub
parent b9910bf114
commit fd8775f13d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 111 additions and 50 deletions

View File

@ -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,
);
};

View File

@ -0,0 +1,5 @@
export interface IDependentFeaturesReadModel {
getChildren(parent: string): Promise<string[]>;
getParents(child: string): Promise<string[]>;
getParentOptions(child: string): Promise<string[]>;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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[]>;
}

View File

@ -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)

View File

@ -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([]);
}
}

View File

@ -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();
}

View File

@ -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);

View File

@ -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',
{