Skip to content

Commit

Permalink
feat: stub for create dependent features (#4769)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Sep 19, 2023
1 parent a71c3fe commit 59f2ae4
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
"caseInsensitiveInOperators": false,
"customRootRolesKillSwitch": false,
"demo": false,
"dependentFeatures": false,
"disableBulkToggle": false,
"disableNotifications": false,
"doraMetrics": false,
Expand Down Expand Up @@ -114,6 +115,7 @@ exports[`should create default config 1`] = `
"caseInsensitiveInOperators": false,
"customRootRolesKillSwitch": false,
"demo": false,
"dependentFeatures": false,
"disableBulkToggle": false,
"disableNotifications": false,
"doraMetrics": false,
Expand Down
4 changes: 4 additions & 0 deletions src/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ import {
createGroupSchema,
doraFeaturesSchema,
projectDoraMetricsSchema,
dependentFeatureSchema,
createDependentFeatureSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
Expand Down Expand Up @@ -375,6 +377,8 @@ export const schemas: UnleashSchemas = {
createFeatureNamingPatternSchema,
doraFeaturesSchema,
projectDoraMetricsSchema,
dependentFeatureSchema,
createDependentFeatureSchema,
};

// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
Expand Down
35 changes: 35 additions & 0 deletions src/lib/openapi/spec/create-dependent-feature-schema.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions src/lib/openapi/spec/dependent-feature-schema.ts
Original file line number Diff line number Diff line change
@@ -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>;
2 changes: 2 additions & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
60 changes: 53 additions & 7 deletions src/lib/routes/admin-api/project/project-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,17 +42,18 @@ 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';
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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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[]>,
Expand Down
31 changes: 31 additions & 0 deletions src/lib/services/feature-toggle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
};
Expand Down Expand Up @@ -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;
7 changes: 6 additions & 1 deletion src/lib/types/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export type IFlagKey =
| 'doraMetrics'
| 'variantTypeNumber'
| 'accessOverview'
| 'privateProjects';
| 'privateProjects'
| 'dependentFeatures';

export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

Expand Down Expand Up @@ -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,
Expand Down
56 changes: 56 additions & 0 deletions src/test/e2e/api/admin/project/features.dependencies.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});

0 comments on commit 59f2ae4

Please sign in to comment.