mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
refactor: feature oriented architecture for feature dependencies (#4771)
This commit is contained in:
parent
59f2ae435e
commit
2843388673
@ -0,0 +1,102 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import Controller from '../../routes/controller';
|
||||||
|
import { OpenApiService } from '../../services';
|
||||||
|
import {
|
||||||
|
CREATE_FEATURE,
|
||||||
|
IFlagResolver,
|
||||||
|
IUnleashConfig,
|
||||||
|
IUnleashServices,
|
||||||
|
} from '../../types';
|
||||||
|
import { Logger } from '../../logger';
|
||||||
|
import {
|
||||||
|
CreateDependentFeatureSchema,
|
||||||
|
createRequestSchema,
|
||||||
|
emptyResponse,
|
||||||
|
getStandardResponses,
|
||||||
|
} from '../../openapi';
|
||||||
|
import { IAuthRequest } from '../../routes/unleash-types';
|
||||||
|
import { InvalidOperationError } from '../../error';
|
||||||
|
import { DependentFeaturesService } from './dependent-features-service';
|
||||||
|
|
||||||
|
interface FeatureParams {
|
||||||
|
featureName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PATH = '/:projectId/features';
|
||||||
|
const PATH_FEATURE = `${PATH}/:featureName`;
|
||||||
|
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
||||||
|
|
||||||
|
type DependentFeaturesServices = Pick<
|
||||||
|
IUnleashServices,
|
||||||
|
'dependentFeaturesService' | 'openApiService'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default class DependentFeaturesController extends Controller {
|
||||||
|
private dependentFeaturesService: DependentFeaturesService;
|
||||||
|
|
||||||
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: IUnleashConfig,
|
||||||
|
{ dependentFeaturesService, openApiService }: DependentFeaturesServices,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
this.dependentFeaturesService = dependentFeaturesService;
|
||||||
|
this.openApiService = openApiService;
|
||||||
|
this.flagResolver = config.flagResolver;
|
||||||
|
this.logger = config.getLogger(
|
||||||
|
'/dependent-features/dependent-feature-service.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: PATH_DEPENDENCIES,
|
||||||
|
handler: this.addFeatureDependency,
|
||||||
|
permission: CREATE_FEATURE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Features'],
|
||||||
|
summary: 'Add a feature dependency.',
|
||||||
|
description:
|
||||||
|
'Add a dependency to a parent feature. Each environment will resolve corresponding dependency independently.',
|
||||||
|
operationId: 'addFeatureDependency',
|
||||||
|
requestBody: createRequestSchema(
|
||||||
|
'createDependentFeatureSchema',
|
||||||
|
),
|
||||||
|
responses: {
|
||||||
|
200: emptyResponse,
|
||||||
|
...getStandardResponses(401, 403, 404),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFeatureDependency(
|
||||||
|
req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { featureName } = req.params;
|
||||||
|
const { variants, enabled, feature } = req.body;
|
||||||
|
|
||||||
|
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
||||||
|
await this.dependentFeaturesService.upsertFeatureDependency(
|
||||||
|
featureName,
|
||||||
|
{
|
||||||
|
variants,
|
||||||
|
enabled,
|
||||||
|
feature,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
res.status(200).end();
|
||||||
|
} else {
|
||||||
|
throw new InvalidOperationError(
|
||||||
|
'Dependent features are not enabled',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CreateDependentFeatureSchema } from '../../openapi';
|
||||||
|
|
||||||
|
export type FeatureDependency =
|
||||||
|
| {
|
||||||
|
parent: string;
|
||||||
|
child: string;
|
||||||
|
enabled: true;
|
||||||
|
variants?: string[];
|
||||||
|
}
|
||||||
|
| { parent: string; child: string; enabled: false };
|
||||||
|
export class DependentFeaturesService {
|
||||||
|
async upsertFeatureDependency(
|
||||||
|
parentFeature: string,
|
||||||
|
dependentFeature: CreateDependentFeatureSchema,
|
||||||
|
): Promise<void> {
|
||||||
|
const { enabled, feature, variants } = dependentFeature;
|
||||||
|
const featureDependency: FeatureDependency =
|
||||||
|
enabled === false
|
||||||
|
? {
|
||||||
|
parent: parentFeature,
|
||||||
|
child: feature,
|
||||||
|
enabled,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
parent: parentFeature,
|
||||||
|
child: feature,
|
||||||
|
enabled: true,
|
||||||
|
variants,
|
||||||
|
};
|
||||||
|
console.log(featureDependency);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { CreateDependentFeatureSchema } from '../../../../../lib/openapi';
|
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
import {
|
import {
|
||||||
IUnleashTest,
|
IUnleashTest,
|
||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../../helpers/test-helper';
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
import getLogger from '../../../../fixtures/no-logger';
|
import { CreateDependentFeatureSchema } from '../../openapi';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
@ -30,6 +30,7 @@ import ProjectArchiveController from './project-archive';
|
|||||||
import { createKnexTransactionStarter } from '../../../db/transaction';
|
import { createKnexTransactionStarter } from '../../../db/transaction';
|
||||||
import { Db } from '../../../db/db';
|
import { Db } from '../../../db/db';
|
||||||
import { InvalidOperationError } from '../../../error';
|
import { InvalidOperationError } from '../../../error';
|
||||||
|
import DependentFeaturesController from '../../../features/dependent-features/dependent-features-controller';
|
||||||
|
|
||||||
export default class ProjectApi extends Controller {
|
export default class ProjectApi extends Controller {
|
||||||
private projectService: ProjectService;
|
private projectService: ProjectService;
|
||||||
@ -112,6 +113,7 @@ export default class ProjectApi extends Controller {
|
|||||||
createKnexTransactionStarter(db),
|
createKnexTransactionStarter(db),
|
||||||
).router,
|
).router,
|
||||||
);
|
);
|
||||||
|
this.use('/', new DependentFeaturesController(config, services).router);
|
||||||
this.use('/', new EnvironmentsController(config, services).router);
|
this.use('/', new EnvironmentsController(config, services).router);
|
||||||
this.use('/', new ProjectHealthReport(config, services).router);
|
this.use('/', new ProjectHealthReport(config, services).router);
|
||||||
this.use('/', new VariantsController(config, services).router);
|
this.use('/', new VariantsController(config, services).router);
|
||||||
|
@ -52,8 +52,7 @@ import {
|
|||||||
TransactionCreator,
|
TransactionCreator,
|
||||||
UnleashTransaction,
|
UnleashTransaction,
|
||||||
} from '../../../db/transaction';
|
} from '../../../db/transaction';
|
||||||
import { BadDataError, InvalidOperationError } from '../../../error';
|
import { BadDataError } from '../../../error';
|
||||||
import { CreateDependentFeatureSchema } from '../../../openapi/spec/create-dependent-feature-schema';
|
|
||||||
|
|
||||||
interface FeatureStrategyParams {
|
interface FeatureStrategyParams {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -100,7 +99,6 @@ const PATH_ENV = `${PATH_FEATURE}/environments/:environment`;
|
|||||||
const BULK_PATH_ENV = `/:projectId/bulk_features/environments/:environment`;
|
const BULK_PATH_ENV = `/:projectId/bulk_features/environments/:environment`;
|
||||||
const PATH_STRATEGIES = `${PATH_ENV}/strategies`;
|
const PATH_STRATEGIES = `${PATH_ENV}/strategies`;
|
||||||
const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
||||||
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
|
||||||
|
|
||||||
type ProjectFeaturesServices = Pick<
|
type ProjectFeaturesServices = Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
@ -299,29 +297,6 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route({
|
|
||||||
method: 'post',
|
|
||||||
path: PATH_DEPENDENCIES,
|
|
||||||
handler: this.addFeatureDependency,
|
|
||||||
permission: CREATE_FEATURE,
|
|
||||||
middleware: [
|
|
||||||
openApiService.validPath({
|
|
||||||
tags: ['Features'],
|
|
||||||
summary: 'Add a feature dependency.',
|
|
||||||
description:
|
|
||||||
'Add a dependency to a parent feature. Each environment will resolve corresponding dependency independently.',
|
|
||||||
operationId: 'addFeatureDependency',
|
|
||||||
requestBody: createRequestSchema(
|
|
||||||
'createDependentFeatureSchema',
|
|
||||||
),
|
|
||||||
responses: {
|
|
||||||
200: emptyResponse,
|
|
||||||
...getStandardResponses(401, 403, 404),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: PATH_STRATEGY,
|
path: PATH_STRATEGY,
|
||||||
@ -997,27 +972,6 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
res.status(200).json(updatedStrategy);
|
res.status(200).json(updatedStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFeatureDependency(
|
|
||||||
req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>,
|
|
||||||
res: Response,
|
|
||||||
): Promise<void> {
|
|
||||||
const { featureName } = req.params;
|
|
||||||
const { variants, enabled, feature } = req.body;
|
|
||||||
|
|
||||||
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
|
||||||
await this.featureService.upsertFeatureDependency(featureName, {
|
|
||||||
variants,
|
|
||||||
enabled,
|
|
||||||
feature,
|
|
||||||
});
|
|
||||||
res.status(200).end();
|
|
||||||
} else {
|
|
||||||
throw new InvalidOperationError(
|
|
||||||
'Dependent features are not enabled',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFeatureStrategies(
|
async getFeatureStrategies(
|
||||||
req: Request<FeatureStrategyParams, any, any, any>,
|
req: Request<FeatureStrategyParams, any, any, any>,
|
||||||
res: Response<FeatureStrategySchema[]>,
|
res: Response<FeatureStrategySchema[]>,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
CREATE_FEATURE_STRATEGY,
|
CREATE_FEATURE_STRATEGY,
|
||||||
StrategyIds,
|
|
||||||
EnvironmentVariantEvent,
|
EnvironmentVariantEvent,
|
||||||
FEATURE_UPDATED,
|
FEATURE_UPDATED,
|
||||||
FeatureArchivedEvent,
|
FeatureArchivedEvent,
|
||||||
@ -23,6 +22,7 @@ import {
|
|||||||
IEventStore,
|
IEventStore,
|
||||||
IFeatureEnvironmentInfo,
|
IFeatureEnvironmentInfo,
|
||||||
IFeatureEnvironmentStore,
|
IFeatureEnvironmentStore,
|
||||||
|
IFeatureNaming,
|
||||||
IFeatureOverview,
|
IFeatureOverview,
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
IFeatureTagStore,
|
IFeatureTagStore,
|
||||||
@ -33,29 +33,29 @@ import {
|
|||||||
IProjectStore,
|
IProjectStore,
|
||||||
ISegment,
|
ISegment,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
|
IStrategyStore,
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
IVariant,
|
IVariant,
|
||||||
|
PotentiallyStaleOnEvent,
|
||||||
Saved,
|
Saved,
|
||||||
SKIP_CHANGE_REQUEST,
|
SKIP_CHANGE_REQUEST,
|
||||||
|
StrategiesOrderChangedEvent,
|
||||||
|
StrategyIds,
|
||||||
Unsaved,
|
Unsaved,
|
||||||
WeightType,
|
WeightType,
|
||||||
StrategiesOrderChangedEvent,
|
|
||||||
PotentiallyStaleOnEvent,
|
|
||||||
IStrategyStore,
|
|
||||||
IFeatureNaming,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { PatternError } from '../error';
|
import {
|
||||||
|
ForbiddenError,
|
||||||
|
FOREIGN_KEY_VIOLATION,
|
||||||
|
OperationDeniedError,
|
||||||
|
PatternError,
|
||||||
|
PermissionError,
|
||||||
|
} from '../error';
|
||||||
import BadDataError from '../error/bad-data-error';
|
import BadDataError from '../error/bad-data-error';
|
||||||
import NameExistsError from '../error/name-exists-error';
|
import NameExistsError from '../error/name-exists-error';
|
||||||
import InvalidOperationError from '../error/invalid-operation-error';
|
import InvalidOperationError from '../error/invalid-operation-error';
|
||||||
import {
|
|
||||||
FOREIGN_KEY_VIOLATION,
|
|
||||||
OperationDeniedError,
|
|
||||||
PermissionError,
|
|
||||||
ForbiddenError,
|
|
||||||
} from '../error';
|
|
||||||
import {
|
import {
|
||||||
constraintSchema,
|
constraintSchema,
|
||||||
featureMetadataSchema,
|
featureMetadataSchema,
|
||||||
@ -96,7 +96,6 @@ import { ISegmentService } from 'lib/segments/segment-service-interface';
|
|||||||
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
|
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
|
||||||
import { checkFeatureFlagNamesAgainstPattern } from '../features/feature-naming-pattern/feature-naming-validation';
|
import { checkFeatureFlagNamesAgainstPattern } from '../features/feature-naming-pattern/feature-naming-validation';
|
||||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||||
import { CreateDependentFeatureSchema } from '../openapi';
|
|
||||||
|
|
||||||
interface IFeatureContext {
|
interface IFeatureContext {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
@ -123,15 +122,6 @@ export type FeatureNameCheckResultWithFeaturePattern =
|
|||||||
featureNaming: IFeatureNaming;
|
featureNaming: IFeatureNaming;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FeatureDependency =
|
|
||||||
| {
|
|
||||||
parent: string;
|
|
||||||
child: string;
|
|
||||||
enabled: true;
|
|
||||||
variants?: string[];
|
|
||||||
}
|
|
||||||
| { parent: string; child: string; enabled: false };
|
|
||||||
|
|
||||||
const oneOf = (values: string[], match: string) => {
|
const oneOf = (values: string[], match: string) => {
|
||||||
return values.some((value) => value === match);
|
return values.some((value) => value === match);
|
||||||
};
|
};
|
||||||
@ -2211,27 +2201,6 @@ class FeatureToggleService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertFeatureDependency(
|
|
||||||
parentFeature: string,
|
|
||||||
dependentFeature: CreateDependentFeatureSchema,
|
|
||||||
): Promise<void> {
|
|
||||||
const { enabled, feature, variants } = dependentFeature;
|
|
||||||
const featureDependency: FeatureDependency =
|
|
||||||
enabled === false
|
|
||||||
? {
|
|
||||||
parent: parentFeature,
|
|
||||||
child: feature,
|
|
||||||
enabled,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
parent: parentFeature,
|
|
||||||
child: feature,
|
|
||||||
enabled: true,
|
|
||||||
variants,
|
|
||||||
};
|
|
||||||
console.log(featureDependency);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FeatureToggleService;
|
export default FeatureToggleService;
|
||||||
|
@ -68,6 +68,7 @@ import {
|
|||||||
createFakeGetActiveUsers,
|
createFakeGetActiveUsers,
|
||||||
createGetActiveUsers,
|
createGetActiveUsers,
|
||||||
} from '../features/instance-stats/getActiveUsers';
|
} from '../features/instance-stats/getActiveUsers';
|
||||||
|
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||||
|
|
||||||
// TODO: will be moved to scheduler feature directory
|
// TODO: will be moved to scheduler feature directory
|
||||||
export const scheduleServices = async (
|
export const scheduleServices = async (
|
||||||
@ -289,6 +290,8 @@ export const createServices = (
|
|||||||
|
|
||||||
const eventAnnouncerService = new EventAnnouncerService(stores, config);
|
const eventAnnouncerService = new EventAnnouncerService(stores, config);
|
||||||
|
|
||||||
|
const dependentFeaturesService = new DependentFeaturesService();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
accountService,
|
accountService,
|
||||||
@ -339,6 +342,7 @@ export const createServices = (
|
|||||||
transactionalFeatureToggleService,
|
transactionalFeatureToggleService,
|
||||||
transactionalGroupService,
|
transactionalGroupService,
|
||||||
privateProjectChecker,
|
privateProjectChecker,
|
||||||
|
dependentFeaturesService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -382,4 +386,5 @@ export {
|
|||||||
InstanceStatsService,
|
InstanceStatsService,
|
||||||
FavoritesService,
|
FavoritesService,
|
||||||
SchedulerService,
|
SchedulerService,
|
||||||
|
DependentFeaturesService,
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,7 @@ import { ISegmentService } from '../segments/segment-service-interface';
|
|||||||
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
||||||
import EventAnnouncerService from 'lib/services/event-announcer-service';
|
import EventAnnouncerService from 'lib/services/event-announcer-service';
|
||||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||||
|
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -99,4 +100,5 @@ export interface IUnleashServices {
|
|||||||
) => FeatureToggleService;
|
) => FeatureToggleService;
|
||||||
transactionalGroupService: (db: Knex.Transaction) => GroupService;
|
transactionalGroupService: (db: Knex.Transaction) => GroupService;
|
||||||
privateProjectChecker: IPrivateProjectChecker;
|
privateProjectChecker: IPrivateProjectChecker;
|
||||||
|
dependentFeaturesService: DependentFeaturesService;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user