mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +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,
|
"caseInsensitiveInOperators": false,
|
||||||
"customRootRolesKillSwitch": false,
|
"customRootRolesKillSwitch": false,
|
||||||
"demo": false,
|
"demo": false,
|
||||||
|
"dependentFeatures": false,
|
||||||
"disableBulkToggle": false,
|
"disableBulkToggle": false,
|
||||||
"disableNotifications": false,
|
"disableNotifications": false,
|
||||||
"doraMetrics": false,
|
"doraMetrics": false,
|
||||||
@ -114,6 +115,7 @@ exports[`should create default config 1`] = `
|
|||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
"customRootRolesKillSwitch": false,
|
"customRootRolesKillSwitch": false,
|
||||||
"demo": false,
|
"demo": false,
|
||||||
|
"dependentFeatures": false,
|
||||||
"disableBulkToggle": false,
|
"disableBulkToggle": false,
|
||||||
"disableNotifications": false,
|
"disableNotifications": false,
|
||||||
"doraMetrics": false,
|
"doraMetrics": false,
|
||||||
|
@ -158,6 +158,8 @@ import {
|
|||||||
createGroupSchema,
|
createGroupSchema,
|
||||||
doraFeaturesSchema,
|
doraFeaturesSchema,
|
||||||
projectDoraMetricsSchema,
|
projectDoraMetricsSchema,
|
||||||
|
dependentFeatureSchema,
|
||||||
|
createDependentFeatureSchema,
|
||||||
} from './spec';
|
} from './spec';
|
||||||
import { IServerOption } from '../types';
|
import { IServerOption } from '../types';
|
||||||
import { mapValues, omitKeys } from '../util';
|
import { mapValues, omitKeys } from '../util';
|
||||||
@ -375,6 +377,8 @@ export const schemas: UnleashSchemas = {
|
|||||||
createFeatureNamingPatternSchema,
|
createFeatureNamingPatternSchema,
|
||||||
doraFeaturesSchema,
|
doraFeaturesSchema,
|
||||||
projectDoraMetricsSchema,
|
projectDoraMetricsSchema,
|
||||||
|
dependentFeatureSchema,
|
||||||
|
createDependentFeatureSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
// 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 './application-usage-schema';
|
||||||
export * from './dora-features-schema';
|
export * from './dora-features-schema';
|
||||||
export * from './project-dora-metrics-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 { applyPatch, Operation } from 'fast-json-patch';
|
||||||
import Controller from '../../controller';
|
import Controller from '../../controller';
|
||||||
import {
|
import {
|
||||||
IUnleashConfig,
|
|
||||||
IUnleashServices,
|
|
||||||
serializeDates,
|
|
||||||
CREATE_FEATURE,
|
CREATE_FEATURE,
|
||||||
CREATE_FEATURE_STRATEGY,
|
CREATE_FEATURE_STRATEGY,
|
||||||
DELETE_FEATURE,
|
DELETE_FEATURE,
|
||||||
DELETE_FEATURE_STRATEGY,
|
DELETE_FEATURE_STRATEGY,
|
||||||
|
IFlagResolver,
|
||||||
|
IUnleashConfig,
|
||||||
|
IUnleashServices,
|
||||||
NONE,
|
NONE,
|
||||||
|
serializeDates,
|
||||||
UPDATE_FEATURE,
|
UPDATE_FEATURE,
|
||||||
UPDATE_FEATURE_ENVIRONMENT,
|
UPDATE_FEATURE_ENVIRONMENT,
|
||||||
UPDATE_FEATURE_STRATEGY,
|
UPDATE_FEATURE_STRATEGY,
|
||||||
IFlagResolver,
|
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import { Logger } from '../../../logger';
|
import { Logger } from '../../../logger';
|
||||||
import { extractUsername } from '../../../util';
|
import { extractUsername } from '../../../util';
|
||||||
@ -42,9 +42,9 @@ import {
|
|||||||
UpdateFeatureStrategySchema,
|
UpdateFeatureStrategySchema,
|
||||||
} from '../../../openapi';
|
} from '../../../openapi';
|
||||||
import {
|
import {
|
||||||
OpenApiService,
|
|
||||||
FeatureToggleService,
|
|
||||||
FeatureTagService,
|
FeatureTagService,
|
||||||
|
FeatureToggleService,
|
||||||
|
OpenApiService,
|
||||||
} from '../../../services';
|
} from '../../../services';
|
||||||
import { querySchema } from '../../../schema/feature-schema';
|
import { querySchema } from '../../../schema/feature-schema';
|
||||||
import { BatchStaleSchema } from '../../../openapi/spec/batch-stale-schema';
|
import { BatchStaleSchema } from '../../../openapi/spec/batch-stale-schema';
|
||||||
@ -52,7 +52,8 @@ import {
|
|||||||
TransactionCreator,
|
TransactionCreator,
|
||||||
UnleashTransaction,
|
UnleashTransaction,
|
||||||
} from '../../../db/transaction';
|
} from '../../../db/transaction';
|
||||||
import { BadDataError } from '../../../error';
|
import { BadDataError, InvalidOperationError } from '../../../error';
|
||||||
|
import { CreateDependentFeatureSchema } from '../../../openapi/spec/create-dependent-feature-schema';
|
||||||
|
|
||||||
interface FeatureStrategyParams {
|
interface FeatureStrategyParams {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -99,6 +100,7 @@ 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,
|
||||||
@ -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({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: PATH_STRATEGY,
|
path: PATH_STRATEGY,
|
||||||
@ -972,6 +997,27 @@ 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[]>,
|
||||||
|
@ -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 { 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;
|
||||||
@ -122,6 +123,15 @@ 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);
|
||||||
};
|
};
|
||||||
@ -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;
|
export default FeatureToggleService;
|
||||||
|
@ -30,7 +30,8 @@ export type IFlagKey =
|
|||||||
| 'doraMetrics'
|
| 'doraMetrics'
|
||||||
| 'variantTypeNumber'
|
| 'variantTypeNumber'
|
||||||
| 'accessOverview'
|
| 'accessOverview'
|
||||||
| 'privateProjects';
|
| 'privateProjects'
|
||||||
|
| 'dependentFeatures';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -130,6 +131,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_DORA_METRICS,
|
process.env.UNLEASH_EXPERIMENTAL_DORA_METRICS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
dependentFeatures: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_DEPENDENT_FEATURES,
|
||||||
|
false,
|
||||||
|
),
|
||||||
variantTypeNumber: parseEnvVarBoolean(
|
variantTypeNumber: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER,
|
process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER,
|
||||||
false,
|
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