Skip to content

Commit fff1446

Browse files
author
Samuel Vazquez
committed
feat: add batching strategy using aliases
1 parent 4e224c7 commit fff1446

File tree

10 files changed

+677
-10
lines changed

10 files changed

+677
-10
lines changed

packages/graphiql-plugin-batch-request/README.md

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,86 @@
22

33
This package provides a plugin that allows sending a batch request to a GraphQL Server and thence into the GraphiQI UI.
44

5+
The plugin scope is for sending multiple GraphQL using 2 main batching approaches:
6+
1. Single operation.
7+
2. Array of operations (GraphQL server must be configured to allow arrays).
8+
9+
### Single operation
10+
Combine multiple operations ad execute them as one.
11+
12+
For example, given the following GraphQL operations:
13+
14+
```graphql
15+
query Query1($arg: String) {
16+
field1
17+
field2(input: $arg)
18+
}
19+
20+
query Query2($arg: String) {
21+
field2(input: $arg)
22+
alias: field3
23+
}
24+
```
25+
26+
These can be merged into one operation:
27+
28+
```graphql
29+
query ($_0_arg: String, $_1_arg: String) {
30+
_0_field1: field1
31+
_0_field2: field2(input: $_0_arg)
32+
_1_field2: field3(input: $_1_arg)
33+
_1_alias: field3
34+
}
35+
```
36+
37+
### Array of operations
38+
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.
39+
40+
For example, given the following GraphQL Requests:
41+
42+
```json
43+
{
44+
"operationName": "Query1",
45+
"query": "query Query1($arg: String) { ... }",
46+
"variables": {
47+
"arg": "foo"
48+
}
49+
}
50+
51+
{
52+
"operationName": "Query2",
53+
"query": "query Query2($arg: String) { ... }",
54+
"variables": {
55+
"arg": "foo"
56+
}
57+
}
58+
59+
```
60+
61+
These can be merged into one GraphQL Array Request:
62+
63+
```json
64+
[
65+
{
66+
"operationName": "Query1",
67+
"query": "query Query1($arg: String) { ... }",
68+
"variables": {
69+
"arg": "foo"
70+
}
71+
},
72+
{
73+
"operationName": "Query2",
74+
"query": "query Query2($arg: String) { ... }",
75+
"variables": {
76+
"arg": "foo"
77+
}
78+
}
79+
]
80+
```
81+
582
## Install
683

7-
Use your favoriton package manager to install the package:
84+
Use your favorite package manager to install the package:
885

986
```sh
1087
npm i -S @graphiql/plugin-batch-request
@@ -18,8 +95,6 @@ npm i -S react react-dom graphql
1895

1996
## Usage
2097

21-
The plugin scope is for sending multiple GraphQL operations as an array, so the GraphQL server must be configured to allow arrays.
22-
2398
```jsx
2499
import { useBatchRequestPlugin } from '@graphiql/plugin-batch-request';
25100
import { createGraphiQLFetcher } from '@graphiql/toolkit';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const base = require('../../jest.config.base')(__dirname);
2+
3+
module.exports = {
4+
...base,
5+
};

packages/graphiql-plugin-batch-request/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"dependencies": {
3131
"@graphiql/react": "^0.15.0",
3232
"@fortawesome/fontawesome-free": "6.2.1",
33+
"@graphql-tools/utils": "9.1.4",
3334
"react-checkbox-tree": "1.8.0"
3435
},
3536
"peerDependencies": {

packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import { PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react';
2-
import { FetcherParams } from '@graphiql/toolkit';
3-
import { GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request';
1+
import { Button, ButtonGroup, PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react';
2+
import { FetcherParamsWithDocument, GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request';
43
import { GraphQLError, parse, print } from 'graphql';
54
import { Kind } from 'graphql/language';
65
import { useState } from 'react';
76
import CheckboxTree from 'react-checkbox-tree';
7+
import { filterDocumentByOperationName } from './filter-document';
8+
import { mergeRequests } from './merge-requests';
89

910
import "@fortawesome/fontawesome-free/css/all.css";
1011
import 'react-checkbox-tree/lib/react-checkbox-tree.css';
1112
import './graphiql-batch-request.d.ts';
1213
import './index.css';
14+
import { FetcherParams } from '@graphiql/toolkit';
15+
16+
enum BatchStrategy { ALIASES, ARRAY };
1317

1418
export function BatchRequestPlugin({
1519
url,
@@ -67,6 +71,7 @@ export function BatchRequestPlugin({
6771
})
6872
);
6973

74+
const [batchingStrategy, setBatchingStrategy] = useState(BatchStrategy.ARRAY);
7075
const [batchResponseLoading, setBatchResponseLoading] = useState(false);
7176
const [executeButtonDisabled, setExecuteButtonDisabled] = useState(
7277
useAllOperations === false
@@ -89,7 +94,7 @@ export function BatchRequestPlugin({
8994
}
9095

9196
const sendBatchRequest = () => {
92-
const operations: FetcherParams[] = [];
97+
const operations: FetcherParamsWithDocument[] = [];
9398
let headers = {};
9499
for (const selectedOperation of selectedOperations) {
95100
const [tabId, selectedOperationName] = selectedOperation.split('|');
@@ -102,20 +107,44 @@ export function BatchRequestPlugin({
102107
)
103108
if (selectedOperationDefinition) {
104109
headers = {...headers, ...tab.headers};
110+
const document = filterDocumentByOperationName(tab.document, selectedOperationDefinition.name?.value);
111+
console.log(`filtered document op: ${selectedOperationDefinition.name?.value}\n`, print(document));
105112
operations.push({
113+
document,
106114
operationName: selectedOperationDefinition.name?.value,
107-
query: print(tab.document),
115+
query: print(document),
108116
variables: tab.variables
109117
})
110118
};
111119
}
112120
}
121+
122+
let payload: FetcherParams[] | FetcherParams = [];
123+
if (batchingStrategy === BatchStrategy.ARRAY) {
124+
payload = operations.map(({query, operationName, variables}) => ({ operationName, query, variables }));
125+
} else if (batchingStrategy === BatchStrategy.ALIASES) {
126+
const mergedRequests = mergeRequests(
127+
operations.map(({document, operationName, variables}) => ({
128+
operationName,
129+
document,
130+
variables
131+
}))
132+
);
133+
134+
console.log('merged requests:\n', print(mergedRequests.document));
135+
136+
payload = {
137+
query: print(mergedRequests.document),
138+
operationName: mergedRequests.operationName,
139+
variables: mergedRequests.variables
140+
}
141+
}
113142

114143
setBatchResponseLoading(true);
115-
144+
116145
window.fetch(url, {
117146
method: 'POST',
118-
body: JSON.stringify(operations),
147+
body: JSON.stringify(payload),
119148
headers: {
120149
'content-type': 'application/json',
121150
...headers
@@ -162,6 +191,32 @@ export function BatchRequestPlugin({
162191
You have selected {selectedOperations.length === 1 ? `${selectedOperations.length} operation.` : `${selectedOperations.length} operations.`}
163192
</p>
164193
</div>
194+
<div className="graphiql-batch-request-strategy-section">
195+
<div>
196+
<div className="graphiql-batch-request-strategy-title">Batch strategy</div>
197+
<div className="graphiql-batch-request-strategy-caption">
198+
How to batch operations.
199+
</div>
200+
</div>
201+
<div>
202+
<ButtonGroup>
203+
<Button
204+
type="button"
205+
className={batchingStrategy === BatchStrategy.ALIASES ? 'active' : ''}
206+
onClick={() => setBatchingStrategy(BatchStrategy.ALIASES)}
207+
>
208+
Aliases
209+
</Button>
210+
<Button
211+
type="button"
212+
className={batchingStrategy === BatchStrategy.ARRAY ? 'active' : ''}
213+
onClick={() => setBatchingStrategy(BatchStrategy.ARRAY)}
214+
>
215+
Array
216+
</Button>
217+
</ButtonGroup>
218+
</div>
219+
</div>
165220
<CheckboxTree
166221
icons={{
167222
expandClose: <i className="fa-solid fa-angle-right" />,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
DocumentNode,
3+
Kind,
4+
FragmentDefinitionNode,
5+
OperationDefinitionNode,
6+
SelectionSetNode
7+
} from 'graphql';
8+
9+
export const filterDocumentByOperationName = (
10+
document: DocumentNode,
11+
operationName?: string
12+
): DocumentNode => {
13+
let filteredOperation: OperationDefinitionNode | undefined;
14+
const fragments: Record<string, FragmentDefinitionNode> = {};
15+
16+
document.definitions.forEach(definition => {
17+
if (definition.kind === Kind.OPERATION_DEFINITION && definition.name?.value === operationName) {
18+
filteredOperation = definition;
19+
}
20+
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
21+
fragments[definition.name.value] = definition;
22+
}
23+
});
24+
25+
if (filteredOperation) {
26+
const filteredFragments = filterSelectionSet(filteredOperation.selectionSet, fragments);
27+
return {
28+
kind: Kind.DOCUMENT,
29+
definitions: [...filteredFragments, filteredOperation]
30+
};
31+
}
32+
33+
return {
34+
kind: Kind.DOCUMENT,
35+
definitions: []
36+
};
37+
}
38+
39+
export const filterSelectionSet = (
40+
selectionSet: SelectionSetNode | undefined,
41+
fragments: Record<string, FragmentDefinitionNode>
42+
): FragmentDefinitionNode[] => {
43+
44+
if (!selectionSet) {
45+
return [];
46+
}
47+
48+
const filteredFragments: FragmentDefinitionNode[] = [];
49+
50+
selectionSet.selections.forEach(selection => {
51+
if(selection.kind === Kind.FRAGMENT_SPREAD && fragments[selection.name.value]) {
52+
const fragment = fragments[selection.name.value];
53+
filteredFragments.push(
54+
fragment,
55+
...filterSelectionSet(fragment.selectionSet, fragments)
56+
);
57+
}
58+
else if (selection.kind === Kind.FIELD || selection.kind === Kind.INLINE_FRAGMENT) {
59+
filteredFragments.push(
60+
...filterSelectionSet(selection.selectionSet, fragments)
61+
);
62+
}
63+
});
64+
65+
return filteredFragments;
66+
}

packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
declare module 'graphiql-batch-request' {
22
import { ComponentType } from 'react';
33
import { DocumentNode, OperationDefinitionNode } from 'graphql/language';
4+
import { FetcherParams } from '@graphiql/toolkit';
45

56
type TabWithOperations = {
67
id: string,
@@ -12,6 +13,8 @@ declare module 'graphiql-batch-request' {
1213

1314
export type TabsWithOperations = Record<string, TabWithOperations>;
1415

16+
export type FetcherParamsWithDocument = FetcherParams & { document: DocumentNode, operationName?: string };
17+
1518
export type GraphiQLBatchRequestProps = {
1619
url: string,
1720
useAllOperations?: boolean

packages/graphiql-plugin-batch-request/src/index.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@
2525
top: 0;
2626
}
2727

28+
.graphiql-batch-request-strategy-section {
29+
align-items: center;
30+
display: flex;
31+
justify-content: space-between;
32+
padding: var(--px-2);
33+
}
34+
35+
.graphiql-batch-request-strategy-title {
36+
font-size: var(--font-size-h4);
37+
font-weight: var(--font-weight-medium);
38+
}
39+
40+
.graphiql-batch-request-strategy-caption {
41+
color: hsla(var(--color-neutral),var(--alpha-secondary));
42+
}
43+
2844
.graphiql-batch-request-content {
2945
margin: var(--px-16) 0 0;
3046
}

0 commit comments

Comments
 (0)