Skip to content

Commit

Permalink
feat: filter by environment status (#5165)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Oct 27, 2023
1 parent 46d7cb2 commit 1c8fab6
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 9 deletions.
10 changes: 9 additions & 1 deletion src/lib/features/feature-search/feature-search-controller.ts
Expand Up @@ -72,17 +72,25 @@ export default class FeatureSearchController extends Controller {
res: Response,
): Promise<void> {
if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
const { query, projectId, type, tag } = req.query;
const { query, projectId, type, tag, status } = req.query;
const userId = req.user.id;
const normalizedTag = tag
?.map((tag) => tag.split(':'))
.filter((tag) => tag.length === 2);
const normalizedStatus = status
?.map((tag) => tag.split(':'))
.filter(
(tag) =>
tag.length === 2 &&
['enabled', 'disabled'].includes(tag[1]),
);
const features = await this.featureSearchService.search({
query,
projectId,
type,
userId,
tag: normalizedTag,
status: normalizedStatus,
});
res.json({ features });
} else {
Expand Down
10 changes: 3 additions & 7 deletions src/lib/features/feature-search/feature-search-service.ts
Expand Up @@ -20,13 +20,9 @@ export class FeatureSearchService {
}

async search(params: IFeatureSearchParams) {
const features = await this.featureStrategiesStore.searchFeatures({
projectId: params.projectId,
query: params.query,
userId: params.userId,
type: params.type,
tag: params.tag,
});
const features = await this.featureStrategiesStore.searchFeatures(
params,
);

return features;
}
Expand Down
28 changes: 28 additions & 0 deletions src/lib/features/feature-search/feature.search.e2e.test.ts
Expand Up @@ -57,6 +57,18 @@ const filterFeaturesByTag = async (tags: string[], expectedCode = 200) => {
.expect(expectedCode);
};

const filterFeaturesByEnvironmentStatus = async (
environmentStatuses: string[],
expectedCode = 200,
) => {
const statuses = environmentStatuses
.map((status) => `status[]=${status}`)
.join('&');
return app.request
.get(`/api/admin/search/features?${statuses}`)
.expect(expectedCode);
};

const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => {
return app.request.get(`/api/admin/search/features`).expect(expectedCode);
};
Expand Down Expand Up @@ -99,6 +111,22 @@ test('should filter features by tag', async () => {
});
});

test('should filter features by environment status', async () => {
await app.createFeature('my_feature_a');
await app.createFeature('my_feature_b');
await app.enableFeature('my_feature_a', 'default');

const { body } = await filterFeaturesByEnvironmentStatus([
'default:enabled',
'nonexistentEnv:disabled',
'default:wrongStatus',
]);

expect(body).toMatchObject({
features: [{ name: 'my_feature_a' }],
});
});

test('filter with invalid tag should ignore filter', async () => {
await app.createFeature('my_feature_a');
await app.createFeature('my_feature_b');
Expand Down
18 changes: 18 additions & 0 deletions src/lib/features/feature-toggle/feature-toggle-strategies-store.ts
Expand Up @@ -522,6 +522,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
query: queryString,
type,
tag,
status,
}: IFeatureSearchParams): Promise<IFeatureOverview[]> {
let query = this.db('features');
if (projectId) {
Expand Down Expand Up @@ -553,6 +554,23 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
if (type) {
query = query.whereIn('features.type', type);
}

if (status && status.length > 0) {
query = query.where((builder) => {
for (const [envName, envStatus] of status) {
builder.orWhere(function () {
this.where(
'feature_environments.environment',
envName,
).andWhere(
'feature_environments.enabled',
envStatus === 'enabled' ? true : false,
);
});
}
});
}

query = query
.modify(FeatureToggleStore.filterByArchived, false)
.leftJoin(
Expand Down
Expand Up @@ -27,6 +27,7 @@ export interface IFeatureSearchParams {
projectId?: string;
type?: string[];
tag?: string[][];
status?: string[][];
}

export interface IFeatureStrategiesStore
Expand Down
15 changes: 14 additions & 1 deletion src/lib/openapi/spec/feature-search-query-parameters.ts
Expand Up @@ -41,7 +41,20 @@ export const featureSearchQueryParameters = [
},
},
description:
'The list of feature tags to filter by. Feature tag has to specify type and value joined with a colon.',
'The list of feature tags to filter by. Feature tag has to specify a type and a value joined with a colon.',
in: 'query',
},
{
name: 'status',
schema: {
type: 'array',
items: {
type: 'string',
example: 'production:enabled',
},
},
description:
'The list of feature environment status to filter by. Feature environment has to specify a name and a status joined with a colon.',
in: 'query',
},
] as const;
Expand Down
20 changes: 20 additions & 0 deletions src/test/e2e/helpers/test-helper.ts
Expand Up @@ -47,6 +47,13 @@ export interface IUnleashHttpAPI {
expectedResponseCode?: number,
): supertest.Test;

enableFeature(
feature: string,
environment: string,
project?: string,
expectedResponseCode?: number,
): supertest.Test;

getFeatures(name?: string, expectedResponseCode?: number): supertest.Test;

getProjectFeatures(
Expand Down Expand Up @@ -219,6 +226,19 @@ function httpApis(
.set('Content-Type', 'application/json')
.expect(expectedResponseCode);
},

enableFeature(
feature: string,
environment,
project = 'default',
expectedResponseCode = 200,
): supertest.Test {
return request
.post(
`/api/admin/projects/${project}/features/${feature}/environments/${environment}/on`,
)
.expect(expectedResponseCode);
},
};
}

Expand Down

0 comments on commit 1c8fab6

Please sign in to comment.