chore(sqllab): Remove validation result from state (#24082)
This commit is contained in:
parent
7e626c04cb
commit
c4242a3657
|
|
@ -84,9 +84,6 @@ export const CLEAR_QUERY_RESULTS = 'CLEAR_QUERY_RESULTS';
|
|||
export const REMOVE_DATA_PREVIEW = 'REMOVE_DATA_PREVIEW';
|
||||
export const CHANGE_DATA_PREVIEW_ID = 'CHANGE_DATA_PREVIEW_ID';
|
||||
|
||||
export const START_QUERY_VALIDATION = 'START_QUERY_VALIDATION';
|
||||
export const QUERY_VALIDATION_RETURNED = 'QUERY_VALIDATION_RETURNED';
|
||||
export const QUERY_VALIDATION_FAILED = 'QUERY_VALIDATION_FAILED';
|
||||
export const COST_ESTIMATE_STARTED = 'COST_ESTIMATE_STARTED';
|
||||
export const COST_ESTIMATE_RETURNED = 'COST_ESTIMATE_RETURNED';
|
||||
export const COST_ESTIMATE_FAILED = 'COST_ESTIMATE_FAILED';
|
||||
|
|
@ -139,21 +136,6 @@ export function resetState() {
|
|||
return { type: RESET_STATE };
|
||||
}
|
||||
|
||||
export function startQueryValidation(query) {
|
||||
Object.assign(query, {
|
||||
id: query.id ? query.id : shortid.generate(),
|
||||
});
|
||||
return { type: START_QUERY_VALIDATION, query };
|
||||
}
|
||||
|
||||
export function queryValidationReturned(query, results) {
|
||||
return { type: QUERY_VALIDATION_RETURNED, query, results };
|
||||
}
|
||||
|
||||
export function queryValidationFailed(query, message, error) {
|
||||
return { type: QUERY_VALIDATION_FAILED, query, message, error };
|
||||
}
|
||||
|
||||
export function updateQueryEditor(alterations) {
|
||||
return { type: UPDATE_QUERY_EDITOR, alterations };
|
||||
}
|
||||
|
|
@ -440,49 +422,6 @@ export function reRunQuery(query) {
|
|||
};
|
||||
}
|
||||
|
||||
export function validateQuery(queryEditor, sql) {
|
||||
return function (dispatch, getState) {
|
||||
const {
|
||||
sqlLab: { unsavedQueryEditor },
|
||||
} = getState();
|
||||
const qe = {
|
||||
...queryEditor,
|
||||
...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
|
||||
};
|
||||
|
||||
const query = {
|
||||
dbId: qe.dbId,
|
||||
sql,
|
||||
sqlEditorId: qe.id,
|
||||
schema: qe.schema,
|
||||
templateParams: qe.templateParams,
|
||||
};
|
||||
dispatch(startQueryValidation(query));
|
||||
|
||||
const postPayload = {
|
||||
schema: query.schema,
|
||||
sql: query.sql,
|
||||
template_params: query.templateParams,
|
||||
};
|
||||
|
||||
return SupersetClient.post({
|
||||
endpoint: `/api/v1/database/${query.dbId}/validate_sql/`,
|
||||
body: JSON.stringify(postPayload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.then(({ json }) => dispatch(queryValidationReturned(query, json.result)))
|
||||
.catch(response =>
|
||||
getClientErrorObject(response.result).then(error => {
|
||||
let message = error.error || error.statusText || t('Unknown error');
|
||||
if (message.includes('CSRF token')) {
|
||||
message = t(COMMON_ERR_MESSAGES.SESSION_TIMED_OUT);
|
||||
}
|
||||
dispatch(queryValidationFailed(query, message, error));
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function postStopQuery(query) {
|
||||
return function (dispatch) {
|
||||
return SupersetClient.post({
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
import { useSchemas, useTables } from 'src/hooks/apiResources';
|
||||
import { useDatabaseFunctionsQuery } from 'src/hooks/apiResources/databaseFunctions';
|
||||
import { useAnnotations } from './useAnnotations';
|
||||
|
||||
type HotKey = {
|
||||
key: string;
|
||||
|
|
@ -96,8 +97,8 @@ const AceEditorWrapper = ({
|
|||
'id',
|
||||
'dbId',
|
||||
'sql',
|
||||
'validationResult',
|
||||
'schema',
|
||||
'templateParams',
|
||||
]);
|
||||
const { data: schemaOptions } = useSchemas({
|
||||
...(autocomplete && { dbId: queryEditor.dbId }),
|
||||
|
|
@ -286,21 +287,12 @@ const AceEditorWrapper = ({
|
|||
|
||||
setWords(words);
|
||||
}
|
||||
|
||||
const getAceAnnotations = () => {
|
||||
const { validationResult } = queryEditor;
|
||||
const resultIsReady = validationResult?.completed;
|
||||
if (resultIsReady && validationResult?.errors?.length) {
|
||||
const errors = validationResult.errors.map((err: any) => ({
|
||||
type: 'error',
|
||||
row: err.line_number - 1,
|
||||
column: err.start_column - 1,
|
||||
text: err.message,
|
||||
}));
|
||||
return errors;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const { data: annotations } = useAnnotations({
|
||||
dbId: queryEditor.dbId,
|
||||
schema: queryEditor.schema,
|
||||
sql: currentSql,
|
||||
templateParams: queryEditor.templateParams,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledAceEditor
|
||||
|
|
@ -313,7 +305,7 @@ const AceEditorWrapper = ({
|
|||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion={autocomplete}
|
||||
value={sql}
|
||||
annotations={getAceAnnotations()}
|
||||
annotations={annotations}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
createWrapper,
|
||||
defaultStore as store,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { api } from 'src/hooks/apiResources/queryApi';
|
||||
import { initialState } from 'src/SqlLab/fixtures';
|
||||
import COMMON_ERR_MESSAGES from 'src/utils/errorMessages';
|
||||
import { useAnnotations } from './useAnnotations';
|
||||
|
||||
const fakeApiResult = {
|
||||
result: [
|
||||
{
|
||||
end_column: null,
|
||||
line_number: 3,
|
||||
message: 'ERROR: syntax error at or near ";"',
|
||||
start_column: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
const expectDbId = 'db1';
|
||||
const expectSchema = 'my_schema';
|
||||
const expectSql = 'SELECT * from example_table';
|
||||
const expectTemplateParams = '{"a": 1, "v": "str"}';
|
||||
const expectValidatorEngine = 'defined_validator';
|
||||
const queryValidationApiRoute = `glob:*/api/v1/database/${expectDbId}/validate_sql/`;
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
t: (str: string) => str,
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
act(() => {
|
||||
store.dispatch(api.util.resetApiState());
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.post(queryValidationApiRoute, fakeApiResult);
|
||||
});
|
||||
|
||||
const initialize = (withValidator = false) => {
|
||||
if (withValidator) {
|
||||
return renderHook(
|
||||
() =>
|
||||
useAnnotations({
|
||||
sql: expectSql,
|
||||
dbId: expectDbId,
|
||||
schema: expectSchema,
|
||||
templateParams: expectTemplateParams,
|
||||
}),
|
||||
{
|
||||
wrapper: createWrapper({
|
||||
useRedux: true,
|
||||
initialState: {
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
databases: {
|
||||
[expectDbId]: {
|
||||
backend: expectValidatorEngine,
|
||||
},
|
||||
},
|
||||
},
|
||||
common: {
|
||||
conf: {
|
||||
SQL_VALIDATORS_BY_ENGINE: {
|
||||
[expectValidatorEngine]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
return renderHook(
|
||||
() =>
|
||||
useAnnotations({
|
||||
sql: expectSql,
|
||||
dbId: expectDbId,
|
||||
schema: expectSchema,
|
||||
templateParams: expectTemplateParams,
|
||||
}),
|
||||
{
|
||||
wrapper: createWrapper({
|
||||
useRedux: true,
|
||||
store,
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
test('skips fetching validation if validator is undefined', () => {
|
||||
const { result } = initialize();
|
||||
expect(result.current.data).toEqual([]);
|
||||
expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('returns validation if validator is configured', async () => {
|
||||
const { result, waitFor } = initialize(true);
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(1),
|
||||
);
|
||||
expect(result.current.data).toEqual(
|
||||
fakeApiResult.result.map(err => ({
|
||||
type: 'error',
|
||||
row: (err.line_number || 0) - 1,
|
||||
column: (err.start_column || 0) - 1,
|
||||
text: err.message,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns server error description', async () => {
|
||||
const errorMessage = 'Unexpected validation api error';
|
||||
fetchMock.post(
|
||||
queryValidationApiRoute,
|
||||
{
|
||||
throws: new Error(errorMessage),
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
const { result, waitFor } = initialize(true);
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(result.current.data).toEqual([
|
||||
{
|
||||
type: 'error',
|
||||
row: 0,
|
||||
column: 0,
|
||||
text: `The server failed to validate your query.\n${errorMessage}`,
|
||||
},
|
||||
]),
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
});
|
||||
|
||||
test('returns sesion expire description when CSRF token expired', async () => {
|
||||
const errorMessage = 'CSRF token expired';
|
||||
fetchMock.post(
|
||||
queryValidationApiRoute,
|
||||
{
|
||||
throws: new Error(errorMessage),
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
const { result, waitFor } = initialize(true);
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(result.current.data).toEqual([
|
||||
{
|
||||
type: 'error',
|
||||
row: 0,
|
||||
column: 0,
|
||||
text: `The server failed to validate your query.\n${COMMON_ERR_MESSAGES.SESSION_TIMED_OUT}`,
|
||||
},
|
||||
]),
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import COMMON_ERR_MESSAGES from 'src/utils/errorMessages';
|
||||
import { VALIDATION_DEBOUNCE_MS } from 'src/SqlLab/constants';
|
||||
import {
|
||||
FetchValidationQueryParams,
|
||||
useQueryValidationsQuery,
|
||||
} from 'src/hooks/apiResources';
|
||||
import { useDebounceValue } from 'src/hooks/useDebounceValue';
|
||||
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
export function useAnnotations(params: FetchValidationQueryParams) {
|
||||
const { sql, dbId, schema, templateParams } = params;
|
||||
const debouncedSql = useDebounceValue(sql, VALIDATION_DEBOUNCE_MS);
|
||||
const hasValidator = useSelector<SqlLabRootState>(({ sqlLab, common }) =>
|
||||
// Check whether or not we can validate the current query based on whether
|
||||
// or not the backend has a validator configured for it.
|
||||
Boolean(
|
||||
common?.conf?.SQL_VALIDATORS_BY_ENGINE?.[
|
||||
sqlLab?.databases?.[dbId || '']?.backend
|
||||
],
|
||||
),
|
||||
);
|
||||
return useQueryValidationsQuery(
|
||||
{
|
||||
dbId,
|
||||
schema,
|
||||
sql: debouncedSql,
|
||||
templateParams,
|
||||
},
|
||||
{
|
||||
skip: !(hasValidator && dbId && sql),
|
||||
selectFromResult: ({ isLoading, isError, error, data }) => {
|
||||
const errorObj = (error ?? {}) as ClientErrorObject;
|
||||
let message =
|
||||
errorObj?.error || errorObj?.statusText || t('Unknown error');
|
||||
if (message.includes('CSRF token')) {
|
||||
message = t(COMMON_ERR_MESSAGES.SESSION_TIMED_OUT);
|
||||
}
|
||||
return {
|
||||
data:
|
||||
!isLoading && data?.length
|
||||
? data.map(err => ({
|
||||
type: 'error',
|
||||
row: (err.line_number || 0) - 1,
|
||||
column: (err.start_column || 0) - 1,
|
||||
text: err.message,
|
||||
}))
|
||||
: isError
|
||||
? [
|
||||
{
|
||||
type: 'error',
|
||||
row: 0,
|
||||
column: 0,
|
||||
text: `The server failed to validate your query.\n${message}`,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -66,7 +66,6 @@ import {
|
|||
scheduleQuery,
|
||||
setActiveSouthPaneTab,
|
||||
updateSavedQuery,
|
||||
validateQuery,
|
||||
} from 'src/SqlLab/actions/sqlLab';
|
||||
import {
|
||||
STATE_TYPE_MAP,
|
||||
|
|
@ -78,7 +77,6 @@ import {
|
|||
INITIAL_NORTH_PERCENT,
|
||||
INITIAL_SOUTH_PERCENT,
|
||||
SET_QUERY_EDITOR_SQL_DEBOUNCE_MS,
|
||||
VALIDATION_DEBOUNCE_MS,
|
||||
WINDOW_RESIZE_THROTTLE_MS,
|
||||
} from 'src/SqlLab/constants';
|
||||
import {
|
||||
|
|
@ -102,8 +100,6 @@ import RunQueryActionButton from '../RunQueryActionButton';
|
|||
import QueryLimitSelect from '../QueryLimitSelect';
|
||||
|
||||
const bootstrapData = getBootstrapData();
|
||||
const validatorMap =
|
||||
bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {};
|
||||
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
|
||||
|
||||
const StyledToolbar = styled.div`
|
||||
|
|
@ -437,37 +433,9 @@ const SqlEditor = ({
|
|||
[setQueryEditorAndSaveSql],
|
||||
);
|
||||
|
||||
const canValidateQuery = () => {
|
||||
// Check whether or not we can validate the current query based on whether
|
||||
// or not the backend has a validator configured for it.
|
||||
if (database) {
|
||||
return validatorMap.hasOwnProperty(database.backend);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const requestValidation = useCallback(
|
||||
sql => {
|
||||
if (database) {
|
||||
dispatch(validateQuery(queryEditor, sql));
|
||||
}
|
||||
},
|
||||
[database, dispatch, queryEditor],
|
||||
);
|
||||
|
||||
const requestValidationWithDebounce = useMemo(
|
||||
() => debounce(requestValidation, VALIDATION_DEBOUNCE_MS),
|
||||
[requestValidation],
|
||||
);
|
||||
|
||||
const onSqlChanged = sql => {
|
||||
dispatch(queryEditorSetSql(queryEditor, sql));
|
||||
setQueryEditorAndSaveSqlWithDebounce(sql);
|
||||
// Request server-side validation of the query text
|
||||
if (canValidateQuery()) {
|
||||
// NB. requestValidation is debounced
|
||||
requestValidationWithDebounce(sql);
|
||||
}
|
||||
};
|
||||
|
||||
// Return the heights for the ace editor and the south pane as an object
|
||||
|
|
|
|||
|
|
@ -236,85 +236,6 @@ export default function sqlLabReducer(state = {}, action) {
|
|||
tables: state.tables.filter(table => !tableIds.includes(table.id)),
|
||||
};
|
||||
},
|
||||
[actions.START_QUERY_VALIDATION]() {
|
||||
return {
|
||||
...state,
|
||||
...alterUnsavedQueryEditorState(
|
||||
state,
|
||||
{
|
||||
validationResult: {
|
||||
id: action.query.id,
|
||||
errors: [],
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
action.query.sqlEditorId,
|
||||
),
|
||||
};
|
||||
},
|
||||
[actions.QUERY_VALIDATION_RETURNED]() {
|
||||
// If the server is very slow about answering us, we might get validation
|
||||
// responses back out of order. This check confirms the response we're
|
||||
// handling corresponds to the most recently dispatched request.
|
||||
//
|
||||
// We don't care about any but the most recent because validations are
|
||||
// only valid for the SQL text they correspond to -- once the SQL has
|
||||
// changed, the old validation doesn't tell us anything useful anymore.
|
||||
const qe = {
|
||||
...getFromArr(state.queryEditors, action.query.sqlEditorId),
|
||||
...(state.unsavedQueryEditor.id === action.query.sqlEditorId &&
|
||||
state.unsavedQueryEditor),
|
||||
};
|
||||
if (qe.validationResult.id !== action.query.id) {
|
||||
return state;
|
||||
}
|
||||
// Otherwise, persist the results on the queryEditor state
|
||||
return {
|
||||
...state,
|
||||
...alterUnsavedQueryEditorState(
|
||||
state,
|
||||
{
|
||||
validationResult: {
|
||||
id: action.query.id,
|
||||
errors: action.results,
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
action.query.sqlEditorId,
|
||||
),
|
||||
};
|
||||
},
|
||||
[actions.QUERY_VALIDATION_FAILED]() {
|
||||
// If the server is very slow about answering us, we might get validation
|
||||
// responses back out of order. This check confirms the response we're
|
||||
// handling corresponds to the most recently dispatched request.
|
||||
//
|
||||
// We don't care about any but the most recent because validations are
|
||||
// only valid for the SQL text they correspond to -- once the SQL has
|
||||
// changed, the old validation doesn't tell us anything useful anymore.
|
||||
const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
|
||||
if (qe.validationResult.id !== action.query.id) {
|
||||
return state;
|
||||
}
|
||||
// Otherwise, persist the results on the queryEditor state
|
||||
let newState = { ...state };
|
||||
const sqlEditor = { id: action.query.sqlEditorId };
|
||||
newState = alterInArr(newState, 'queryEditors', sqlEditor, {
|
||||
validationResult: {
|
||||
id: action.query.id,
|
||||
errors: [
|
||||
{
|
||||
line_number: 1,
|
||||
start_column: 1,
|
||||
end_column: 1,
|
||||
message: `The server failed to validate your query.\n${action.message}`,
|
||||
},
|
||||
],
|
||||
completed: true,
|
||||
},
|
||||
});
|
||||
return newState;
|
||||
},
|
||||
[actions.COST_ESTIMATE_STARTED]() {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { JsonObject, QueryResponse } from '@superset-ui/core';
|
||||
import { SupersetError } from 'src/components/ErrorMessage/types';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import { ToastType } from 'src/components/MessageToasts/types';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
|
|
@ -39,10 +38,6 @@ export interface QueryEditor {
|
|||
autorun: boolean;
|
||||
sql: string;
|
||||
remoteId: number | null;
|
||||
validationResult?: {
|
||||
completed: boolean;
|
||||
errors: SupersetError[];
|
||||
};
|
||||
hideLeftBar?: boolean;
|
||||
latestQueryId?: string | null;
|
||||
templateParams?: string;
|
||||
|
|
|
|||
|
|
@ -30,3 +30,4 @@ export * from './charts';
|
|||
export * from './dashboards';
|
||||
export * from './tables';
|
||||
export * from './schemas';
|
||||
export * from './queryValidations';
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export const supersetClientQuery: BaseQueryFn<
|
|||
|
||||
export const api = createApi({
|
||||
reducerPath: 'queryApi',
|
||||
tagTypes: ['Schemas', 'Tables', 'DatabaseFunctions'],
|
||||
tagTypes: ['Schemas', 'Tables', 'DatabaseFunctions', 'QueryValidations'],
|
||||
endpoints: () => ({}),
|
||||
baseQuery: supersetClientQuery,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
createWrapper,
|
||||
defaultStore as store,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { api } from 'src/hooks/apiResources/queryApi';
|
||||
import { useQueryValidationsQuery } from './queryValidations';
|
||||
|
||||
const fakeApiResult = {
|
||||
result: [
|
||||
{
|
||||
end_column: null,
|
||||
line_number: 3,
|
||||
message: 'ERROR: syntax error at or near ";"',
|
||||
start_column: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const expectedResult = fakeApiResult.result;
|
||||
|
||||
const expectDbId = 'db1';
|
||||
const expectSchema = 'my_schema';
|
||||
const expectSql = 'SELECT * from example_table';
|
||||
const expectTemplateParams = '{"a": 1, "v": "str"}';
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
act(() => {
|
||||
store.dispatch(api.util.resetApiState());
|
||||
});
|
||||
});
|
||||
|
||||
test('returns api response mapping json result', async () => {
|
||||
const queryValidationApiRoute = `glob:*/api/v1/database/${expectDbId}/validate_sql/`;
|
||||
fetchMock.post(queryValidationApiRoute, fakeApiResult);
|
||||
const { result, waitFor } = renderHook(
|
||||
() =>
|
||||
useQueryValidationsQuery({
|
||||
dbId: expectDbId,
|
||||
sql: expectSql,
|
||||
schema: expectSchema,
|
||||
templateParams: expectTemplateParams,
|
||||
}),
|
||||
{
|
||||
wrapper: createWrapper({
|
||||
useRedux: true,
|
||||
store,
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1),
|
||||
);
|
||||
expect(result.current.data).toEqual(expectedResult);
|
||||
expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1);
|
||||
expect(
|
||||
JSON.parse(`${fetchMock.calls(queryValidationApiRoute)[0][1]?.body}`),
|
||||
).toEqual({
|
||||
schema: expectSchema,
|
||||
sql: expectSql,
|
||||
template_params: JSON.parse(expectTemplateParams),
|
||||
});
|
||||
act(() => {
|
||||
result.current.refetch();
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(queryValidationApiRoute).length).toBe(2),
|
||||
);
|
||||
expect(result.current.data).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('returns cached data without api request', async () => {
|
||||
const queryValidationApiRoute = `glob:*/api/v1/database/${expectDbId}/validate_sql/`;
|
||||
fetchMock.post(queryValidationApiRoute, fakeApiResult);
|
||||
const { result, waitFor, rerender } = renderHook(
|
||||
() =>
|
||||
useQueryValidationsQuery({
|
||||
dbId: expectDbId,
|
||||
sql: expectSql,
|
||||
schema: expectSchema,
|
||||
templateParams: expectTemplateParams,
|
||||
}),
|
||||
{
|
||||
wrapper: createWrapper({
|
||||
useRedux: true,
|
||||
store,
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
|
||||
expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1);
|
||||
rerender();
|
||||
await waitFor(() => expect(result.current.data).toEqual(expectedResult));
|
||||
expect(fetchMock.calls(queryValidationApiRoute).length).toBe(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { api, JsonResponse } from './queryApi';
|
||||
|
||||
export type FetchValidationQueryParams = {
|
||||
dbId?: string | number;
|
||||
schema?: string;
|
||||
sql: string;
|
||||
templateParams?: string;
|
||||
};
|
||||
|
||||
type ValidationResult = {
|
||||
end_column: number | null;
|
||||
line_number: number | null;
|
||||
message: string | null;
|
||||
start_column: number | null;
|
||||
};
|
||||
|
||||
const queryValidationApi = api.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
queryValidations: builder.query<
|
||||
ValidationResult[],
|
||||
FetchValidationQueryParams
|
||||
>({
|
||||
providesTags: ['QueryValidations'],
|
||||
query: ({ dbId, schema, sql, templateParams }) => {
|
||||
let template_params = templateParams;
|
||||
try {
|
||||
template_params = JSON.parse(templateParams || '');
|
||||
} catch (e) {
|
||||
template_params = undefined;
|
||||
}
|
||||
const postPayload = {
|
||||
schema,
|
||||
sql,
|
||||
...(template_params && { template_params }),
|
||||
};
|
||||
return {
|
||||
method: 'post',
|
||||
endpoint: `/api/v1/database/${dbId}/validate_sql/`,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(postPayload),
|
||||
transformResponse: ({ json }: JsonResponse) => json.result,
|
||||
};
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useQueryValidationsQuery } = queryValidationApi;
|
||||
Loading…
Reference in New Issue