1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +02:00

fix: metadata filter support for admin/features

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Christopher Kolstad 2022-07-14 15:35:50 +02:00
parent ba21adf3ae
commit bc3cf81e94
No known key found for this signature in database
GPG Key ID: 559ACB0E3DB5538A
6 changed files with 70 additions and 17 deletions

View File

@ -86,16 +86,26 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
async getAll( async getAll(
query: { query: {
archived?: boolean; archived?: boolean;
project?: string; project?: string | string[];
stale?: boolean; stale?: boolean;
tag?: string[][];
namePrefix?: string;
} = { archived: false }, } = { archived: false },
): Promise<FeatureToggle[]> { ): Promise<FeatureToggle[]> {
const { archived, ...rest } = query; const { archived, tag, project, namePrefix } = query;
const rows = await this.db let queryBuilder = this.db(TABLE)
.select(FEATURE_COLUMNS) .select(FEATURE_COLUMNS)
.from(TABLE) .modify(FeatureToggleStore.filterByArchived, archived)
.where(rest) .modify(FeatureToggleStore.filterByProject, project)
.modify(FeatureToggleStore.filterByArchived, archived); .modify(FeatureToggleStore.filterByNamePrefix, namePrefix);
if (tag) {
const tagQuery = this.db
.from('feature_tag')
.select('feature_name')
.whereIn(['tag_type', 'tag_value'], tag);
queryBuilder = queryBuilder.whereIn('features.name', tagQuery);
}
const rows = await queryBuilder;
return rows.map(this.rowToFeature); return rows.map(this.rowToFeature);
} }
@ -152,6 +162,44 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
: queryBuilder.whereNull('archived_at'); : queryBuilder.whereNull('archived_at');
}; };
static filterByTags: Knex.QueryCallbackWithArgs = (
queryBuilder: Knex.QueryBuilder,
tag?: string[][],
) => {
if (tag && tag.length > 0) {
const tagQuery = queryBuilder
.from('feature_tag')
.select('feature_name')
.whereIn(['tag_type', 'tag_value'], tag);
return queryBuilder.whereIn('feature.name', tagQuery);
}
return queryBuilder;
};
static filterByProject: Knex.QueryCallbackWithArgs = (
queryBuilder: Knex.QueryBuilder,
project?: string | string[],
) => {
if (project) {
if (Array.isArray(project) && project.length > 0) {
return queryBuilder.whereIn('features.project', project);
} else {
return queryBuilder.where({ project });
}
}
return queryBuilder;
};
static filterByNamePrefix: Knex.QueryCallbackWithArgs = (
queryBuilder: Knex.QueryBuilder,
namePrefix?: string,
) => {
if (namePrefix) {
return queryBuilder.whereILike('name', `${namePrefix}%`);
}
return queryBuilder;
};
rowToFeature(row: FeaturesTable): FeatureToggle { rowToFeature(row: FeaturesTable): FeatureToggle {
if (!row) { if (!row) {
throw new NotFoundError('No feature toggle found'); throw new NotFoundError('No feature toggle found');

View File

@ -177,7 +177,11 @@ class FeatureController extends Controller {
req: Request, req: Request,
res: Response<FeaturesSchema>, res: Response<FeaturesSchema>,
): Promise<void> { ): Promise<void> {
const features = await this.service.getMetadataForAllFeatures(false); const query = await this.prepQuery(req.query);
const features = await this.service.getMetadataForAllFeatures(
false,
query,
);
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,

View File

@ -988,8 +988,9 @@ class FeatureToggleService {
async getMetadataForAllFeatures( async getMetadataForAllFeatures(
archived: boolean, archived: boolean,
query?: Partial<IFeatureToggleQuery>,
): Promise<FeatureToggle[]> { ): Promise<FeatureToggle[]> {
return this.featureToggleStore.getAll({ archived }); return this.featureToggleStore.getAll({ ...query, archived });
} }
async getMetadataForAllFeaturesByProjectId( async getMetadataForAllFeaturesByProjectId(

View File

@ -3,8 +3,10 @@ import { Store } from './store';
export interface IFeatureToggleQuery { export interface IFeatureToggleQuery {
archived: boolean; archived: boolean;
project: string; project: string | string[];
namePrefix?: string;
stale: boolean; stale: boolean;
tag?: string[][];
} }
export interface IFeatureToggleStore extends Store<FeatureToggle, string> { export interface IFeatureToggleStore extends Store<FeatureToggle, string> {

View File

@ -180,8 +180,8 @@ afterAll(async () => {
test('returns list of feature toggles', async () => test('returns list of feature toggles', async () =>
app.request app.request
.get('/api/admin/features') .get('/api/admin/features')
.expect('Content-Type', /json/)
.expect(200) .expect(200)
.expect('Content-Type', /json/)
.expect((res) => { .expect((res) => {
expect(res.body.features).toHaveLength(4); expect(res.body.features).toHaveLength(4);
})); }));
@ -628,8 +628,8 @@ test('Can get features tagged by tag', async () => {
.expect(201); .expect(201);
return app.request return app.request
.get(`/api/admin/features?tag=${tag.type}:${tag.value}`) .get(`/api/admin/features?tag=${tag.type}:${tag.value}`)
.expect('Content-Type', /json/)
.expect(200) .expect(200)
.expect('Content-Type', /json/)
.expect((res) => { .expect((res) => {
expect(res.body.features).toHaveLength(1); expect(res.body.features).toHaveLength(1);
expect(res.body.features[0].name).toBe(feature1Name); expect(res.body.features[0].name).toBe(feature1Name);
@ -665,8 +665,8 @@ test('Can query for multiple tags using OR', async () => {
.get( .get(
`/api/admin/features?tag[]=${tag.type}:${tag.value}&tag[]=${tag2.type}:${tag2.value}`, `/api/admin/features?tag[]=${tag.type}:${tag.value}&tag[]=${tag2.type}:${tag2.value}`,
) )
.expect('Content-Type', /json/)
.expect(200) .expect(200)
.expect('Content-Type', /json/)
.expect((res) => { .expect((res) => {
expect(res.body.features).toHaveLength(2); expect(res.body.features).toHaveLength(2);
expect(res.body.features.some((f) => f.name === feature1Name)).toBe( expect(res.body.features.some((f) => f.name === feature1Name)).toBe(
@ -715,8 +715,8 @@ test('Querying with multiple filters ANDs the filters', async () => {
.expect(201); .expect(201);
await app.request await app.request
.get(`/api/admin/features?tag=${tag.type}:${tag.value}`) .get(`/api/admin/features?tag=${tag.type}:${tag.value}`)
.expect('Content-Type', /json/)
.expect(200) .expect(200)
.expect('Content-Type', /json/)
.expect((res) => expect(res.body.features).toHaveLength(2)); .expect((res) => expect(res.body.features).toHaveLength(2));
await app.request await app.request
.get(`/api/admin/features?namePrefix=test&tag=${tag.type}:${tag.value}`) .get(`/api/admin/features?namePrefix=test&tag=${tag.type}:${tag.value}`)

View File

@ -26,10 +26,8 @@ const fetchSegments = (): Promise<ISegment[]> => {
}; };
const fetchFeatures = (): Promise<IFeatureToggleClient[]> => { const fetchFeatures = (): Promise<IFeatureToggleClient[]> => {
return app.request //@ts-expect-error
.get(FEATURES_ADMIN_BASE_PATH) return app.services.featureToggleService.getFeatureToggles({}, false);
.expect(200)
.then((res) => res.body.features);
}; };
const fetchClientFeatures = (): Promise<IFeatureToggleClient[]> => { const fetchClientFeatures = (): Promise<IFeatureToggleClient[]> => {