forked from extern/bruno
feat: graphql support (#65)
This commit is contained in:
parent
530af1f929
commit
2aef7c61a4
@ -92,8 +92,6 @@ export default class QueryEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||||
this.cachedValue = this.props.value;
|
|
||||||
this.editor.setValue(this.props.value);
|
|
||||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||||
}
|
}
|
||||||
this.ignoreChangeEvent = false;
|
this.ignoreChangeEvent = false;
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
.react-tabs__tab-list {
|
div.tabs {
|
||||||
border-bottom: none !important;
|
div.tab {
|
||||||
padding-top: 0;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.react-tabs__tab {
|
|
||||||
padding: 6px 0px;
|
padding: 6px 0px;
|
||||||
border: none;
|
border: none;
|
||||||
user-select: none;
|
|
||||||
border-bottom: solid 2px transparent;
|
border-bottom: solid 2px transparent;
|
||||||
margin-right: 20px;
|
margin-right: 1.25rem;
|
||||||
color: rgb(125 125 125);
|
color: var(--color-tab-inactive);
|
||||||
outline: none !important;
|
cursor: pointer;
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active,
|
&:active,
|
||||||
@ -27,36 +19,12 @@ const StyledWrapper = styled.div`
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&.active {
|
||||||
display: none !important;
|
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||||
|
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tabs__tab--selected {
|
|
||||||
border: none;
|
|
||||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
|
||||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
|
||||||
border-color: var(--color-tab-active-border) !important;
|
|
||||||
background: inherit;
|
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:active,
|
|
||||||
&:focus-within,
|
|
||||||
&:focus-visible,
|
|
||||||
&:target {
|
|
||||||
border: none;
|
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
|
||||||
border-color: var(--color-tab-active-border) !important;
|
|
||||||
background: inherit;
|
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default StyledWrapper;
|
export default StyledWrapper;
|
||||||
|
@ -1,26 +1,115 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
import find from 'lodash/find';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||||
|
import { useTheme } from 'providers/Theme';
|
||||||
|
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import useGraphqlSchema from './useGraphqlSchema';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const GraphQLRequestPane = ({ onRunQuery, schema, leftPaneWidth, value, onQueryChange }) => {
|
const GraphQLRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const tabs = useSelector((state) => state.tabs.tabs);
|
||||||
|
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||||
|
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
|
||||||
|
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||||
|
const {
|
||||||
|
storedTheme
|
||||||
|
} = useTheme();
|
||||||
|
|
||||||
|
let {
|
||||||
|
schema,
|
||||||
|
loadSchema,
|
||||||
|
isLoading: isSchemaLoading,
|
||||||
|
error: schemaError
|
||||||
|
} = useGraphqlSchema(url);
|
||||||
|
|
||||||
|
const loadGqlSchema = () => {
|
||||||
|
if(!isSchemaLoading) {
|
||||||
|
loadSchema();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQueryChange = (value) => {
|
||||||
|
dispatch(
|
||||||
|
updateRequestGraphqlQuery({
|
||||||
|
query: value,
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||||
|
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||||
|
|
||||||
|
const selectTab = (tab) => {
|
||||||
|
dispatch(
|
||||||
|
updateRequestPaneTab({
|
||||||
|
uid: item.uid,
|
||||||
|
requestPaneTab: tab
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabPanel = (tab) => {
|
||||||
|
switch (tab) {
|
||||||
|
case 'query': {
|
||||||
|
return <QueryEditor theme={storedTheme} schema={schema} width={leftPaneWidth} onSave={onSave} value={query} onRun={onRun} onEdit={onQueryChange} />;
|
||||||
|
}
|
||||||
|
case 'headers': {
|
||||||
|
return <RequestHeaders item={item} collection={collection} />;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return <div className="mt-4">404 | Not found</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!activeTabUid) {
|
||||||
|
return <div>Something went wrong</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||||
|
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||||
|
return <div className="pb-4 px-4">An error occured!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTabClassname = (tabName) => {
|
||||||
|
return classnames(`tab select-none ${tabName}`, {
|
||||||
|
active: tabName === focusedTab.requestPaneTab
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="h-full">
|
<StyledWrapper className="flex flex-col h-full relative">
|
||||||
<Tabs className="react-tabs mt-1 flex flex-grow flex-col h-full" forceRenderTabPanel>
|
<div className="flex items-center tabs" role="tablist">
|
||||||
<TabList>
|
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||||
<Tab tabIndex="-1">Query</Tab>
|
Query
|
||||||
<Tab tabIndex="-1">Headers</Tab>
|
</div>
|
||||||
</TabList>
|
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||||
<TabPanel>
|
Headers
|
||||||
<div className="mt-4">
|
</div>
|
||||||
<QueryEditor schema={schema} width={leftPaneWidth} value={value} onRunQuery={onRunQuery} onEdit={onQueryChange} />
|
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
|
||||||
|
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
|
||||||
|
{isSchemaLoading ? (
|
||||||
|
<IconLoader2 className="animate-spin" size={18} strokeWidth={1.5}/>
|
||||||
|
) : null}
|
||||||
|
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
|
||||||
|
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
|
||||||
|
<span className='ml-1'>{schema ? 'Schema' : 'Load Schema'}</span>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
{/* <div className='flex items-center cursor-pointer hover:underline ml-2'>
|
||||||
<TabPanel>
|
<IconBook size={18} strokeWidth={1.5} /><span className='ml-1'>Docs</span>
|
||||||
<RequestHeaders />
|
</div> */}
|
||||||
</TabPanel>
|
</div>
|
||||||
</Tabs>
|
</div>
|
||||||
|
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||||
|
import { simpleHash } from 'utils/common';
|
||||||
|
|
||||||
|
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||||
|
|
||||||
|
const fetchSchema = (endpoint) => {
|
||||||
|
const introspectionQuery = getIntrospectionQuery();
|
||||||
|
const queryParams = {
|
||||||
|
query: introspectionQuery
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(queryParams)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const useGraphqlSchema = (endpoint) => {
|
||||||
|
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [schema, setSchema] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(localStorageKey);
|
||||||
|
if(!saved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buildClientSchema(JSON.parse(saved));
|
||||||
|
} catch {
|
||||||
|
localStorage.setItem(localStorageKey, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadSchema = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
fetchSchema(endpoint)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((s) => {
|
||||||
|
if (s && s.data) {
|
||||||
|
setSchema(buildClientSchema(s.data));
|
||||||
|
setIsLoading(false);
|
||||||
|
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
|
||||||
|
toast.success('Graphql Schema loaded successfully');
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(err);
|
||||||
|
toast.error('Error occured while loading Graphql Schema');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
schema,
|
||||||
|
loadSchema,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGraphqlSchema;
|
@ -5,12 +5,30 @@ const StyledWrapper = styled.div`
|
|||||||
background: ${(props) => props.theme.codemirror.bg};
|
background: ${(props) => props.theme.codemirror.bg};
|
||||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||||
/* todo: find a better way */
|
/* todo: find a better way */
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 220px);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.cm-editor {
|
textarea.cm-editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Todo: dark mode temporary fix
|
||||||
|
// Clean this
|
||||||
|
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||||
|
color: #9cdcfe !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai span.cm-string {
|
||||||
|
color: #ce9178 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai span.cm-number{
|
||||||
|
color: #b5cea8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai span.cm-atom{
|
||||||
|
color: #569cd6 !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default StyledWrapper;
|
export default StyledWrapper;
|
||||||
|
@ -38,6 +38,7 @@ export default class QueryEditor extends React.Component {
|
|||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
mode: 'graphql',
|
mode: 'graphql',
|
||||||
theme: this.props.editorTheme || 'graphiql',
|
theme: this.props.editorTheme || 'graphiql',
|
||||||
|
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||||
keyMap: 'sublime',
|
keyMap: 'sublime',
|
||||||
autoCloseBrackets: true,
|
autoCloseBrackets: true,
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
@ -75,54 +76,51 @@ export default class QueryEditor extends React.Component {
|
|||||||
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||||
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||||
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||||
|
|
||||||
'Cmd-Enter': () => {
|
'Cmd-Enter': () => {
|
||||||
if (this.props.onRunQuery) {
|
if (this.props.onRun) {
|
||||||
this.props.onRunQuery();
|
this.props.onRun();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Ctrl-Enter': () => {
|
'Ctrl-Enter': () => {
|
||||||
if (this.props.onRunQuery) {
|
if (this.props.onRun) {
|
||||||
this.props.onRunQuery();
|
this.props.onRun();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'Shift-Ctrl-C': () => {
|
'Shift-Ctrl-C': () => {
|
||||||
if (this.props.onCopyQuery) {
|
if (this.props.onCopyQuery) {
|
||||||
this.props.onCopyQuery();
|
this.props.onCopyQuery();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'Shift-Ctrl-P': () => {
|
'Shift-Ctrl-P': () => {
|
||||||
if (this.props.onPrettifyQuery) {
|
if (this.props.onPrettifyQuery) {
|
||||||
this.props.onPrettifyQuery();
|
this.props.onPrettifyQuery();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
|
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
|
||||||
|
|
||||||
'Shift-Ctrl-F': () => {
|
'Shift-Ctrl-F': () => {
|
||||||
if (this.props.onPrettifyQuery) {
|
if (this.props.onPrettifyQuery) {
|
||||||
this.props.onPrettifyQuery();
|
this.props.onPrettifyQuery();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'Shift-Ctrl-M': () => {
|
'Shift-Ctrl-M': () => {
|
||||||
if (this.props.onMergeQuery) {
|
if (this.props.onMergeQuery) {
|
||||||
this.props.onMergeQuery();
|
this.props.onMergeQuery();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Cmd-S': () => {
|
'Cmd-S': () => {
|
||||||
if (this.props.onRunQuery) {
|
if (this.props.onSave) {
|
||||||
// empty
|
this.props.onSave();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'Ctrl-S': () => {
|
'Ctrl-S': () => {
|
||||||
if (this.props.onRunQuery) {
|
if (this.props.onSave) {
|
||||||
// empty
|
this.props.onSave();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
'Cmd-F': 'findPersistent',
|
||||||
|
'Ctrl-F': 'findPersistent'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
if (editor) {
|
if (editor) {
|
||||||
@ -149,6 +147,10 @@ export default class QueryEditor extends React.Component {
|
|||||||
this.cachedValue = this.props.value;
|
this.cachedValue = this.props.value;
|
||||||
this.editor.setValue(this.props.value);
|
this.editor.setValue(this.props.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||||
|
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||||
|
}
|
||||||
this.ignoreChangeEvent = false;
|
this.ignoreChangeEvent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +166,7 @@ export default class QueryEditor extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper
|
<StyledWrapper
|
||||||
className="h-full"
|
className="h-full w-full"
|
||||||
aria-label="Query Editor"
|
aria-label="Query Editor"
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
this._node = node;
|
this._node = node;
|
||||||
@ -173,8 +175,11 @@ export default class QueryEditor extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyUp = (_cm, event) => {
|
_onKeyUp = (_cm, e) => {
|
||||||
if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) {
|
if (e.metaKey || e.ctrlKey || e.altKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {
|
||||||
this.editor.execCommand('autocomplete');
|
this.editor.execCommand('autocomplete');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||||
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||||
|
|
||||||
const onUrlChange = (value) => {
|
const onUrlChange = (value) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -12,7 +12,6 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|||||||
import RequestNotFound from './RequestNotFound';
|
import RequestNotFound from './RequestNotFound';
|
||||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||||
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
|
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
@ -65,11 +64,6 @@ const RequestTabPanel = () => {
|
|||||||
setDragging(true);
|
setDragging(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
let schema = null;
|
|
||||||
// let {
|
|
||||||
// schema
|
|
||||||
// } = useGraphqlSchema('https://api.spacex.land/graphql');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
@ -105,8 +99,6 @@ const RequestTabPanel = () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const onGraphqlQueryChange = (value) => {};
|
|
||||||
const runQuery = async () => {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
|
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
|
||||||
@ -118,11 +110,9 @@ const RequestTabPanel = () => {
|
|||||||
<div className="px-4" style={{ width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)' }}>
|
<div className="px-4" style={{ width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)' }}>
|
||||||
{item.type === 'graphql-request' ? (
|
{item.type === 'graphql-request' ? (
|
||||||
<GraphQLRequestPane
|
<GraphQLRequestPane
|
||||||
onRunQuery={runQuery}
|
item={item}
|
||||||
schema={schema}
|
collection={collection}
|
||||||
leftPaneWidth={leftPaneWidth}
|
leftPaneWidth={leftPaneWidth}
|
||||||
value={item.request.body.graphql.query}
|
|
||||||
onQueryChange={onGraphqlQueryChange}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import CloneCollectionItem from './CloneCollectionItem';
|
|||||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||||
|
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
@ -69,7 +70,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
addTab({
|
addTab({
|
||||||
uid: item.uid,
|
uid: item.uid,
|
||||||
collectionUid: collection.uid
|
collectionUid: collection.uid,
|
||||||
|
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections'
|
|||||||
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
||||||
|
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||||
@ -42,7 +43,8 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
addTab({
|
addTab({
|
||||||
uid: uid,
|
uid: uid,
|
||||||
collectionUid: collection.uid
|
collectionUid: collection.uid,
|
||||||
|
requestPaneTab: getDefaultRequestPaneTab({type: values.requestType})
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
onClose();
|
onClose();
|
||||||
@ -77,27 +79,27 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
|||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div className="hidden">
|
<div>
|
||||||
<label htmlFor="requestName" className="block font-semibold">
|
<label htmlFor="requestName" className="block font-semibold">
|
||||||
Type
|
Type
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<input
|
<input
|
||||||
id="http"
|
id="http-request"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="requestType"
|
name="requestType"
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value="http"
|
value="http-request"
|
||||||
checked={formik.values.requestType === 'http-request'}
|
checked={formik.values.requestType === 'http-request'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||||
Http
|
Http
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="graphql"
|
id="graphql-request"
|
||||||
className="ml-4 cursor-pointer"
|
className="ml-4 cursor-pointer"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="requestType"
|
name="requestType"
|
||||||
@ -105,16 +107,16 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
|||||||
formik.setFieldValue('requestMethod', 'POST');
|
formik.setFieldValue('requestMethod', 'POST');
|
||||||
formik.handleChange(event);
|
formik.handleChange(event);
|
||||||
}}
|
}}
|
||||||
value="graphql"
|
value="graphql-request"
|
||||||
checked={formik.values.requestType === 'graphql-request'}
|
checked={formik.values.requestType === 'graphql-request'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||||
Graphql
|
Graphql
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="mt-4">
|
||||||
<label htmlFor="requestName" className="block font-semibold">
|
<label htmlFor="requestName" className="block font-semibold">
|
||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
|
@ -54,7 +54,7 @@ const Wrapper = styled.div`
|
|||||||
&:hover div.drag-request-border {
|
&:hover div.drag-request-border {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-left: solid 1px var(--color-request-dragbar-background-active);
|
border-left: solid 1px ${(props) => props.theme.sidebar.dragbar};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -22,6 +22,10 @@ const GlobalStyle = createGlobalStyle`
|
|||||||
padding: .215rem .6rem .215rem .6rem;
|
padding: .215rem .6rem .215rem .6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-xs {
|
||||||
|
padding: .2rem .4rem .2rem .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-md {
|
.btn-md {
|
||||||
padding: .4rem 1.1rem;
|
padding: .4rem 1.1rem;
|
||||||
line-height: 1.47;
|
line-height: 1.47;
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
|
||||||
|
|
||||||
const useGraphqlSchema = (endpoint) => {
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
|
||||||
const [schema, setSchema] = useState(null);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
const introspectionQuery = getIntrospectionQuery();
|
|
||||||
const queryParams = {
|
|
||||||
query: introspectionQuery
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(queryParams)
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((s) => {
|
|
||||||
if (s && s.data) {
|
|
||||||
setSchema(buildClientSchema(s.data));
|
|
||||||
setIsLoaded(true);
|
|
||||||
} else {
|
|
||||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setError(err);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLoaded,
|
|
||||||
schema,
|
|
||||||
error
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useGraphqlSchema;
|
|
@ -3,8 +3,8 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
idbConnectionReady: false,
|
idbConnectionReady: false,
|
||||||
leftSidebarWidth: 270,
|
leftSidebarWidth: 222,
|
||||||
leftMenuBarOpen: true,
|
leftMenuBarOpen: false,
|
||||||
screenWidth: 500,
|
screenWidth: 500,
|
||||||
showHomePage: false
|
showHomePage: false
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,8 @@ import {
|
|||||||
findEnvironmentInCollection,
|
findEnvironmentInCollection,
|
||||||
isItemAFolder,
|
isItemAFolder,
|
||||||
refreshUidsInItem,
|
refreshUidsInItem,
|
||||||
interpolateEnvironmentVars
|
interpolateEnvironmentVars,
|
||||||
|
getDefaultRequestPaneTab
|
||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { collectionSchema, itemSchema, environmentsSchema } from '@usebruno/schema';
|
import { collectionSchema, itemSchema, environmentsSchema } from '@usebruno/schema';
|
||||||
import { waitForNextTick } from 'utils/common';
|
import { waitForNextTick } from 'utils/common';
|
||||||
@ -108,7 +109,8 @@ export const createCollection = (collectionName) => (dispatch, getState) => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
addTab({
|
addTab({
|
||||||
uid: requestItem.uid,
|
uid: requestItem.uid,
|
||||||
collectionUid: newCollection.uid
|
collectionUid: newCollection.uid,
|
||||||
|
requestPaneTab: getDefaultRequestPaneTab(requestItem)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -635,7 +637,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
addTab({
|
addTab({
|
||||||
uid: item.uid,
|
uid: item.uid,
|
||||||
collectionUid: collection.uid
|
collectionUid: collection.uid,
|
||||||
|
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -606,6 +606,22 @@ export const collectionsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateRequestGraphqlQuery: (state, action) => {
|
||||||
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||||
|
|
||||||
|
if (item && isItemARequest(item)) {
|
||||||
|
if (!item.draft) {
|
||||||
|
item.draft = cloneDeep(item);
|
||||||
|
}
|
||||||
|
item.draft.request.body.mode = 'graphql';
|
||||||
|
item.draft.request.body.graphql = item.draft.request.body.graphql || {};
|
||||||
|
item.draft.request.body.graphql.query = action.payload.query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
updateRequestMethod: (state, action) => {
|
updateRequestMethod: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
@ -791,6 +807,7 @@ export const {
|
|||||||
deleteMultipartFormParam,
|
deleteMultipartFormParam,
|
||||||
updateRequestBodyMode,
|
updateRequestBodyMode,
|
||||||
updateRequestBody,
|
updateRequestBody,
|
||||||
|
updateRequestGraphqlQuery,
|
||||||
updateRequestMethod,
|
updateRequestMethod,
|
||||||
localCollectionAddFileEvent,
|
localCollectionAddFileEvent,
|
||||||
localCollectionAddDirectoryEvent,
|
localCollectionAddDirectoryEvent,
|
||||||
|
@ -23,7 +23,7 @@ export const tabsSlice = createSlice({
|
|||||||
uid: action.payload.uid,
|
uid: action.payload.uid,
|
||||||
collectionUid: action.payload.collectionUid,
|
collectionUid: action.payload.collectionUid,
|
||||||
requestPaneWidth: null,
|
requestPaneWidth: null,
|
||||||
requestPaneTab: 'params',
|
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||||
responsePaneTab: 'response'
|
responsePaneTab: 'response'
|
||||||
});
|
});
|
||||||
state.activeTabUid = action.payload.uid;
|
state.activeTabUid = action.payload.uid;
|
||||||
|
@ -22,6 +22,7 @@ const darkTheme = {
|
|||||||
color: '#ccc',
|
color: '#ccc',
|
||||||
muted: '#9d9d9d',
|
muted: '#9d9d9d',
|
||||||
bg: '#252526',
|
bg: '#252526',
|
||||||
|
dragbar: '#8a8a8a',
|
||||||
|
|
||||||
workspace: {
|
workspace: {
|
||||||
bg: '#3D3D3D'
|
bg: '#3D3D3D'
|
||||||
|
@ -22,6 +22,7 @@ const lightTheme = {
|
|||||||
color: 'rgb(52, 52, 52)',
|
color: 'rgb(52, 52, 52)',
|
||||||
muted: '#4b5563',
|
muted: '#4b5563',
|
||||||
bg: '#F3F3F3',
|
bg: '#F3F3F3',
|
||||||
|
dragbar: 'rgb(200, 200, 200)',
|
||||||
|
|
||||||
workspace: {
|
workspace: {
|
||||||
bg: '#e1e1e1'
|
bg: '#e1e1e1'
|
||||||
|
@ -196,7 +196,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
json: si.draft.request.body.json,
|
json: si.draft.request.body.json,
|
||||||
text: si.draft.request.body.text,
|
text: si.draft.request.body.text,
|
||||||
xml: si.draft.request.body.xml,
|
xml: si.draft.request.body.xml,
|
||||||
multipartForm: si.draft.request.body.multipartForm,
|
graphql: si.draft.request.body.graphql,
|
||||||
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
||||||
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||||
}
|
}
|
||||||
@ -214,6 +214,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
json: si.request.body.json,
|
json: si.request.body.json,
|
||||||
text: si.request.body.text,
|
text: si.request.body.text,
|
||||||
xml: si.request.body.xml,
|
xml: si.request.body.xml,
|
||||||
|
graphql: si.request.body.graphql,
|
||||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||||
}
|
}
|
||||||
@ -410,3 +411,13 @@ export const interpolateEnvironmentVars = (item, variables) => {
|
|||||||
|
|
||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDefaultRequestPaneTab = (item) => {
|
||||||
|
if(item.type === 'http-request') {
|
||||||
|
return 'params';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(item.type === 'graphql-request') {
|
||||||
|
return 'query';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
|
import get from 'lodash/get';
|
||||||
import each from 'lodash/each';
|
import each from 'lodash/each';
|
||||||
import filter from 'lodash/filter';
|
import filter from 'lodash/filter';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { rawRequest, gql } from 'graphql';
|
|
||||||
import { sendHttpRequestInBrowser } from './browser';
|
import { sendHttpRequestInBrowser } from './browser';
|
||||||
import { isElectron } from 'utils/common/platform';
|
import { isElectron } from 'utils/common/platform';
|
||||||
import cancelTokens, { deleteCancelToken } from 'utils/network/cancelTokens';
|
import cancelTokens, { deleteCancelToken } from 'utils/network/cancelTokens';
|
||||||
|
|
||||||
export const sendNetworkRequest = async (item, options) => {
|
export const sendNetworkRequest = async (item, options) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (item.type === 'http-request') {
|
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||||
const timeStart = Date.now();
|
const timeStart = Date.now();
|
||||||
sendHttpRequest(item.draft ? item.draft.request : item.request, options)
|
sendHttpRequest(item.draft ? item.draft.request : item.request, options)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -79,6 +79,15 @@ const sendHttpRequest = async (request, options) => {
|
|||||||
axiosRequest.data = params;
|
axiosRequest.data = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.body.mode === 'graphql') {
|
||||||
|
const graphqlQuery = {
|
||||||
|
query: get(request, 'body.graphql.query'),
|
||||||
|
variables: JSON.parse(get(request, 'body.graphql.variables') || '{}')
|
||||||
|
};
|
||||||
|
axiosRequest.headers['content-type'] = 'application/json';
|
||||||
|
axiosRequest.data = graphqlQuery;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('>>> Sending Request');
|
console.log('>>> Sending Request');
|
||||||
console.log(axiosRequest);
|
console.log(axiosRequest);
|
||||||
|
|
||||||
@ -90,21 +99,6 @@ const sendHttpRequest = async (request, options) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendGraphqlRequest = async (request) => {
|
|
||||||
const query = gql`
|
|
||||||
${request.request.body.graphql.query}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const { data, errors, extensions, headers, status } = await rawRequest(request.request.url, query);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
headers,
|
|
||||||
data,
|
|
||||||
errors
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -29,13 +29,19 @@ const keyValueSchema = Yup.object({
|
|||||||
const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined();
|
const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined();
|
||||||
const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required');
|
const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required');
|
||||||
|
|
||||||
|
const graphqlBodySchema = Yup.object({
|
||||||
|
query: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
|
||||||
|
variables: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
|
||||||
|
}).noUnknown(true).strict();
|
||||||
|
|
||||||
const requestBodySchema = Yup.object({
|
const requestBodySchema = Yup.object({
|
||||||
mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm']).required('mode is required'),
|
mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql']).required('mode is required'),
|
||||||
json: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
|
json: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
|
||||||
text: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
|
text: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
|
||||||
xml: Yup.string().max(10240, 'xml must be 10240 characters or less').nullable(),
|
xml: Yup.string().max(10240, 'xml must be 10240 characters or less').nullable(),
|
||||||
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
|
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
|
||||||
multipartForm: Yup.array().of(keyValueSchema).nullable(),
|
multipartForm: Yup.array().of(keyValueSchema).nullable(),
|
||||||
|
graphql: graphqlBodySchema.nullable(),
|
||||||
}).noUnknown(true).strict();
|
}).noUnknown(true).strict();
|
||||||
|
|
||||||
// Right now, the request schema is very tightly coupled with http request
|
// Right now, the request schema is very tightly coupled with http request
|
||||||
|
Loading…
Reference in New Issue
Block a user