mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-23 01:16:27 +02:00
feat: Server side sort by (#5250)
This commit is contained in:
parent
9688955d4b
commit
43298e16e2
@ -81,6 +81,8 @@ export default class FeatureSearchController extends Controller {
|
|||||||
status,
|
status,
|
||||||
cursor,
|
cursor,
|
||||||
limit = '50',
|
limit = '50',
|
||||||
|
sortOrder,
|
||||||
|
sortBy,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const normalizedTag = tag
|
const normalizedTag = tag
|
||||||
@ -95,6 +97,9 @@ export default class FeatureSearchController extends Controller {
|
|||||||
);
|
);
|
||||||
const normalizedLimit =
|
const normalizedLimit =
|
||||||
Number(limit) > 0 && Number(limit) <= 50 ? Number(limit) : 50;
|
Number(limit) > 0 && Number(limit) <= 50 ? Number(limit) : 50;
|
||||||
|
const normalizedSortBy: string = sortBy ? sortBy : 'createdAt';
|
||||||
|
const normalizedSortOrder =
|
||||||
|
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
||||||
const { features, nextCursor, total } =
|
const { features, nextCursor, total } =
|
||||||
await this.featureSearchService.search({
|
await this.featureSearchService.search({
|
||||||
query,
|
query,
|
||||||
@ -105,6 +110,8 @@ export default class FeatureSearchController extends Controller {
|
|||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
cursor,
|
cursor,
|
||||||
limit: normalizedLimit,
|
limit: normalizedLimit,
|
||||||
|
sortBy: normalizedSortBy,
|
||||||
|
sortOrder: normalizedSortOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.header('Link', nextLink(req, nextCursor));
|
res.header('Link', nextLink(req, nextCursor));
|
||||||
|
@ -43,6 +43,21 @@ const searchFeatures = async (
|
|||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sortFeatures = async (
|
||||||
|
{
|
||||||
|
sortBy = '',
|
||||||
|
sortOrder = '',
|
||||||
|
projectId = 'default',
|
||||||
|
}: FeatureSearchQueryParameters,
|
||||||
|
expectedCode = 200,
|
||||||
|
) => {
|
||||||
|
return app.request
|
||||||
|
.get(
|
||||||
|
`/api/admin/search/features?sortBy=${sortBy}&sortOrder=${sortOrder}&projectId=${projectId}`,
|
||||||
|
)
|
||||||
|
.expect(expectedCode);
|
||||||
|
};
|
||||||
|
|
||||||
const searchFeaturesWithCursor = async (
|
const searchFeaturesWithCursor = async (
|
||||||
{
|
{
|
||||||
query = '',
|
query = '',
|
||||||
@ -243,3 +258,80 @@ test('should return features without query', async () => {
|
|||||||
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should sort features', async () => {
|
||||||
|
await app.createFeature('my_feature_a');
|
||||||
|
await app.createFeature('my_feature_c');
|
||||||
|
await app.createFeature('my_feature_b');
|
||||||
|
await app.enableFeature('my_feature_c', 'default');
|
||||||
|
|
||||||
|
const { body: ascName } = await sortFeatures({
|
||||||
|
sortBy: 'name',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ascName).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{ name: 'my_feature_a' },
|
||||||
|
{ name: 'my_feature_b' },
|
||||||
|
{ name: 'my_feature_c' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: descName } = await sortFeatures({
|
||||||
|
sortBy: 'name',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(descName).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{ name: 'my_feature_c' },
|
||||||
|
{ name: 'my_feature_b' },
|
||||||
|
{ name: 'my_feature_a' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: defaultCreatedAt } = await sortFeatures({
|
||||||
|
sortBy: '',
|
||||||
|
sortOrder: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(defaultCreatedAt).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{ name: 'my_feature_a' },
|
||||||
|
{ name: 'my_feature_c' },
|
||||||
|
{ name: 'my_feature_b' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: environmentAscSort } = await sortFeatures({
|
||||||
|
sortBy: 'environment:default',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(environmentAscSort).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{ name: 'my_feature_a' },
|
||||||
|
{ name: 'my_feature_b' },
|
||||||
|
{ name: 'my_feature_c' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: environmentDescSort } = await sortFeatures({
|
||||||
|
sortBy: 'environment:default',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(environmentDescSort).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{ name: 'my_feature_c' },
|
||||||
|
{ name: 'my_feature_a' },
|
||||||
|
{ name: 'my_feature_b' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -532,6 +532,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
status,
|
status,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
|
sortOrder,
|
||||||
|
sortBy,
|
||||||
}: IFeatureSearchParams): Promise<{
|
}: IFeatureSearchParams): Promise<{
|
||||||
features: IFeatureOverview[];
|
features: IFeatureOverview[];
|
||||||
total: number;
|
total: number;
|
||||||
@ -681,7 +683,27 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
if (cursor) {
|
if (cursor) {
|
||||||
query = query.where('features.created_at', '>=', cursor);
|
query = query.where('features.created_at', '>=', cursor);
|
||||||
}
|
}
|
||||||
query = query.orderBy('features.created_at', 'asc');
|
|
||||||
|
const sortByMapping = {
|
||||||
|
name: 'feature_name',
|
||||||
|
type: 'type',
|
||||||
|
lastSeenAt: 'env_last_seen_at',
|
||||||
|
};
|
||||||
|
if (sortBy.startsWith('environment:')) {
|
||||||
|
const [, envName] = sortBy.split(':');
|
||||||
|
query = query
|
||||||
|
.orderByRaw(
|
||||||
|
`CASE WHEN feature_environments.environment = ? THEN feature_environments.enabled ELSE NULL END ${sortOrder}`,
|
||||||
|
[envName],
|
||||||
|
)
|
||||||
|
.orderBy('created_at', 'asc');
|
||||||
|
} else if (sortByMapping[sortBy]) {
|
||||||
|
query = query
|
||||||
|
.orderBy(sortByMapping[sortBy], sortOrder)
|
||||||
|
.orderBy('created_at', 'asc');
|
||||||
|
} else {
|
||||||
|
query = query.orderBy('created_at', sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
const total = await countQuery
|
const total = await countQuery
|
||||||
.countDistinct({ total: 'features.name' })
|
.countDistinct({ total: 'features.name' })
|
||||||
|
@ -30,6 +30,8 @@ export interface IFeatureSearchParams {
|
|||||||
status?: string[][];
|
status?: string[][];
|
||||||
limit: number;
|
limit: number;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
|
sortBy: string;
|
||||||
|
sortOrder: 'asc' | 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureStrategiesStore
|
export interface IFeatureStrategiesStore
|
||||||
|
@ -7,7 +7,7 @@ export const featureSearchQueryParameters = [
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
example: 'feature_a',
|
example: 'feature_a',
|
||||||
},
|
},
|
||||||
description: 'The search query for the feature or tag',
|
description: 'The search query for the feature name or tag',
|
||||||
in: 'query',
|
in: 'query',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -77,6 +77,26 @@ export const featureSearchQueryParameters = [
|
|||||||
'The number of feature environments to return in a page. By default it is set to 50.',
|
'The number of feature environments to return in a page. By default it is set to 50.',
|
||||||
in: 'query',
|
in: 'query',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'sortBy',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'type',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The field to sort the results by. By default it is set to "createdAt".',
|
||||||
|
in: 'query',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortOrder',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'desc',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The sort order for the sortBy. By default it is det to "asc".',
|
||||||
|
in: 'query',
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type FeatureSearchQueryParameters = Partial<
|
export type FeatureSearchQueryParameters = Partial<
|
||||||
|
Loading…
Reference in New Issue
Block a user