mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-15 17:50:48 +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 { 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