Skip to content

Commit 9e2b20f

Browse files
committed
add query merging feature
1 parent 18f246c commit 9e2b20f

File tree

4 files changed

+145
-77
lines changed

4 files changed

+145
-77
lines changed

README.md

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Isomorphic.
2222
- Runs on most browsers.
2323
- You don't need to install Node.js ecosystem on your computer.
24+
- Query merging to reduce request number.
2425

2526
## Overview
2627

@@ -172,6 +173,88 @@ login({
172173
})
173174
```
174175

176+
### Query Merging: Merge Multiple Queries into One Request
177+
178+
`graphql.js` supports **query merging** that allows you to collect all the requests into one request.
179+
180+
Assume we've these queries on server, define them just like before we do:
181+
```js
182+
var fetchPost = graph.query(`{
183+
post(id: $id) {
184+
id
185+
title
186+
text
187+
}
188+
}`)
189+
190+
var fetchComments = graph.query(`{
191+
commentsOfPost: comments(postId: $postId) {
192+
comment
193+
owner {
194+
name
195+
}
196+
}
197+
}`)
198+
```
199+
200+
Use **`.merge(mergeName, variables)`** command to put them into a merge buffer:
201+
202+
```js
203+
var postId = 123
204+
205+
// This won't send a request.
206+
fetchPost.merge('buildPage', { id: postId }).then(function (response) {
207+
console.log(response.post)
208+
})
209+
210+
// This also won't send a request.
211+
fetchComments.merge('buildPage', { postId: postId }).then(function (response) {
212+
console.log(response.commentsOfPost)
213+
})
214+
215+
// This will send a merged request:
216+
graph.commit('buildPage').then(function (response) {
217+
// All base fields will be in response return.
218+
console.log(response.post)
219+
console.log(response.commentsOfPost)
220+
})
221+
```
222+
223+
This will create the following merged query generated by **graphql.js**:
224+
225+
```graphql
226+
query ($merge024533__id: ID!, $merge141499__postId: ID!) {
227+
merge024533_post: {
228+
post(id: $merge024533__id) {
229+
id
230+
title
231+
text
232+
}
233+
}
234+
merge141499_commentsOfPost: {
235+
comments(postId: $merge141499__postId) {
236+
comment
237+
owner {
238+
name
239+
}
240+
}
241+
}
242+
}
243+
```
244+
245+
And variables will be generated, too:
246+
247+
```js
248+
{
249+
"merge024533__id": 123,
250+
"merge141499__postId": 123
251+
}
252+
```
253+
254+
> The `merge{number}` aliases won't be passed into your responses, since they will be used for initial seperation.
255+
256+
> ⚠️ **Important Restriction**: You cannot use multiple root fields using query merging.
257+
175258
#### Direct Execution with `.run` and ES6 Template Tag
176259

177260
If your query doesn't need any variables, it will generate a lazy execution query by default.
@@ -569,8 +652,8 @@ You can pass `debug: true` to options parameter to get a console output looks li
569652
fragment login_auth on User { token, ...info }
570653
571654
VARIABLES: {
572-
"email": "p@protel.com.tr",
573-
"password": "12345678"
655+
"email": "john@doe.com",
656+
"password": "123123"
574657
}
575658
576659
sending as form url-data

example/index.html

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,47 @@
22
<head>
33
<script src="../graphql.js"></script>
44
<script>
5-
var graph = graphql("https://xxx.pod-1.api-dev.simprapos.com/admin", {
5+
var graph = graphql("https://example.com/graphql", {
66
alwaysAutodeclare: true,
77
asJSON: true,
8-
debug: true,
9-
headers: {
10-
'Auth-Token': 'xxx'
11-
},
12-
fragments: {
13-
tax: "on Tax { id, name }",
14-
reason: "on Reason { id, name }"
15-
}
8+
debug: true
169
})
1710

18-
var allTaxes = graph.query(`
19-
taxes(with_inactive: true) {
20-
... tax
21-
}
22-
`)
11+
var postId = 123
2312

24-
var allReasons = graph.query(`
25-
reasons(with_inactive: true, kind: [returned, cancelled]) {
26-
... reason
13+
var fetchPost = graph.query(`{
14+
post(id: $id) {
15+
id
16+
title
17+
text
2718
}
28-
`)
19+
}`)
2920

30-
var createTax = graph.mutate(`
31-
tax_create(input: $input) {
32-
tax {
33-
...tax
21+
var fetchComments = graph.query(`{
22+
comments(postId: $postId) {
23+
comment
24+
owner {
25+
name
3426
}
3527
}
36-
`)
37-
38-
allTaxes.merge('fetchAll').then(function (response) {
39-
console.log('Taxes', response)
40-
})
41-
42-
allReasons.merge('fetchAll').then(function (response) {
43-
console.log('Reasons', response)
44-
})
28+
}`)
4529

46-
createTax.merge('seed', { 'input!TaxCreateInput': { name: { locale: 'en', text:"18"}, rate: 18 } }).then(function (response) {
47-
console.log('CreateTax 1', response)
30+
// This won't send a request.
31+
fetchPost.merge('buildPage', { id: postId }).then(function (post) {
32+
console.log(post)
4833
})
4934

50-
createTax.merge('seed', { 'input!TaxCreateInput': { name: { locale: 'en', text:"8"}, rate: 8 } }).then(function (response) {
51-
console.log('CreateTax 2', response)
35+
// This also won't send a request.
36+
fetchComments.merge('buildPage', { postId: postId }).then(function (comments) {
37+
console.log(comments)
5238
})
5339

54-
createTax.merge('seed', { 'input!TaxCreateInput': { name: { locale: 'en', text:"12"}, rate: 12 } }).then(function (response) {
55-
console.log('CreateTax 3', response)
40+
// This will send a merged request:
41+
graph.commit('buildPage').then(function (response) {
42+
// All base fields will be in response return.
43+
console.log(response.post)
44+
console.log(response.comments)
5645
})
57-
58-
graph.commit('fetchAll').then(function (r) {
59-
console.log(r)
60-
})
61-
graph.commit('seed').then(function (r) {
62-
console.log(r)
63-
})
64-
65-
// setTimeout(function () {
66-
// graph.commit('fetchAll')
67-
// }, 1000)
68-
6946
</script>
7047
</head>
7148
<body>

graphql.js

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@
275275
return this.autoDeclare(this.processQuery(query, this._fragments), variables)
276276
}
277277

278+
GraphQLClient.prototype.parseType = function (query) {
279+
var match = query.trim().match(/^(query|mutation|subscription)/)
280+
if (!match) return 'query'
281+
return match[1]
282+
}
283+
278284
GraphQLClient.prototype.createSenderFunction = function (debug) {
279285
var that = this
280286
return function (query, originalQuery, type) {
@@ -308,6 +314,16 @@
308314
}
309315

310316
caller.merge = function (mergeName, variables) {
317+
if (!type) {
318+
type = that.parseType(query)
319+
query = query.trim()
320+
.replace(/^(query|mutation|subscription)\s*/, '').trim()
321+
.replace(GraphQLClient.AUTODECLARE_PATTERN, '').trim()
322+
.replace(/^\{|\}$/g, '')
323+
}
324+
if (!originalQuery) {
325+
originalQuery = query
326+
}
311327
that._transaction[mergeName] = that._transaction[mergeName] || {
312328
query: [],
313329
mutation: []
@@ -329,16 +345,22 @@
329345
}
330346

331347
GraphQLClient.prototype.commit = function (mergeName) {
348+
if (!this._transaction[mergeName]) {
349+
throw new Error("You cannot commit the merge " + mergeName + " without creating it first.")
350+
}
332351
var that = this
333352
var resolveMap = {}
334353
var mergedVariables = {}
335354
var mergedQueries = {}
336355
Object.keys(this._transaction[mergeName]).forEach(function (method) {
337356
if (that._transaction[mergeName][method].length === 0) return
338357
var subQuery = that._transaction[mergeName][method].map(function (merge) {
339-
var reqId = 'merge' + Math.random().toString().split('.')[1].substr(0, 4)
358+
var reqId = 'merge' + Math.random().toString().split('.')[1].substr(0, 6)
340359
resolveMap[reqId] = merge.resolver
341360
var query = merge.query.replace(/\$([^\.\,\s\)]*)/g, function (_, m) {
361+
if (!merge.variables) {
362+
throw new Error('Unused variable on merge ' + mergeName + ': $' + m[0])
363+
}
342364
var matchingKey = Object.keys(merge.variables).filter(function (key) {
343365
return key === m || key.match(new RegExp('^' + m + '!'))
344366
})[0]
@@ -347,7 +369,13 @@
347369
mergedVariables[method][variable] = merge.variables[matchingKey]
348370
return '$' + variable.split('!')[0]
349371
})
350-
return reqId + '_' + query.trim().match(/^[^\(]+/)[0] + ': ' + query
372+
var alias = query.trim().match(/^[^\(]+\:/)
373+
if (!alias) {
374+
alias = query.replace(/^\{|\}$/gm, '').trim().match(/^[^\(\{]+/)[0] + ':'
375+
} else {
376+
query = query.replace(/^[^\(]+\:/, '')
377+
}
378+
return reqId + '_' + alias + query
351379
}).join('\n')
352380

353381
mergedQueries[method] = mergedQueries[method] || []
@@ -363,6 +391,9 @@
363391
responses.forEach(function (response) {
364392
Object.keys(response).forEach(function (mergeKey) {
365393
var parsedKey = mergeKey.match(/^(merge\d+)\_(.*)/)
394+
if (!parsedKey) {
395+
throw new Error('Multiple root keys detected on response. Merging doesn\'t support it yet.')
396+
}
366397
var reqId = parsedKey[1]
367398
var fieldName = parsedKey[2]
368399
var newResponse = {}
@@ -459,29 +490,6 @@
459490
return query
460491
}
461492

462-
// GraphQLClient.prototype.startTransaction = function () {
463-
// if (this.transaction) {
464-
// console.groupEnd()
465-
// console.error('[graphql]: transaction is already started')
466-
// return
467-
// }
468-
// if (this.options.debug) {
469-
// console.group('%c[graphql]: transaction started, following requests will be collected until end', 'font-weight: bold')
470-
// }
471-
// this.transaction = []
472-
// }
473-
474-
// GraphQLClient.prototype.endTransaction = function () {
475-
// if (!this.transaction) {
476-
// console.error('[graphql]: cannot end a transaction which is not started')
477-
// return
478-
// }
479-
// if (this.options.debug) {
480-
// console.groupEnd()
481-
// }
482-
// this.transaction = null
483-
// }
484-
485493
;(function (root, factory) {
486494
if (typeof define === 'function' && define.amd) {
487495
define(function () {

0 commit comments

Comments
 (0)