graphql.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. (function () {
  2. function __extend() {
  3. var extended = {}, deep = false, i = 0, length = arguments.length
  4. if (Object.prototype.toString.call(arguments[0]) == '[object Boolean]') {
  5. deep = arguments[0]
  6. i++
  7. }
  8. var merge = function (obj) {
  9. for (var prop in obj) {
  10. if (Object.prototype.hasOwnProperty.call(obj, prop)) {
  11. if (deep && Object.prototype.toString.call(obj[prop]) == '[object Object]') {
  12. extended[prop] = __extend(true, extended[prop], obj[prop])
  13. } else {
  14. extended[prop] = obj[prop]
  15. }
  16. }
  17. }
  18. }
  19. for (; i < length; i++) {
  20. var obj = arguments[i]
  21. merge(obj)
  22. }
  23. return extended
  24. }
  25. function __unique(array) {
  26. return array.filter(function onlyUnique(value, index, self) {
  27. return self.indexOf(value) === index;
  28. })
  29. }
  30. if (typeof XMLHttpRequest !== 'undefined') {
  31. function __doRequest(
  32. method, url, contentType, accept, headers, body, _onRequestError, callback
  33. ) {
  34. var xhr = new XMLHttpRequest
  35. xhr.open(method, url, true)
  36. xhr.setRequestHeader('Content-Type', contentType)
  37. xhr.setRequestHeader('Accept', accept)
  38. for (var key in headers) { xhr.setRequestHeader(key, headers[key]) }
  39. xhr.onerror = function () { callback(xhr, xhr.status) }
  40. xhr.onload = function () {
  41. try {
  42. callback(JSON.parse(xhr.responseText), xhr.status)
  43. }
  44. catch (e) {
  45. callback(xhr, xhr.status)
  46. }
  47. }
  48. xhr.send(body)
  49. }
  50. } else if (typeof require === 'function') {
  51. function __doRequest(
  52. method, url, contentType, accept, headers, body, onRequestError, callback
  53. ) {
  54. var http = require('http'), https = require('https'), URL = require('url'), uri = URL.parse(url)
  55. var req = (uri.protocol === 'https:' ? https : http).request({
  56. protocol: uri.protocol,
  57. hostname: uri.hostname,
  58. port: uri.port,
  59. path: uri.path,
  60. method: method.toUpperCase(),
  61. headers: __extend({ 'Content-type': contentType, 'Accept': accept }, headers)
  62. }, function (response) {
  63. var str = ''
  64. response.setEncoding('utf8')
  65. response.on('data', function (chunk) { str += chunk })
  66. response.on('end', function () {
  67. callback(JSON.parse(str), response.statusCode)
  68. })
  69. })
  70. if (typeof onRequestError === 'function') {
  71. req.on('error', function (err) {
  72. onRequestError(err);
  73. });
  74. }
  75. req.write(body)
  76. req.end()
  77. }
  78. }
  79. function __request(debug, method, url, headers, data, asJson, onRequestError, callback) {
  80. if (!url) {
  81. return;
  82. }
  83. if (asJson) {
  84. var body = JSON.stringify({ query: data.query, variables: data.variables });
  85. } else {
  86. var body = "query=" + encodeURIComponent(data.query) + "&variables=" + encodeURIComponent(JSON.stringify(data.variables))
  87. }
  88. if (debug) {
  89. console.groupCollapsed('[graphql]: '
  90. + method.toUpperCase() + ' ' + url + ': '
  91. + data.query.split(/\n/)[0].substr(0, 50) + '... with '
  92. + JSON.stringify(data.variables).substr(0, 50) + '...')
  93. console.log('QUERY: %c%s', 'font-weight: bold', data.query)
  94. console.log('VARIABLES: %c%s\n\nsending as ' + (asJson ? 'json' : 'form url-data'), 'font-weight: bold', JSON.stringify(data.variables, null, 2), data.variables)
  95. console.groupEnd()
  96. }
  97. for (var key in headers) {
  98. if (typeof headers[key] === 'function') {
  99. headers[key] = headers[key]()
  100. }
  101. }
  102. __doRequest(
  103. method,
  104. url,
  105. asJson ? 'application/json' : 'application/x-www-form-urlencoded',
  106. 'application/json',
  107. headers,
  108. body,
  109. onRequestError,
  110. callback
  111. )
  112. }
  113. function __isTagCall(strings) {
  114. return Object.prototype.toString.call(strings) == '[object Array]' && strings.raw
  115. }
  116. function GraphQLClient(url, options) {
  117. if (!(this instanceof GraphQLClient)) {
  118. var client = new GraphQLClient(url, options, true)
  119. var _lazy = client._sender
  120. for (var m in client) {
  121. if (typeof client[m] == 'function') {
  122. _lazy[m] = client[m].bind(client)
  123. if (client[m].declare) _lazy[m].declare = client[m].declare.bind(client)
  124. if (client[m].run) _lazy[m].run = client[m].run.bind(client)
  125. }
  126. }
  127. return _lazy
  128. } else if (arguments[2] !== true) {
  129. throw new Error("You cannot create GraphQLClient instance. Please call GraphQLClient as function.")
  130. }
  131. if (!options)
  132. options = {}
  133. if (!options.fragments)
  134. options.fragments = {}
  135. this.url = url
  136. this.options = options || {}
  137. this._fragments = this.buildFragments(options.fragments)
  138. this._sender = this.createSenderFunction(options.debug)
  139. this.createHelpers(this._sender)
  140. this._transaction = {}
  141. }
  142. // "fragment auth.login" will be "fragment auth_login"
  143. FRAGMENT_SEPERATOR = "_"
  144. // The autodeclare keyword.
  145. GraphQLClient.AUTODECLARE_PATTERN = /\(@autodeclare\)|\(@autotype\)/g
  146. GraphQLClient.FRAGMENT_PATTERN = /\.\.\.\s*([A-Za-z0-9\.\_]+)/g
  147. // Flattens nested object
  148. /*
  149. * {a: {b: {c: 1, d: 2}}} => {"a.b.c": 1, "a.b.d": 2}
  150. */
  151. GraphQLClient.prototype.flatten = function (object) {
  152. var prefix = arguments[1] || "", out = arguments[2] || {}, name
  153. for (name in object) {
  154. if (object.hasOwnProperty(name)) {
  155. typeof object[name] == "object"
  156. ? this.flatten(object[name], prefix + name + FRAGMENT_SEPERATOR, out)
  157. : out[prefix + name] = object[name]
  158. }
  159. }
  160. return out
  161. }
  162. GraphQLClient.prototype.setUrl = function (url) {
  163. this.url = url
  164. return this.url
  165. }
  166. GraphQLClient.prototype.getUrl = function () {
  167. return this.url
  168. }
  169. // Gets path from object
  170. /*
  171. * {a: {b: {c: 1, d: 2}}}, "a.b.c" => 1
  172. */
  173. GraphQLClient.prototype.fragmentPath = function (fragments, path) {
  174. var getter = new Function("fragments", "return fragments." + path.replace(/\./g, FRAGMENT_SEPERATOR))
  175. var obj = getter(fragments)
  176. if (path != "on" && (!obj || typeof obj != "string")) {
  177. throw new Error("Fragment " + path + " not found")
  178. }
  179. return obj
  180. }
  181. GraphQLClient.prototype.collectFragments = function (query, fragments) {
  182. var that = this
  183. var fragmentRegexp = GraphQLClient.FRAGMENT_PATTERN
  184. var collectedFragments = []
  185. ; (query.match(fragmentRegexp) || []).forEach(function (fragment) {
  186. var path = fragment.replace(fragmentRegexp, function (_, $m) { return $m })
  187. var fragment = that.fragmentPath(fragments, path)
  188. if (fragment) {
  189. var pathRegexp = new RegExp(fragmentRegexp.source.replace(/\((.*)\)/, path) + '$')
  190. if (fragment.match(pathRegexp)) {
  191. throw new Error("Recursive fragment usage detected on " + path + ".")
  192. }
  193. collectedFragments.push(fragment)
  194. // Collect sub fragments
  195. var alreadyCollectedFragments = collectedFragments.filter(function (alreadyCollected) {
  196. return alreadyCollected.match(new RegExp("fragment " + path))
  197. })
  198. if (alreadyCollectedFragments.length > 0 && fragmentRegexp.test(fragment)) {
  199. that.collectFragments(fragment, fragments).forEach(function (fragment) {
  200. collectedFragments.unshift(fragment)
  201. })
  202. }
  203. }
  204. })
  205. return __unique(collectedFragments)
  206. }
  207. GraphQLClient.prototype.processQuery = function (query, fragments) {
  208. if (typeof query == 'object' && query.hasOwnProperty('kind') && query.hasOwnProperty('definitions')) {
  209. throw new Error("Do not use graphql AST to send requests. Please generate query as string first using `graphql.print(query)`")
  210. }
  211. var fragmentRegexp = GraphQLClient.FRAGMENT_PATTERN
  212. var collectedFragments = this.collectFragments(query, fragments)
  213. query = query.replace(fragmentRegexp, function (_, $m) {
  214. return "... " + $m.split(".").join(FRAGMENT_SEPERATOR)
  215. })
  216. return [query].concat(collectedFragments.filter(function (fragment) {
  217. // Remove already used fragments
  218. return !query.match(fragment)
  219. })).join("\n")
  220. }
  221. GraphQLClient.prototype.autoDeclare = function (query, variables) {
  222. var that = this
  223. var typeMap = {
  224. string: "String",
  225. number: function (value) {
  226. return value % 1 === 0 ? "Int" : "Float";
  227. },
  228. boolean: "Boolean"
  229. }
  230. return query.replace(GraphQLClient.AUTODECLARE_PATTERN, function () {
  231. var types = []
  232. for (var key in variables) {
  233. var value = variables[key]
  234. var keyAndType = key.split("!")
  235. var mapping = typeMap[typeof (value)]
  236. var mappedType = typeof (mapping) === "function" ? mapping(value) : mapping
  237. if (!key.match("!") && keyAndType[0].match(/_?id/i)) {
  238. mappedType = "ID"
  239. }
  240. var type = (keyAndType[1] || mappedType)
  241. if (type) {
  242. types.push("$" + keyAndType[0] + ": " + type + "!")
  243. }
  244. }
  245. types = types.join(", ")
  246. return types ? "(" + types + ")" : ""
  247. })
  248. }
  249. GraphQLClient.prototype.cleanAutoDeclareAnnotations = function (variables) {
  250. if (!variables) variables = {}
  251. var newVariables = {}
  252. for (var key in variables) {
  253. var value = variables[key]
  254. var keyAndType = key.split("!")
  255. newVariables[keyAndType[0]] = value
  256. }
  257. return newVariables
  258. }
  259. GraphQLClient.prototype.buildFragments = function (fragments) {
  260. var that = this
  261. fragments = this.flatten(fragments || {})
  262. var fragmentObject = {}
  263. for (var name in fragments) {
  264. var fragment = fragments[name]
  265. if (typeof fragment == "object") {
  266. fragmentObject[name] = that.buildFragments(fragment)
  267. } else {
  268. fragmentObject[name] = "\nfragment " + name + " " + fragment
  269. }
  270. }
  271. return fragmentObject
  272. }
  273. GraphQLClient.prototype.buildQuery = function (query, variables) {
  274. return this.autoDeclare(this.processQuery(query, this._fragments), variables)
  275. }
  276. GraphQLClient.prototype.parseType = function (query) {
  277. var match = query.trim().match(/^(query|mutation|subscription)/)
  278. if (!match) return 'query'
  279. return match[1]
  280. }
  281. GraphQLClient.prototype.createSenderFunction = function (debug) {
  282. var that = this
  283. return function (query, originalQuery, type) {
  284. if (__isTagCall(query)) {
  285. return that.run(that.ql.apply(that, arguments))
  286. }
  287. var caller = function (variables, requestOptions) {
  288. if (!requestOptions) requestOptions = {}
  289. if (!variables) variables = {}
  290. var fragmentedQuery = that.buildQuery(query, variables)
  291. headers = __extend((that.options.headers || {}), (requestOptions.headers || {}))
  292. return new Promise(function (resolve, reject) {
  293. __request(debug, that.options.method || "post", that.getUrl(), headers, {
  294. query: fragmentedQuery,
  295. variables: that.cleanAutoDeclareAnnotations(variables)
  296. }, !!that.options.asJSON, that.options.onRequestError, function (response, status) {
  297. if (status == 200) {
  298. if (response.errors) {
  299. reject(response.errors)
  300. } else if (response.data) {
  301. resolve(response.data)
  302. } else {
  303. resolve(response)
  304. }
  305. } else {
  306. reject(response)
  307. }
  308. })
  309. })
  310. }
  311. caller.merge = function (mergeName, variables) {
  312. if (!type) {
  313. type = that.parseType(query)
  314. query = query.trim()
  315. .replace(/^(query|mutation|subscription)\s*/, '').trim()
  316. .replace(GraphQLClient.AUTODECLARE_PATTERN, '').trim()
  317. .replace(/^\{|\}$/g, '')
  318. }
  319. if (!originalQuery) {
  320. originalQuery = query
  321. }
  322. that._transaction[mergeName] = that._transaction[mergeName] || {
  323. query: [],
  324. mutation: []
  325. }
  326. return new Promise(function (resolve) {
  327. that._transaction[mergeName][type].push({
  328. type: type,
  329. query: originalQuery,
  330. variables: variables,
  331. resolver: resolve
  332. })
  333. })
  334. }
  335. if (arguments.length > 3) {
  336. return caller.apply(null, Array.prototype.slice.call(arguments, 3))
  337. }
  338. return caller
  339. }
  340. }
  341. GraphQLClient.prototype.commit = function (mergeName) {
  342. if (!this._transaction[mergeName]) {
  343. throw new Error("You cannot commit the merge " + mergeName + " without creating it first.")
  344. }
  345. var that = this
  346. var resolveMap = {}
  347. var mergedVariables = {}
  348. var mergedQueries = {}
  349. Object.keys(this._transaction[mergeName]).forEach(function (method) {
  350. if (that._transaction[mergeName][method].length === 0) return
  351. var subQuery = that._transaction[mergeName][method].map(function (merge) {
  352. var reqId = 'merge' + Math.random().toString().split('.')[1].substr(0, 6)
  353. resolveMap[reqId] = merge.resolver
  354. var query = merge.query.replace(/\$([^\.\,\s\)]*)/g, function (_, m) {
  355. if (!merge.variables) {
  356. throw new Error('Unused variable on merge ' + mergeName + ': $' + m[0])
  357. }
  358. var matchingKey = Object.keys(merge.variables).filter(function (key) {
  359. return key === m || key.match(new RegExp('^' + m + '!'))
  360. })[0]
  361. var variable = reqId + '__' + matchingKey
  362. mergedVariables[method] = mergedVariables[method] || {}
  363. mergedVariables[method][variable] = merge.variables[matchingKey]
  364. return '$' + variable.split('!')[0]
  365. })
  366. var alias = query.trim().match(/^[^\(]+\:/)
  367. if (!alias) {
  368. alias = query.replace(/^\{|\}$/gm, '').trim().match(/^[^\(\{]+/)[0] + ':'
  369. } else {
  370. query = query.replace(/^[^\(]+\:/, '')
  371. }
  372. return reqId + '_' + alias + query
  373. }).join('\n')
  374. mergedQueries[method] = mergedQueries[method] || []
  375. mergedQueries[method].push(method + " (@autodeclare) {\n" + subQuery + "\n }")
  376. })
  377. return Promise.all(Object.keys(mergedQueries).map(function (method) {
  378. var query = mergedQueries[method].join('\n')
  379. var variables = mergedVariables[method]
  380. return that._sender(query, query, null, variables)
  381. })).then(function (responses) {
  382. var newResponses = {}
  383. responses.forEach(function (response) {
  384. Object.keys(response).forEach(function (mergeKey) {
  385. var parsedKey = mergeKey.match(/^(merge\d+)\_(.*)/)
  386. if (!parsedKey) {
  387. throw new Error('Multiple root keys detected on response. Merging doesn\'t support it yet.')
  388. }
  389. var reqId = parsedKey[1]
  390. var fieldName = parsedKey[2]
  391. var newResponse = {}
  392. newResponse[fieldName] = response[mergeKey]
  393. newResponses[fieldName] = (newResponses[fieldName] || []).concat([response[mergeKey]])
  394. resolveMap[reqId](newResponse)
  395. })
  396. })
  397. return newResponses
  398. }).catch(function (responses) {
  399. return { error: true, errors: responses }
  400. }).finally(function (responses) {
  401. that._transaction[mergeName] = { query: [], mutation: [] }
  402. return responses
  403. })
  404. }
  405. GraphQLClient.prototype.createHelpers = function (sender) {
  406. var that = this
  407. function helper(query) {
  408. if (__isTagCall(query)) {
  409. that.__prefix = this.prefix
  410. that.__suffix = this.suffix
  411. var result = that.run(that.ql.apply(that, arguments))
  412. that.__prefix = ""
  413. that.__suffix = ""
  414. return result
  415. }
  416. var caller = sender(this.prefix + " " + query + " " + this.suffix, query.trim(), this.type)
  417. if (arguments.length > 1 && arguments[1] != null) {
  418. return caller.apply(null, Array.prototype.slice.call(arguments, 1))
  419. }
  420. return caller
  421. }
  422. var helpers = [
  423. { method: 'mutate', type: 'mutation' },
  424. { method: 'query', type: 'query' },
  425. { method: 'subscribe', type: 'subscription' }
  426. ]
  427. helpers.forEach(function (m) {
  428. that[m.method] = function (query, variables, options) {
  429. if (that.options.alwaysAutodeclare === true || (options && options.declare === true)) {
  430. return helper.call({ type: m.type, prefix: m.type + " (@autodeclare) {", suffix: "}" }, query, variables)
  431. }
  432. return helper.call({ type: m.type, prefix: m.type, suffix: "" }, query, variables)
  433. }
  434. that[m.method].run = function (query, options) {
  435. return that[m.method](query, options)({})
  436. }
  437. })
  438. this.run = function (query) {
  439. return sender(query, originalQuery, m.type, {})
  440. }
  441. }
  442. GraphQLClient.prototype.fragments = function () {
  443. return this._fragments
  444. }
  445. GraphQLClient.prototype.getOptions = function () {
  446. return this.options || {}
  447. }
  448. GraphQLClient.prototype.headers = function (newHeaders) {
  449. return this.options.headers = __extend(this.options.headers, newHeaders)
  450. }
  451. GraphQLClient.prototype.fragment = function (fragment) {
  452. if (typeof fragment == 'string') {
  453. var _fragment = this._fragments[fragment.replace(/\./g, FRAGMENT_SEPERATOR)]
  454. if (!_fragment) {
  455. throw "Fragment " + fragment + " not found!"
  456. }
  457. return _fragment.trim()
  458. } else {
  459. this.options.fragments = __extend(true, this.options.fragments, fragment)
  460. this._fragments = this.buildFragments(this.options.fragments)
  461. return this._fragments
  462. }
  463. }
  464. GraphQLClient.prototype.ql = function (strings) {
  465. var that = this
  466. fragments = Array.prototype.slice.call(arguments, 1)
  467. fragments = fragments.map(function (fragment) {
  468. if (typeof fragment == 'string') {
  469. return fragment.match(/fragment\s+([^\s]*)\s/)[1]
  470. }
  471. })
  472. var query = (typeof strings == 'string') ? strings : strings.reduce(function (acc, seg, i) {
  473. return acc + fragments[i - 1] + seg
  474. })
  475. query = this.buildQuery(query)
  476. query = ((this.__prefix || "") + " " + query + " " + (this.__suffix || "")).trim()
  477. return query
  478. }
  479. ; (function (root, factory) {
  480. if (typeof define === 'function' && define.amd) {
  481. define(function () {
  482. return (root.graphql = factory(GraphQLClient))
  483. });
  484. } else if (typeof module === 'object' && module.exports) {
  485. module.exports = factory(root.GraphQLClient)
  486. } else {
  487. root.graphql = factory(root.GraphQLClient)
  488. }
  489. }(this, function () {
  490. return GraphQLClient
  491. }))
  492. })()