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:
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