Skip to content

Commit

Permalink
[data / saved query] BWCA all the routes (#158790)
Browse files Browse the repository at this point in the history
## Summary

All routes
- Use versioned router
- Moved to internal path
- Validate responses
- All responses are typed with response types, separate from internal
api types. This is to help prevent unacknowledged changes to the api.
- Version is included in all requests

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
mattkime and kibanamachine authored Jun 19, 2023
1 parent 229e8ca commit 9e74fcf
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 51 deletions.
2 changes: 2 additions & 0 deletions src/plugins/data/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ export const UI_SETTINGS = {
DATE_FORMAT: 'dateFormat',
DATEFORMAT_TZ: 'dateFormat:tz',
} as const;

export const SAVED_QUERY_BASE_URL = '/internal/saved_query';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { createSavedQueryService } from './saved_query_service';
import { httpServiceMock } from '@kbn/core/public/mocks';
import type { SavedQueryAttributes } from '../../../common';
import { SAVED_QUERY_BASE_URL } from '../../../common/constants';

const http = httpServiceMock.createStartContract();

Expand All @@ -22,6 +23,8 @@ const {
getSavedQueryCount,
} = createSavedQueryService(http);

const version = '1';

const savedQueryAttributes: SavedQueryAttributes = {
title: 'foo',
description: 'bar',
Expand All @@ -43,8 +46,9 @@ describe('saved query service', () => {
it('should post the stringified given attributes', async () => {
await createQuery(savedQueryAttributes);
expect(http.post).toBeCalled();
expect(http.post).toHaveBeenCalledWith('/api/saved_query/_create', {
expect(http.post).toHaveBeenCalledWith(`${SAVED_QUERY_BASE_URL}/_create`, {
body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}',
version,
});
});
});
Expand All @@ -53,8 +57,9 @@ describe('saved query service', () => {
it('should put the ID & stringified given attributes', async () => {
await updateQuery('foo', savedQueryAttributes);
expect(http.put).toBeCalled();
expect(http.put).toHaveBeenCalledWith('/api/saved_query/foo', {
expect(http.put).toHaveBeenCalledWith(`${SAVED_QUERY_BASE_URL}/foo`, {
body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}',
version,
});
});
});
Expand All @@ -67,7 +72,7 @@ describe('saved query service', () => {
});
const result = await getAllSavedQueries();
expect(http.post).toBeCalled();
expect(http.post).toHaveBeenCalledWith('/api/saved_query/_all');
expect(http.post).toHaveBeenCalledWith(`${SAVED_QUERY_BASE_URL}/_all`, { version });
expect(result).toEqual([{ attributes: savedQueryAttributes }]);
});
});
Expand All @@ -80,8 +85,9 @@ describe('saved query service', () => {
});
const result = await findSavedQueries();
expect(http.post).toBeCalled();
expect(http.post).toHaveBeenCalledWith('/api/saved_query/_find', {
expect(http.post).toHaveBeenCalledWith(`${SAVED_QUERY_BASE_URL}/_find`, {
body: '{"page":1,"perPage":50,"search":""}',
version,
});
expect(result).toEqual({
queries: [{ attributes: savedQueryAttributes }],
Expand All @@ -94,23 +100,23 @@ describe('saved query service', () => {
it('should get the given ID', async () => {
await getSavedQuery('my_id');
expect(http.get).toBeCalled();
expect(http.get).toHaveBeenCalledWith('/api/saved_query/my_id');
expect(http.get).toHaveBeenCalledWith(`${SAVED_QUERY_BASE_URL}/my_id`, { version });
});
});

describe('deleteSavedQuery', function () {
it('should delete the given ID', async () => {
await deleteSavedQuery('my_id');
expect(http.delete).toBeCalled();
expect(http.delete).toHaveBeenCalledWith('/api/saved_query/my_id');
expect(http.delete).toHaveBeenCalledWith(`${SAVED_QUERY_BASE_URL}/my_id`, { version });
});
});

describe('getSavedQueryCount', function () {
it('should get the total', async () => {
await getSavedQueryCount();
expect(http.get).toBeCalled();
expect(http.get).toHaveBeenCalledWith('/api/saved_query/_count');
expect(http.get).toHaveBeenCalledWith(`${SAVED_QUERY_BASE_URL}/_count`, { version });
});
});
});
21 changes: 14 additions & 7 deletions src/plugins/data/public/query/saved_query/saved_query_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,32 @@
import { HttpStart } from '@kbn/core/public';
import { SavedQuery } from './types';
import type { SavedQueryAttributes } from '../../../common';
import { SAVED_QUERY_BASE_URL } from '../../../common/constants';

const version = '1';

export const createSavedQueryService = (http: HttpStart) => {
const createQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => {
const savedQuery = await http.post<SavedQuery>('/api/saved_query/_create', {
const savedQuery = await http.post<SavedQuery>(`${SAVED_QUERY_BASE_URL}/_create`, {
body: JSON.stringify(attributes),
version,
});
return savedQuery;
};

const updateQuery = async (id: string, attributes: SavedQueryAttributes) => {
const savedQuery = await http.put<SavedQuery>(`/api/saved_query/${id}`, {
const savedQuery = await http.put<SavedQuery>(`${SAVED_QUERY_BASE_URL}/${id}`, {
body: JSON.stringify(attributes),
version,
});
return savedQuery;
};

// we have to tell the saved objects client how many to fetch, otherwise it defaults to fetching 20 per page
const getAllSavedQueries = async (): Promise<SavedQuery[]> => {
const { savedQueries } = await http.post<{ savedQueries: SavedQuery[] }>(
'/api/saved_query/_all'
`${SAVED_QUERY_BASE_URL}/_all`,
{ version }
);
return savedQueries;
};
Expand All @@ -42,23 +48,24 @@ export const createSavedQueryService = (http: HttpStart) => {
const { total, savedQueries: queries } = await http.post<{
savedQueries: SavedQuery[];
total: number;
}>('/api/saved_query/_find', {
}>(`${SAVED_QUERY_BASE_URL}/_find`, {
body: JSON.stringify({ page, perPage, search }),
version,
});

return { total, queries };
};

const getSavedQuery = (id: string): Promise<SavedQuery> => {
return http.get<SavedQuery>(`/api/saved_query/${id}`);
return http.get<SavedQuery>(`${SAVED_QUERY_BASE_URL}/${id}`, { version });
};

const deleteSavedQuery = (id: string) => {
return http.delete<{}>(`/api/saved_query/${id}`);
return http.delete<{}>(`${SAVED_QUERY_BASE_URL}/${id}`, { version });
};

const getSavedQueryCount = async (): Promise<number> => {
return http.get<number>('/api/saved_query/_count');
return http.get<number>(`${SAVED_QUERY_BASE_URL}/_count`, { version });
};

return {
Expand Down
129 changes: 129 additions & 0 deletions src/plugins/data/server/query/route_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { SerializableRecord } from '@kbn/utility-types';

/*
* These types are used to define the shape of the response from the saved query API
* separate but similar to other types to draw attention to REST api return changes
*/

interface MatchAllFilterMetaRestResponse extends FilterMetaRestResponse, SerializableRecord {
field: string;
formattedValue: string;
}

type PhrasesFilterMetaRestResponse = FilterMetaRestResponse & {
params: PhraseFilterValue[]; // The unformatted values
field?: string;
};

interface RangeFilterParamsRestResponse extends SerializableRecord {
from?: number | string;
to?: number | string;
gt?: number | string;
lt?: number | string;
gte?: number | string;
lte?: number | string;
format?: string;
}

type RangeFilterMetaRestResponse = FilterMetaRestResponse & {
params?: RangeFilterParamsRestResponse;
field?: string;
formattedValue?: string;
type: 'range';
};

type PhraseFilterValue = string | number | boolean;

interface PhraseFilterMetaParamsRestResponse extends SerializableRecord {
query: PhraseFilterValue; // The unformatted value
}

type PhraseFilterMetaRestResponse = FilterMetaRestResponse & {
params?: PhraseFilterMetaParamsRestResponse;
field?: string;
index?: string;
};

type FilterMetaParamsRestResponse =
| FilterRestResponse
| FilterRestResponse[]
| RangeFilterMetaRestResponse
| RangeFilterParamsRestResponse
| PhraseFilterMetaRestResponse
| PhraseFilterMetaParamsRestResponse
| PhrasesFilterMetaRestResponse
| MatchAllFilterMetaRestResponse
| string
| string[]
| boolean
| boolean[]
| number
| number[];

interface QueryRestResponse {
query: string | { [key: string]: any };
language: string;
}

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type FilterMetaRestResponse = {
alias?: string | null;
disabled?: boolean;
negate?: boolean;
// controlledBy is there to identify who owns the filter
controlledBy?: string;
// allows grouping of filters
group?: string;
// index and type are optional only because when you create a new filter, there are no defaults
index?: string;
isMultiIndex?: boolean;
type?: string;
key?: string;
params?: FilterMetaParamsRestResponse;
value?: string;
};

type FilterStateStoreRestResponse = 'appState' | 'globalState';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type FilterRestResponse = {
$state?: {
store: FilterStateStoreRestResponse;
};
meta: FilterMetaRestResponse;
query?: Record<string, any>;
};

interface RefreshIntervalRestResponse {
pause: boolean;
value: number;
}

interface TimeRangeRestResponse {
from: string;
to: string;
mode?: 'absolute' | 'relative';
}

type SavedQueryTimeFilterRestResponse = TimeRangeRestResponse & {
refreshInterval: RefreshIntervalRestResponse;
};

export interface SavedQueryRestResponse {
id: string;
attributes: {
filters: FilterRestResponse[];
title: string;
description: string;
query: QueryRestResponse;
timefilter?: SavedQueryTimeFilterRestResponse | undefined;
};
}
Loading

0 comments on commit 9e74fcf

Please sign in to comment.