mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-10 17:53:36 +02:00
fix: metadata filter support for admin/features
Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
ba21adf3ae
commit
bc3cf81e94
@ -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');
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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> {
|
||||||
|
@ -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}`)
|
||||||
|
@ -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[]> => {
|
||||||
|
Loading…
Reference in New Issue
Block a user