1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: total count in search results (#5235)

This commit is contained in:
Mateusz Kwasniewski 2023-11-01 09:19:42 +01:00 committed by GitHub
parent cb2ffdd796
commit 74bbc7799e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 47 additions and 22 deletions

View File

@ -95,7 +95,7 @@ export default class FeatureSearchController extends Controller {
);
const normalizedLimit =
Number(limit) > 0 && Number(limit) <= 50 ? Number(limit) : 50;
const { features, nextCursor } =
const { features, nextCursor, total } =
await this.featureSearchService.search({
query,
projectId,
@ -108,7 +108,7 @@ export default class FeatureSearchController extends Controller {
});
res.header('Link', nextLink(req, nextCursor));
res.json({ features });
res.json({ features, total });
} else {
throw new InvalidOperationError(
'Feature Search API is not enabled',

View File

@ -22,10 +22,11 @@ export class FeatureSearchService {
async search(params: IFeatureSearchParams) {
// fetch one more item than needed to get a cursor of the next item
const features = await this.featureStrategiesStore.searchFeatures({
...params,
limit: params.limit + 1,
});
const { features, total } =
await this.featureStrategiesStore.searchFeatures({
...params,
limit: params.limit + 1,
});
const nextCursor =
features.length > params.limit
@ -39,6 +40,7 @@ export class FeatureSearchService {
? features.slice(0, -1)
: features,
nextCursor,
total,
};
}
}

View File

@ -102,6 +102,7 @@ test('should search matching features by name', async () => {
expect(body).toMatchObject({
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
total: 2,
});
});
@ -120,6 +121,7 @@ test('should paginate with cursor', async () => {
expect(firstPage).toMatchObject({
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
total: 4,
});
const { body: secondPage, headers: secondHeaders } = await getPage(
@ -128,6 +130,7 @@ test('should paginate with cursor', async () => {
expect(secondPage).toMatchObject({
features: [{ name: 'my_feature_c' }, { name: 'my_feature_d' }],
total: 4,
});
expect(secondHeaders.link).toBe('');

View File

@ -325,8 +325,10 @@ export default class FakeFeatureStrategiesStore
return Promise.resolve([]);
}
searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]> {
return Promise.resolve([]);
searchFeatures(
params: IFeatureSearchParams,
): Promise<{ features: IFeatureOverview[]; total: number }> {
return Promise.resolve({ features: [], total: 0 });
}
getAllByFeatures(

View File

@ -532,14 +532,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
status,
cursor,
limit,
}: IFeatureSearchParams): Promise<IFeatureOverview[]> {
let query = this.db('features').limit(limit);
}: IFeatureSearchParams): Promise<{
features: IFeatureOverview[];
total: number;
}> {
let query = this.db('features');
if (projectId) {
query = query.where({ project: projectId });
}
if (queryString?.trim()) {
// todo: we can run cheaper query when no colon is detected
// todo: we can run a cheaper query when no colon is detected
const tagQuery = this.db
.from('feature_tag')
.select('feature_name')
@ -582,11 +585,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
});
}
if (cursor) {
query = query.where('features.created_at', '>=', cursor);
}
query = query.orderBy('features.created_at', 'asc');
query = query
.modify(FeatureToggleStore.filterByArchived, false)
.leftJoin(
@ -601,6 +599,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
)
.leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name');
const countQuery = query.clone();
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
query.leftJoin('last_seen_at_metrics', function () {
this.on(
@ -670,14 +670,24 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
];
}
query = query.select(selectColumns);
if (cursor) {
query = query.where('features.created_at', '>=', cursor);
}
query = query.orderBy('features.created_at', 'asc');
const total = await countQuery
.countDistinct({ total: 'features.name' })
.first();
query = query.select(selectColumns).limit(limit);
const rows = await query;
if (rows.length > 0) {
const overview = this.getFeatureOverviewData(getUniqueRows(rows));
return sortEnvironments(overview);
const features = sortEnvironments(overview);
return { features, total: Number(total?.total) || 0 };
}
return [];
return { features: [], total: 0 };
}
async getFeatureOverview({

View File

@ -59,7 +59,9 @@ export interface IFeatureStrategiesStore
getFeatureOverview(
params: IFeatureProjectUserParams,
): Promise<IFeatureOverview[]>;
searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]>;
searchFeatures(
params: IFeatureSearchParams,
): Promise<{ features: IFeatureOverview[]; total: number }>;
getStrategyById(id: string): Promise<IFeatureStrategy>;
updateStrategy(
id: string,

View File

@ -71,10 +71,10 @@ export const featureSearchQueryParameters = [
name: 'limit',
schema: {
type: 'string',
example: '10',
example: '50',
},
description:
'The number of results 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',
},
] as const;

View File

@ -24,6 +24,12 @@ export const searchFeaturesSchema = {
description:
'The full list of features in this project (excluding archived features)',
},
total: {
type: 'number',
description:
'Total count of the features matching search and filter criteria',
example: 10,
},
},
components: {
schemas: {