1
0
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:
Mateusz Kwasniewski 2023-09-19 11:23:21 +02:00 committed by GitHub
parent 59f2ae435e
commit 2843388673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 160 additions and 94 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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[]>,

View File

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

View File

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

View File

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