mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01: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 { CreateDependentFeatureSchema } from '../../../../../lib/openapi';
|
||||
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||
import {
|
||||
IUnleashTest,
|
||||
setupAppWithCustomConfig,
|
||||
} from '../../../helpers/test-helper';
|
||||
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
||||
import getLogger from '../../../../fixtures/no-logger';
|
||||
} from '../../../test/e2e/helpers/test-helper';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import { CreateDependentFeatureSchema } from '../../openapi';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
@ -30,6 +30,7 @@ import ProjectArchiveController from './project-archive';
|
||||
import { createKnexTransactionStarter } from '../../../db/transaction';
|
||||
import { Db } from '../../../db/db';
|
||||
import { InvalidOperationError } from '../../../error';
|
||||
import DependentFeaturesController from '../../../features/dependent-features/dependent-features-controller';
|
||||
|
||||
export default class ProjectApi extends Controller {
|
||||
private projectService: ProjectService;
|
||||
@ -112,6 +113,7 @@ export default class ProjectApi extends Controller {
|
||||
createKnexTransactionStarter(db),
|
||||
).router,
|
||||
);
|
||||
this.use('/', new DependentFeaturesController(config, services).router);
|
||||
this.use('/', new EnvironmentsController(config, services).router);
|
||||
this.use('/', new ProjectHealthReport(config, services).router);
|
||||
this.use('/', new VariantsController(config, services).router);
|
||||
|
@ -52,8 +52,7 @@ import {
|
||||
TransactionCreator,
|
||||
UnleashTransaction,
|
||||
} from '../../../db/transaction';
|
||||
import { BadDataError, InvalidOperationError } from '../../../error';
|
||||
import { CreateDependentFeatureSchema } from '../../../openapi/spec/create-dependent-feature-schema';
|
||||
import { BadDataError } from '../../../error';
|
||||
|
||||
interface FeatureStrategyParams {
|
||||
projectId: string;
|
||||
@ -100,7 +99,6 @@ const PATH_ENV = `${PATH_FEATURE}/environments/:environment`;
|
||||
const BULK_PATH_ENV = `/:projectId/bulk_features/environments/:environment`;
|
||||
const PATH_STRATEGIES = `${PATH_ENV}/strategies`;
|
||||
const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
||||
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
||||
|
||||
type ProjectFeaturesServices = Pick<
|
||||
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({
|
||||
method: 'get',
|
||||
path: PATH_STRATEGY,
|
||||
@ -997,27 +972,6 @@ export default class ProjectFeaturesController extends Controller {
|
||||
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(
|
||||
req: Request<FeatureStrategyParams, any, any, any>,
|
||||
res: Response<FeatureStrategySchema[]>,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
StrategyIds,
|
||||
EnvironmentVariantEvent,
|
||||
FEATURE_UPDATED,
|
||||
FeatureArchivedEvent,
|
||||
@ -23,6 +22,7 @@ import {
|
||||
IEventStore,
|
||||
IFeatureEnvironmentInfo,
|
||||
IFeatureEnvironmentStore,
|
||||
IFeatureNaming,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureTagStore,
|
||||
@ -33,29 +33,29 @@ import {
|
||||
IProjectStore,
|
||||
ISegment,
|
||||
IStrategyConfig,
|
||||
IStrategyStore,
|
||||
IUnleashConfig,
|
||||
IUnleashStores,
|
||||
IVariant,
|
||||
PotentiallyStaleOnEvent,
|
||||
Saved,
|
||||
SKIP_CHANGE_REQUEST,
|
||||
StrategiesOrderChangedEvent,
|
||||
StrategyIds,
|
||||
Unsaved,
|
||||
WeightType,
|
||||
StrategiesOrderChangedEvent,
|
||||
PotentiallyStaleOnEvent,
|
||||
IStrategyStore,
|
||||
IFeatureNaming,
|
||||
} from '../types';
|
||||
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 NameExistsError from '../error/name-exists-error';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
import {
|
||||
FOREIGN_KEY_VIOLATION,
|
||||
OperationDeniedError,
|
||||
PermissionError,
|
||||
ForbiddenError,
|
||||
} from '../error';
|
||||
import {
|
||||
constraintSchema,
|
||||
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 { checkFeatureFlagNamesAgainstPattern } from '../features/feature-naming-pattern/feature-naming-validation';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import { CreateDependentFeatureSchema } from '../openapi';
|
||||
|
||||
interface IFeatureContext {
|
||||
featureName: string;
|
||||
@ -123,15 +122,6 @@ export type FeatureNameCheckResultWithFeaturePattern =
|
||||
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) => {
|
||||
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;
|
||||
|
@ -68,6 +68,7 @@ import {
|
||||
createFakeGetActiveUsers,
|
||||
createGetActiveUsers,
|
||||
} from '../features/instance-stats/getActiveUsers';
|
||||
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||
|
||||
// TODO: will be moved to scheduler feature directory
|
||||
export const scheduleServices = async (
|
||||
@ -289,6 +290,8 @@ export const createServices = (
|
||||
|
||||
const eventAnnouncerService = new EventAnnouncerService(stores, config);
|
||||
|
||||
const dependentFeaturesService = new DependentFeaturesService();
|
||||
|
||||
return {
|
||||
accessService,
|
||||
accountService,
|
||||
@ -339,6 +342,7 @@ export const createServices = (
|
||||
transactionalFeatureToggleService,
|
||||
transactionalGroupService,
|
||||
privateProjectChecker,
|
||||
dependentFeaturesService,
|
||||
};
|
||||
};
|
||||
|
||||
@ -382,4 +386,5 @@ export {
|
||||
InstanceStatsService,
|
||||
FavoritesService,
|
||||
SchedulerService,
|
||||
DependentFeaturesService,
|
||||
};
|
||||
|
@ -44,6 +44,7 @@ import { ISegmentService } from '../segments/segment-service-interface';
|
||||
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
||||
import EventAnnouncerService from 'lib/services/event-announcer-service';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||
|
||||
export interface IUnleashServices {
|
||||
accessService: AccessService;
|
||||
@ -99,4 +100,5 @@ export interface IUnleashServices {
|
||||
) => FeatureToggleService;
|
||||
transactionalGroupService: (db: Knex.Transaction) => GroupService;
|
||||
privateProjectChecker: IPrivateProjectChecker;
|
||||
dependentFeaturesService: DependentFeaturesService;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user