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:
parent
cb2ffdd796
commit
74bbc7799e
@ -95,7 +95,7 @@ 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 { features, nextCursor } =
|
const { features, nextCursor, total } =
|
||||||
await this.featureSearchService.search({
|
await this.featureSearchService.search({
|
||||||
query,
|
query,
|
||||||
projectId,
|
projectId,
|
||||||
@ -108,7 +108,7 @@ export default class FeatureSearchController extends Controller {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.header('Link', nextLink(req, nextCursor));
|
res.header('Link', nextLink(req, nextCursor));
|
||||||
res.json({ features });
|
res.json({ features, total });
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidOperationError(
|
throw new InvalidOperationError(
|
||||||
'Feature Search API is not enabled',
|
'Feature Search API is not enabled',
|
||||||
|
@ -22,7 +22,8 @@ export class FeatureSearchService {
|
|||||||
|
|
||||||
async search(params: IFeatureSearchParams) {
|
async search(params: IFeatureSearchParams) {
|
||||||
// fetch one more item than needed to get a cursor of the next item
|
// fetch one more item than needed to get a cursor of the next item
|
||||||
const features = await this.featureStrategiesStore.searchFeatures({
|
const { features, total } =
|
||||||
|
await this.featureStrategiesStore.searchFeatures({
|
||||||
...params,
|
...params,
|
||||||
limit: params.limit + 1,
|
limit: params.limit + 1,
|
||||||
});
|
});
|
||||||
@ -39,6 +40,7 @@ export class FeatureSearchService {
|
|||||||
? features.slice(0, -1)
|
? features.slice(0, -1)
|
||||||
: features,
|
: features,
|
||||||
nextCursor,
|
nextCursor,
|
||||||
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@ test('should search matching features by name', async () => {
|
|||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
||||||
|
total: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -120,6 +121,7 @@ test('should paginate with cursor', async () => {
|
|||||||
|
|
||||||
expect(firstPage).toMatchObject({
|
expect(firstPage).toMatchObject({
|
||||||
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
||||||
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: secondPage, headers: secondHeaders } = await getPage(
|
const { body: secondPage, headers: secondHeaders } = await getPage(
|
||||||
@ -128,6 +130,7 @@ test('should paginate with cursor', async () => {
|
|||||||
|
|
||||||
expect(secondPage).toMatchObject({
|
expect(secondPage).toMatchObject({
|
||||||
features: [{ name: 'my_feature_c' }, { name: 'my_feature_d' }],
|
features: [{ name: 'my_feature_c' }, { name: 'my_feature_d' }],
|
||||||
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(secondHeaders.link).toBe('');
|
expect(secondHeaders.link).toBe('');
|
||||||
|
@ -325,8 +325,10 @@ export default class FakeFeatureStrategiesStore
|
|||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]> {
|
searchFeatures(
|
||||||
return Promise.resolve([]);
|
params: IFeatureSearchParams,
|
||||||
|
): Promise<{ features: IFeatureOverview[]; total: number }> {
|
||||||
|
return Promise.resolve({ features: [], total: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllByFeatures(
|
getAllByFeatures(
|
||||||
|
@ -532,14 +532,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
status,
|
status,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
}: IFeatureSearchParams): Promise<IFeatureOverview[]> {
|
}: IFeatureSearchParams): Promise<{
|
||||||
let query = this.db('features').limit(limit);
|
features: IFeatureOverview[];
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
let query = this.db('features');
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
query = query.where({ project: projectId });
|
query = query.where({ project: projectId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryString?.trim()) {
|
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
|
const tagQuery = this.db
|
||||||
.from('feature_tag')
|
.from('feature_tag')
|
||||||
.select('feature_name')
|
.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
|
query = query
|
||||||
.modify(FeatureToggleStore.filterByArchived, false)
|
.modify(FeatureToggleStore.filterByArchived, false)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
@ -601,6 +599,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
)
|
)
|
||||||
.leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name');
|
.leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name');
|
||||||
|
|
||||||
|
const countQuery = query.clone();
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||||
query.leftJoin('last_seen_at_metrics', function () {
|
query.leftJoin('last_seen_at_metrics', function () {
|
||||||
this.on(
|
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;
|
const rows = await query;
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const overview = this.getFeatureOverviewData(getUniqueRows(rows));
|
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({
|
async getFeatureOverview({
|
||||||
|
@ -59,7 +59,9 @@ export interface IFeatureStrategiesStore
|
|||||||
getFeatureOverview(
|
getFeatureOverview(
|
||||||
params: IFeatureProjectUserParams,
|
params: IFeatureProjectUserParams,
|
||||||
): Promise<IFeatureOverview[]>;
|
): Promise<IFeatureOverview[]>;
|
||||||
searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]>;
|
searchFeatures(
|
||||||
|
params: IFeatureSearchParams,
|
||||||
|
): Promise<{ features: IFeatureOverview[]; total: number }>;
|
||||||
getStrategyById(id: string): Promise<IFeatureStrategy>;
|
getStrategyById(id: string): Promise<IFeatureStrategy>;
|
||||||
updateStrategy(
|
updateStrategy(
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -71,10 +71,10 @@ export const featureSearchQueryParameters = [
|
|||||||
name: 'limit',
|
name: 'limit',
|
||||||
schema: {
|
schema: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
example: '10',
|
example: '50',
|
||||||
},
|
},
|
||||||
description:
|
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',
|
in: 'query',
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
@ -24,6 +24,12 @@ export const searchFeaturesSchema = {
|
|||||||
description:
|
description:
|
||||||
'The full list of features in this project (excluding archived features)',
|
'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: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
Loading…
Reference in New Issue
Block a user