From 5b9e1a6db4c2c0bd186b04de52359aa86eea190a Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 13 Jan 2023 11:48:22 -0800 Subject: [PATCH 01/16] feat: add graphiql-batch-request-plugin --- package.json | 3 +- .../graphiql-plugin-batch-request/.eslintrc | 3 + .../graphiql-plugin-batch-request/.gitignore | 25 +++ .../CHANGELOG.md | 1 + .../graphiql-plugin-batch-request/README.md | 1 + .../examples/index.html | 72 +++++++ .../graphiql-plugin-batch-request/index.html | 23 +++ .../package.json | 50 +++++ .../resources/copy-types.mjs | 11 + .../src/index.css | 51 +++++ .../src/index.tsx | 194 ++++++++++++++++++ .../src/test-plugin.tsx | 26 +++ .../src/vite-env.d.ts | 1 + .../tsconfig.json | 22 ++ .../tsconfig.node.json | 9 + .../vite.config.ts | 31 +++ yarn.lock | 26 ++- 17 files changed, 545 insertions(+), 4 deletions(-) create mode 100644 packages/graphiql-plugin-batch-request/.eslintrc create mode 100644 packages/graphiql-plugin-batch-request/.gitignore create mode 100644 packages/graphiql-plugin-batch-request/CHANGELOG.md create mode 100644 packages/graphiql-plugin-batch-request/README.md create mode 100644 packages/graphiql-plugin-batch-request/examples/index.html create mode 100644 packages/graphiql-plugin-batch-request/index.html create mode 100644 packages/graphiql-plugin-batch-request/package.json create mode 100644 packages/graphiql-plugin-batch-request/resources/copy-types.mjs create mode 100644 packages/graphiql-plugin-batch-request/src/index.css create mode 100644 packages/graphiql-plugin-batch-request/src/index.tsx create mode 100644 packages/graphiql-plugin-batch-request/src/test-plugin.tsx create mode 100644 packages/graphiql-plugin-batch-request/src/vite-env.d.ts create mode 100644 packages/graphiql-plugin-batch-request/tsconfig.json create mode 100644 packages/graphiql-plugin-batch-request/tsconfig.node.json create mode 100644 packages/graphiql-plugin-batch-request/vite.config.ts diff --git a/package.json b/package.json index 3b914553657..3d73786c66a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "npm": "please_use_yarn_instead" }, "scripts": { - "build": "yarn build:clean && yarn build:packages && yarn build:graphiql-react && yarn build:graphiql-plugin-explorer && yarn build:graphiql-plugin-code-exporter && yarn build:graphiql", + "build": "yarn build:clean && yarn build:packages && yarn build:graphiql-react && yarn build:graphiql-plugin-explorer && yarn build:graphiql-plugin-code-exporter && yarn build:graphiql-plugin-batch-request && yarn build:graphiql", "build-bundles": "yarn prebuild-bundles && wsrun -p -m -s build-bundles", "build-bundles-clean": "rimraf '{packages,examples,plugins}/**/{bundle,cdn,webpack}' && yarn workspace graphiql run build-bundles-clean", "build-clean": "wsrun -m build-clean ", @@ -39,6 +39,7 @@ "build:graphiql": "yarn tsc resources/tsconfig.graphiql.json", "build:graphiql-plugin-explorer": "yarn workspace @graphiql/plugin-explorer run build", "build:graphiql-plugin-code-exporter": "yarn workspace @graphiql/plugin-code-exporter run build", + "build:graphiql-plugin-batch-request": "yarn workspace @graphiql/plugin-batch-request run build", "build:graphiql-react": "yarn workspace @graphiql/react run build", "build:packages": "yarn tsc", "build:watch": "yarn tsc --watch", diff --git a/packages/graphiql-plugin-batch-request/.eslintrc b/packages/graphiql-plugin-batch-request/.eslintrc new file mode 100644 index 00000000000..98f785433f6 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["plugin:react/jsx-runtime"] +} diff --git a/packages/graphiql-plugin-batch-request/.gitignore b/packages/graphiql-plugin-batch-request/.gitignore new file mode 100644 index 00000000000..81275974cf0 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +types +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/graphiql-plugin-batch-request/CHANGELOG.md b/packages/graphiql-plugin-batch-request/CHANGELOG.md new file mode 100644 index 00000000000..f28cf288d54 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/CHANGELOG.md @@ -0,0 +1 @@ +# @graphiql/plugin-batch-request \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/README.md b/packages/graphiql-plugin-batch-request/README.md new file mode 100644 index 00000000000..ee5b78fc9c7 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/README.md @@ -0,0 +1 @@ +# GraphiQL Batch Request Plugin \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/examples/index.html b/packages/graphiql-plugin-batch-request/examples/index.html new file mode 100644 index 00000000000..8a339b1aa74 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/examples/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + + +
Loading...
+ + + + + + + + + diff --git a/packages/graphiql-plugin-batch-request/index.html b/packages/graphiql-plugin-batch-request/index.html new file mode 100644 index 00000000000..86cd1076b9a --- /dev/null +++ b/packages/graphiql-plugin-batch-request/index.html @@ -0,0 +1,23 @@ + + + + + + + + + +
+ + + diff --git a/packages/graphiql-plugin-batch-request/package.json b/packages/graphiql-plugin-batch-request/package.json new file mode 100644 index 00000000000..f18b809248e --- /dev/null +++ b/packages/graphiql-plugin-batch-request/package.json @@ -0,0 +1,50 @@ +{ + "name": "@graphiql/plugin-batch-request", + "version": "0.0.1", + "repository": { + "type": "git", + "url": "https://github.com/graphql/graphiql", + "directory": "packages/graphiql-plugin-batch-request" + }, + "main": "dist/graphiql-plugin-batch-request.cjs.js", + "module": "dist/graphiql-plugin-batch-request.es.js", + "types": "types/index.d.ts", + "license": "MIT", + "keywords": [ + "react", + "graphql", + "graphiql", + "plugin", + "batch-request" + ], + "files": [ + "dist", + "src", + "types" + ], + "scripts": { + "dev": "vite", + "build": "tsc --emitDeclarationOnly && node resources/copy-types.mjs && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@graphiql/react": "^0.15.0", + "@fortawesome/fontawesome-free": "6.2.1", + "react-checkbox-tree": "1.8.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^1.3.0", + "typescript": "^4.6.3", + "vite": "^2.9.13", + "graphql": "^16.4.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "@graphiql/toolkit": "^0.8.0", + "graphiql": "^2.2.0" + } +} diff --git a/packages/graphiql-plugin-batch-request/resources/copy-types.mjs b/packages/graphiql-plugin-batch-request/resources/copy-types.mjs new file mode 100644 index 00000000000..8f6175f68c7 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/resources/copy-types.mjs @@ -0,0 +1,11 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const base = path.resolve(path.dirname(__filename), '..'); + +fs.copyFileSync( + path.resolve(base, 'src', 'graphiql-explorer.d.ts'), + path.resolve(base, 'types', 'graphiql-explorer.d.ts'), +); diff --git a/packages/graphiql-plugin-batch-request/src/index.css b/packages/graphiql-plugin-batch-request/src/index.css new file mode 100644 index 00000000000..51c98a788c1 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/index.css @@ -0,0 +1,51 @@ +/*.cm-invalidchar { + color: hsla(var(--color-neutral),var(--alpha-tertiary))!important; +}*/ + +.graphiql-batch-request-header { + display: flex; + justify-content: space-between; + position: relative; +} + +.graphiql-batch-request-header-content { + display: flex; + flex-direction: column; + min-width: 0; +} + +.graphiql-batch-request-title { + font-weight: var(--font-weight-medium); + font-size: var(--font-size-h2); + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.graphiql-batch-request-send { + height: 100%; + position: absolute; + right: 0; + top: 0; +} + +.graphiql-batch-request-content { + margin: var(--px-16) 0 0; +} + +.graphiql-batch-request-content>* { + color: hsla(var(--color-neutral),var(--alpha-secondary)); + margin-top: var(--px-20); +} + +.graphiql-batch-request-content .rct-node-leaf .rct-title { + color: hsl(var(--color-tertiary)); +} + +.graphiql-execute-button:disabled { + background-color: hsl(var(--color-neutral)); +} + +.graphiql-execute-button:disabled:hover { + background-color: hsl(var(--color-neutral)); +} diff --git a/packages/graphiql-plugin-batch-request/src/index.tsx b/packages/graphiql-plugin-batch-request/src/index.tsx new file mode 100644 index 00000000000..afaceada47d --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/index.tsx @@ -0,0 +1,194 @@ +import { parse, print } from 'graphql'; +import { Kind, OperationDefinitionNode } from 'graphql/language'; +import { + GraphiQLPlugin, + useEditorContext, + Spinner, + StopIcon, + PlayIcon +} from '@graphiql/react'; +import { useMemo, useRef, useState } from 'react'; +import CheckboxTree from 'react-checkbox-tree'; + +import 'react-checkbox-tree/lib/react-checkbox-tree.css'; +import "@fortawesome/fontawesome-free/css/all.css"; +import './index.css'; + +export type GraphiQLBatchRequestProps = { + url: string, + useAllOperations?: boolean +} + +type TabWithOperation = { + id: string, + operations: OperationDefinitionNode[], + variables: any, + headers: any +} + +function BatchRequestPlugin({ + url, + useAllOperations = false +}: GraphiQLBatchRequestProps) { + const { tabs, responseEditor } = useEditorContext({ nonNull: true }); + let parsingError = ''; + + let tabsWithOperations: {[tabId: string]: TabWithOperation } = {}; + try { + tabsWithOperations = tabs + .filter(tab => tab.query !== '' && tab.query !== null) + .map(tab => ({ + id: tab.id, + operations: parse(tab.query as string).definitions.filter(definition => definition.kind === Kind.OPERATION_DEFINITION), + variables: tab.variables && tab.variables.trim() !== '' ? JSON.parse(tab.variables) : undefined, + headers: tab.headers && tab.headers.trim() !== '' ? JSON.parse(tab.headers) : undefined + })) + .reduce((acc, tabWithOperation) => { + acc[tabWithOperation.id] = tabWithOperation; + return acc; + }, {} as any); + } catch(e: any) { + parsingError = e.message; + } + + const operationValues: string[] = []; + const nodes = Object.values(tabsWithOperations).map((tabWithOperation, i) => ({ + value: tabWithOperation.id, + label: `Tab ${i + 1}`, + children: tabWithOperation.operations.map((operation, j) => { + const operationValue = operation.name?.value ? `${tabWithOperation.id}|${operation.name.value}` : `${tabWithOperation.id}|${j}`; + operationValues.push(operationValue); + return { + value: operationValue, + label: operation.name?.value ?? 'Unnamed operation' + } + }) + })); + + const [batchResponseLoading, setBatchResponseLoading] = useState(false); + const [executeButtonDisabled, setExecuteButtonDisabled] = useState(useAllOperations === false); + const [selectedOperations, setSelectedOperations] = useState(useAllOperations ? operationValues : []); + const [expandedOperations, setExpandedOperations] = useState(Object.keys(tabsWithOperations)); + + + if (parsingError !== '') { + return ( + <> +

Error parsing queries, verify your queries syntax in the tabs:

+

{parsingError}

+ + ) + } + + const sendBatchRequest = () => { + const operations: any[] = []; + let headers = {}; + for (const selectedOperation of selectedOperations) { + const [tabId, selectedOperationName] = selectedOperation.split('|'); + const tab = tabsWithOperations[tabId] + if (tab) { + const selectedOperationDefinition = tab.operations.find((operation, i) => + operation.name?.value === selectedOperationName || `${tab.id}|${i}` === selectedOperation + ) + if (selectedOperationDefinition) { + headers = {...headers, ...tab.headers}; + operations.push({ + operationName: selectedOperationDefinition.name?.value, + query: print(selectedOperationDefinition), + variables: tab.variables + }) + }; + } + } + + setBatchResponseLoading(true); + + window.fetch(url, { + method: 'POST', + body: JSON.stringify(operations), + headers: { + 'content-type': 'application/json', + ...headers + } + }).then(response => response.json()) + .then(json => { + setBatchResponseLoading(false); + responseEditor?.setValue(JSON.stringify(json, null, 2)) + }) + }; + + return ( +
+
+
+
Batch Request
+
+
+ +
+
+
+
+

+ A batch GraphQL request requires at least 1 operation. +

+

0 ? 'block' : 'none' + }}> + You have selected {selectedOperations.length === 1 ? `${selectedOperations.length} operation.` : `${selectedOperations.length} operations.`} +

+
+ { + setSelectedOperations(checked); + setExecuteButtonDisabled(checked.length === 0); + }} + onExpand={setExpandedOperations} + expandOnClick + /> + { batchResponseLoading ? : null } +
+
+ ); +} + +export function useBatchRequestPlugin(props: GraphiQLBatchRequestProps) { + const propsRef = useRef(props); + propsRef.current = props; + return useMemo( + () => ({ + title: 'Batch Request', + icon: () => ( + + + + + + ), + content: () => , + }), + [], + ); +} \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/src/test-plugin.tsx b/packages/graphiql-plugin-batch-request/src/test-plugin.tsx new file mode 100644 index 00000000000..90e60290adc --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/test-plugin.tsx @@ -0,0 +1,26 @@ +import { render } from 'react-dom'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQL } from 'graphiql'; +import * as React from 'react'; +import { useBatchRequestPlugin } from './index'; + +// const url = 'https://swapi-graphql.netlify.app/.netlify/functions/index' +const url = 'https://api.spacex.land/graphql' +const fetcher = createGraphiQLFetcher({ url }); + +const App = () => { + const [query, setQuery] = React.useState(''); + const batchRequestPlugin = useBatchRequestPlugin({ url }); + const defaultEditorToolsVisibility = true; + return ( + + ); +} + +render(, document.getElementById('root')); \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/src/vite-env.d.ts b/packages/graphiql-plugin-batch-request/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/graphiql-plugin-batch-request/tsconfig.json b/packages/graphiql-plugin-batch-request/tsconfig.json new file mode 100644 index 00000000000..872ce46c3eb --- /dev/null +++ b/packages/graphiql-plugin-batch-request/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationDir": "types", + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/graphiql-plugin-batch-request/tsconfig.node.json b/packages/graphiql-plugin-batch-request/tsconfig.node.json new file mode 100644 index 00000000000..9d31e2aed93 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/graphiql-plugin-batch-request/vite.config.ts b/packages/graphiql-plugin-batch-request/vite.config.ts new file mode 100644 index 00000000000..91ec9e3b367 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + lib: { + entry: 'src/index.tsx', + fileName: 'graphiql-plugin-batch-request', + name: 'GraphiQLPluginBatchRequest', + formats: ['cjs', 'es', 'umd'], + }, + rollupOptions: { + external: ['@graphiql/react', 'graphql', 'react', 'react-dom'], + output: { + chunkFileNames: '[name].[format].js', + globals: { + '@graphiql/react': 'GraphiQL.React', + graphql: 'GraphiQL.GraphQL', + react: 'React', + 'react-dom': 'ReactDOM', + }, + }, + }, + commonjsOptions: { + esmExternals: true, + requireReturnsDefault: 'auto', + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 899782e88e3..43c77f62133 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2307,6 +2307,11 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@fortawesome/fontawesome-free@6.2.1": + version "6.2.1" + resolved "https://artylab.expedia.biz/api/npm/public-npm-virtual/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.1.tgz#344baf6ff9eaad7a73cff067d8c56bfc11ae5304" + integrity sha512-viouXhegu/TjkvYQoiRZK3aax69dGXxgEjpvZW81wIJdxm5Fnvp3VVIP4VHKqX4SvFw6qpmkILkD4RJWAdrt7A== + "@graphql-tools/batch-execute@8.5.1": version "8.5.1" resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.5.1.tgz#fa3321d58c64041650be44250b1ebc3aab0ba7a9" @@ -5772,6 +5777,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.2.5: + version "2.3.2" + resolved "https://artylab.expedia.biz/api/npm/public-npm-virtual/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + clean-css@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" @@ -12208,7 +12218,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12889,7 +12899,7 @@ nanoid@3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -nanoid@^3.3.3: +nanoid@^3.0.0, nanoid@^3.3.3: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== @@ -14716,7 +14726,7 @@ prop-types@15.7.2, prop-types@^15.6.1, prop-types@^15.6.2: object-assign "^4.1.1" react-is "^16.8.1" -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -14953,6 +14963,16 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-checkbox-tree@1.8.0: + version "1.8.0" + resolved "https://artylab.expedia.biz/api/npm/public-npm-virtual/react-checkbox-tree/-/react-checkbox-tree-1.8.0.tgz#10dc3e86df25d92559ab6857c7ada4fe285abc03" + integrity sha512-ufC4aorihOvjLpvY1beab2hjVLGZbDTFRzw62foG0+th+KX7e/sdmWu/nD1ZS/U5Yr0rWGwedGH5GOtR0IkUXw== + dependencies: + classnames "^2.2.5" + lodash "^4.17.10" + nanoid "^3.0.0" + prop-types "^15.5.8" + react-clientside-effect@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a" From 76402817b5c96f5f4f194c7703ef14a1255bbf62 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 13 Jan 2023 11:55:34 -0800 Subject: [PATCH 02/16] feat: remove examples for now --- .../examples/index.html | 129 ------------------ 1 file changed, 129 deletions(-) delete mode 100644 packages/graphiql-plugin-code-exporter/examples/index.html diff --git a/packages/graphiql-plugin-code-exporter/examples/index.html b/packages/graphiql-plugin-code-exporter/examples/index.html deleted file mode 100644 index 887b01251f1..00000000000 --- a/packages/graphiql-plugin-code-exporter/examples/index.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - -
Loading...
- - - - - - - - - - From 75cd66dd19933b40cd2f7c8d58301c87a65a25da Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 13 Jan 2023 11:59:34 -0800 Subject: [PATCH 03/16] feat: remove examples for now --- .../examples/index.html | 72 ---------- .../examples/index.html | 129 ++++++++++++++++++ 2 files changed, 129 insertions(+), 72 deletions(-) delete mode 100644 packages/graphiql-plugin-batch-request/examples/index.html create mode 100644 packages/graphiql-plugin-code-exporter/examples/index.html diff --git a/packages/graphiql-plugin-batch-request/examples/index.html b/packages/graphiql-plugin-batch-request/examples/index.html deleted file mode 100644 index 8a339b1aa74..00000000000 --- a/packages/graphiql-plugin-batch-request/examples/index.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - -
Loading...
- - - - - - - - - diff --git a/packages/graphiql-plugin-code-exporter/examples/index.html b/packages/graphiql-plugin-code-exporter/examples/index.html new file mode 100644 index 00000000000..887b01251f1 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/examples/index.html @@ -0,0 +1,129 @@ + + + + + + + + + + + + +
Loading...
+ + + + + + + + + + From 04914b4e39a83f18da90072743142064c41e8bb7 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Tue, 17 Jan 2023 12:43:17 -0800 Subject: [PATCH 04/16] feat: types in d.ts --- .../resources/copy-types.mjs | 4 +- .../src/graphiql-batch-request.d.ts | 22 ++++ .../src/index.css | 4 - .../src/index.tsx | 108 ++++++++++-------- 4 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts diff --git a/packages/graphiql-plugin-batch-request/resources/copy-types.mjs b/packages/graphiql-plugin-batch-request/resources/copy-types.mjs index 8f6175f68c7..79f3f5a619d 100644 --- a/packages/graphiql-plugin-batch-request/resources/copy-types.mjs +++ b/packages/graphiql-plugin-batch-request/resources/copy-types.mjs @@ -6,6 +6,6 @@ const __filename = fileURLToPath(import.meta.url); const base = path.resolve(path.dirname(__filename), '..'); fs.copyFileSync( - path.resolve(base, 'src', 'graphiql-explorer.d.ts'), - path.resolve(base, 'types', 'graphiql-explorer.d.ts'), + path.resolve(base, 'src', 'graphiql-batch-request.d.ts'), + path.resolve(base, 'types', 'graphiql-batch-request.d.ts'), ); diff --git a/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts new file mode 100644 index 00000000000..8c889e85dcf --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts @@ -0,0 +1,22 @@ +declare module 'graphiql-batch-request' { + import { ComponentType } from 'react'; + import { OperationDefinitionNode } from 'graphql/language'; + + type TabWithOperations = { + id: string, + operations: OperationDefinitionNode[], + variables: Record, + headers: Record + } + + export type TabsWithOperations = Record; + + export type GraphiQLBatchRequestProps = { + url: string, + useAllOperations?: boolean + } + + const GraphiQLBatchRequest: ComponentType; + + export default GraphiQLBatchRequest; +} diff --git a/packages/graphiql-plugin-batch-request/src/index.css b/packages/graphiql-plugin-batch-request/src/index.css index 51c98a788c1..cbadf42deb7 100644 --- a/packages/graphiql-plugin-batch-request/src/index.css +++ b/packages/graphiql-plugin-batch-request/src/index.css @@ -1,7 +1,3 @@ -/*.cm-invalidchar { - color: hsla(var(--color-neutral),var(--alpha-tertiary))!important; -}*/ - .graphiql-batch-request-header { display: flex; justify-content: space-between; diff --git a/packages/graphiql-plugin-batch-request/src/index.tsx b/packages/graphiql-plugin-batch-request/src/index.tsx index afaceada47d..3d49499e3a0 100644 --- a/packages/graphiql-plugin-batch-request/src/index.tsx +++ b/packages/graphiql-plugin-batch-request/src/index.tsx @@ -1,31 +1,18 @@ -import { parse, print } from 'graphql'; -import { Kind, OperationDefinitionNode } from 'graphql/language'; import { - GraphiQLPlugin, - useEditorContext, - Spinner, - StopIcon, - PlayIcon + GraphiQLPlugin, PlayIcon, Spinner, + StopIcon, useEditorContext } from '@graphiql/react'; import { useMemo, useRef, useState } from 'react'; +import { parse, print } from 'graphql'; +import { Kind } from 'graphql/language'; import CheckboxTree from 'react-checkbox-tree'; +import { GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request'; -import 'react-checkbox-tree/lib/react-checkbox-tree.css'; import "@fortawesome/fontawesome-free/css/all.css"; +import 'react-checkbox-tree/lib/react-checkbox-tree.css'; +import './graphiql-batch-request.d.ts'; import './index.css'; -export type GraphiQLBatchRequestProps = { - url: string, - useAllOperations?: boolean -} - -type TabWithOperation = { - id: string, - operations: OperationDefinitionNode[], - variables: any, - headers: any -} - function BatchRequestPlugin({ url, useAllOperations = false @@ -33,18 +20,24 @@ function BatchRequestPlugin({ const { tabs, responseEditor } = useEditorContext({ nonNull: true }); let parsingError = ''; - let tabsWithOperations: {[tabId: string]: TabWithOperation } = {}; + let tabsWithOperations: TabsWithOperations = {}; try { tabsWithOperations = tabs .filter(tab => tab.query !== '' && tab.query !== null) .map(tab => ({ id: tab.id, - operations: parse(tab.query as string).definitions.filter(definition => definition.kind === Kind.OPERATION_DEFINITION), - variables: tab.variables && tab.variables.trim() !== '' ? JSON.parse(tab.variables) : undefined, - headers: tab.headers && tab.headers.trim() !== '' ? JSON.parse(tab.headers) : undefined + operations: parse(tab.query as string).definitions.filter( + definition => definition.kind === Kind.OPERATION_DEFINITION + ), + variables: tab.variables && tab.variables.trim() !== '' + ? JSON.parse(tab.variables) : + undefined, + headers: tab.headers && tab.headers.trim() !== '' + ? JSON.parse(tab.headers) : + undefined })) - .reduce((acc, tabWithOperation) => { - acc[tabWithOperation.id] = tabWithOperation; + .reduce((acc, tabWithOperations) => { + acc[tabWithOperations.id] = tabWithOperations; return acc; }, {} as any); } catch(e: any) { @@ -52,23 +45,34 @@ function BatchRequestPlugin({ } const operationValues: string[] = []; - const nodes = Object.values(tabsWithOperations).map((tabWithOperation, i) => ({ - value: tabWithOperation.id, - label: `Tab ${i + 1}`, - children: tabWithOperation.operations.map((operation, j) => { - const operationValue = operation.name?.value ? `${tabWithOperation.id}|${operation.name.value}` : `${tabWithOperation.id}|${j}`; - operationValues.push(operationValue); - return { - value: operationValue, - label: operation.name?.value ?? 'Unnamed operation' - } + const nodes = Object.values(tabsWithOperations).map( + (tabWithOperations, i) => ({ + value: tabWithOperations.id, + label: `Tab ${i + 1}`, + children: tabWithOperations.operations.map((operation, j) => { + const operationValue = operation.name?.value + ? `${tabWithOperations.id}|${operation.name.value}` + : `${tabWithOperations.id}|${j}`; + operationValues.push(operationValue); + + return { + value: operationValue, + label: operation.name?.value ?? 'Unnamed operation' + } + }) }) - })); + ); const [batchResponseLoading, setBatchResponseLoading] = useState(false); - const [executeButtonDisabled, setExecuteButtonDisabled] = useState(useAllOperations === false); - const [selectedOperations, setSelectedOperations] = useState(useAllOperations ? operationValues : []); - const [expandedOperations, setExpandedOperations] = useState(Object.keys(tabsWithOperations)); + const [executeButtonDisabled, setExecuteButtonDisabled] = useState( + useAllOperations === false + ); + const [selectedOperations, setSelectedOperations] = useState( + useAllOperations ? operationValues : [] + ); + const [expandedOperations, setExpandedOperations] = useState( + Object.keys(tabsWithOperations) + ); if (parsingError !== '') { @@ -87,17 +91,19 @@ function BatchRequestPlugin({ const [tabId, selectedOperationName] = selectedOperation.split('|'); const tab = tabsWithOperations[tabId] if (tab) { - const selectedOperationDefinition = tab.operations.find((operation, i) => - operation.name?.value === selectedOperationName || `${tab.id}|${i}` === selectedOperation + const selectedOperationDefinition = tab.operations.find( + (operation, i) => + operation.name?.value === selectedOperationName || + `${tab.id}|${i}` === selectedOperation ) - if (selectedOperationDefinition) { - headers = {...headers, ...tab.headers}; - operations.push({ - operationName: selectedOperationDefinition.name?.value, - query: print(selectedOperationDefinition), - variables: tab.variables - }) - }; + if (selectedOperationDefinition) { + headers = {...headers, ...tab.headers}; + operations.push({ + operationName: selectedOperationDefinition.name?.value, + query: print(selectedOperationDefinition), + variables: tab.variables + }) + }; } } @@ -154,6 +160,8 @@ function BatchRequestPlugin({ , + expandOpen: , parentClose: null, parentOpen: null, leaf: null From d661a0ed6443cbf9c17a66de3e15fe82de6a2f7e Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Tue, 17 Jan 2023 13:00:04 -0800 Subject: [PATCH 05/16] feat: use FetcherParams type from tookit --- packages/graphiql-plugin-batch-request/src/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/graphiql-plugin-batch-request/src/index.tsx b/packages/graphiql-plugin-batch-request/src/index.tsx index 3d49499e3a0..715fefb75bf 100644 --- a/packages/graphiql-plugin-batch-request/src/index.tsx +++ b/packages/graphiql-plugin-batch-request/src/index.tsx @@ -2,6 +2,7 @@ import { GraphiQLPlugin, PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react'; +import { FetcherParams } from '@graphiql/toolkit'; import { useMemo, useRef, useState } from 'react'; import { parse, print } from 'graphql'; import { Kind } from 'graphql/language'; @@ -18,8 +19,8 @@ function BatchRequestPlugin({ useAllOperations = false }: GraphiQLBatchRequestProps) { const { tabs, responseEditor } = useEditorContext({ nonNull: true }); + let parsingError = ''; - let tabsWithOperations: TabsWithOperations = {}; try { tabsWithOperations = tabs @@ -85,7 +86,7 @@ function BatchRequestPlugin({ } const sendBatchRequest = () => { - const operations: any[] = []; + const operations: FetcherParams[] = []; let headers = {}; for (const selectedOperation of selectedOperations) { const [tabId, selectedOperationName] = selectedOperation.split('|'); From ef8cb3a712679a29b2f8434290f9a6bd557ac63b Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Tue, 17 Jan 2023 13:25:10 -0800 Subject: [PATCH 06/16] feat: use types for parsing errors (operation, headers, variables) --- packages/graphiql-plugin-batch-request/src/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/graphiql-plugin-batch-request/src/index.tsx b/packages/graphiql-plugin-batch-request/src/index.tsx index 715fefb75bf..18a6a6471e2 100644 --- a/packages/graphiql-plugin-batch-request/src/index.tsx +++ b/packages/graphiql-plugin-batch-request/src/index.tsx @@ -4,7 +4,7 @@ import { } from '@graphiql/react'; import { FetcherParams } from '@graphiql/toolkit'; import { useMemo, useRef, useState } from 'react'; -import { parse, print } from 'graphql'; +import { GraphQLError, parse, print } from 'graphql'; import { Kind } from 'graphql/language'; import CheckboxTree from 'react-checkbox-tree'; import { GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request'; @@ -41,8 +41,10 @@ function BatchRequestPlugin({ acc[tabWithOperations.id] = tabWithOperations; return acc; }, {} as any); - } catch(e: any) { - parsingError = e.message; + } catch(e: unknown) { + if (e instanceof GraphQLError || e instanceof SyntaxError) { + parsingError = e.message; + } } const operationValues: string[] = []; From e98c0160bc1e9d23dd50a5cff8b66c6dcef0093f Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Wed, 25 Jan 2023 14:24:52 -0600 Subject: [PATCH 07/16] feat: use default registry and fix sending queries with fragments --- .../graphiql-plugin-batch-request/index.html | 1 - .../src/batch-request-plugin.tsx | 187 +++++++++++++++++ .../src/graphiql-batch-request.d.ts | 3 +- .../src/index.css | 4 + .../src/index.tsx | 188 +----------------- .../src/test-plugin.tsx | 3 +- yarn.lock | 6 +- 7 files changed, 202 insertions(+), 190 deletions(-) create mode 100644 packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx diff --git a/packages/graphiql-plugin-batch-request/index.html b/packages/graphiql-plugin-batch-request/index.html index 86cd1076b9a..71925ece42f 100644 --- a/packages/graphiql-plugin-batch-request/index.html +++ b/packages/graphiql-plugin-batch-request/index.html @@ -15,7 +15,6 @@ -
diff --git a/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx b/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx new file mode 100644 index 00000000000..d45872113d7 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx @@ -0,0 +1,187 @@ +import { PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react'; +import { FetcherParams } from '@graphiql/toolkit'; +import { GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request'; +import { GraphQLError, parse, print } from 'graphql'; +import { Kind } from 'graphql/language'; +import { useState } from 'react'; +import CheckboxTree from 'react-checkbox-tree'; + +import "@fortawesome/fontawesome-free/css/all.css"; +import 'react-checkbox-tree/lib/react-checkbox-tree.css'; +import './graphiql-batch-request.d.ts'; +import './index.css'; + +export function BatchRequestPlugin({ + url, + useAllOperations = false +}: GraphiQLBatchRequestProps) { + const { tabs, responseEditor } = useEditorContext({ nonNull: true }); + + let parsingError = ''; + let tabsWithOperations: TabsWithOperations = {}; + try { + tabsWithOperations = tabs + .filter(tab => tab.query !== '' && tab.query !== null) + .map(tab => { + const document = parse(tab.query as string); + return { + id: tab.id, + document, + operations: document.definitions.filter( + definition => definition.kind === Kind.OPERATION_DEFINITION + ), + variables: tab.variables && tab.variables.trim() !== '' + ? JSON.parse(tab.variables) : + undefined, + headers: tab.headers && tab.headers.trim() !== '' + ? JSON.parse(tab.headers) : + undefined + } + }) + .reduce((acc, tabWithOperations) => { + acc[tabWithOperations.id] = tabWithOperations; + return acc; + }, {} as any); + } catch(e: unknown) { + if (e instanceof GraphQLError || e instanceof SyntaxError) { + parsingError = e.message; + } + } + + const operationValues: string[] = []; + const nodes = Object.values(tabsWithOperations).map( + (tabWithOperations, i) => ({ + value: tabWithOperations.id, + label: `Tab ${i + 1}`, + children: tabWithOperations.operations.map((operation, j) => { + const operationValue = operation.name?.value + ? `${tabWithOperations.id}|${operation.name.value}` + : `${tabWithOperations.id}|${j}`; + operationValues.push(operationValue); + + return { + value: operationValue, + label: operation.name?.value ?? 'Unnamed operation' + } + }) + }) + ); + + const [batchResponseLoading, setBatchResponseLoading] = useState(false); + const [executeButtonDisabled, setExecuteButtonDisabled] = useState( + useAllOperations === false + ); + const [selectedOperations, setSelectedOperations] = useState( + useAllOperations ? operationValues : [] + ); + const [expandedOperations, setExpandedOperations] = useState( + Object.keys(tabsWithOperations) + ); + + + if (parsingError !== '') { + return ( + <> +

Error parsing queries, verify your queries syntax in the tabs:

+

{parsingError}

+ + ) + } + + const sendBatchRequest = () => { + const operations: FetcherParams[] = []; + let headers = {}; + for (const selectedOperation of selectedOperations) { + const [tabId, selectedOperationName] = selectedOperation.split('|'); + const tab = tabsWithOperations[tabId] + if (tab) { + const selectedOperationDefinition = tab.operations.find( + (operation, i) => + operation.name?.value === selectedOperationName || + `${tab.id}|${i}` === selectedOperation + ) + if (selectedOperationDefinition) { + headers = {...headers, ...tab.headers}; + operations.push({ + operationName: selectedOperationDefinition.name?.value, + query: print(tab.document), + variables: tab.variables + }) + }; + } + } + + setBatchResponseLoading(true); + + window.fetch(url, { + method: 'POST', + body: JSON.stringify(operations), + headers: { + 'content-type': 'application/json', + ...headers + } + }).then(response => response.json()) + .then(json => { + setBatchResponseLoading(false); + responseEditor?.setValue(JSON.stringify(json, null, 2)) + }) + }; + + return ( +
+
+
+
Batch Request
+
+
+ +
+
+
+
+

+ A batch GraphQL request requires at least 1 operation. +

+

0 ? 'block' : 'none' + }}> + You have selected {selectedOperations.length === 1 ? `${selectedOperations.length} operation.` : `${selectedOperations.length} operations.`} +

+
+ , + expandOpen: , + parentClose: null, + parentOpen: null, + leaf: null + }} + nodes={nodes} + checked={selectedOperations} + expanded={expandedOperations} + onCheck={checked => { + setSelectedOperations(checked); + setExecuteButtonDisabled(checked.length === 0); + }} + onExpand={setExpandedOperations} + expandOnClick + /> + { batchResponseLoading ? : null } +
+
+ ); +} \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts index 8c889e85dcf..b078818b71e 100644 --- a/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts +++ b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts @@ -1,9 +1,10 @@ declare module 'graphiql-batch-request' { import { ComponentType } from 'react'; - import { OperationDefinitionNode } from 'graphql/language'; + import { DocumentNode, OperationDefinitionNode } from 'graphql/language'; type TabWithOperations = { id: string, + document: DocumentNode, operations: OperationDefinitionNode[], variables: Record, headers: Record diff --git a/packages/graphiql-plugin-batch-request/src/index.css b/packages/graphiql-plugin-batch-request/src/index.css index cbadf42deb7..67302949b49 100644 --- a/packages/graphiql-plugin-batch-request/src/index.css +++ b/packages/graphiql-plugin-batch-request/src/index.css @@ -45,3 +45,7 @@ .graphiql-execute-button:disabled:hover { background-color: hsl(var(--color-neutral)); } + +.react-checkbox-tree { + overflow-y: auto; +} diff --git a/packages/graphiql-plugin-batch-request/src/index.tsx b/packages/graphiql-plugin-batch-request/src/index.tsx index 18a6a6471e2..500b23283b1 100644 --- a/packages/graphiql-plugin-batch-request/src/index.tsx +++ b/packages/graphiql-plugin-batch-request/src/index.tsx @@ -1,189 +1,9 @@ import { - GraphiQLPlugin, PlayIcon, Spinner, - StopIcon, useEditorContext + GraphiQLPlugin } from '@graphiql/react'; -import { FetcherParams } from '@graphiql/toolkit'; -import { useMemo, useRef, useState } from 'react'; -import { GraphQLError, parse, print } from 'graphql'; -import { Kind } from 'graphql/language'; -import CheckboxTree from 'react-checkbox-tree'; -import { GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request'; - -import "@fortawesome/fontawesome-free/css/all.css"; -import 'react-checkbox-tree/lib/react-checkbox-tree.css'; -import './graphiql-batch-request.d.ts'; -import './index.css'; - -function BatchRequestPlugin({ - url, - useAllOperations = false -}: GraphiQLBatchRequestProps) { - const { tabs, responseEditor } = useEditorContext({ nonNull: true }); - - let parsingError = ''; - let tabsWithOperations: TabsWithOperations = {}; - try { - tabsWithOperations = tabs - .filter(tab => tab.query !== '' && tab.query !== null) - .map(tab => ({ - id: tab.id, - operations: parse(tab.query as string).definitions.filter( - definition => definition.kind === Kind.OPERATION_DEFINITION - ), - variables: tab.variables && tab.variables.trim() !== '' - ? JSON.parse(tab.variables) : - undefined, - headers: tab.headers && tab.headers.trim() !== '' - ? JSON.parse(tab.headers) : - undefined - })) - .reduce((acc, tabWithOperations) => { - acc[tabWithOperations.id] = tabWithOperations; - return acc; - }, {} as any); - } catch(e: unknown) { - if (e instanceof GraphQLError || e instanceof SyntaxError) { - parsingError = e.message; - } - } - - const operationValues: string[] = []; - const nodes = Object.values(tabsWithOperations).map( - (tabWithOperations, i) => ({ - value: tabWithOperations.id, - label: `Tab ${i + 1}`, - children: tabWithOperations.operations.map((operation, j) => { - const operationValue = operation.name?.value - ? `${tabWithOperations.id}|${operation.name.value}` - : `${tabWithOperations.id}|${j}`; - operationValues.push(operationValue); - - return { - value: operationValue, - label: operation.name?.value ?? 'Unnamed operation' - } - }) - }) - ); - - const [batchResponseLoading, setBatchResponseLoading] = useState(false); - const [executeButtonDisabled, setExecuteButtonDisabled] = useState( - useAllOperations === false - ); - const [selectedOperations, setSelectedOperations] = useState( - useAllOperations ? operationValues : [] - ); - const [expandedOperations, setExpandedOperations] = useState( - Object.keys(tabsWithOperations) - ); - - - if (parsingError !== '') { - return ( - <> -

Error parsing queries, verify your queries syntax in the tabs:

-

{parsingError}

- - ) - } - - const sendBatchRequest = () => { - const operations: FetcherParams[] = []; - let headers = {}; - for (const selectedOperation of selectedOperations) { - const [tabId, selectedOperationName] = selectedOperation.split('|'); - const tab = tabsWithOperations[tabId] - if (tab) { - const selectedOperationDefinition = tab.operations.find( - (operation, i) => - operation.name?.value === selectedOperationName || - `${tab.id}|${i}` === selectedOperation - ) - if (selectedOperationDefinition) { - headers = {...headers, ...tab.headers}; - operations.push({ - operationName: selectedOperationDefinition.name?.value, - query: print(selectedOperationDefinition), - variables: tab.variables - }) - }; - } - } - - setBatchResponseLoading(true); - - window.fetch(url, { - method: 'POST', - body: JSON.stringify(operations), - headers: { - 'content-type': 'application/json', - ...headers - } - }).then(response => response.json()) - .then(json => { - setBatchResponseLoading(false); - responseEditor?.setValue(JSON.stringify(json, null, 2)) - }) - }; - - return ( -
-
-
-
Batch Request
-
-
- -
-
-
-
-

- A batch GraphQL request requires at least 1 operation. -

-

0 ? 'block' : 'none' - }}> - You have selected {selectedOperations.length === 1 ? `${selectedOperations.length} operation.` : `${selectedOperations.length} operations.`} -

-
- , - expandOpen: , - parentClose: null, - parentOpen: null, - leaf: null - }} - nodes={nodes} - checked={selectedOperations} - expanded={expandedOperations} - onCheck={checked => { - setSelectedOperations(checked); - setExecuteButtonDisabled(checked.length === 0); - }} - onExpand={setExpandedOperations} - expandOnClick - /> - { batchResponseLoading ? : null } -
-
- ); -} +import { GraphiQLBatchRequestProps } from 'graphiql-batch-request'; +import { BatchRequestPlugin } from './batch-request-plugin'; +import { useMemo, useRef } from 'react'; export function useBatchRequestPlugin(props: GraphiQLBatchRequestProps) { const propsRef = useRef(props); diff --git a/packages/graphiql-plugin-batch-request/src/test-plugin.tsx b/packages/graphiql-plugin-batch-request/src/test-plugin.tsx index 90e60290adc..cc5b7e8eb67 100644 --- a/packages/graphiql-plugin-batch-request/src/test-plugin.tsx +++ b/packages/graphiql-plugin-batch-request/src/test-plugin.tsx @@ -5,7 +5,8 @@ import * as React from 'react'; import { useBatchRequestPlugin } from './index'; // const url = 'https://swapi-graphql.netlify.app/.netlify/functions/index' -const url = 'https://api.spacex.land/graphql' +// const url = 'https://api.spacex.land/graphql' +const url = 'https://countries.trevorblades.com/graphql'; const fetcher = createGraphiQLFetcher({ url }); const App = () => { diff --git a/yarn.lock b/yarn.lock index 43c77f62133..112af345511 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2309,7 +2309,7 @@ "@fortawesome/fontawesome-free@6.2.1": version "6.2.1" - resolved "https://artylab.expedia.biz/api/npm/public-npm-virtual/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.1.tgz#344baf6ff9eaad7a73cff067d8c56bfc11ae5304" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.1.tgz#344baf6ff9eaad7a73cff067d8c56bfc11ae5304" integrity sha512-viouXhegu/TjkvYQoiRZK3aax69dGXxgEjpvZW81wIJdxm5Fnvp3VVIP4VHKqX4SvFw6qpmkILkD4RJWAdrt7A== "@graphql-tools/batch-execute@8.5.1": @@ -5779,7 +5779,7 @@ class-utils@^0.3.5: classnames@^2.2.5: version "2.3.2" - resolved "https://artylab.expedia.biz/api/npm/public-npm-virtual/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== clean-css@^4.2.3: @@ -14965,7 +14965,7 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: react-checkbox-tree@1.8.0: version "1.8.0" - resolved "https://artylab.expedia.biz/api/npm/public-npm-virtual/react-checkbox-tree/-/react-checkbox-tree-1.8.0.tgz#10dc3e86df25d92559ab6857c7ada4fe285abc03" + resolved "https://registry.yarnpkg.com/react-checkbox-tree/-/react-checkbox-tree-1.8.0.tgz#10dc3e86df25d92559ab6857c7ada4fe285abc03" integrity sha512-ufC4aorihOvjLpvY1beab2hjVLGZbDTFRzw62foG0+th+KX7e/sdmWu/nD1ZS/U5Yr0rWGwedGH5GOtR0IkUXw== dependencies: classnames "^2.2.5" From fd044cf7293f9b6fb870a864c0f7925af190552a Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Wed, 25 Jan 2023 14:52:30 -0600 Subject: [PATCH 08/16] feat: move test plugin module outside of src --- packages/graphiql-plugin-batch-request/index.html | 2 +- .../graphiql-plugin-batch-request/{src => }/test-plugin.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/graphiql-plugin-batch-request/{src => }/test-plugin.tsx (94%) diff --git a/packages/graphiql-plugin-batch-request/index.html b/packages/graphiql-plugin-batch-request/index.html index 71925ece42f..6ca3d557708 100644 --- a/packages/graphiql-plugin-batch-request/index.html +++ b/packages/graphiql-plugin-batch-request/index.html @@ -17,6 +17,6 @@
- + diff --git a/packages/graphiql-plugin-batch-request/src/test-plugin.tsx b/packages/graphiql-plugin-batch-request/test-plugin.tsx similarity index 94% rename from packages/graphiql-plugin-batch-request/src/test-plugin.tsx rename to packages/graphiql-plugin-batch-request/test-plugin.tsx index cc5b7e8eb67..4560e011ce9 100644 --- a/packages/graphiql-plugin-batch-request/src/test-plugin.tsx +++ b/packages/graphiql-plugin-batch-request/test-plugin.tsx @@ -2,7 +2,7 @@ import { render } from 'react-dom'; import { createGraphiQLFetcher } from '@graphiql/toolkit'; import { GraphiQL } from 'graphiql'; import * as React from 'react'; -import { useBatchRequestPlugin } from './index'; +import { useBatchRequestPlugin } from './src/index'; // const url = 'https://swapi-graphql.netlify.app/.netlify/functions/index' // const url = 'https://api.spacex.land/graphql' From eb9d98ebb3b57b5b56bec9d5bcf685fc5fbc07c7 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Wed, 25 Jan 2023 22:41:19 -0600 Subject: [PATCH 09/16] chore: update README.md --- .../graphiql-plugin-batch-request/README.md | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/graphiql-plugin-batch-request/README.md b/packages/graphiql-plugin-batch-request/README.md index ee5b78fc9c7..fb98c0a5848 100644 --- a/packages/graphiql-plugin-batch-request/README.md +++ b/packages/graphiql-plugin-batch-request/README.md @@ -1 +1,56 @@ -# GraphiQL Batch Request Plugin \ No newline at end of file +# GraphiQL Batch Request Plugin + +This package provides a plugin that allows sending a batch request to a GraphQL Server into the GraphiQI UI. + +## Install + +Use your favoriton package manager to install the package: + +```sh +npm i -S @graphiql/plugin-batch-request +``` + +The following packages are peer dependencies, so make sure you have them installed as well: + +```sh +npm i -S react react-dom graphql +``` + +## Usage + +The plugin scope is for sending multiple GraphQL operations as an array, so GraphQL server requires to be configured to allow +arrays. + +```jsx +import { useBatchRequestPlugin } from '@graphiql/plugin-batch-request'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQL } from 'graphiql'; +import { useState } from 'react'; + +import 'graphiql/graphiql.css'; +import '@graphiql/plugin-batch-request/dist/style.css'; + +const url = 'https://countries.trevorblades.com/graphql'; + +const fetcher = createGraphiQLFetcher({ + url +}); + +function GraphiQLWithExplorer() { + const [query, setQuery] = useState(DEFAULT_QUERY); + const batchRequestPlugin = useBatchRequestPlugin({ url }); + return ( + + ); +} +``` + + +### Example + +![sending a batch request to spacex GraphQL server](https://user-images.githubusercontent.com/6611331/212411159-336abe77-5f0a-4453-9de3-62abe039168f.mov) \ No newline at end of file From 16653b2027da084cacb1b0afc1b9b335bbb232de Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Wed, 25 Jan 2023 22:46:16 -0600 Subject: [PATCH 10/16] chore: update README.md --- packages/graphiql-plugin-batch-request/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/graphiql-plugin-batch-request/README.md b/packages/graphiql-plugin-batch-request/README.md index fb98c0a5848..90c0f86db88 100644 --- a/packages/graphiql-plugin-batch-request/README.md +++ b/packages/graphiql-plugin-batch-request/README.md @@ -53,4 +53,6 @@ function GraphiQLWithExplorer() { ### Example -![sending a batch request to spacex GraphQL server](https://user-images.githubusercontent.com/6611331/212411159-336abe77-5f0a-4453-9de3-62abe039168f.mov) \ No newline at end of file +Sending a batch request to spacex GraphQL server: + +https://user-images.githubusercontent.com/6611331/212411159-336abe77-5f0a-4453-9de3-62abe039168f.mov \ No newline at end of file From 4e224c77bce93c334b480e2678acc47c9a2fc7d2 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Mon, 30 Jan 2023 12:45:55 -0600 Subject: [PATCH 11/16] chore: update README.md --- packages/graphiql-plugin-batch-request/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/graphiql-plugin-batch-request/README.md b/packages/graphiql-plugin-batch-request/README.md index 90c0f86db88..264625c6e2b 100644 --- a/packages/graphiql-plugin-batch-request/README.md +++ b/packages/graphiql-plugin-batch-request/README.md @@ -1,6 +1,6 @@ # GraphiQL Batch Request Plugin -This package provides a plugin that allows sending a batch request to a GraphQL Server into the GraphiQI UI. +This package provides a plugin that allows sending a batch request to a GraphQL Server and thence into the GraphiQI UI. ## Install @@ -18,8 +18,7 @@ npm i -S react react-dom graphql ## Usage -The plugin scope is for sending multiple GraphQL operations as an array, so GraphQL server requires to be configured to allow -arrays. +The plugin scope is for sending multiple GraphQL operations as an array, so the GraphQL server must be configured to allow arrays. ```jsx import { useBatchRequestPlugin } from '@graphiql/plugin-batch-request'; From fff1446b82f205249f96b589dd79c3791e12ca28 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 3 Feb 2023 13:29:43 -0600 Subject: [PATCH 12/16] feat: add batching strategy using aliases --- .../graphiql-plugin-batch-request/README.md | 81 ++++- .../jest.config.js | 5 + .../package.json | 1 + .../src/batch-request-plugin.tsx | 69 +++- .../src/filter-document.ts | 66 ++++ .../src/graphiql-batch-request.d.ts | 3 + .../src/index.css | 16 + .../src/merge-requests.ts | 300 ++++++++++++++++++ .../filter-document-by-operation-name.test.ts | 120 +++++++ yarn.lock | 26 ++ 10 files changed, 677 insertions(+), 10 deletions(-) create mode 100644 packages/graphiql-plugin-batch-request/jest.config.js create mode 100644 packages/graphiql-plugin-batch-request/src/filter-document.ts create mode 100644 packages/graphiql-plugin-batch-request/src/merge-requests.ts create mode 100644 packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts diff --git a/packages/graphiql-plugin-batch-request/README.md b/packages/graphiql-plugin-batch-request/README.md index 264625c6e2b..ce44df8ebd7 100644 --- a/packages/graphiql-plugin-batch-request/README.md +++ b/packages/graphiql-plugin-batch-request/README.md @@ -2,9 +2,86 @@ This package provides a plugin that allows sending a batch request to a GraphQL Server and thence into the GraphiQI UI. +The plugin scope is for sending multiple GraphQL using 2 main batching approaches: +1. Single operation. +2. Array of operations (GraphQL server must be configured to allow arrays). + +### Single operation +Combine multiple operations ad execute them as one. + +For example, given the following GraphQL operations: + +```graphql +query Query1($arg: String) { + field1 + field2(input: $arg) +} + +query Query2($arg: String) { + field2(input: $arg) + alias: field3 +} +``` + +These can be merged into one operation: + +```graphql +query ($_0_arg: String, $_1_arg: String) { + _0_field1: field1 + _0_field2: field2(input: $_0_arg) + _1_field2: field3(input: $_1_arg) + _1_alias: field3 +} +``` + +### Array of operations +Combine multiple GraphQL Requests and combine them into one GraphQL Request using an array, having the server recognize the request as an array of operations instead of a single one, and handle each operation separately. + +For example, given the following GraphQL Requests: + +```json +{ + "operationName": "Query1", + "query": "query Query1($arg: String) { ... }", + "variables": { + "arg": "foo" + } +} + +{ + "operationName": "Query2", + "query": "query Query2($arg: String) { ... }", + "variables": { + "arg": "foo" + } +} + +``` + +These can be merged into one GraphQL Array Request: + +```json +[ + { + "operationName": "Query1", + "query": "query Query1($arg: String) { ... }", + "variables": { + "arg": "foo" + } + }, + { + "operationName": "Query2", + "query": "query Query2($arg: String) { ... }", + "variables": { + "arg": "foo" + } + } +] +``` + ## Install -Use your favoriton package manager to install the package: +Use your favorite package manager to install the package: ```sh npm i -S @graphiql/plugin-batch-request @@ -18,8 +95,6 @@ npm i -S react react-dom graphql ## Usage -The plugin scope is for sending multiple GraphQL operations as an array, so the GraphQL server must be configured to allow arrays. - ```jsx import { useBatchRequestPlugin } from '@graphiql/plugin-batch-request'; import { createGraphiQLFetcher } from '@graphiql/toolkit'; diff --git a/packages/graphiql-plugin-batch-request/jest.config.js b/packages/graphiql-plugin-batch-request/jest.config.js new file mode 100644 index 00000000000..d5f2ae82f35 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/jest.config.js @@ -0,0 +1,5 @@ +const base = require('../../jest.config.base')(__dirname); + +module.exports = { + ...base, +}; diff --git a/packages/graphiql-plugin-batch-request/package.json b/packages/graphiql-plugin-batch-request/package.json index f18b809248e..25dd2e060eb 100644 --- a/packages/graphiql-plugin-batch-request/package.json +++ b/packages/graphiql-plugin-batch-request/package.json @@ -30,6 +30,7 @@ "dependencies": { "@graphiql/react": "^0.15.0", "@fortawesome/fontawesome-free": "6.2.1", + "@graphql-tools/utils": "9.1.4", "react-checkbox-tree": "1.8.0" }, "peerDependencies": { diff --git a/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx b/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx index d45872113d7..a1bded8d67d 100644 --- a/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx +++ b/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx @@ -1,15 +1,19 @@ -import { PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react'; -import { FetcherParams } from '@graphiql/toolkit'; -import { GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request'; +import { Button, ButtonGroup, PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react'; +import { FetcherParamsWithDocument, GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request'; import { GraphQLError, parse, print } from 'graphql'; import { Kind } from 'graphql/language'; import { useState } from 'react'; import CheckboxTree from 'react-checkbox-tree'; +import { filterDocumentByOperationName } from './filter-document'; +import { mergeRequests } from './merge-requests'; import "@fortawesome/fontawesome-free/css/all.css"; import 'react-checkbox-tree/lib/react-checkbox-tree.css'; import './graphiql-batch-request.d.ts'; import './index.css'; +import { FetcherParams } from '@graphiql/toolkit'; + +enum BatchStrategy { ALIASES, ARRAY }; export function BatchRequestPlugin({ url, @@ -67,6 +71,7 @@ export function BatchRequestPlugin({ }) ); + const [batchingStrategy, setBatchingStrategy] = useState(BatchStrategy.ARRAY); const [batchResponseLoading, setBatchResponseLoading] = useState(false); const [executeButtonDisabled, setExecuteButtonDisabled] = useState( useAllOperations === false @@ -89,7 +94,7 @@ export function BatchRequestPlugin({ } const sendBatchRequest = () => { - const operations: FetcherParams[] = []; + const operations: FetcherParamsWithDocument[] = []; let headers = {}; for (const selectedOperation of selectedOperations) { const [tabId, selectedOperationName] = selectedOperation.split('|'); @@ -102,20 +107,44 @@ export function BatchRequestPlugin({ ) if (selectedOperationDefinition) { headers = {...headers, ...tab.headers}; + const document = filterDocumentByOperationName(tab.document, selectedOperationDefinition.name?.value); + console.log(`filtered document op: ${selectedOperationDefinition.name?.value}\n`, print(document)); operations.push({ + document, operationName: selectedOperationDefinition.name?.value, - query: print(tab.document), + query: print(document), variables: tab.variables }) }; } } + + let payload: FetcherParams[] | FetcherParams = []; + if (batchingStrategy === BatchStrategy.ARRAY) { + payload = operations.map(({query, operationName, variables}) => ({ operationName, query, variables })); + } else if (batchingStrategy === BatchStrategy.ALIASES) { + const mergedRequests = mergeRequests( + operations.map(({document, operationName, variables}) => ({ + operationName, + document, + variables + })) + ); + + console.log('merged requests:\n', print(mergedRequests.document)); + + payload = { + query: print(mergedRequests.document), + operationName: mergedRequests.operationName, + variables: mergedRequests.variables + } + } setBatchResponseLoading(true); - + window.fetch(url, { method: 'POST', - body: JSON.stringify(operations), + body: JSON.stringify(payload), headers: { 'content-type': 'application/json', ...headers @@ -162,6 +191,32 @@ export function BatchRequestPlugin({ You have selected {selectedOperations.length === 1 ? `${selectedOperations.length} operation.` : `${selectedOperations.length} operations.`}

+
+
+
Batch strategy
+
+ How to batch operations. +
+
+
+ + + + +
+
, diff --git a/packages/graphiql-plugin-batch-request/src/filter-document.ts b/packages/graphiql-plugin-batch-request/src/filter-document.ts new file mode 100644 index 00000000000..46106bb5926 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/filter-document.ts @@ -0,0 +1,66 @@ +import { + DocumentNode, + Kind, + FragmentDefinitionNode, + OperationDefinitionNode, + SelectionSetNode +} from 'graphql'; + +export const filterDocumentByOperationName = ( + document: DocumentNode, + operationName?: string +): DocumentNode => { + let filteredOperation: OperationDefinitionNode | undefined; + const fragments: Record = {}; + + document.definitions.forEach(definition => { + if (definition.kind === Kind.OPERATION_DEFINITION && definition.name?.value === operationName) { + filteredOperation = definition; + } + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + fragments[definition.name.value] = definition; + } + }); + + if (filteredOperation) { + const filteredFragments = filterSelectionSet(filteredOperation.selectionSet, fragments); + return { + kind: Kind.DOCUMENT, + definitions: [...filteredFragments, filteredOperation] + }; + } + + return { + kind: Kind.DOCUMENT, + definitions: [] + }; +} + +export const filterSelectionSet = ( + selectionSet: SelectionSetNode | undefined, + fragments: Record +): FragmentDefinitionNode[] => { + + if (!selectionSet) { + return []; + } + + const filteredFragments: FragmentDefinitionNode[] = []; + + selectionSet.selections.forEach(selection => { + if(selection.kind === Kind.FRAGMENT_SPREAD && fragments[selection.name.value]) { + const fragment = fragments[selection.name.value]; + filteredFragments.push( + fragment, + ...filterSelectionSet(fragment.selectionSet, fragments) + ); + } + else if (selection.kind === Kind.FIELD || selection.kind === Kind.INLINE_FRAGMENT) { + filteredFragments.push( + ...filterSelectionSet(selection.selectionSet, fragments) + ); + } + }); + + return filteredFragments; +} \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts index b078818b71e..c8b59805aa1 100644 --- a/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts +++ b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts @@ -1,6 +1,7 @@ declare module 'graphiql-batch-request' { import { ComponentType } from 'react'; import { DocumentNode, OperationDefinitionNode } from 'graphql/language'; + import { FetcherParams } from '@graphiql/toolkit'; type TabWithOperations = { id: string, @@ -12,6 +13,8 @@ declare module 'graphiql-batch-request' { export type TabsWithOperations = Record; + export type FetcherParamsWithDocument = FetcherParams & { document: DocumentNode, operationName?: string }; + export type GraphiQLBatchRequestProps = { url: string, useAllOperations?: boolean diff --git a/packages/graphiql-plugin-batch-request/src/index.css b/packages/graphiql-plugin-batch-request/src/index.css index 67302949b49..2968db93552 100644 --- a/packages/graphiql-plugin-batch-request/src/index.css +++ b/packages/graphiql-plugin-batch-request/src/index.css @@ -25,6 +25,22 @@ top: 0; } +.graphiql-batch-request-strategy-section { + align-items: center; + display: flex; + justify-content: space-between; + padding: var(--px-2); +} + +.graphiql-batch-request-strategy-title { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} + +.graphiql-batch-request-strategy-caption { + color: hsla(var(--color-neutral),var(--alpha-secondary)); +} + .graphiql-batch-request-content { margin: var(--px-16) 0 0; } diff --git a/packages/graphiql-plugin-batch-request/src/merge-requests.ts b/packages/graphiql-plugin-batch-request/src/merge-requests.ts new file mode 100644 index 00000000000..e365631e3f1 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/src/merge-requests.ts @@ -0,0 +1,300 @@ +// copied from https://github.com/ardatan/graphql-tools/blob/master/packages/batch-execute/src/prefix.ts + +import { + visit, + Kind, + OperationDefinitionNode, + DocumentNode, + FragmentDefinitionNode, + VariableDefinitionNode, + SelectionNode, + FragmentSpreadNode, + VariableNode, + InlineFragmentNode, + FieldNode, +} from 'graphql'; +import { ExecutionRequest, getOperationASTFromRequest } from '@graphql-tools/utils'; + +/** + * Merge multiple queries into a single query in such a way that query results + * can be split and transformed as if they were obtained by running original queries. + * + * Merging algorithm involves several transformations: + * 1. Replace top-level fragment spreads with inline fragments (... on Query {}) + * 2. Add unique aliases to all top-level query fields (including those on inline fragments) + * 3. Prefix all variable definitions and variable usages + * 4. Prefix names (and spreads) of fragments + * + * i.e transform: + * [ + * `query Foo($id: ID!) { foo, bar(id: $id), ...FooQuery } + * fragment FooQuery on Query { baz }`, + * + * `query Bar($id: ID!) { foo: baz, bar(id: $id), ... on Query { baz } }` + * ] + * to: + * query ( + * $graphqlTools1_id: ID! + * $graphqlTools2_id: ID! + * ) { + * graphqlTools1_foo: foo, + * graphqlTools1_bar: bar(id: $graphqlTools1_id) + * ... on Query { + * graphqlTools1__baz: baz + * } + * graphqlTools1__foo: baz + * graphqlTools1__bar: bar(id: $graphqlTools1__id) + * ... on Query { + * graphqlTools1__baz: baz + * } + * } + */ +export function mergeRequests( + requests: Array +): ExecutionRequest { + const mergedVariables: Record = Object.create(null); + const mergedVariableDefinitions: Array = []; + const mergedSelections: Array = []; + const mergedFragmentDefinitions: Array = []; + let mergedExtensions: Record = Object.create(null); + + for (const index in requests) { + const request = requests[index]; + const prefixedRequest = prefixRequest(`_${index}_`, request); + + for (const def of prefixedRequest.document.definitions) { + if (def.kind === Kind.OPERATION_DEFINITION) { + mergedSelections.push(...def.selectionSet.selections); + if (def.variableDefinitions) { + mergedVariableDefinitions.push(...def.variableDefinitions); + } + } + if (def.kind === Kind.FRAGMENT_DEFINITION) { + mergedFragmentDefinitions.push(def); + } + } + Object.assign(mergedVariables, prefixedRequest.variables); + mergedExtensions = { + ...mergedExtensions, + ...request.extensions + }; + } + + const firstRequest = requests[0]; + const operationType = firstRequest.operationType ?? getOperationASTFromRequest(firstRequest).operation; + const mergedOperationDefinition: OperationDefinitionNode = { + kind: Kind.OPERATION_DEFINITION, + operation: operationType, + variableDefinitions: mergedVariableDefinitions, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: mergedSelections, + }, + }; + const operationName = firstRequest.operationName ?? firstRequest.info?.operation?.name?.value; + if (operationName) { + (mergedOperationDefinition as any).name = { + kind: Kind.NAME, + value: operationName, + }; + } + + return { + document: { + kind: Kind.DOCUMENT, + definitions: [mergedOperationDefinition, ...mergedFragmentDefinitions], + }, + variables: mergedVariables, + extensions: mergedExtensions, + context: requests[0].context, + info: requests[0].info, + operationType, + }; +} + +function prefixRequest(prefix: string, request: ExecutionRequest): ExecutionRequest { + const executionVariables = request.variables ?? {}; + + const prefixNode = (node: VariableNode | FragmentDefinitionNode | FragmentSpreadNode) => prefixNodeName(node, prefix); + + let prefixedDocument = aliasTopLevelFields(prefix, request.document); + + const executionVariableNames = Object.keys(executionVariables); + const hasFragmentDefinitions = request.document.definitions.some(def => def.kind === Kind.FRAGMENT_DEFINITION); + const fragmentSpreadImpl: Record = {}; + + if (executionVariableNames.length > 0 || hasFragmentDefinitions) { + prefixedDocument = visit(prefixedDocument, { + [Kind.VARIABLE]: prefixNode, + [Kind.FRAGMENT_DEFINITION]: prefixNode, + [Kind.FRAGMENT_SPREAD]: node => { + node = prefixNodeName(node, prefix); + fragmentSpreadImpl[node.name.value] = true; + return node; + }, + }) as DocumentNode; + } + + const prefixedVariables: Record = {}; + + for (const variableName of executionVariableNames) { + prefixedVariables[prefix + variableName] = executionVariables[variableName]; + } + + if (hasFragmentDefinitions) { + prefixedDocument = { + ...prefixedDocument, + definitions: prefixedDocument.definitions.filter(def => { + return !(def.kind === Kind.FRAGMENT_DEFINITION) || fragmentSpreadImpl[def.name.value]; + }), + }; + } + + return { + document: prefixedDocument, + variables: prefixedVariables + }; +} + +/** + * Adds prefixed aliases to top-level fields of the query. + * + * @see aliasFieldsInSelection for implementation details + */ +function aliasTopLevelFields(prefix: string, document: DocumentNode): DocumentNode { + const transformer = { + [Kind.OPERATION_DEFINITION]: (def: OperationDefinitionNode) => { + const { selections } = def.selectionSet; + return { + ...def, + selectionSet: { + ...def.selectionSet, + selections: aliasFieldsInSelection(prefix, selections, document), + }, + }; + }, + }; + return visit(document, transformer, { + [Kind.DOCUMENT]: [`definitions`], + } as any); +} + +/** + * Add aliases to fields of the selection, including top-level fields of inline fragments. + * Fragment spreads are converted to inline fragments and their top-level fields are also aliased. + * + * Note that this method is shallow. It adds aliases only to the top-level fields and doesn't + * descend to field sub-selections. + * + * For example, transforms: + * { + * foo + * ... on Query { foo } + * ...FragmentWithBarField + * } + * To: + * { + * graphqlTools1_foo: foo + * ... on Query { graphqlTools1_foo: foo } + * ... on Query { graphqlTools1_bar: bar } + * } + */ +function aliasFieldsInSelection( + prefix: string, + selections: ReadonlyArray, + document: DocumentNode +): Array { + return selections.map(selection => { + switch (selection.kind) { + case Kind.INLINE_FRAGMENT: + return aliasFieldsInInlineFragment(prefix, selection, document); + case Kind.FRAGMENT_SPREAD: { + const inlineFragment = inlineFragmentSpread(selection, document); + return aliasFieldsInInlineFragment(prefix, inlineFragment, document); + } + case Kind.FIELD: + default: + return aliasField(selection, prefix); + } + }); +} + +/** + * Add aliases to top-level fields of the inline fragment. + * Returns new inline fragment node. + * + * For Example, transforms: + * ... on Query { foo, ... on Query { bar: foo } } + * To + * ... on Query { graphqlTools1_foo: foo, ... on Query { graphqlTools1_bar: foo } } + */ +function aliasFieldsInInlineFragment( + prefix: string, + fragment: InlineFragmentNode, + document: DocumentNode +): InlineFragmentNode { + return { + ...fragment, + selectionSet: { + ...fragment.selectionSet, + selections: aliasFieldsInSelection(prefix, fragment.selectionSet.selections, document), + }, + }; +} + +/** + * Replaces fragment spread with inline fragment + * + * Example: + * query { ...Spread } + * fragment Spread on Query { bar } + * + * Transforms to: + * query { ... on Query { bar } } + */ +function inlineFragmentSpread(spread: FragmentSpreadNode, document: DocumentNode): InlineFragmentNode { + const fragment = document.definitions.find( + def => def.kind === Kind.FRAGMENT_DEFINITION && def.name.value === spread.name.value + ) as FragmentDefinitionNode; + if (!fragment) { + throw new Error(`Fragment ${spread.name.value} does not exist`); + } + const { typeCondition, selectionSet } = fragment; + return { + kind: Kind.INLINE_FRAGMENT, + typeCondition, + selectionSet, + directives: spread.directives, + }; +} + +function prefixNodeName( + node: T, + prefix: string +): T { + return { + ...node, + name: { + ...node.name, + value: `${prefix}${node.name.value}`, + }, + }; +} + +/** + * Returns a new FieldNode with prefixed alias + * + * Example. Given prefix === "graphqlTools1_" transforms: + * { foo } -> { graphqlTools1_foo: foo } + * { foo: bar } -> { graphqlTools1_foo: bar } + */ +function aliasField(field: FieldNode, aliasPrefix: string): FieldNode { + const aliasNode = field.alias ?? field.name; + return { + ...field, + alias: { + ...aliasNode, + value: `${aliasPrefix}${aliasNode.value}`, + }, + }; +} \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts new file mode 100644 index 00000000000..abdd91afb67 --- /dev/null +++ b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts @@ -0,0 +1,120 @@ +import { filterDocumentByOperationName } from '../src/filter-document'; +import { FragmentDefinitionNode, Kind, OperationDefinitionNode, parse } from 'graphql'; + +describe('filterDocumentByOperationName', () => { + it('should filter document with only 1 operation definition', () => { + const document = parse(` + fragment ItemFragment on Item { id } + query GetItem($id: ID!) { + item(id: $id) { ...ItemFragment } + } + `); + + const filteredDocument = filterDocumentByOperationName(document, 'GetItem'); + expect(filteredDocument.definitions.length).toEqual(2); + + const operationDefinition = filteredDocument.definitions.find(definition => + definition.kind === Kind.OPERATION_DEFINITION + ) as OperationDefinitionNode | undefined; + expect(operationDefinition?.name?.value).toEqual('GetItem'); + + const fragmentDefinition = filteredDocument.definitions.find(definition => + definition.kind === Kind.FRAGMENT_DEFINITION + ) as FragmentDefinitionNode | undefined; + expect(fragmentDefinition?.name?.value).toEqual('ItemFragment'); + }); + + it('should filter document with multiple operation definitions', () => { + const document = parse(` + fragment ItemFragment on Item { id } + fragment UserFragment on User { id } + fragment ReviewsFragment on Review { + id + user { ...UserFragment } + } + query GetItem($id: ID!) { + item(id: $id) { ...ItemFragment } + } + query GetItemsAndReviews { + reviews { ...ReviewsFragment } + items { ...ItemFragment } + } + `); + + const filteredDocument = filterDocumentByOperationName(document, 'GetItemsAndReviews'); + expect(filteredDocument.definitions.length).toEqual(4); + + const operationDefinition = filteredDocument.definitions.find(definition => + definition.kind === Kind.OPERATION_DEFINITION + ) as OperationDefinitionNode | undefined; + expect(operationDefinition?.name?.value).toEqual('GetItemsAndReviews'); + + const fragmentDefinitions = filteredDocument.definitions.filter(definition => + definition.kind === Kind.FRAGMENT_DEFINITION + ) as FragmentDefinitionNode[] | undefined; + expect( + fragmentDefinitions?.map(def => def.name.value) + ).toEqual( + ['ReviewsFragment', 'UserFragment', 'ItemFragment'] + ); + }); + + it('should filter document with multiple operations and 1 anonymous operation', () => { + const document = parse(` + fragment ItemFragment on Item { id } + fragment UserFragment on User { id } + fragment ReviewsFragment on Review { + id + user { ...UserFragment } + } + query GetItem($id: ID!) { + item(id: $id) { ...ItemFragment } + } + query GetItemsAndReviews { + reviews { ...ReviewsFragment } + items { ...ItemFragment } + } + { + reviews(ids: [1,2,3]) { ...ReviewsFragment } + } + `); + + const filteredDocument = filterDocumentByOperationName(document); + expect(filteredDocument.definitions.length).toEqual(3); + + const operationDefinition = filteredDocument.definitions.find(definition => + definition.kind === Kind.OPERATION_DEFINITION + ) as OperationDefinitionNode | undefined; + expect(operationDefinition?.name?.value).toBeUndefined(); + + const fragmentDefinitions = filteredDocument.definitions.filter(definition => + definition.kind === Kind.FRAGMENT_DEFINITION + ) as FragmentDefinitionNode[] | undefined; + expect( + fragmentDefinitions?.map(def => def.name.value) + ).toEqual( + ['ReviewsFragment', 'UserFragment'] + ); + }); + + it.only('should not filter document when no operation defitinion matches provided operation name', () => { + const document = parse(` + fragment ItemFragment on Item { id } + fragment UserFragment on User { id } + fragment ReviewsFragment on Review { + id + user { ...UserFragment } + } + query GetItem($id: ID!) { + item(id: $id) { ...ItemFragment } + } + query GetItemsAndReviews { + reviews { ...ReviewsFragment } + items { ...ItemFragment } + } + `); + + const filteredDocument = filterDocumentByOperationName(document, 'MyAwesomeQuery'); + expect(filteredDocument.definitions.length).toEqual(0); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 013ffd3a508..36c23aa2a8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2312,6 +2312,25 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.1.tgz#344baf6ff9eaad7a73cff067d8c56bfc11ae5304" integrity sha512-viouXhegu/TjkvYQoiRZK3aax69dGXxgEjpvZW81wIJdxm5Fnvp3VVIP4VHKqX4SvFw6qpmkILkD4RJWAdrt7A== +"@graphiql/react@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.15.0.tgz#b10fc40c5a85e7b5b845907d57bfd8be92753037" + integrity sha512-kJqkdf6d4Cck05Wt5yCDZXWfs7HZgcpuoWq/v8nOa698qVaNMM3qdG4CpRsZEexku0DSSJzWWuanxd5x+sRcFg== + dependencies: + "@graphiql/toolkit" "^0.8.0" + "@reach/combobox" "^0.17.0" + "@reach/dialog" "^0.17.0" + "@reach/listbox" "^0.17.0" + "@reach/menu-button" "^0.17.0" + "@reach/tooltip" "^0.17.0" + "@reach/visually-hidden" "^0.17.0" + codemirror "^5.65.3" + codemirror-graphql "^2.0.2" + copy-to-clipboard "^3.2.0" + graphql-language-service "^5.1.0" + markdown-it "^12.2.0" + set-value "^4.1.0" + "@graphql-tools/batch-execute@8.5.1": version "8.5.1" resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.5.1.tgz#fa3321d58c64041650be44250b1ebc3aab0ba7a9" @@ -2480,6 +2499,13 @@ dependencies: tslib "^2.4.0" +"@graphql-tools/utils@9.1.4": + version "9.1.4" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.1.4.tgz#2c9e0aefc9655dd73247667befe3c850ec014f3f" + integrity sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg== + dependencies: + tslib "^2.4.0" + "@graphql-tools/wrap@8.5.1": version "8.5.1" resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-8.5.1.tgz#d4bd1f89850bb1ce0209f35f66d002080b439495" From 379f95dd02e8b52dfd08835165791065033e217f Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 3 Feb 2023 18:29:24 -0600 Subject: [PATCH 13/16] feat: unique fragments --- .../src/filter-document.ts | 23 +++++++++++-------- .../filter-document-by-operation-name.test.ts | 17 +++++++++++++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/graphiql-plugin-batch-request/src/filter-document.ts b/packages/graphiql-plugin-batch-request/src/filter-document.ts index 46106bb5926..5747eec3450 100644 --- a/packages/graphiql-plugin-batch-request/src/filter-document.ts +++ b/packages/graphiql-plugin-batch-request/src/filter-document.ts @@ -26,7 +26,10 @@ export const filterDocumentByOperationName = ( const filteredFragments = filterSelectionSet(filteredOperation.selectionSet, fragments); return { kind: Kind.DOCUMENT, - definitions: [...filteredFragments, filteredOperation] + definitions: [ + ...Object.values(filteredFragments), + filteredOperation + ] }; } @@ -39,26 +42,28 @@ export const filterDocumentByOperationName = ( export const filterSelectionSet = ( selectionSet: SelectionSetNode | undefined, fragments: Record -): FragmentDefinitionNode[] => { +): Record => { if (!selectionSet) { - return []; + return {}; } - const filteredFragments: FragmentDefinitionNode[] = []; + let filteredFragments: Record = {}; selectionSet.selections.forEach(selection => { if(selection.kind === Kind.FRAGMENT_SPREAD && fragments[selection.name.value]) { const fragment = fragments[selection.name.value]; - filteredFragments.push( - fragment, + filteredFragments = { + ...filteredFragments, + [selection.name.value]: fragment, ...filterSelectionSet(fragment.selectionSet, fragments) - ); + }; } else if (selection.kind === Kind.FIELD || selection.kind === Kind.INLINE_FRAGMENT) { - filteredFragments.push( + filteredFragments = { + ...filteredFragments, ...filterSelectionSet(selection.selectionSet, fragments) - ); + }; } }); diff --git a/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts index abdd91afb67..193e475f08d 100644 --- a/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts +++ b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts @@ -97,7 +97,7 @@ describe('filterDocumentByOperationName', () => { ); }); - it.only('should not filter document when no operation defitinion matches provided operation name', () => { + it('should not filter document when no operation defitinion matches provided operation name', () => { const document = parse(` fragment ItemFragment on Item { id } fragment UserFragment on User { id } @@ -117,4 +117,19 @@ describe('filterDocumentByOperationName', () => { const filteredDocument = filterDocumentByOperationName(document, 'MyAwesomeQuery'); expect(filteredDocument.definitions.length).toEqual(0); }); + + it('should not filter document without repeated fagment definitions', () => { + const document = parse(` + fragment ItemFragment on Item { id } + query GetItem { + item_1: item(id: 1) { ...ItemFragment } + item_2: item(id: 2) { ...ItemFragment } + item_3: item(id: 3) { ...ItemFragment } + item_4: item(id: 4) { ...ItemFragment } + } + `); + + const filteredDocument = filterDocumentByOperationName(document, 'GetItem'); + expect(filteredDocument.definitions.length).toEqual(2); + }); }); \ No newline at end of file From 009370fa4c4c88462761248c5b5a2a16378875b7 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 3 Feb 2023 18:53:55 -0600 Subject: [PATCH 14/16] chore: update README demo --- packages/graphiql-plugin-batch-request/README.md | 6 +++--- packages/graphiql-plugin-batch-request/test-plugin.tsx | 5 +++-- .../test/filter-document-by-operation-name.test.ts | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/graphiql-plugin-batch-request/README.md b/packages/graphiql-plugin-batch-request/README.md index ce44df8ebd7..f86dc41b14e 100644 --- a/packages/graphiql-plugin-batch-request/README.md +++ b/packages/graphiql-plugin-batch-request/README.md @@ -2,7 +2,7 @@ This package provides a plugin that allows sending a batch request to a GraphQL Server and thence into the GraphiQI UI. -The plugin scope is for sending multiple GraphQL using 2 main batching approaches: +The plugin scope is for sending multiple GraphQL using 2 main batching strategy: 1. Single operation. 2. Array of operations (GraphQL server must be configured to allow arrays). @@ -127,6 +127,6 @@ function GraphiQLWithExplorer() { ### Example -Sending a batch request to spacex GraphQL server: +Sending a batch request to the countries GraphQL server: -https://user-images.githubusercontent.com/6611331/212411159-336abe77-5f0a-4453-9de3-62abe039168f.mov \ No newline at end of file +https://user-images.githubusercontent.com/6611331/216736177-2d8d6153-b246-48ef-8e97-687beea6f9fc.mov \ No newline at end of file diff --git a/packages/graphiql-plugin-batch-request/test-plugin.tsx b/packages/graphiql-plugin-batch-request/test-plugin.tsx index 4560e011ce9..2c0c50f3901 100644 --- a/packages/graphiql-plugin-batch-request/test-plugin.tsx +++ b/packages/graphiql-plugin-batch-request/test-plugin.tsx @@ -4,9 +4,10 @@ import { GraphiQL } from 'graphiql'; import * as React from 'react'; import { useBatchRequestPlugin } from './src/index'; -// const url = 'https://swapi-graphql.netlify.app/.netlify/functions/index' -// const url = 'https://api.spacex.land/graphql' +// const url = 'https://swapi-graphql.netlify.app/.netlify/functions/index'; +// const url = 'https://api.spacex.land/graphql'; const url = 'https://countries.trevorblades.com/graphql'; + const fetcher = createGraphiQLFetcher({ url }); const App = () => { diff --git a/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts index 193e475f08d..6c59da11fcc 100644 --- a/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts +++ b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts @@ -132,4 +132,4 @@ describe('filterDocumentByOperationName', () => { const filteredDocument = filterDocumentByOperationName(document, 'GetItem'); expect(filteredDocument.definitions.length).toEqual(2); }); -}); \ No newline at end of file +}); From dbb1fdb1984e12838b38b18af2bcdf83f03f0d80 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 3 Feb 2023 22:55:53 -0600 Subject: [PATCH 15/16] feat: improve code readability --- .../src/filter-document.ts | 85 +++++++++---------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/packages/graphiql-plugin-batch-request/src/filter-document.ts b/packages/graphiql-plugin-batch-request/src/filter-document.ts index 5747eec3450..be8a950bf6d 100644 --- a/packages/graphiql-plugin-batch-request/src/filter-document.ts +++ b/packages/graphiql-plugin-batch-request/src/filter-document.ts @@ -1,8 +1,5 @@ -import { - DocumentNode, - Kind, - FragmentDefinitionNode, - OperationDefinitionNode, +import { + DocumentNode, FragmentDefinitionNode, Kind, OperationDefinitionNode, SelectionSetNode } from 'graphql'; @@ -13,21 +10,54 @@ export const filterDocumentByOperationName = ( let filteredOperation: OperationDefinitionNode | undefined; const fragments: Record = {}; - document.definitions.forEach(definition => { - if (definition.kind === Kind.OPERATION_DEFINITION && definition.name?.value === operationName) { + for (const definition of document.definitions) { + if ( + definition.kind === Kind.OPERATION_DEFINITION && + definition.name?.value === operationName + ) { filteredOperation = definition; - } - if (definition.kind === Kind.FRAGMENT_DEFINITION) { + } else if (definition.kind === Kind.FRAGMENT_DEFINITION) { fragments[definition.name.value] = definition; } - }); + } + + const getFragmentDefinitions = ( + selectionSet: SelectionSetNode | undefined + ): Record => { + + if (!selectionSet) { + return {}; + } + + let filteredFragments: Record = {}; + + for(const selection of selectionSet.selections) { + if(selection.kind === Kind.FRAGMENT_SPREAD) { + const fragment = fragments[selection.name.value]; + filteredFragments = { + ...filteredFragments, + [selection.name.value]: fragment, + ...getFragmentDefinitions(fragment.selectionSet) + }; + } else { + // technically at this point the only SelectionNode types we are looking for are + // FieldNode (Kind.FIELD) and InlineFragmentNode (Kind.INLINE_FRAGMENT) + // but leting validation handle that. + filteredFragments = { + ...filteredFragments, + ...getFragmentDefinitions(selection.selectionSet) + }; + } + } + + return filteredFragments; + } if (filteredOperation) { - const filteredFragments = filterSelectionSet(filteredOperation.selectionSet, fragments); return { kind: Kind.DOCUMENT, definitions: [ - ...Object.values(filteredFragments), + ...Object.values(getFragmentDefinitions(filteredOperation.selectionSet)), filteredOperation ] }; @@ -37,35 +67,4 @@ export const filterDocumentByOperationName = ( kind: Kind.DOCUMENT, definitions: [] }; -} - -export const filterSelectionSet = ( - selectionSet: SelectionSetNode | undefined, - fragments: Record -): Record => { - - if (!selectionSet) { - return {}; - } - - let filteredFragments: Record = {}; - - selectionSet.selections.forEach(selection => { - if(selection.kind === Kind.FRAGMENT_SPREAD && fragments[selection.name.value]) { - const fragment = fragments[selection.name.value]; - filteredFragments = { - ...filteredFragments, - [selection.name.value]: fragment, - ...filterSelectionSet(fragment.selectionSet, fragments) - }; - } - else if (selection.kind === Kind.FIELD || selection.kind === Kind.INLINE_FRAGMENT) { - filteredFragments = { - ...filteredFragments, - ...filterSelectionSet(selection.selectionSet, fragments) - }; - } - }); - - return filteredFragments; } \ No newline at end of file From a3ed4f225505571839949f88d0a7552066c9fbc5 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 3 Feb 2023 23:03:08 -0600 Subject: [PATCH 16/16] feat: improve code readability --- .../src/filter-document.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/graphiql-plugin-batch-request/src/filter-document.ts b/packages/graphiql-plugin-batch-request/src/filter-document.ts index be8a950bf6d..6b180e36158 100644 --- a/packages/graphiql-plugin-batch-request/src/filter-document.ts +++ b/packages/graphiql-plugin-batch-request/src/filter-document.ts @@ -21,7 +21,7 @@ export const filterDocumentByOperationName = ( } } - const getFragmentDefinitions = ( + const getSelectedFragments = ( selectionSet: SelectionSetNode | undefined ): Record => { @@ -29,35 +29,35 @@ export const filterDocumentByOperationName = ( return {}; } - let filteredFragments: Record = {}; + let selectedFragments: Record = {}; for(const selection of selectionSet.selections) { if(selection.kind === Kind.FRAGMENT_SPREAD) { - const fragment = fragments[selection.name.value]; - filteredFragments = { - ...filteredFragments, - [selection.name.value]: fragment, - ...getFragmentDefinitions(fragment.selectionSet) + const selectedFragment = fragments[selection.name.value]; + selectedFragments = { + ...selectedFragments, + [selection.name.value]: selectedFragment, + ...getSelectedFragments(selectedFragment.selectionSet) }; } else { // technically at this point the only SelectionNode types we are looking for are // FieldNode (Kind.FIELD) and InlineFragmentNode (Kind.INLINE_FRAGMENT) // but leting validation handle that. - filteredFragments = { - ...filteredFragments, - ...getFragmentDefinitions(selection.selectionSet) + selectedFragments = { + ...selectedFragments, + ...getSelectedFragments(selection.selectionSet) }; } } - return filteredFragments; + return selectedFragments; } if (filteredOperation) { return { kind: Kind.DOCUMENT, definitions: [ - ...Object.values(getFragmentDefinitions(filteredOperation.selectionSet)), + ...Object.values(getSelectedFragments(filteredOperation.selectionSet)), filteredOperation ] };