mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
feat: stub for create dependent features (#4769)
This commit is contained in:
parent
a71c3fe43a
commit
59f2ae435e
@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
|
||||
"caseInsensitiveInOperators": false,
|
||||
"customRootRolesKillSwitch": false,
|
||||
"demo": false,
|
||||
"dependentFeatures": false,
|
||||
"disableBulkToggle": false,
|
||||
"disableNotifications": false,
|
||||
"doraMetrics": false,
|
||||
@ -114,6 +115,7 @@ exports[`should create default config 1`] = `
|
||||
"caseInsensitiveInOperators": false,
|
||||
"customRootRolesKillSwitch": false,
|
||||
"demo": false,
|
||||
"dependentFeatures": false,
|
||||
"disableBulkToggle": false,
|
||||
"disableNotifications": false,
|
||||
"doraMetrics": false,
|
||||
|
@ -158,6 +158,8 @@ import {
|
||||
createGroupSchema,
|
||||
doraFeaturesSchema,
|
||||
projectDoraMetricsSchema,
|
||||
dependentFeatureSchema,
|
||||
createDependentFeatureSchema,
|
||||
} from './spec';
|
||||
import { IServerOption } from '../types';
|
||||
import { mapValues, omitKeys } from '../util';
|
||||
@ -375,6 +377,8 @@ export const schemas: UnleashSchemas = {
|
||||
createFeatureNamingPatternSchema,
|
||||
doraFeaturesSchema,
|
||||
projectDoraMetricsSchema,
|
||||
dependentFeatureSchema,
|
||||
createDependentFeatureSchema,
|
||||
};
|
||||
|
||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
||||
|
35
src/lib/openapi/spec/create-dependent-feature-schema.ts
Normal file
35
src/lib/openapi/spec/create-dependent-feature-schema.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const createDependentFeatureSchema = {
|
||||
$id: '#/components/schemas/createDependentFeatureSchema',
|
||||
type: 'object',
|
||||
description: 'Feature dependency on a parent feature in write model',
|
||||
required: ['feature'],
|
||||
properties: {
|
||||
feature: {
|
||||
type: 'string',
|
||||
description: 'The name of the feature we depend on.',
|
||||
example: 'parent_feature',
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether the parent feature should be enabled. When `false` variants are ignored. `true` by default.',
|
||||
example: false,
|
||||
},
|
||||
variants: {
|
||||
type: 'array',
|
||||
description:
|
||||
'The list of variants the parent feature should resolve to. Leave empty when you only want to check the `enabled` status.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
example: ['variantA', 'variantB'],
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type CreateDependentFeatureSchema = FromSchema<
|
||||
typeof createDependentFeatureSchema
|
||||
>;
|
14
src/lib/openapi/spec/dependent-feature-schema.ts
Normal file
14
src/lib/openapi/spec/dependent-feature-schema.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { createDependentFeatureSchema } from './create-dependent-feature-schema';
|
||||
|
||||
export const dependentFeatureSchema = {
|
||||
$id: '#/components/schemas/dependentFeatureSchema',
|
||||
type: 'object',
|
||||
description: 'Feature dependency on a parent feature in read model',
|
||||
required: ['feature'],
|
||||
additionalProperties: false,
|
||||
properties: createDependentFeatureSchema.properties,
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type DependentFeatureSchema = FromSchema<typeof dependentFeatureSchema>;
|
@ -157,3 +157,5 @@ export * from './create-group-schema';
|
||||
export * from './application-usage-schema';
|
||||
export * from './dora-features-schema';
|
||||
export * from './project-dora-metrics-schema';
|
||||
export * from './dependent-feature-schema';
|
||||
export * from './create-dependent-feature-schema';
|
||||
|
@ -2,18 +2,18 @@ import { Request, Response } from 'express';
|
||||
import { applyPatch, Operation } from 'fast-json-patch';
|
||||
import Controller from '../../controller';
|
||||
import {
|
||||
IUnleashConfig,
|
||||
IUnleashServices,
|
||||
serializeDates,
|
||||
CREATE_FEATURE,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
DELETE_FEATURE,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
IFlagResolver,
|
||||
IUnleashConfig,
|
||||
IUnleashServices,
|
||||
NONE,
|
||||
serializeDates,
|
||||
UPDATE_FEATURE,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
IFlagResolver,
|
||||
} from '../../../types';
|
||||
import { Logger } from '../../../logger';
|
||||
import { extractUsername } from '../../../util';
|
||||
@ -42,9 +42,9 @@ import {
|
||||
UpdateFeatureStrategySchema,
|
||||
} from '../../../openapi';
|
||||
import {
|
||||
OpenApiService,
|
||||
FeatureToggleService,
|
||||
FeatureTagService,
|
||||
FeatureToggleService,
|
||||
OpenApiService,
|
||||
} from '../../../services';
|
||||
import { querySchema } from '../../../schema/feature-schema';
|
||||
import { BatchStaleSchema } from '../../../openapi/spec/batch-stale-schema';
|
||||
@ -52,7 +52,8 @@ import {
|
||||
TransactionCreator,
|
||||
UnleashTransaction,
|
||||
} from '../../../db/transaction';
|
||||
import { BadDataError } from '../../../error';
|
||||
import { BadDataError, InvalidOperationError } from '../../../error';
|
||||
import { CreateDependentFeatureSchema } from '../../../openapi/spec/create-dependent-feature-schema';
|
||||
|
||||
interface FeatureStrategyParams {
|
||||
projectId: string;
|
||||
@ -99,6 +100,7 @@ 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,
|
||||
@ -297,6 +299,29 @@ 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,
|
||||
@ -972,6 +997,27 @@ 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[]>,
|
||||
|
@ -96,6 +96,7 @@ 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;
|
||||
@ -122,6 +123,15 @@ 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);
|
||||
};
|
||||
@ -2201,6 +2211,27 @@ 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;
|
||||
|
@ -30,7 +30,8 @@ export type IFlagKey =
|
||||
| 'doraMetrics'
|
||||
| 'variantTypeNumber'
|
||||
| 'accessOverview'
|
||||
| 'privateProjects';
|
||||
| 'privateProjects'
|
||||
| 'dependentFeatures';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -130,6 +131,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_DORA_METRICS,
|
||||
false,
|
||||
),
|
||||
dependentFeatures: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_DEPENDENT_FEATURES,
|
||||
false,
|
||||
),
|
||||
variantTypeNumber: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER,
|
||||
false,
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { CreateDependentFeatureSchema } from '../../../../../lib/openapi';
|
||||
import {
|
||||
IUnleashTest,
|
||||
setupAppWithCustomConfig,
|
||||
} from '../../../helpers/test-helper';
|
||||
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
||||
import getLogger from '../../../../fixtures/no-logger';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_dependencies', getLogger);
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
dependentFeatures: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
const addFeatureDependency = async (
|
||||
parentFeature: string,
|
||||
payload: CreateDependentFeatureSchema,
|
||||
expectedCode = 200,
|
||||
) => {
|
||||
return app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${parentFeature}/dependencies`,
|
||||
)
|
||||
.send(payload)
|
||||
.expect(expectedCode);
|
||||
};
|
||||
|
||||
test('should add feature dependency', async () => {
|
||||
const parent = uuidv4();
|
||||
const child = uuidv4();
|
||||
await app.createFeature(parent);
|
||||
await app.createFeature(child);
|
||||
|
||||
await addFeatureDependency(parent, {
|
||||
feature: child,
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user