mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
feat: introduce offset based search instead of cursor (#5274)
This commit is contained in:
parent
06d62278dc
commit
4bacd3e055
@ -35,37 +35,37 @@ const StyledContentContainer = styled(Box)(() => ({
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const InfiniteProjectOverview = () => {
|
const PAGE_LIMIT = 25;
|
||||||
|
|
||||||
|
const PaginatedProjectOverview = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { project, loading: projectLoading } = useProject(projectId, {
|
const { project, loading: projectLoading } = useProject(projectId, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
});
|
});
|
||||||
const [prevCursors, setPrevCursors] = useState<string[]>([]);
|
const [currentOffset, setCurrentOffset] = useState(0);
|
||||||
const [currentCursor, setCurrentCursor] = useState('');
|
|
||||||
const {
|
const {
|
||||||
features: searchFeatures,
|
features: searchFeatures,
|
||||||
nextCursor,
|
|
||||||
total,
|
total,
|
||||||
refetch,
|
refetch,
|
||||||
loading,
|
loading,
|
||||||
} = useFeatureSearch(currentCursor, projectId, { refreshInterval });
|
} = useFeatureSearch(currentOffset, PAGE_LIMIT, projectId, {
|
||||||
|
refreshInterval,
|
||||||
|
});
|
||||||
|
|
||||||
const { members, features, health, description, environments, stats } =
|
const { members, features, health, description, environments, stats } =
|
||||||
project;
|
project;
|
||||||
const fetchNextPage = () => {
|
const fetchNextPage = () => {
|
||||||
if (!loading && nextCursor !== currentCursor && nextCursor !== '') {
|
if (!loading) {
|
||||||
setPrevCursors([...prevCursors, currentCursor]);
|
setCurrentOffset(Math.min(total, currentOffset + PAGE_LIMIT));
|
||||||
setCurrentCursor(nextCursor);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const fetchPrevPage = () => {
|
const fetchPrevPage = () => {
|
||||||
const prevCursor = prevCursors.pop();
|
setCurrentOffset(Math.max(0, currentOffset - PAGE_LIMIT));
|
||||||
if (prevCursor) {
|
|
||||||
setCurrentCursor(prevCursor);
|
|
||||||
}
|
|
||||||
setPrevCursors([...prevCursors]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasPreviousPage = currentOffset > 0;
|
||||||
|
const hasNextPage = currentOffset + PAGE_LIMIT < total;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<ProjectInfo
|
<ProjectInfo
|
||||||
@ -91,10 +91,14 @@ const InfiniteProjectOverview = () => {
|
|||||||
onChange={refetch}
|
onChange={refetch}
|
||||||
total={total}
|
total={total}
|
||||||
/>
|
/>
|
||||||
{prevCursors.length > 0 ? (
|
<ConditionallyRender
|
||||||
<Box onClick={fetchPrevPage}>Prev</Box>
|
condition={hasPreviousPage}
|
||||||
) : null}
|
show={<Box onClick={fetchPrevPage}>Prev</Box>}
|
||||||
{nextCursor && <Box onClick={fetchNextPage}>Next</Box>}
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasNextPage}
|
||||||
|
show={<Box onClick={fetchNextPage}>Next</Box>}
|
||||||
|
/>
|
||||||
</StyledProjectToggles>
|
</StyledProjectToggles>
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
@ -118,7 +122,7 @@ const ProjectOverview = () => {
|
|||||||
setLastViewed(projectId);
|
setLastViewed(projectId);
|
||||||
}, [projectId, setLastViewed]);
|
}, [projectId, setLastViewed]);
|
||||||
|
|
||||||
if (featureSearchFrontend) return <InfiniteProjectOverview />;
|
if (featureSearchFrontend) return <PaginatedProjectOverview />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
|
@ -6,14 +6,12 @@ import handleErrorResponses from '../httpErrorResponseHandler';
|
|||||||
|
|
||||||
type IFeatureSearchResponse = {
|
type IFeatureSearchResponse = {
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
nextCursor: string;
|
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IUseFeatureSearchOutput {
|
interface IUseFeatureSearchOutput {
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
total: number;
|
total: number;
|
||||||
nextCursor: string;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
@ -22,19 +20,18 @@ interface IUseFeatureSearchOutput {
|
|||||||
const fallbackData: {
|
const fallbackData: {
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
total: number;
|
total: number;
|
||||||
nextCursor: string;
|
|
||||||
} = {
|
} = {
|
||||||
features: [],
|
features: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
nextCursor: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFeatureSearch = (
|
export const useFeatureSearch = (
|
||||||
cursor: string,
|
offset: number,
|
||||||
|
limit: number,
|
||||||
projectId = '',
|
projectId = '',
|
||||||
options: SWRConfiguration = {},
|
options: SWRConfiguration = {},
|
||||||
): IUseFeatureSearchOutput => {
|
): IUseFeatureSearchOutput => {
|
||||||
const { KEY, fetcher } = getFeatureSearchFetcher(projectId, cursor);
|
const { KEY, fetcher } = getFeatureSearchFetcher(projectId, offset, limit);
|
||||||
const { data, error, mutate } = useSWR<IFeatureSearchResponse>(
|
const { data, error, mutate } = useSWR<IFeatureSearchResponse>(
|
||||||
KEY,
|
KEY,
|
||||||
fetcher,
|
fetcher,
|
||||||
@ -54,15 +51,12 @@ export const useFeatureSearch = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// temporary experiment
|
const getFeatureSearchFetcher = (
|
||||||
const getQueryParam = (queryParam: string, path: string | null) => {
|
projectId: string,
|
||||||
const url = new URL(path || '', 'https://getunleash.io');
|
offset: number,
|
||||||
const params = new URLSearchParams(url.search);
|
limit: number,
|
||||||
return params.get(queryParam) || '';
|
) => {
|
||||||
};
|
const KEY = `api/admin/search/features?projectId=${projectId}&offset=${offset}&limit=${limit}`;
|
||||||
|
|
||||||
const getFeatureSearchFetcher = (projectId: string, cursor: string) => {
|
|
||||||
const KEY = `api/admin/search/features?projectId=${projectId}&cursor=${cursor}&limit=25`;
|
|
||||||
|
|
||||||
const fetcher = () => {
|
const fetcher = () => {
|
||||||
const path = formatApiPath(KEY);
|
const path = formatApiPath(KEY);
|
||||||
@ -70,15 +64,7 @@ const getFeatureSearchFetcher = (projectId: string, cursor: string) => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
})
|
||||||
.then(handleErrorResponses('Feature search'))
|
.then(handleErrorResponses('Feature search'))
|
||||||
.then(async (res) => {
|
.then((res) => res.json());
|
||||||
const json = await res.json();
|
|
||||||
// TODO: try using Link as key
|
|
||||||
const nextCursor = getQueryParam(
|
|
||||||
'cursor',
|
|
||||||
res.headers.get('link'),
|
|
||||||
);
|
|
||||||
return { ...json, nextCursor };
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { Response, Request } from 'express';
|
import { Response } from 'express';
|
||||||
import Controller from '../../routes/controller';
|
import Controller from '../../routes/controller';
|
||||||
import { FeatureSearchService, OpenApiService } from '../../services';
|
import { FeatureSearchService, OpenApiService } from '../../services';
|
||||||
import {
|
import {
|
||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
ITag,
|
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
NONE,
|
NONE,
|
||||||
@ -16,7 +15,6 @@ import {
|
|||||||
FeatureSearchQueryParameters,
|
FeatureSearchQueryParameters,
|
||||||
featureSearchQueryParameters,
|
featureSearchQueryParameters,
|
||||||
} from '../../openapi/spec/feature-search-query-parameters';
|
} from '../../openapi/spec/feature-search-query-parameters';
|
||||||
import { nextLink } from './next-link';
|
|
||||||
|
|
||||||
const PATH = '/features';
|
const PATH = '/features';
|
||||||
|
|
||||||
@ -79,7 +77,7 @@ export default class FeatureSearchController extends Controller {
|
|||||||
type,
|
type,
|
||||||
tag,
|
tag,
|
||||||
status,
|
status,
|
||||||
cursor,
|
offset,
|
||||||
limit = '50',
|
limit = '50',
|
||||||
sortOrder,
|
sortOrder,
|
||||||
sortBy,
|
sortBy,
|
||||||
@ -97,24 +95,23 @@ 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 normalizedOffset = Number(offset) > 0 ? Number(limit) : 0;
|
||||||
const normalizedSortBy: string = sortBy ? sortBy : 'createdAt';
|
const normalizedSortBy: string = sortBy ? sortBy : 'createdAt';
|
||||||
const normalizedSortOrder =
|
const normalizedSortOrder =
|
||||||
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
||||||
const { features, nextCursor, total } =
|
const { features, total } = await this.featureSearchService.search({
|
||||||
await this.featureSearchService.search({
|
query,
|
||||||
query,
|
projectId,
|
||||||
projectId,
|
type,
|
||||||
type,
|
userId,
|
||||||
userId,
|
tag: normalizedTag,
|
||||||
tag: normalizedTag,
|
status: normalizedStatus,
|
||||||
status: normalizedStatus,
|
offset: normalizedOffset,
|
||||||
cursor,
|
limit: normalizedLimit,
|
||||||
limit: normalizedLimit,
|
sortBy: normalizedSortBy,
|
||||||
sortBy: normalizedSortBy,
|
sortOrder: normalizedSortOrder,
|
||||||
sortOrder: normalizedSortOrder,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
res.header('Link', nextLink(req, nextCursor));
|
|
||||||
res.json({ features, total });
|
res.json({ features, total });
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidOperationError(
|
throw new InvalidOperationError(
|
||||||
|
@ -25,21 +25,11 @@ export class FeatureSearchService {
|
|||||||
const { features, total } =
|
const { features, total } =
|
||||||
await this.featureStrategiesStore.searchFeatures({
|
await this.featureStrategiesStore.searchFeatures({
|
||||||
...params,
|
...params,
|
||||||
limit: params.limit + 1,
|
limit: params.limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextCursor =
|
|
||||||
features.length > params.limit
|
|
||||||
? features[features.length - 1].createdAt.toJSON()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// do not return the items with the next cursor
|
|
||||||
return {
|
return {
|
||||||
features:
|
features,
|
||||||
features.length > params.limit
|
|
||||||
? features.slice(0, -1)
|
|
||||||
: features,
|
|
||||||
nextCursor,
|
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -58,26 +58,22 @@ const sortFeatures = async (
|
|||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchFeaturesWithCursor = async (
|
const searchFeaturesWithOffset = async (
|
||||||
{
|
{
|
||||||
query = '',
|
query = '',
|
||||||
projectId = 'default',
|
projectId = 'default',
|
||||||
cursor = '',
|
offset = '0',
|
||||||
limit = '10',
|
limit = '10',
|
||||||
}: FeatureSearchQueryParameters,
|
}: FeatureSearchQueryParameters,
|
||||||
expectedCode = 200,
|
expectedCode = 200,
|
||||||
) => {
|
) => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(
|
.get(
|
||||||
`/api/admin/search/features?query=${query}&projectId=${projectId}&cursor=${cursor}&limit=${limit}`,
|
`/api/admin/search/features?query=${query}&projectId=${projectId}&offset=${offset}&limit=${limit}`,
|
||||||
)
|
)
|
||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPage = async (url: string, expectedCode = 200) => {
|
|
||||||
return app.request.get(url).expect(expectedCode);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterFeaturesByType = async (types: string[], expectedCode = 200) => {
|
const filterFeaturesByType = async (types: string[], expectedCode = 200) => {
|
||||||
const typeParams = types.map((type) => `type[]=${type}`).join('&');
|
const typeParams = types.map((type) => `type[]=${type}`).join('&');
|
||||||
return app.request
|
return app.request
|
||||||
@ -121,16 +117,16 @@ test('should search matching features by name', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should paginate with cursor', async () => {
|
test('should paginate with offset', async () => {
|
||||||
await app.createFeature('my_feature_a');
|
await app.createFeature('my_feature_a');
|
||||||
await app.createFeature('my_feature_b');
|
await app.createFeature('my_feature_b');
|
||||||
await app.createFeature('my_feature_c');
|
await app.createFeature('my_feature_c');
|
||||||
await app.createFeature('my_feature_d');
|
await app.createFeature('my_feature_d');
|
||||||
|
|
||||||
const { body: firstPage, headers: firstHeaders } =
|
const { body: firstPage, headers: firstHeaders } =
|
||||||
await searchFeaturesWithCursor({
|
await searchFeaturesWithOffset({
|
||||||
query: 'feature',
|
query: 'feature',
|
||||||
cursor: '',
|
offset: '0',
|
||||||
limit: '2',
|
limit: '2',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,16 +135,17 @@ test('should paginate with cursor', async () => {
|
|||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: secondPage, headers: secondHeaders } = await getPage(
|
const { body: secondPage, headers: secondHeaders } =
|
||||||
firstHeaders.link,
|
await searchFeaturesWithOffset({
|
||||||
);
|
query: 'feature',
|
||||||
|
offset: '2',
|
||||||
|
limit: '2',
|
||||||
|
});
|
||||||
|
|
||||||
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,
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(secondHeaders.link).toBe('');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should filter features by type', async () => {
|
test('should filter features by type', async () => {
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import { nextLink } from './next-link';
|
|
||||||
import { Request } from 'express';
|
|
||||||
|
|
||||||
describe('nextLink', () => {
|
|
||||||
it('should generate the correct next link with a cursor', () => {
|
|
||||||
const req = {
|
|
||||||
baseUrl: '/api/events',
|
|
||||||
path: '/',
|
|
||||||
query: { page: '2', limit: '10' },
|
|
||||||
} as Pick<Request, 'baseUrl' | 'path' | 'query'>;
|
|
||||||
|
|
||||||
const cursor = 'abc123';
|
|
||||||
const result = nextLink(req, cursor);
|
|
||||||
|
|
||||||
expect(result).toBe('/api/events/?page=2&limit=10&cursor=abc123');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate the correct next link without a cursor', () => {
|
|
||||||
const req = {
|
|
||||||
baseUrl: '/api/events',
|
|
||||||
path: '/',
|
|
||||||
query: { page: '2', limit: '10' },
|
|
||||||
} as Pick<Request, 'baseUrl' | 'path' | 'query'>;
|
|
||||||
|
|
||||||
const result = nextLink(req);
|
|
||||||
|
|
||||||
expect(result).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exclude existing cursor from query parameters', () => {
|
|
||||||
const req = {
|
|
||||||
baseUrl: '/api/events',
|
|
||||||
path: '/',
|
|
||||||
query: { page: '2', limit: '10', cursor: 'oldCursor' },
|
|
||||||
} as Pick<Request, 'baseUrl' | 'path' | 'query'>;
|
|
||||||
|
|
||||||
const cursor = 'newCursor';
|
|
||||||
const result = nextLink(req, cursor);
|
|
||||||
|
|
||||||
expect(result).toBe('/api/events/?page=2&limit=10&cursor=newCursor');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty query parameters correctly', () => {
|
|
||||||
const req = {
|
|
||||||
baseUrl: '/api/events',
|
|
||||||
path: '/',
|
|
||||||
query: {},
|
|
||||||
} as Pick<Request, 'baseUrl' | 'path' | 'query'>;
|
|
||||||
|
|
||||||
const cursor = 'abc123';
|
|
||||||
const result = nextLink(req, cursor);
|
|
||||||
|
|
||||||
expect(result).toBe('/api/events/?cursor=abc123');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,18 +0,0 @@
|
|||||||
import { Request } from 'express';
|
|
||||||
|
|
||||||
export function nextLink(
|
|
||||||
req: Pick<Request, 'baseUrl' | 'path' | 'query'>,
|
|
||||||
cursor?: string,
|
|
||||||
): string {
|
|
||||||
if (!cursor) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${req.baseUrl}${req.path}?`;
|
|
||||||
|
|
||||||
const params = new URLSearchParams(req.query as Record<string, string>);
|
|
||||||
|
|
||||||
params.set('cursor', cursor);
|
|
||||||
|
|
||||||
return `${url}${params.toString()}`;
|
|
||||||
}
|
|
@ -253,7 +253,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
environment: string,
|
environment: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.db('feature_strategies')
|
await this.db('feature_strategies')
|
||||||
.where({ feature_name: featureName, environment })
|
.where({
|
||||||
|
feature_name: featureName,
|
||||||
|
environment,
|
||||||
|
})
|
||||||
.del();
|
.del();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,8 +299,14 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
environment,
|
environment,
|
||||||
})
|
})
|
||||||
.orderBy([
|
.orderBy([
|
||||||
{ column: 'sort_order', order: 'asc' },
|
{
|
||||||
{ column: 'created_at', order: 'asc' },
|
column: 'sort_order',
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'created_at',
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
stopTimer();
|
stopTimer();
|
||||||
return rows.map(mapRow);
|
return rows.map(mapRow);
|
||||||
@ -530,7 +539,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
type,
|
type,
|
||||||
tag,
|
tag,
|
||||||
status,
|
status,
|
||||||
cursor,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
sortBy,
|
sortBy,
|
||||||
@ -680,10 +689,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor) {
|
|
||||||
query = query.where('features.created_at', '>=', cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortByMapping = {
|
const sortByMapping = {
|
||||||
name: 'feature_name',
|
name: 'feature_name',
|
||||||
type: 'type',
|
type: 'type',
|
||||||
@ -709,15 +714,24 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
.countDistinct({ total: 'features.name' })
|
.countDistinct({ total: 'features.name' })
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
query = query.select(selectColumns).limit(limit * environmentCount);
|
query = query
|
||||||
|
.select(selectColumns)
|
||||||
|
.limit(limit * environmentCount)
|
||||||
|
.offset(offset * environmentCount);
|
||||||
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));
|
||||||
const features = sortEnvironments(overview);
|
const features = sortEnvironments(overview);
|
||||||
return { features, total: Number(total?.total) || 0 };
|
return {
|
||||||
|
features,
|
||||||
|
total: Number(total?.total) || 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { features: [], total: 0 };
|
return {
|
||||||
|
features: [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatureOverview({
|
async getFeatureOverview({
|
||||||
@ -915,7 +929,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
environment: String,
|
environment: String,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.db(T.featureStrategies)
|
await this.db(T.featureStrategies)
|
||||||
.where({ project_name: projectId, environment })
|
.where({
|
||||||
|
project_name: projectId,
|
||||||
|
environment,
|
||||||
|
})
|
||||||
.del();
|
.del();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ export interface IFeatureSearchParams {
|
|||||||
type?: string[];
|
type?: string[];
|
||||||
tag?: string[][];
|
tag?: string[][];
|
||||||
status?: string[][];
|
status?: string[][];
|
||||||
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
cursor?: string;
|
|
||||||
sortBy: string;
|
sortBy: string;
|
||||||
sortOrder: 'asc' | 'desc';
|
sortOrder: 'asc' | 'desc';
|
||||||
}
|
}
|
||||||
|
@ -58,13 +58,13 @@ export const featureSearchQueryParameters = [
|
|||||||
in: 'query',
|
in: 'query',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cursor',
|
name: 'offset',
|
||||||
schema: {
|
schema: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
example: '2023-10-31T09:21:04.056Z',
|
example: '50',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'The next feature created at date the client has not seen. Used for cursor-based pagination. Empty if starting from the beginning.',
|
'The number of features to skip when returning a page. By default it is set to 0.',
|
||||||
in: 'query',
|
in: 'query',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user