Skip to content

Commit

Permalink
feat: Api to list available parent options (#4833)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Sep 26, 2023
1 parent 0938b2e commit e030b67
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import {
IFlagResolver,
IUnleashConfig,
IUnleashServices,
NONE,
UPDATE_FEATURE,
} from '../../types';
import { Logger } from '../../logger';
import {
CreateDependentFeatureSchema,
createRequestSchema,
createResponseSchema,
emptyResponse,
getStandardResponses,
ParentFeatureOptionsSchema,
} from '../../openapi';
import { IAuthRequest } from '../../routes/unleash-types';
import { InvalidOperationError } from '../../error';
Expand All @@ -31,6 +34,7 @@ interface DeleteDependencyParams {
const PATH = '/:projectId/features';
const PATH_FEATURE = `${PATH}/:child`;
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;

type DependentFeaturesServices = Pick<
Expand Down Expand Up @@ -137,6 +141,26 @@ export default class DependentFeaturesController extends Controller {
}),
],
});

this.route({
method: 'get',
path: PATH_PARENTS,
handler: this.getParentOptions,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Features'],
summary: 'List parent options.',
description:
'List available parents who have no transitive dependencies.',
operationId: 'listParentOptions',
responses: {
200: createResponseSchema('parentFeatureOptionsSchema'),
...getStandardResponses(401, 403, 404),
},
}),
],
});
}

async addFeatureDependency(
Expand Down Expand Up @@ -200,4 +224,21 @@ export default class DependentFeaturesController extends Controller {
);
}
}

async getParentOptions(
req: IAuthRequest<FeatureParams, any, any>,
res: Response<ParentFeatureOptionsSchema>,
): Promise<void> {
const { child } = req.params;

if (this.config.flagResolver.isEnabled('dependentFeatures')) {
const parentOptions =
await this.dependentFeaturesService.getParentOptions(child);
res.send(parentOptions);
} else {
throw new InvalidOperationError(
'Dependent features are not enabled',
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@ export class DependentFeaturesService {
async deleteFeatureDependencies(feature: string): Promise<void> {
await this.dependentFeaturesStore.deleteAll(feature);
}

async getParentOptions(feature: string): Promise<string[]> {
return this.dependentFeaturesStore.getParentOptions(feature);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface IDependentFeaturesStore {
getChildren(parent: string): Promise<string[]>;
delete(dependency: FeatureDependencyId): Promise<void>;
deleteAll(child: string): Promise<void>;
getParentOptions(child: string): Promise<string[]>;
}
17 changes: 17 additions & 0 deletions src/lib/features/dependent-features/dependent-features-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
return rows.map((row) => row.child);
}

async getParentOptions(child: string): Promise<string[]> {
const result = await this.db('features as f')
.where('f.name', child)
.select('f.project');
if (result.length === 0) {
return [];
}
const rows = await this.db('features as f')
.leftJoin('dependent_features as df', 'f.name', 'df.child')
.where('f.project', result[0].project)
.andWhere('f.name', '!=', child)
.andWhere('df.child', null)
.select('f.name');

return rows.map((item) => item.name);
}

async delete(dependency: FeatureDependencyId): Promise<void> {
await this.db('dependent_features')
.where('parent', dependency.parent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,21 @@ const deleteFeatureDependencies = async (
.expect(expectedCode);
};

const getParentOptions = async (childFeature: string, expectedCode = 200) => {
return app.request
.get(`/api/admin/projects/default/features/${childFeature}/parents`)
.expect(expectedCode);
};

test('should add and delete feature dependencies', async () => {
const parent = uuidv4();
const child = uuidv4();
await app.createFeature(parent);
await app.createFeature(child);

const { body: parentOptions } = await getParentOptions(child);
expect(parentOptions).toStrictEqual([parent]);

// save explicit enabled and variants
await addFeatureDependency(child, {
feature: parent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export class FakeDependentFeaturesStore implements IDependentFeaturesStore {
return Promise.resolve([]);
}

getParentOptions(): Promise<string[]> {
return Promise.resolve([]);
}

delete(): Promise<void> {
return Promise.resolve();
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import {
updateFeatureStrategySegmentsSchema,
dependentFeatureSchema,
createDependentFeatureSchema,
parentFeatureOptionsSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
Expand Down Expand Up @@ -387,6 +388,7 @@ export const schemas: UnleashSchemas = {
updateFeatureStrategySegmentsSchema,
dependentFeatureSchema,
createDependentFeatureSchema,
parentFeatureOptionsSchema,
};

// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
Expand Down
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,4 @@ export * from './segments-schema';
export * from './update-feature-strategy-segments-schema';
export * from './dependent-feature-schema';
export * from './create-dependent-feature-schema';
export * from './parent-feature-options-schema';
16 changes: 16 additions & 0 deletions src/lib/openapi/spec/parent-feature-options-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FromSchema } from 'json-schema-to-ts';

export const parentFeatureOptionsSchema = {
$id: '#/components/schemas/parentFeatureOptionsSchema',
type: 'array',
description:
'A list of parent feature names available for a given child feature. Features that have their own parents are excluded.',
items: {
type: 'string',
},
components: {},
} as const;

export type ParentFeatureOptionsSchema = FromSchema<
typeof parentFeatureOptionsSchema
>;

0 comments on commit e030b67

Please sign in to comment.