Skip to content

Commit

Permalink
feat: Server side sort by (#5250)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Nov 3, 2023
1 parent 9688955 commit 43298e1
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/lib/features/feature-search/feature-search-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export default class FeatureSearchController extends Controller {
status,
cursor,
limit = '50',
sortOrder,
sortBy,
} = req.query;
const userId = req.user.id;
const normalizedTag = tag
Expand All @@ -95,6 +97,9 @@ export default class FeatureSearchController extends Controller {
);
const normalizedLimit =
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 } =
await this.featureSearchService.search({
query,
Expand All @@ -105,6 +110,8 @@ export default class FeatureSearchController extends Controller {
status: normalizedStatus,
cursor,
limit: normalizedLimit,
sortBy: normalizedSortBy,
sortOrder: normalizedSortOrder,
});

res.header('Link', nextLink(req, nextCursor));
Expand Down
92 changes: 92 additions & 0 deletions src/lib/features/feature-search/feature.search.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ const searchFeatures = async (
.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 (
{
query = '',
Expand Down Expand Up @@ -243,3 +258,80 @@ test('should return features without query', async () => {
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,
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
status,
cursor,
limit,
sortOrder,
sortBy,
}: IFeatureSearchParams): Promise<{
features: IFeatureOverview[];
total: number;
Expand Down Expand Up @@ -681,7 +683,27 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
if (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
.countDistinct({ total: 'features.name' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface IFeatureSearchParams {
status?: string[][];
limit: number;
cursor?: string;
sortBy: string;
sortOrder: 'asc' | 'desc';
}

export interface IFeatureStrategiesStore
Expand Down
22 changes: 21 additions & 1 deletion src/lib/openapi/spec/feature-search-query-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const featureSearchQueryParameters = [
type: 'string',
example: 'feature_a',
},
description: 'The search query for the feature or tag',
description: 'The search query for the feature name or tag',
in: 'query',
},
{
Expand Down Expand Up @@ -77,6 +77,26 @@ export const featureSearchQueryParameters = [
'The number of feature environments to return in a page. By default it is set to 50.',
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;

export type FeatureSearchQueryParameters = Partial<
Expand Down

0 comments on commit 43298e1

Please sign in to comment.