diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index cb7a7e49b786b..708f9a3346784 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -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({ diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx index a89c46506fce7..5f7d92e943a80 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx @@ -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 ( ); }; diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts new file mode 100644 index 0000000000000..ddabbea55bb59 --- /dev/null +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts @@ -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 }, + ); +}); diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts new file mode 100644 index 0000000000000..80c25706abcb9 --- /dev/null +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts @@ -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(({ 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}`, + }, + ] + : [], + }; + }, + }, + ); +} diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index 19464061f2046..57b4b1581fba1 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -59,7 +59,6 @@ import { scheduleQuery, setActiveSouthPaneTab, updateSavedQuery, - validateQuery, } from 'src/SqlLab/actions/sqlLab'; import { STATE_TYPE_MAP, @@ -71,7 +70,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 { @@ -95,8 +93,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` @@ -426,37 +422,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 diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js index 6ff81f03da88e..6dcd07a77b68b 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js @@ -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, diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts index f7ab930b4393d..b1a8812471c02 100644 --- a/superset-frontend/src/SqlLab/types.ts +++ b/superset-frontend/src/SqlLab/types.ts @@ -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; diff --git a/superset-frontend/src/hooks/apiResources/index.ts b/superset-frontend/src/hooks/apiResources/index.ts index 81d77b5d11a50..dbc9882258175 100644 --- a/superset-frontend/src/hooks/apiResources/index.ts +++ b/superset-frontend/src/hooks/apiResources/index.ts @@ -30,3 +30,4 @@ export * from './charts'; export * from './dashboards'; export * from './tables'; export * from './schemas'; +export * from './queryValidations'; diff --git a/superset-frontend/src/hooks/apiResources/queryApi.ts b/superset-frontend/src/hooks/apiResources/queryApi.ts index 5ccc878b0c501..4994f0229efbc 100644 --- a/superset-frontend/src/hooks/apiResources/queryApi.ts +++ b/superset-frontend/src/hooks/apiResources/queryApi.ts @@ -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, }); diff --git a/superset-frontend/src/hooks/apiResources/queryValidations.test.ts b/superset-frontend/src/hooks/apiResources/queryValidations.test.ts new file mode 100644 index 0000000000000..f1f1f4eb4ade6 --- /dev/null +++ b/superset-frontend/src/hooks/apiResources/queryValidations.test.ts @@ -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); +}); diff --git a/superset-frontend/src/hooks/apiResources/queryValidations.ts b/superset-frontend/src/hooks/apiResources/queryValidations.ts new file mode 100644 index 0000000000000..722c320049013 --- /dev/null +++ b/superset-frontend/src/hooks/apiResources/queryValidations.ts @@ -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;