mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
console: allow same named queries and unnamed queries on allowlist file upload
https://github.com/hasura/graphql-engine-mono/pull/1906 GitOrigin-RevId: bdd752f49213a2056f39050d40d3dc299dc07819
This commit is contained in:
parent
c2261e3bb8
commit
5de2ef7d31
@ -8,6 +8,7 @@
|
|||||||
- server: support EdDSA algorithm and key type for JWT
|
- server: support EdDSA algorithm and key type for JWT
|
||||||
- server: fix GraphQL type for single-row returning functions (close #7109)
|
- server: fix GraphQL type for single-row returning functions (close #7109)
|
||||||
- console: add support for creation of indexes for Postgres data sources
|
- console: add support for creation of indexes for Postgres data sources
|
||||||
|
- console: allow same named queries and unnamed queries on allowlist file upload
|
||||||
|
|
||||||
## v2.0.6
|
## v2.0.6
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import styles from './AllowedQueries.scss';
|
|||||||
import ExpandableEditor from '../../../Common/Layout/ExpandableEditor/Editor';
|
import ExpandableEditor from '../../../Common/Layout/ExpandableEditor/Editor';
|
||||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
||||||
|
|
||||||
import { readFile, parseQueryString } from './utils';
|
import { readFile, parseQueryString, renameDuplicates } from './utils';
|
||||||
import { showErrorNotification } from '../../Common/Notification';
|
import { showErrorNotification } from '../../Common/Notification';
|
||||||
import { addAllowedQueries } from '../../../../metadata/actions';
|
import { addAllowedQueries } from '../../../../metadata/actions';
|
||||||
import { allowedQueriesCollection } from '../../../../metadata/utils';
|
import { allowedQueriesCollection } from '../../../../metadata/utils';
|
||||||
@ -20,10 +20,11 @@ const defaultManualQuery: AllowedQueriesCollection = {
|
|||||||
|
|
||||||
type AddAllowedQueryProps = {
|
type AddAllowedQueryProps = {
|
||||||
dispatch: Dispatch;
|
dispatch: Dispatch;
|
||||||
|
allowedQueries: AllowedQueriesCollection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddAllowedQuery: React.FC<AddAllowedQueryProps> = props => {
|
const AddAllowedQuery: React.FC<AddAllowedQueryProps> = props => {
|
||||||
const { dispatch } = props;
|
const { dispatch, allowedQueries } = props;
|
||||||
|
|
||||||
const [manualQuery, setManualQuery] = useState<AllowedQueriesCollection>(
|
const [manualQuery, setManualQuery] = useState<AllowedQueriesCollection>(
|
||||||
defaultManualQuery
|
defaultManualQuery
|
||||||
@ -44,7 +45,8 @@ const AddAllowedQuery: React.FC<AddAllowedQueryProps> = props => {
|
|||||||
const addFileQueries = (content: string) => {
|
const addFileQueries = (content: string) => {
|
||||||
try {
|
try {
|
||||||
const fileQueries = parseQueryString(content);
|
const fileQueries = parseQueryString(content);
|
||||||
dispatch(addAllowedQueries(fileQueries, toggle));
|
const updatedQueries = renameDuplicates(fileQueries, allowedQueries);
|
||||||
|
dispatch(addAllowedQueries(updatedQueries, toggle));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(
|
dispatch(
|
||||||
showErrorNotification('Uploading operations failed', error.message)
|
showErrorNotification('Uploading operations failed', error.message)
|
||||||
@ -73,7 +75,11 @@ const AddAllowedQuery: React.FC<AddAllowedQueryProps> = props => {
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.add_mar_bottom_mid}>
|
<div className={styles.add_mar_bottom_mid}>
|
||||||
<b>Operation name:</b>
|
<b>Query name:</b>
|
||||||
|
<Tooltip
|
||||||
|
message="This is an identifier for the query in the collection.
|
||||||
|
This should be unique in the collection and can be different from the operation name of the query."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -27,7 +27,10 @@ const AllowedQueries: React.FC<Props> = props => {
|
|||||||
<div className={`${styles.add_mar_top} ${styles.wd60}`}>
|
<div className={`${styles.add_mar_top} ${styles.wd60}`}>
|
||||||
<AllowedQueriesNotes />
|
<AllowedQueriesNotes />
|
||||||
<hr className="my-lg" />
|
<hr className="my-lg" />
|
||||||
<AddAllowedQuery dispatch={dispatch} />
|
<AddAllowedQuery
|
||||||
|
dispatch={dispatch}
|
||||||
|
allowedQueries={allowedQueries}
|
||||||
|
/>
|
||||||
<hr className="my-lg" />
|
<hr className="my-lg" />
|
||||||
<AllowedQueriesList
|
<AllowedQueriesList
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
import { AllowedQueriesCollection } from '../../../../metadata/reducer';
|
import { AllowedQueriesCollection } from '../../../../metadata/reducer';
|
||||||
import { Dispatch } from '../../../../types';
|
import { Dispatch } from '../../../../types';
|
||||||
import { getCollectionNames, checkLastQuery } from './utils';
|
import { getCollectionNames, checkLastQuery } from './utils';
|
||||||
|
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
||||||
|
|
||||||
type AllowedQueriesListProps = {
|
type AllowedQueriesListProps = {
|
||||||
dispatch: Dispatch;
|
dispatch: Dispatch;
|
||||||
@ -34,6 +35,7 @@ const AllowedQueriesList: React.FC<AllowedQueriesListProps> = props => {
|
|||||||
return allowedQueries.map((query, i) => {
|
return allowedQueries.map((query, i) => {
|
||||||
const queryName = query.name;
|
const queryName = query.name;
|
||||||
const collectionName = query.collection;
|
const collectionName = query.collection;
|
||||||
|
const queryId = `${queryName}_${collectionName}_${i}`;
|
||||||
|
|
||||||
const collapsedLabel = () => (
|
const collapsedLabel = () => (
|
||||||
<div>
|
<div>
|
||||||
@ -45,17 +47,17 @@ const AllowedQueriesList: React.FC<AllowedQueriesListProps> = props => {
|
|||||||
const expandedLabel = collapsedLabel;
|
const expandedLabel = collapsedLabel;
|
||||||
|
|
||||||
const queryEditorExpanded = () => {
|
const queryEditorExpanded = () => {
|
||||||
const modifiedQuery = modifiedQueries[queryName] || { ...query };
|
const modifiedQuery = modifiedQueries[queryId] || { ...query };
|
||||||
|
|
||||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newModifiedQueries = { ...modifiedQueries };
|
const newModifiedQueries = { ...modifiedQueries };
|
||||||
newModifiedQueries[queryName].name = e.target.value;
|
newModifiedQueries[queryId].name = e.target.value;
|
||||||
setModifiedQueries(newModifiedQueries);
|
setModifiedQueries(newModifiedQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQueryChange = (val: string) => {
|
const handleQueryChange = (val: string) => {
|
||||||
const newModifiedQueries = { ...modifiedQueries };
|
const newModifiedQueries = { ...modifiedQueries };
|
||||||
newModifiedQueries[queryName].query = val;
|
newModifiedQueries[queryId].query = val;
|
||||||
setModifiedQueries(newModifiedQueries);
|
setModifiedQueries(newModifiedQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,7 +65,11 @@ const AllowedQueriesList: React.FC<AllowedQueriesListProps> = props => {
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.add_mar_bottom_mid}>
|
<div className={styles.add_mar_bottom_mid}>
|
||||||
<b>Operation name:</b>
|
<b>Query name:</b>
|
||||||
|
<Tooltip
|
||||||
|
message="This is an identifier for the query in the collection.
|
||||||
|
This should be unique in the collection and can be different from the operation name of the query."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -96,13 +102,13 @@ const AllowedQueriesList: React.FC<AllowedQueriesListProps> = props => {
|
|||||||
|
|
||||||
const editorExpandCallback = () => {
|
const editorExpandCallback = () => {
|
||||||
const newModifiedQueries = { ...modifiedQueries };
|
const newModifiedQueries = { ...modifiedQueries };
|
||||||
newModifiedQueries[queryName] = { ...query };
|
newModifiedQueries[queryId] = { ...query };
|
||||||
setModifiedQueries(newModifiedQueries);
|
setModifiedQueries(newModifiedQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editorCollapseCallback = () => {
|
const editorCollapseCallback = () => {
|
||||||
const newModifiedQueries = { ...modifiedQueries };
|
const newModifiedQueries = { ...modifiedQueries };
|
||||||
delete newModifiedQueries[queryName];
|
delete newModifiedQueries[queryId];
|
||||||
setModifiedQueries(newModifiedQueries);
|
setModifiedQueries(newModifiedQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,7 +116,7 @@ const AllowedQueriesList: React.FC<AllowedQueriesListProps> = props => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
updateAllowedQuery(
|
updateAllowedQuery(
|
||||||
queryName,
|
queryName,
|
||||||
modifiedQueries[queryName],
|
modifiedQueries[queryId],
|
||||||
collectionName
|
collectionName
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -126,7 +132,7 @@ const AllowedQueriesList: React.FC<AllowedQueriesListProps> = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={queryName}>
|
<div key={queryId}>
|
||||||
<ExpandableEditor
|
<ExpandableEditor
|
||||||
editorExpanded={queryEditorExpanded}
|
editorExpanded={queryEditorExpanded}
|
||||||
property={`query-${i}`}
|
property={`query-${i}`}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { parse, print, visit, DefinitionNode } from 'graphql';
|
import { parse, print, visit, DefinitionNode } from 'graphql';
|
||||||
import { AllowedQueriesCollection } from '../../../../metadata/reducer';
|
import { AllowedQueriesCollection } from '../../../../metadata/reducer';
|
||||||
|
import { allowedQueriesCollection } from '../../../../metadata/utils';
|
||||||
|
|
||||||
export type NewDefinitionNode = DefinitionNode & {
|
export type NewDefinitionNode = DefinitionNode & {
|
||||||
name?: {
|
name?: {
|
||||||
@ -69,6 +70,7 @@ const getQueryString = (
|
|||||||
return queryString;
|
return queryString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// parses the query string and returns an array of queries
|
||||||
export const parseQueryString = (queryString: string) => {
|
export const parseQueryString = (queryString: string) => {
|
||||||
const queries: { name: string; query: string }[] = [];
|
const queries: { name: string; query: string }[] = [];
|
||||||
|
|
||||||
@ -99,28 +101,16 @@ export const parseQueryString = (queryString: string) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
queryDefs.forEach(queryDef => {
|
queryDefs.forEach(queryDef => {
|
||||||
if (!queryDef.name) {
|
const queryName = queryDef.name ? queryDef.name.value : `unnamed`;
|
||||||
throw new Error(`Operation without name found: ${print(queryDef)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
name: queryDef.name.value,
|
name: queryName,
|
||||||
query: getQueryString(queryDef, fragmentDefs, definitionHash),
|
query: getQueryString(queryDef, fragmentDefs, definitionHash),
|
||||||
};
|
};
|
||||||
|
|
||||||
queries.push(query);
|
queries.push(query);
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryNames = queries.map(q => q.name);
|
|
||||||
const duplicateNames = queryNames.filter(
|
|
||||||
(q, i) => queryNames.indexOf(q) !== i
|
|
||||||
);
|
|
||||||
if (duplicateNames.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Operations with duplicate names found: ${duplicateNames.join(', ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return queries;
|
return queries;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,6 +127,35 @@ export const getQueriesInCollection = (
|
|||||||
return queries;
|
return queries;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// check if the uploaded queries have same names within the file, or among the already present queries
|
||||||
|
export const renameDuplicates = (
|
||||||
|
fileQueries: { name: string; query: string }[],
|
||||||
|
allQueries: AllowedQueriesCollection[]
|
||||||
|
) => {
|
||||||
|
// we only allow addition to allowedQueriesCollection from console atm
|
||||||
|
const allowListQueries = getQueriesInCollection(
|
||||||
|
allowedQueriesCollection,
|
||||||
|
allQueries
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryNames = new Set();
|
||||||
|
allowListQueries.forEach(query => queryNames.add(query.name));
|
||||||
|
|
||||||
|
const updatedQueries = fileQueries.map(query => {
|
||||||
|
let queryName = query.name;
|
||||||
|
if (queryNames.has(queryName)) {
|
||||||
|
let num = 1;
|
||||||
|
while (queryNames.has(queryName)) {
|
||||||
|
queryName = `${query.name}_${num++}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryNames.add(queryName);
|
||||||
|
return { name: queryName, query: query.query };
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedQueries;
|
||||||
|
};
|
||||||
|
|
||||||
export const checkLastQuery = (
|
export const checkLastQuery = (
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
queries: AllowedQueriesCollection[]
|
queries: AllowedQueriesCollection[]
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AllowedQueries_Utils.ts renameDuplicates should rename duplicate queries 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"name": "getAuthors_1",
|
||||||
|
"query": "query getAuthors {
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "getAuthors_2",
|
||||||
|
"query": "query getAuthors {
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "getAuthors_3",
|
||||||
|
"query": "query getAuthors {
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "unnamed",
|
||||||
|
"query": "{
|
||||||
|
student {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "unnamed_1",
|
||||||
|
"query": "{
|
||||||
|
student {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
roll
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "unnamed_2",
|
||||||
|
"query": "{
|
||||||
|
student {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "getArticles",
|
||||||
|
"query": "query getArticles {
|
||||||
|
article {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "getArticle",
|
||||||
|
"query": "query getArticle {
|
||||||
|
article {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
...frag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment frag on Starship {
|
||||||
|
name
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"name": "addArticles",
|
||||||
|
"query": "mutation addArticles {
|
||||||
|
insert_articles {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
@ -0,0 +1,94 @@
|
|||||||
|
export const uploadedFileData = `# will be ignored by the allow-list
|
||||||
|
type Starship {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
length(unit: LengthUnit = METER): Float
|
||||||
|
}
|
||||||
|
|
||||||
|
# will be ignored by the allow-list
|
||||||
|
scalar parsec
|
||||||
|
|
||||||
|
# will be ignored by the allow-list
|
||||||
|
enum Episode {
|
||||||
|
NEWHOPE
|
||||||
|
EMPIRE
|
||||||
|
JEDI
|
||||||
|
}
|
||||||
|
|
||||||
|
# will be stored in the allow-list
|
||||||
|
query getAuthors{
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query getAuthors{
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query getAuthors{
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query {
|
||||||
|
student {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query {
|
||||||
|
student {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
roll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query {
|
||||||
|
student {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment frag on Starship {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
# will be stored in the allow-list
|
||||||
|
query getArticles {
|
||||||
|
article {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# will be stored in the allow-list after patching in the fragment
|
||||||
|
query getArticle {
|
||||||
|
article {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
...frag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# will be stored in the allow-list
|
||||||
|
mutation addArticles {
|
||||||
|
insert_articles {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -0,0 +1,20 @@
|
|||||||
|
import { allowedQueriesCollection } from '../../../../metadata/utils';
|
||||||
|
import { renameDuplicates, parseQueryString } from '../AllowedQueries/utils';
|
||||||
|
import { uploadedFileData } from './fixtures/allow-list';
|
||||||
|
|
||||||
|
describe('AllowedQueries_Utils.ts', () => {
|
||||||
|
describe('renameDuplicates', () => {
|
||||||
|
it('should rename duplicate queries', () => {
|
||||||
|
const allowedListQueries = [
|
||||||
|
{
|
||||||
|
name: 'getAuthors',
|
||||||
|
query: 'query getAuthors { author {id name} }',
|
||||||
|
collection: allowedQueriesCollection,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const fileQueries = parseQueryString(uploadedFileData);
|
||||||
|
const updatedQueries = renameDuplicates(fileQueries, allowedListQueries);
|
||||||
|
expect(updatedQueries).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user