├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── COLLABORATORS.md
├── LICENSE
├── README.md
├── _doc
└── index.md
├── lib
└── graphqls2s.min.js
├── package-lock.json
├── package.json
├── src
├── graphmetadata.js
├── graphqls2s.js
└── utilities.js
├── test
├── .DS_Store
├── browser
│ ├── graphqls2s.js
│ └── index.html
└── node
│ └── graphqls2s.js
└── webpack.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parserOptions": {
9 | "sourceType": "module"
10 | },
11 | "rules": {
12 | "no-console":0,
13 | "linebreak-style": [
14 | "error",
15 | "unix"
16 | ],
17 | "quotes": [
18 | "error",
19 | "single"
20 | ],
21 | "semi": [
22 | "error",
23 | "never"
24 | ]
25 | }
26 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | *.orig
3 | node_modules/*
4 | lib/
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test/
2 | webpack.config.js
3 | _doc/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [0.22.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.21.0...v0.22.0) (2021-10-07)
6 |
7 | ## [0.21.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.20.2...v0.21.0) (2021-09-29)
8 |
9 |
10 | ### Bug Fixes
11 |
12 | * 2 vulnerabilities (1 high, 1 critical) ([37a860d](https://github.com/nicolasdao/graphql-s2s/commit/37a860dc34d0d2419e65432acfb391ef084f8911))
13 | * All vulnerabilities ([8e43828](https://github.com/nicolasdao/graphql-s2s/commit/8e438283221a2eccd1239a1ffe02743f237192b0))
14 |
15 | ### [0.20.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.20.1...v0.20.2) (2019-07-21)
16 |
17 |
18 |
19 | ### [0.20.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.20.0...v0.20.1) (2019-07-21)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * Vulnerability issues reported from npm and github ([8ef7783](https://github.com/nicolasdao/graphql-s2s/commit/8ef7783))
25 |
26 |
27 |
28 |
29 | # [0.20.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.19.2...v0.20.0) (2019-05-05)
30 |
31 |
32 |
33 |
34 | ## [0.19.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.19.1...v0.19.2) (2019-03-14)
35 |
36 |
37 |
38 |
39 | ## [0.19.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.19.0...v0.19.1) (2019-02-23)
40 |
41 |
42 |
43 |
44 | # [0.19.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.18.2...v0.19.0) (2019-02-23)
45 |
46 |
47 | ### Features
48 |
49 | * Add support for description label ([1480efd](https://github.com/nicolasdao/graphql-s2s/commit/1480efd))
50 |
51 |
52 |
53 |
54 | ## [0.18.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.18.1...v0.18.2) (2019-01-28)
55 |
56 |
57 |
58 |
59 | ## [0.18.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.18.0...v0.18.1) (2018-11-02)
60 |
61 |
62 | ### Features
63 |
64 | * Add support for RegExp in function 'queryAST.containsProp' ([6337909](https://github.com/nicolasdao/graphql-s2s/commit/6337909))
65 |
66 |
67 |
68 |
69 | # [0.18.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.17.4...v0.18.0) (2018-10-27)
70 |
71 |
72 | ### Features
73 |
74 | * Add support for detecting properties in GraphQl queries ([f9ac884](https://github.com/nicolasdao/graphql-s2s/commit/f9ac884))
75 |
76 |
77 |
78 |
79 | ## [0.17.4](https://github.com/nicolasdao/graphql-s2s/compare/v0.17.3...v0.17.4) (2018-10-25)
80 |
81 |
82 |
83 |
84 | ## [0.17.3](https://github.com/nicolasdao/graphql-s2s/compare/v0.17.2...v0.17.3) (2018-10-25)
85 |
86 |
87 |
88 |
89 | ## [0.17.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.17.1...v0.17.2) (2018-09-26)
90 |
91 |
92 |
93 |
94 | ## [0.17.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.17.0...v0.17.1) (2018-09-06)
95 |
96 |
97 |
98 |
99 | # [0.17.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.16.5...v0.17.0) (2018-08-14)
100 |
101 |
102 |
103 |
104 | ## [0.16.5](https://github.com/nicolasdao/graphql-s2s/compare/v0.16.4...v0.16.5) (2018-06-17)
105 |
106 |
107 |
108 |
109 | ## [0.16.4](https://github.com/nicolasdao/graphql-s2s/compare/v0.16.3...v0.16.4) (2018-06-11)
110 |
111 |
112 |
113 |
114 | ## [0.16.3](https://github.com/nicolasdao/graphql-s2s/compare/v0.16.2...v0.16.3) (2018-06-11)
115 |
116 |
117 |
118 |
119 | ## [0.16.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.16.1...v0.16.2) (2018-05-13)
120 |
121 |
122 |
123 |
124 | ## [0.16.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.16.0...v0.16.1) (2018-05-07)
125 |
126 |
127 | ### Bug Fixes
128 |
129 | * Getting one step closer to support directive after complex generic types ([9fcbfe0](https://github.com/nicolasdao/graphql-s2s/commit/9fcbfe0))
130 |
131 |
132 |
133 |
134 | # [0.16.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.15.1...v0.16.0) (2018-04-29)
135 |
136 |
137 |
138 |
139 | ## [0.15.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.15.0...v0.15.1) (2018-04-29)
140 |
141 |
142 |
143 |
144 | # [0.15.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.14.3...v0.15.0) (2018-04-15)
145 |
146 |
147 |
148 |
149 | ## [0.14.3](https://github.com/nicolasdao/graphql-s2s/compare/v0.14.2...v0.14.3) (2018-04-15)
150 |
151 |
152 |
153 |
154 | ## [0.14.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.14.1...v0.14.2) (2018-04-15)
155 |
156 |
157 |
158 |
159 | ## [0.14.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.14.0...v0.14.1) (2018-04-11)
160 |
161 |
162 |
163 |
164 | # [0.14.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.13.1...v0.14.0) (2018-04-11)
165 |
166 |
167 |
168 |
169 | ## [0.13.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.13.0...v0.13.1) (2018-04-11)
170 |
171 |
172 |
173 |
174 | # [0.13.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.12.1...v0.13.0) (2018-03-07)
175 |
176 |
177 | ### Features
178 |
179 | * Add support for generic typing with more than one type ([4d2106e](https://github.com/nicolasdao/graphql-s2s/commit/4d2106e))
180 |
181 |
182 |
183 |
184 | ## [0.12.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.12.0...v0.12.1) (2018-03-07)
185 |
186 |
187 |
188 |
189 | # [0.12.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.11.2...v0.12.0) (2018-03-07)
190 |
191 |
192 |
193 |
194 | ## [0.11.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.11.1...v0.11.2) (2018-02-26)
195 |
196 |
197 |
198 |
199 | ## [0.11.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.11.0...v0.11.1) (2018-02-21)
200 |
201 |
202 | ### Bug Fixes
203 |
204 | * getQueryAST breaks when the argument of the query contains an array of complex objects ([177baef](https://github.com/nicolasdao/graphql-s2s/commit/177baef))
205 |
206 |
207 |
208 |
209 | # [0.11.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.9.6...v0.11.0) (2018-02-11)
210 |
211 |
212 | ### Features
213 |
214 | * Add new propertyPaths method on the QueryAST object ([3a6fa2b](https://github.com/nicolasdao/graphql-s2s/commit/3a6fa2b))
215 |
216 |
217 |
218 |
219 | ## [0.9.6](https://github.com/nicolasdao/graphql-s2s/compare/v0.9.5...v0.9.6) (2018-02-01)
220 |
221 |
222 | ### Bug Fixes
223 |
224 | * Required fields throw errors in 'buildQuery' ([e9d3133](https://github.com/nicolasdao/graphql-s2s/commit/e9d3133))
225 |
226 |
227 |
228 |
229 | ## [0.9.5](https://github.com/nicolasdao/graphql-s2s/compare/v0.9.4...v0.9.5) (2018-02-01)
230 |
231 |
232 |
233 |
234 | ## [0.9.4](https://github.com/nicolasdao/graphql-s2s/compare/v0.9.3...v0.9.4) (2018-01-19)
235 |
236 |
237 | ### Bug Fixes
238 |
239 | * Boolean is not supported while analysing graphql queries ([380f92b](https://github.com/nicolasdao/graphql-s2s/commit/380f92b))
240 |
241 |
242 |
243 |
244 | ## [0.9.3](https://github.com/nicolasdao/graphql-s2s/compare/v0.9.2...v0.9.3) (2018-01-14)
245 |
246 |
247 |
248 |
249 | ## [0.9.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.9.1...v0.9.2) (2018-01-13)
250 |
251 |
252 | ### Bug Fixes
253 |
254 | * Operation name does not work when multiple queries are defined in the request. ([9d648b9](https://github.com/nicolasdao/graphql-s2s/commit/9d648b9))
255 |
256 |
257 |
258 |
259 | ## [0.9.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.9.0...v0.9.1) (2018-01-12)
260 |
261 |
262 | ### Bug Fixes
263 |
264 | * getQueryAST throws an error when variables are of type array. ([51eab6b](https://github.com/nicolasdao/graphql-s2s/commit/51eab6b))
265 |
266 |
267 |
268 |
269 | # [0.9.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.8.0...v0.9.0) (2018-01-12)
270 |
271 |
272 | ### Features
273 |
274 | * Add new 'paths' api on the query AST object ([06dfa24](https://github.com/nicolasdao/graphql-s2s/commit/06dfa24))
275 |
276 |
277 |
278 |
279 | # [0.8.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.7.0...v0.8.0) (2018-01-12)
280 |
281 |
282 | ### Features
283 |
284 | * Add new 'some' api on the queryAST object ([fbc951f](https://github.com/nicolasdao/graphql-s2s/commit/fbc951f))
285 |
286 |
287 |
288 |
289 | # [0.7.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.6.0...v0.7.0) (2018-01-12)
290 |
291 |
292 | ### Bug Fixes
293 |
294 | * Defragging strips out the metadata from the AST ([a8444bb](https://github.com/nicolasdao/graphql-s2s/commit/a8444bb))
295 |
296 |
297 | ### Features
298 |
299 | * Add support for defragmenting a query (i.e. injecting all fragments into the operation) ([058c49c](https://github.com/nicolasdao/graphql-s2s/commit/058c49c))
300 |
301 |
302 |
303 |
304 | # [0.6.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.5.0...v0.6.0) (2018-01-11)
305 |
306 |
307 | ### Features
308 |
309 | * Add support for dealing with schema queries ([19785e3](https://github.com/nicolasdao/graphql-s2s/commit/19785e3))
310 |
311 |
312 |
313 |
314 | # [0.5.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.4.1...v0.5.0) (2018-01-11)
315 |
316 |
317 | ### Features
318 |
319 | * Add support for analysing Graphql Queries, modifying them, and rebuilding them ([1821fad](https://github.com/nicolasdao/graphql-s2s/commit/1821fad))
320 |
321 |
322 |
323 |
324 | ## [0.4.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.4.0...v0.4.1) (2018-01-09)
325 |
326 |
327 | ### Bug Fixes
328 |
329 | * getQueryASP fails when the query is empty ([116ff0f](https://github.com/nicolasdao/graphql-s2s/commit/116ff0f))
330 |
331 |
332 |
333 |
334 | # [0.4.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.3.3...v0.4.0) (2018-01-08)
335 |
336 |
337 | ### Features
338 |
339 | * Add new 'getQueryAST' api whoch allows to inspect the current graphql request to extract metadata ([2163ecb](https://github.com/nicolasdao/graphql-s2s/commit/2163ecb))
340 |
341 |
342 |
343 |
344 | ## [0.3.3](https://github.com/nicolasdao/graphql-s2s/compare/v0.3.2...v0.3.3) (2018-01-08)
345 |
346 |
347 |
348 |
349 | ## [0.3.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.3.1...v0.3.2) (2017-11-27)
350 |
351 |
352 | ### Bug Fixes
353 |
354 | * Add support for 'scalar' keyword ([351e2e5](https://github.com/nicolasdao/graphql-s2s/commit/351e2e5))
355 | * Add support for 'union' keyword ([e89358e](https://github.com/nicolasdao/graphql-s2s/commit/e89358e))
356 | * Compile ES6 to ES5 to add support for both 'scalar' and 'union' keywords ([3bb4992](https://github.com/nicolasdao/graphql-s2s/commit/3bb4992))
357 |
358 |
359 |
360 |
361 | ## [0.3.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.3.0...v0.3.1) (2017-10-28)
362 |
363 |
364 | ### Bug Fixes
365 |
366 | * Bug [#1](https://github.com/nicolasdao/graphql-s2s/issues/1). Add support for the 'extend' keyword ([8de6018](https://github.com/nicolasdao/graphql-s2s/commit/8de6018))
367 |
368 |
369 |
370 |
371 | # [0.3.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.2.1...v0.3.0) (2017-07-28)
372 |
373 |
374 | ### Features
375 |
376 | * Add support for alias name on generic types ([2436a0f](https://github.com/nicolasdao/graphql-s2s/commit/2436a0f))
377 |
378 |
379 |
380 |
381 | ## [0.2.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.2.0...v0.2.1) (2017-06-13)
382 |
383 |
384 | ### Bug Fixes
385 |
386 | * Remove babel-polyfill ([cc24afe](https://github.com/nicolasdao/graphql-s2s/commit/cc24afe))
387 |
388 |
389 |
390 |
391 | # [0.2.0](https://github.com/nicolasdao/graphql-s2s/compare/v0.1.2...v0.2.0) (2017-06-13)
392 |
393 |
394 | ### Features
395 |
396 | * Convert project to ES5 so it can run in the browser. Using webpack, eslint and babel + adding support for browser testing ([4bfbb77](https://github.com/nicolasdao/graphql-s2s/commit/4bfbb77))
397 |
398 |
399 |
400 |
401 | ## [0.1.2](https://github.com/nicolasdao/graphql-s2s/compare/v0.1.1...v0.1.2) (2017-06-13)
402 |
403 |
404 | ### Bug Fixes
405 |
406 | * Fix lint issues ([4b9d69d](https://github.com/nicolasdao/graphql-s2s/commit/4b9d69d))
407 | * Lint all code ([d744392](https://github.com/nicolasdao/graphql-s2s/commit/d744392))
408 |
409 |
410 |
411 |
412 | ## [0.1.1](https://github.com/nicolasdao/graphql-s2s/compare/v0.0.9...v0.1.1) (2017-06-12)
413 |
414 |
415 |
416 |
417 | ## [0.0.9](https://github.com/nicolasdao/graphql-s2s/compare/v0.0.8...v0.0.9) (2017-06-12)
418 |
419 |
420 | ### Bug Fixes
421 |
422 | * Rename one API to something more meaningfull(getSchemaParts -> getSchemaAST) ([811d873](https://github.com/nicolasdao/graphql-s2s/commit/811d873))
423 |
424 |
425 |
426 |
427 | ## [0.0.8](https://github.com/nicolasdao/graphql-s2s/compare/v0.0.6...v0.0.8) (2017-06-06)
428 |
429 |
430 | ### Bug Fixes
431 |
432 | * Amend test description + add a collaborators.md file ([86915ec](https://github.com/nicolasdao/graphql-s2s/commit/86915ec))
433 | * support for complex commenting + amended documentation. ([7648c0f](https://github.com/nicolasdao/graphql-s2s/commit/7648c0f))
434 |
435 |
436 |
437 |
438 | ## [0.0.7](https://github.com/nicolasdao/graphql-s2s/compare/v0.0.6...v0.0.7) (2017-06-06)
439 |
440 |
441 | ### Bug Fixes
442 |
443 | * support for complex commenting + amended documentation. ([7648c0f](https://github.com/nicolasdao/graphql-s2s/commit/7648c0f))
444 |
445 |
446 |
447 |
448 | ## [0.0.6](https://github.com/nicolasdao/graphql-s2s/compare/v0.0.5...v0.0.6) (2017-06-02)
449 |
450 |
451 |
452 |
453 | ## [0.0.5](https://github.com/nicolasdao/graphql-s2s/compare/v0.0.4...v0.0.5) (2017-06-02)
454 |
455 |
456 |
457 |
458 | ## [0.0.4](https://github.com/neapers/graphql-s2s/compare/v0.0.3...v0.0.4) (2017-06-01)
459 |
460 |
461 |
462 |
463 | ## [0.0.3](https://github.com/neapers/graphql-s2s/compare/0.0.2...v0.0.3) (2017-06-01)
464 |
--------------------------------------------------------------------------------
/COLLABORATORS.md:
--------------------------------------------------------------------------------
1 | # Collaborators
2 |
3 | - [Nicolas Dao](https://github.com/nicolasdao)
4 | - [Brendan Johnson](https://github.com/BrendanJohnson)
5 | - [Boris Berak](https://github.com/bberak)
6 | - [Pankaj Parkar](https://github.com/pankajparkar)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018, Neap Pty Ltd.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * Neither the name of Neap Pty Ltd nor the
12 | names of its contributors may be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL NEAP PTY LTD BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | > This project is still maintained but has been superseded by [graphql-schemax](https://github.com/nicolasdao/graphql-schemax). graphql-schemax takes the approach of compiling standard JSON object into a GraphQL Schema string.
3 |
4 | # GraghQL Schema-2-Schema Transpiler · [](https://www.npmjs.com/package/graphql-s2s) [](https://travis-ci.org/nicolasdao/graphql-s2s) [](https://opensource.org/licenses/BSD-3-Clause) [](#this-is-what-we-re-up-to) [](https://www.npmjs.com/package/graphql-s2s)
5 |
6 | # Table Of Contents
7 | > * [What It Does](#what-it-does)
8 | > * [Install](#install)
9 | > * [Getting Started](#getting-started)
10 | > - [Basic](#basic)
11 | > - [Type Inheritance](#type-inheritance)
12 | > - [Generic Types](#generic-types)
13 | > - [Metadata Decoration](#metadata-decoration)
14 | > - [Deconstructing - Transforming - Rebuilding Queries](#deconstructing---transforming---rebuilding-queries)
15 | > * [How To](#how-to)
16 | > - [How to use a custom name on generic types?](#how-to-use-a-custom-name-on-generic-types)
17 | > * [Examples](#examples)
18 | > * [Contribute](#contribute)
19 | > * [About Neap](#this-is-what-we-re-up-to)
20 | > * [License](#license)
21 |
22 | # What It Does
23 | GraphQL S2S enriches the standard GraphQL Schema string used by both [graphql.js](https://github.com/graphql/graphql-js) and the [Apollo Server](https://github.com/apollographql/graphql-tools). The enriched schema supports:
24 | * [**Type Inheritance**](#type-inheritance)
25 | * [**Generic Types**](#generic-types)
26 | * [**Metadata Decoration**](#metadata-decoration)
27 | * [**Deconstructing - Transforming - Rebuilding Queries**](#deconstructing---transforming---rebuilding-queries)
28 |
29 | # Install
30 | ### node
31 | ```js
32 | npm install graphql-s2s --save
33 | ```
34 | ### browser
35 | ```html
36 |
37 | ```
38 | > Using the awesome [unpkg.com](https://unpkg.com), all versions are supported at https://unpkg.com/graphql-s2s@__*:VERSION*__/lib/graphqls2s.min.js.
39 | The API will be accessible through the __*graphqls2s*__ object.
40 |
41 | It is also possible to embed it after installing the _graphql-s2s_ npm package:
42 | ```html
43 |
44 | ```
45 |
46 | # Getting Started
47 | ## Basic
48 | ```js
49 | const { transpileSchema } = require('graphql-s2s').graphqls2s
50 | const { makeExecutableSchema } = require('graphql-tools')
51 |
52 | const schema = `
53 | type Node {
54 | id: ID!
55 | }
56 |
57 | type Person inherits Node {
58 | firstname: String
59 | lastname: String
60 | }
61 |
62 | type Student inherits Person {
63 | nickname: String
64 | }
65 |
66 | type Query {
67 | students: [Student]
68 | }
69 | `
70 |
71 | const resolver = {
72 | Query: {
73 | students(root, args, context) {
74 | // replace this dummy code with your own logic to extract students.
75 | return [{ id: 1, firstname: "Carry", lastname: "Connor", nickname: "Cannie" }]
76 | }
77 | }
78 | };
79 |
80 | const executableSchema = makeExecutableSchema({
81 | typeDefs: [transpileSchema(schema)],
82 | resolvers: resolver
83 | })
84 | ```
85 |
86 | ## Type Inheritance
87 |
88 | ### Single Inheritance
89 |
90 | ```js
91 | const schema = `
92 | type Node {
93 | id: ID!
94 | }
95 |
96 | # Inheriting from the 'Node' type
97 | type Person inherits Node {
98 | firstname: String
99 | lastname: String
100 | }
101 |
102 | # Inheriting from the 'Person' type
103 | type Student inherits Person {
104 | nickname: String
105 | }
106 | `
107 | ```
108 |
109 | ### Multiple Inheritance
110 |
111 | ```js
112 | const schema = `
113 |
114 | type Node {
115 | id: ID!
116 | }
117 |
118 | type Address {
119 | streetAddress: String
120 | city: String
121 | state: String
122 | }
123 |
124 | # Inheriting from the 'Node' & 'Adress' type
125 | type Person inherits Node, Address {
126 | id: ID!
127 | streetAddress: String
128 | city: String
129 | state: String
130 | firstname: String
131 | lastname: String
132 | }
133 |
134 | `
135 | ```
136 |
137 | More details in the [code below](#type-inheritance).
138 |
139 | ## Generic Types
140 |
141 | ```js
142 | const schema = `
143 | # Defining a generic type
144 | type Paged {
145 | data: [T]
146 | cursor: ID
147 | }
148 |
149 | type Question {
150 | name: String!
151 | text: String!
152 | }
153 |
154 | # Using the generic type
155 | type Student {
156 | name: String
157 | questions: Paged
158 | }
159 |
160 | # Using the generic type
161 | type Teacher {
162 | name: String
163 | students: Paged
164 | }
165 | `
166 | ```
167 |
168 | More details in the [code below](#generic-types).
169 |
170 | ## Metadata Decoration
171 |
172 | ```js
173 | const schema = `
174 | # Defining a custom 'node' metadata attribute
175 | @node
176 | type Node {
177 | id: ID!
178 | }
179 |
180 | type Student inherits Node {
181 | name: String
182 |
183 | # Defining another custom 'edge' metadata, and supporting a generic type
184 | @edge(some other metadata using whatever syntax I want)
185 | questions: [String]
186 | }
187 | `
188 | ```
189 |
190 | The enriched schema provides a richer and more compact notation. The transpiler converts the enriched schema into the standard expected by [graphql.js](https://github.com/graphql/graphql-js) (using the _buildSchema_ method) as well as the [Apollo Server](https://github.com/apollographql/graphql-tools). For more details on how to extract those extra information from the string schema, use the method _getSchemaAST_ (example in section [_Metadata Decoration_](#metadata-decoration)).
191 |
192 | _Metadata_ can be added to decorate the schema types and properties. Add whatever you want as long as it starts with _@_ and start hacking your schema. The original intent of that feature was to decorate the schema with metadata _@node_ and _@edge_ so we could add metadata about the nature of the relations between types.
193 |
194 | Metadata can also be used to customize generic types names as shown in section [How to use a custom name on generic types?](#how-to-use-a-custom-name-on-generic-types).
195 |
196 | ## Deconstructing - Transforming - Rebuilding Queries
197 |
198 | This feature allows your GraphQl server to deconstruct any GraphQl query as an AST that can then be filtered and modified based on your requirements. That AST can then be rebuilt as a valid GraphQL query. A great example of that feature in action is the [__graphql-authorize__](https://github.com/nicolasdao/graphql-authorize.git) middleware for [__graphql-serverless__](https://github.com/nicolasdao/graphql-serverless) which filters query's properties based on the user's rights.
199 |
200 | For a concrete example, refer to the [code below](#deconstructing---transforming---rebuilding-queries-1).
201 |
202 | # How To
203 | ## How to use a custom name on generic types?
204 |
205 | Use the special keyword `@alias` as follow:
206 |
207 | ```js
208 | const schema = `
209 | type Post {
210 | code: String
211 | }
212 |
213 | type Brand {
214 | id: ID!
215 | name: String
216 | posts: Page
217 | }
218 |
219 | @alias((T) => T + 's')
220 | type Page {
221 | data: [T]
222 | }
223 | `
224 | ```
225 |
226 | After transpilation, the resulting schema is:
227 |
228 | ```js
229 | const output = transpileSchema(schema)
230 | // output:
231 | // =======
232 | // type Post {
233 | // code: String
234 | // }
235 | //
236 | // type Brand {
237 | // id: ID!
238 | // name: String
239 | // posts: Posts
240 | // }
241 | //
242 | // type Posts {
243 | // data: [Post]
244 | // }
245 |
246 | ````
247 |
248 | # Examples
249 | _WARNING: the following examples will be based on '[graphql-tools](https://github.com/apollographql/graphql-tools)' from the Apollo team, but the string schema could also be used with the 'buildSchema' method from graphql.js_
250 |
251 | ### Type Inheritance
252 | _NOTE: The examples below only use 'type', but it would also work on 'input' and 'interface'_
253 |
254 | __*Before graphql-s2s*__
255 | ```js
256 | const schema = `
257 | type Teacher {
258 | id: ID!
259 | creationDate: String
260 |
261 | firstname: String!
262 | middlename: String
263 | lastname: String!
264 | age: Int!
265 | gender: String
266 |
267 | title: String!
268 | }
269 |
270 | type Student {
271 | id: ID!
272 | creationDate: String
273 |
274 | firstname: String!
275 | middlename: String
276 | lastname: String!
277 | age: Int!
278 | gender: String
279 |
280 | nickname: String!
281 | }`
282 |
283 | ```
284 | __*After graphql-s2s*__
285 | ```js
286 | const schema = `
287 | type Node {
288 | id: ID!
289 | creationDate: String
290 | }
291 |
292 | type Person inherits Node {
293 | firstname: String!
294 | middlename: String
295 | lastname: String!
296 | age: Int!
297 | gender: String
298 | }
299 |
300 | type Teacher inherits Person {
301 | title: String!
302 | }
303 |
304 | type Student inherits Person {
305 | nickname: String!
306 | }`
307 |
308 | ```
309 |
310 | __*Full code example*__
311 |
312 | ```js
313 | const { transpileSchema } = require('graphql-s2s').graphqls2s
314 | const { makeExecutableSchema } = require('graphql-tools')
315 | const { students, teachers } = require('./dummydata.json')
316 |
317 | const schema = `
318 | type Node {
319 | id: ID!
320 | creationDate: String
321 | }
322 |
323 | type Person inherits Node {
324 | firstname: String!
325 | middlename: String
326 | lastname: String!
327 | age: Int!
328 | gender: String
329 | }
330 |
331 | type Teacher inherits Person {
332 | title: String!
333 | }
334 |
335 | type Student inherits Person {
336 | nickname: String!
337 | questions: [Question]
338 | }
339 |
340 | type Question inherits Node {
341 | name: String!
342 | text: String!
343 | }
344 |
345 | type Query {
346 | # ### GET all users
347 | #
348 | students: [Student]
349 |
350 | # ### GET all teachers
351 | #
352 | teachers: [Teacher]
353 | }
354 | `
355 |
356 | const resolver = {
357 | Query: {
358 | students(root, args, context) {
359 | return Promise.resolve(students)
360 | },
361 |
362 | teachers(root, args, context) {
363 | return Promise.resolve(teachers)
364 | }
365 | }
366 | }
367 |
368 | const executableSchema = makeExecutableSchema({
369 | typeDefs: [transpileSchema(schema)],
370 | resolvers: resolver
371 | })
372 | ```
373 |
374 | ### Generic Types
375 | _NOTE: The examples below only use 'type', but it would also work on 'input'_
376 |
377 | __*Before graphql-s2s*__
378 | ```js
379 | const schema = `
380 | type Teacher {
381 | id: ID!
382 | creationDate: String
383 | firstname: String!
384 | middlename: String
385 | lastname: String!
386 | age: Int!
387 | gender: String
388 | title: String!
389 | }
390 |
391 | type Student {
392 | id: ID!
393 | creationDate: String
394 | firstname: String!
395 | middlename: String
396 | lastname: String!
397 | age: Int!
398 | gender: String
399 | nickname: String!
400 | questions: Questions
401 | }
402 |
403 | type Question {
404 | id: ID!
405 | creationDate: String
406 | name: String!
407 | text: String!
408 | }
409 |
410 | type Teachers {
411 | data: [Teacher]
412 | cursor: ID
413 | }
414 |
415 | type Students {
416 | data: [Student]
417 | cursor: ID
418 | }
419 |
420 | type Questions {
421 | data: [Question]
422 | cursor: ID
423 | }
424 |
425 | type Query {
426 | # ### GET all users
427 | #
428 | students: Students
429 |
430 | # ### GET all teachers
431 | #
432 | teachers: Teachers
433 | }
434 | `
435 |
436 | ```
437 | __*After graphql-s2s*__
438 | ```js
439 | const schema = `
440 | type Paged {
441 | data: [T]
442 | cursor: ID
443 | }
444 |
445 | type Node {
446 | id: ID!
447 | creationDate: String
448 | }
449 |
450 | type Person inherits Node {
451 | firstname: String!
452 | middlename: String
453 | lastname: String!
454 | age: Int!
455 | gender: String
456 | }
457 |
458 | type Teacher inherits Person {
459 | title: String!
460 | }
461 |
462 | type Student inherits Person {
463 | nickname: String!
464 | questions: Paged
465 | }
466 |
467 | type Question inherits Node {
468 | name: String!
469 | text: String!
470 | }
471 |
472 | input Filter {
473 | field: FilterFields!,
474 | value: String!
475 | }
476 |
477 | enum TeachersFilterFields {
478 | firstName
479 | lastName
480 | }
481 |
482 | type Query {
483 | # ### GET all users
484 | #
485 | students: Paged
486 |
487 | # ### GET all teachers
488 | # You can use generic types on parameters, too.
489 | #
490 | teachers(filter: Filter): Paged
491 | }
492 | `
493 | ```
494 | This is very similar to C# or Java generic classes. What the transpiler will do is to simply recreate 3 types (one for Paged\, Paged\ and Paged\), and one input (Filter\). If we take the Paged\ example, the transpiled type will be:
495 | ```js
496 | type PagedQuestion {
497 | data: [Question]
498 | cursor: ID
499 | }
500 | ```
501 |
502 | __*Full code example*__
503 |
504 | ```js
505 | const { transpileSchema } = require('graphql-s2s').graphqls2s
506 | const { makeExecutableSchema } = require('graphql-tools')
507 | const { students, teachers } = require('./dummydata.json')
508 |
509 | const schema = `
510 | type Paged {
511 | data: [T]
512 | cursor: ID
513 | }
514 |
515 | type Node {
516 | id: ID!
517 | creationDate: String
518 | }
519 |
520 | type Person inherits Node {
521 | firstname: String!
522 | middlename: String
523 | lastname: String!
524 | age: Int!
525 | gender: String
526 | }
527 |
528 | type Teacher inherits Person {
529 | title: String!
530 | }
531 |
532 | type Student inherits Person {
533 | nickname: String!
534 | questions: Paged
535 | }
536 |
537 | type Question inherits Node {
538 | name: String!
539 | text: String!
540 | }
541 |
542 | type Query {
543 | # ### GET all users
544 | #
545 | students: Paged
546 |
547 | # ### GET all teachers
548 | #
549 | teachers: Paged
550 | }
551 | `
552 |
553 | const resolver = {
554 | Query: {
555 | students(root, args, context) {
556 | return Promise.resolve({ data: students.map(s => ({ __proto__:s, questions: { data: s.questions, cursor: null }})), cursor: null })
557 | },
558 |
559 | teachers(root, args, context) {
560 | return Promise.resolve({ data: teachers, cursor: null });
561 | }
562 | }
563 | }
564 |
565 | const executableSchema = makeExecutableSchema({
566 | typeDefs: [transpileSchema(schema)],
567 | resolvers: resolver
568 | })
569 | ```
570 |
571 | ### Metadata Decoration
572 | Define your own custom metadata and decorate your GraphQL schema with new types of data. Let's imagine we want to explicitely add metadata about the type of relations between nodes, we could write something like this:
573 | ```js
574 | const { getSchemaAST } = require('graphql-s2s').graphqls2s
575 | const schema = `
576 | @node
577 | type User {
578 | @edge('<-[CREATEDBY]-')
579 | posts: [Post]
580 | }
581 | `
582 |
583 | const schemaObjects = getSchemaAST(schema);
584 |
585 | // -> schemaObjects
586 | // {
587 | // "type": "TYPE",
588 | // "name": "User",
589 | // "metadata": {
590 | // "name": "node",
591 | // "body": "",
592 | // "schemaType": "TYPE",
593 | // "schemaName": "User", "parent": null
594 | // },
595 | // "genericType": null,
596 | // "blockProps": [{
597 | // "comments": "",
598 | // "details": {
599 | // "name": "posts",
600 | // "metadata": {
601 | // "name": "edge",
602 | // "body": "(\'<-[CREATEDBY]-\')",
603 | // "schemaType": "PROPERTY",
604 | // "schemaName": "posts: [Post]",
605 | // "parent": {
606 | // "type": "TYPE",
607 | // "name": "User",
608 | // "metadata": {
609 | // "type": "TYPE",
610 | // "name": "node"
611 | // }
612 | // }
613 | // },
614 | // "params": null,
615 | // "result": {
616 | // "originName": "[Post]",
617 | // "isGen": false,
618 | // "name": "[Post]"
619 | // }
620 | // },
621 | // "value": "posts: [Post]"
622 | // }],
623 | // "inherits": null,
624 | // "implements": null
625 | // }
626 | ```
627 | ### Deconstructing - Transforming - Rebuilding Queries
628 | This feature allows your GraphQl server to deconstruct any GraphQl query as an AST that can then be filtered and modified based on your requirements. That AST can then be rebuilt as a valid GraphQL query. A great example of that feature in action is the [__graphql-authorize__](https://github.com/nicolasdao/graphql-authorize.git) middleware for [__graphql-serverless__](https://github.com/nicolasdao/graphql-serverless) which filters query's properties based on the user's rights.
629 |
630 | ```js
631 | const { getQueryAST, buildQuery, getSchemaAST } = require('graphql-s2s').graphqls2s
632 | const schema = `
633 | type Property {
634 | name: String
635 | @auth
636 | address: String
637 | }
638 |
639 | input InputWhere {
640 | name: String
641 | locations: [LocationInput]
642 | }
643 |
644 | input LocationInput {
645 | type: String
646 | value: String
647 | }
648 |
649 | type Query {
650 | properties(where: InputWhere): [Property]
651 | }`
652 |
653 | const query = `
654 | query {
655 | properties(where: { name: "Love", locations: [{ type: "house", value: "Bellevue hill" }] }){
656 | name
657 | address
658 | }
659 | }`
660 |
661 | const schemaAST = getSchemaAST(schema)
662 | const queryAST = getQueryAST(query, null, schemaAST)
663 | const rebuiltQuery = buildQuery(queryAST.filter(x => !x.metadata || x.metadata.name != 'auth'))
664 |
665 | // query {
666 | // properties(where:{name:"Love",locations:[{type:"house",value:"Bellevue hill"}]}){
667 | // name
668 | // }
669 | // }
670 | ```
671 |
672 | Notice that the original query was requesting the `address` property. Because we decorated that property with the custom metadata `@auth` (feature demonstrated previously [Metadata Decoration](#metadata-decoration)), we were able to filter that property to then rebuilt the query without it.
673 |
674 | #### API
675 |
676 | __*getQueryAST(query, operationName, schemaAST, options): QueryAST*__
677 |
678 | Returns an GraphQl query AST.
679 |
680 | | Arguments | type | Description |
681 | | :------------- |:-------:| :------------ |
682 | | query | String | GraphQl Query. |
683 | | operationName | String | GraphQl query operation. Only useful if multiple operations are defined in a single query, otherwise use `null`. |
684 | | schemaAST | Object | Original GraphQl schema AST obtained thanks to the `getSchemaAST` function. |
685 | | options.defrag | Boolean | If set to true and if the query contained fragments, then all fragments are replaced by their explicit definition in the AST. |
686 |
687 | __*QueryAST Object Structure*__
688 |
689 | | Properties | type | Description |
690 | | :--------- |:------:| :------------ |
691 | | name | String | Field's name. |
692 | | kind | String | Field's kind. |
693 | | type | String | Field's type. |
694 | | metadata | String | Field's metadata. |
695 | | args | Array | Array of argument objects. |
696 | | properties | Array | Array of QueryAST objects. |
697 |
698 | __*QueryAST.filter((ast:QueryAST) => ...): QueryAST*__
699 |
700 | Returns a new QueryAST object where only ASTs complying to the predicate `ast => ...` are left.
701 |
702 | __*QueryAST.propertyPaths((ast:QueryAST) => ...): [String]*__
703 |
704 | Returns an array of strings. Each one represents the path to the query property that matches the predicate `ast => ...`.
705 |
706 | __*QueryAST.containsProp(property:String): Boolean*__
707 |
708 | Returns a boolean indicating the presence of a property in the GraphQl query. Example:
709 |
710 | ```js
711 | const schema = `
712 | type User {
713 | id: ID!
714 | name: String
715 | details: UserDetails
716 | }
717 |
718 | type UserDetails {
719 | gender: String
720 | }
721 |
722 | type Query {
723 | users: [User]
724 | }
725 | `
726 | const query = `
727 | {
728 | users {
729 | id
730 | details {
731 | gender
732 | }
733 | }
734 | }`
735 | const schemaAST = getSchemaAST(schema)
736 | const queryAST = getQueryAST(query, null, schemaAST)
737 | queryAST.containsProp('users.id') // true
738 | queryAST.containsProp('users.details.gender') // true
739 | queryAST.containsProp('details.gender') // true
740 | queryAST.containsProp('users.name') // false
741 | ```
742 |
743 | __*QueryAST.some((ast:QueryAST) => ...): Boolean*__
744 |
745 | Returns a boolean indicating whether the QueryAST contains at least one AST matching the predicate `ast => ...`.
746 |
747 | __*buildQuery(ast:QueryAST): String*__
748 |
749 | Rebuilds a valid GraphQl query from a QueryAST object.
750 |
751 | # Contribute
752 | ## Step 1. Don't Forget To Test Your Feature
753 | We only accept pull request that have been thoroughly tested. To do so, simply add your test under the `test/browser/graphqls2s.js` file.
754 |
755 | Once that's done, simply run your the following command to test your features:
756 | ```
757 | npm run test:dev
758 | ```
759 | This sets an environment variable that configure the project to load the main dependency from the _src_ folder (source code in ES6) instead of the _lib_ folder (transpiled ES5 code).
760 |
761 | ## Step 2. Compile & Rerun Your Test Before Pushing
762 | ```
763 | npm run dev
764 | npm run build
765 | npm test
766 | ```
767 | This project is built using Javascript ES6. Each version is also transpiled to ES5 using Babel through Webpack 2, so this project can run in the browser. In order to write unit test only once instead of duplicating it for each version of Javascript, the all unit tests have been written using Javascript ES5 in mocha. That means that if you want to test the project after some changes, you will need to first transpile the project to ES5.
768 |
769 | # This Is What We re Up To
770 | We are Neap, an Australian Technology consultancy powering the startup ecosystem in Sydney. We simply love building Tech and also meeting new people, so don't hesitate to connect with us at [https://neap.co](https://neap.co).
771 |
772 | Our other open-sourced projects:
773 | #### GraphQL
774 | * [__*graphql-s2s*__](https://github.com/nicolasdao/graphql-s2s): Add GraphQL Schema support for type inheritance, generic typing, metadata decoration. Transpile the enriched GraphQL string schema into the standard string schema understood by graphql.js and the Apollo server client.
775 | * [__*schemaglue*__](https://github.com/nicolasdao/schemaglue): Naturally breaks down your monolithic graphql schema into bits and pieces and then glue them back together.
776 | * [__*graphql-authorize*__](https://github.com/nicolasdao/graphql-authorize.git): Authorization middleware for [graphql-serverless](https://github.com/nicolasdao/graphql-serverless). Add inline authorization straight into your GraphQl schema to restrict access to certain fields based on your user's rights.
777 |
778 | #### React & React Native
779 | * [__*react-native-game-engine*__](https://github.com/bberak/react-native-game-engine): A lightweight game engine for react native.
780 | * [__*react-native-game-engine-handbook*__](https://github.com/bberak/react-native-game-engine-handbook): A React Native app showcasing some examples using react-native-game-engine.
781 |
782 | #### General Purposes
783 | * [__*core-async*__](https://github.com/nicolasdao/core-async): JS implementation of the Clojure core.async library aimed at implementing CSP (Concurrent Sequential Process) programming style. Designed to be used with the npm package 'co'.
784 | * [__*jwt-pwd*__](https://github.com/nicolasdao/jwt-pwd): Tiny encryption helper to manage JWT tokens and encrypt and validate passwords using methods such as md5, sha1, sha256, sha512, ripemd160.
785 |
786 | #### Google Cloud Platform
787 | * [__*google-cloud-bucket*__](https://github.com/nicolasdao/google-cloud-bucket): Nodejs package to manage Google Cloud Buckets and perform CRUD operations against them.
788 | * [__*google-cloud-bigquery*__](https://github.com/nicolasdao/google-cloud-bigquery): Nodejs package to manage Google Cloud BigQuery datasets, and tables and perform CRUD operations against them.
789 | * [__*google-cloud-tasks*__](https://github.com/nicolasdao/google-cloud-tasks): Nodejs package to push tasks to Google Cloud Tasks. Include pushing batches.
790 |
791 | # License
792 | Copyright (c) 2017-2019, Neap Pty Ltd.
793 | All rights reserved.
794 |
795 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
796 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
797 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
798 | * Neither the name of Neap Pty Ltd nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
799 |
800 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
801 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
802 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
803 | DISCLAIMED. IN NO EVENT SHALL NEAP PTY LTD BE LIABLE FOR ANY
804 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
805 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
806 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
807 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
808 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
809 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
810 |
811 | 
812 |
--------------------------------------------------------------------------------
/_doc/index.md:
--------------------------------------------------------------------------------
1 | # Tips On How To Debug
2 | ## Tip 1 - Have a look at the overal AST object
3 |
4 | Most of the bugs we've received so far come from errors in the way the schema is parsed into an AST. If there is an error in that AST, then the rebuilt schema is also compromised. So when an error similar to `My transpiled schema is not working` arises, start by looking into the output of the `_getSchemaBits` function in the `src/graphqls2s.js` file.
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphql-s2s",
3 | "version": "0.22.0",
4 | "description": "Transpile an enriched GraphQL string schema (e.g. support for metadata, inheritance, generic types, ...) into the standard string schema understood by graphql.js and the Apollo server client.",
5 | "main": "./src/graphqls2s.js",
6 | "scripts": {
7 | "build": "WEBPACK_ENV=build webpack",
8 | "dev": "WEBPACK_ENV=dev webpack",
9 | "lint": "eslint src/ test/ --fix",
10 | "push": "git push --follow-tags origin master && npm publish",
11 | "rls": "standard-version --release-as",
12 | "test": "mocha ./test/node/*.js",
13 | "test:dev": "MOCHA_ENV=dev mocha ./test/node/*.js",
14 | "v": "node -e \"console.log(require('./package.json').version)\""
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/nicolasdao/graphql-s2s.git"
19 | },
20 | "keywords": [
21 | "graphql",
22 | "schema",
23 | "transpiler",
24 | "metadata",
25 | "inheritance",
26 | "generic",
27 | "types",
28 | "neap"
29 | ],
30 | "author": "Nicolas Dao",
31 | "contributors": [
32 | {
33 | "name": "Nicolas Dao",
34 | "email": "nic@neap.co",
35 | "url": "https://github.com/nicolasdao"
36 | },
37 | {
38 | "name": "Brendan Johnson",
39 | "email": "brendan@neap.co",
40 | "url": "https://github.com/BrendanJohnson"
41 | },
42 | {
43 | "name": "Boris Berak",
44 | "email": "boris@neap.co",
45 | "url": "https://github.com/bberak"
46 | },
47 | {
48 | "name": "Pankaj Parkar",
49 | "email": "pankajparkar@hotmail.com",
50 | "url": "https://github.com/pankajparkar"
51 | }
52 | ],
53 | "license": "BSD-3-Clause",
54 | "bugs": {
55 | "url": "https://github.com/nicolasdao/graphql-s2s/issues"
56 | },
57 | "homepage": "https://github.com/nicolasdao/graphql-s2s#readme",
58 | "publishConfig": {
59 | "access": "public"
60 | },
61 | "dependencies": {
62 | "graphql": "^0.11.7",
63 | "lodash": "^4.17.21",
64 | "shortid": "^2.2.16"
65 | },
66 | "devDependencies": {
67 | "@babel/core": "^7.15.8",
68 | "@babel/preset-env": "^7.15.8",
69 | "@babel/runtime-corejs3": "^7.15.4",
70 | "babel-loader": "^8.2.2",
71 | "chai": "^4.3.4",
72 | "core-js": "^3.18.2",
73 | "eslint": "^7.32.0",
74 | "mocha": "^9.1.2",
75 | "standard-version": "^9.3.1",
76 | "webpack": "^5.58.0",
77 | "webpack-cli": "^4.9.0"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/graphmetadata.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018, Neap Pty Ltd.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | const _ = require('lodash')
9 | const { chain, log, escapeGraphQlSchema, removeMultiSpaces, matchLeftNonGreedy, newShortId } = require('./utilities')
10 |
11 | const carrReturnEsc = '░'
12 | const tabEsc = '_t_'
13 |
14 | /**
15 | * Remove directives
16 | * @param {String} schema Escaped schema (i.e., without tabs or carriage returns. The CR have been replaced by '░' )
17 | * @return {String} output.schema Schema without directives
18 | * @return {Array} output.directives
19 | * @return {String} output.directives[0].name Directive's name
20 | * @return {String} output.directives[0].definition Directive's definition
21 | * @return {Array} output.directives[0].instances
22 | * @return {String} output.directives[0].instances[0].id Unique identifier that replaces the directive's instance value
23 | * @return {String} output.directives[0].instances[0].value Directive's instance value
24 | */
25 | const removeDirectives = (schema = '') => {
26 | if (!schema)
27 | return { schema, directives:null }
28 |
29 | schema += '░'
30 | const directives = []
31 | const d = schema.match(/directive\s(.*?)@(.*?)(\((.*?)\)\son\s(.*?)░|\son\s(.*?)░)/mg) || []
32 | d.forEach(directive => {
33 | const directiveName = directive.match(/@(.*?)[\s(]/)[0].replace(/(░)\s/g,'').trim().replace('(','')
34 | schema = schema.replace(directive, '')
35 | if (!schema.match(/░$/))
36 | schema += '░'
37 |
38 | const dInstances = schema.match(new RegExp(`${directiveName}(.*?)░`, 'g')) || []
39 | const instances = []
40 | dInstances.forEach(dInst => {
41 | const id = `_${newShortId()}_`
42 | const inst = dInst.replace(/░$/,'')
43 | schema = schema.replace(inst, id)
44 | instances.push({ id, value: inst })
45 | })
46 |
47 | directives.push({ name: directiveName.replace('@',''), body: directive, directive: true, directiveValues: instances })
48 | })
49 |
50 | // Get the rogue directives, i.e., the directives defined immediately after a field (must be on the same line)
51 | // and have not been escaped before because they do not have an explicit definition in the current schema (scenario
52 | // of AWS AppSync where the @aws_subscribe is defined outside of the developer reach)
53 | //
54 | // IMPORTANT: The code below mutates the 'schema' variable
55 | const rogueDirectives = (schema.replace(/░/g,'░░').match(/░\s*[a-zA-Z0-9_]+([^░]*?)@(.*?)░/g) || [])
56 | .map(m => m.replace(/^(.*?)@/, '@').replace(/\s*░$/, ''))
57 | .reduce((acc,m) => {
58 | m = m.trim().replace(/{$/, '')
59 | const directiveName = m.match(/^@[a-zA-Z0-9_]+/)[0].slice(1)
60 | const directiveInstanceId = `_${newShortId()}_`
61 | schema = schema.replace(m, directiveInstanceId)
62 | if (acc[directiveName])
63 | acc[directiveName].directiveValues.push({ id: directiveInstanceId, value: m })
64 | else {
65 | acc.push(directiveName)
66 | acc[directiveName] = {
67 | name: directiveName,
68 | body: '',
69 | directive: true,
70 | directiveValues: [{ id: directiveInstanceId, value: m }]
71 | }
72 | }
73 | return acc
74 | }, [])
75 |
76 | if (rogueDirectives.length > 0)
77 | directives.push(...rogueDirectives.map(x => rogueDirectives[x]))
78 |
79 | return { schema, directives }
80 | }
81 |
82 | const reinsertDirectives = (schema='', directives=[]) => {
83 | if (!schema)
84 | return schema
85 |
86 | const directiveDefinitions = directives.map(x => x.body).join('░')
87 | directives.forEach(({ directiveValues=[] }) => directiveValues.forEach(({ id, value }) => {
88 | schema = schema.replace(id, value)
89 | }))
90 |
91 | return `${directiveDefinitions}${schema}`
92 | }
93 |
94 | /**
95 | * Extracts the graph metadata as well as the directives from a GraphQL schema
96 | *
97 | * @param {string} schema GraphQL schema containing Graph metadata (e.g. @node, @edge, ...)
98 | * @return {Array} graphMetadata
99 | * @return {String} graphMetadata.escSchema Escaped schema
100 | * @return {String} graphMetadata[0].name
101 | * @return {String} graphMetadata[0].body
102 | * @return {String} graphMetadata[0].schemaType
103 | * @return {String} graphMetadata[0].schemaName
104 | * @return {String} graphMetadata[0].parent
105 | * @return {String} graphMetadata[0].directive
106 | * @return {String} graphMetadata[0].directiveValues
107 | */
108 | const extractGraphMetadata = (schema = '') => {
109 | const { schema:escSchema, directives } = removeDirectives(escapeGraphQlSchema(schema, carrReturnEsc, tabEsc).replace(/_t_/g, ' '))
110 | const attrMatches = escSchema.match(/@(.*?)(░)(.*?)({|░)/mg)
111 | let graphQlMetadata = chain(_(attrMatches).map(m => chain(m.split(carrReturnEsc)).next(parts => {
112 | if (parts.length < 2)
113 | throw new Error(`Schema error: Misused metadata attribute in '${parts.join(' ')}.'`)
114 |
115 | const typeMatch = `${parts[0].trim()} `.match(/@(.*?)(\s|{|\(|\[)/)
116 | if (!typeMatch) {
117 | const msg = `Schema error: Impossible to extract type from metadata attribute ${parts[0]}`
118 | log(msg)
119 | throw new Error(msg)
120 | }
121 |
122 | const attrName = typeMatch[1].trim()
123 | const attrBody = parts[0].replace(`@${attrName}`, '').trim()
124 |
125 | const { schemaType, value } = chain(removeMultiSpaces(parts[1].trim())).next(t => t.match(/^(type\s|input\s|enum\s|interface\s)/)
126 | ? chain(t.split(' ')).next(bits => ({ schemaType: bits[0].toUpperCase(), value: bits[1].replace(/ /g, '').replace(/{$/, '') })).val()
127 | : { schemaType: 'PROPERTY', value: t }).val()
128 |
129 | const parent = schemaType == 'PROPERTY'
130 | ? chain(escSchema.split(m).join('___replace___')).next(s => matchLeftNonGreedy(s, '(type |input |enum |interface )', '___replace___'))
131 | .next(m2 => {
132 | if (!m2) throw new Error(`Schema error: Property '${value}' with metadata '@${value}' does not live within any schema type (e.g. type, enum, interface, input, ...)`)
133 | const parentSchemaType = m2[1].trim().toUpperCase()
134 | const parentSchemaTypeName = m2[2].replace(/{/g, ' ').replace(/░/g, ' ').trim().split(' ')[0]
135 | return { type: parentSchemaType, name: parentSchemaTypeName }
136 | })
137 | .val()
138 | : null
139 |
140 | return { name: attrName, body: attrBody, schemaType: schemaType, schemaName: value, parent: parent }
141 | }).val()))
142 | .next(metadata => metadata.map(m => m.schemaType == 'PROPERTY'
143 | ? (m.parent
144 | ? chain(metadata.find(x => x.schemaType == m.parent.type && x.schemaName == m.parent.name))
145 | .next(v => v ? (() => { m.parent.metadata = { type: v.schemaType, name: v.name }; return m })() : m)
146 | .val()
147 | : m)
148 | : m))
149 | .next(metadata => _.toArray(metadata).concat(directives))
150 | .val() || []
151 |
152 | graphQlMetadata.escSchema = escSchema
153 |
154 | return graphQlMetadata
155 | }
156 |
157 | /**
158 | * [description]
159 | * @param {String} schema Schema with non-standard syntax.
160 | * @return {String} output.stdSchema Schema without all the non-standard metadata.
161 | * @return {[Metatdata]} output.metadata Array of Metadata object
162 | */
163 | const removeGraphMetadata = (schema = '') => {
164 | const meta = extractGraphMetadata(schema) || []
165 | const directives = meta.filter(m => m.directive)
166 | const schemaWithNoMeta = (reinsertDirectives(meta.escSchema.replace(/@(.*?)░/g, ''), directives) || '').replace(/░/g, '\n')
167 | return { stdSchema: schemaWithNoMeta, metadata: meta }
168 | }
169 |
170 | module.exports = {
171 | extractGraphMetadata,
172 | removeGraphMetadata
173 | }
174 |
175 |
176 |
177 |
--------------------------------------------------------------------------------
/src/graphqls2s.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018, Neap Pty Ltd.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | // Inheritance:
10 | // ============
11 | // _getObjWithExtensions: This function is the one that compiles types that inherits from others.
12 | //
13 | // Generic Types:
14 | // ==============
15 | // _createNewSchemaObjectFromGeneric: This function is the one that creates new types from Generic Types.
16 |
17 | const _ = require('lodash')
18 | const { chain, log, escapeGraphQlSchema, getQueryAST, buildQuery, newShortId, isScalarType } = require('./utilities')
19 | const { extractGraphMetadata, removeGraphMetadata } = require('./graphmetadata')
20 |
21 | const GENERICTYPEREGEX = /<(.*?)>/
22 | const TYPENAMEREGEX = /type\s(.*?){/
23 | const INPUTNAMEREGEX = /input\s(.*?){/
24 | const ENUMNAMEREGEX = /enum\s(.*?){/
25 | const INTERFACENAMEREGEX = /interface\s(.*?){/
26 | const ABSTRACTNAMEREGEX = /abstract\s(.*?){/
27 | const INHERITSREGEX = /inherits\s+[\w<>]+(?:\s*,\s*\w+)*/g
28 | const IMPLEMENTSREGEX = /implements\s(.*?)\{/mg
29 | const PROPERTYPARAMSREGEX = /\((.*?)\)/
30 |
31 | const TYPE_REGEX = { regex: /(extend type|type)\s(.*?){(.*?)░([^#]*?)}/mg, type: 'type' }
32 | const INPUT_REGEX = { regex: /(extend input|input)\s(.*?){(.*?)░([^#]*?)}/mg, type: 'input' }
33 | const ENUM_REGEX = { regex: /enum\s(.*?){(.*?)░([^#]*?)}/mg, type: 'enum' }
34 | const INTERFACE_REGEX = { regex: /(extend interface|interface)\s(.*?){(.*?)░([^#]*?)}/mg, type: 'interface' }
35 | const ABSTRACT_REGEX = { regex: /(extend abstract|abstract)\s(.*?){(.*?)░([^#]*?)}/mg, type: 'abstract' }
36 | const SCALAR_REGEX = { regex: /(.{1}|.{0})scalar\s(.*?)([^\s]*?)(?![a-zA-Z0-9])/mg, type: 'scalar' }
37 | const UNION_REGEX = { regex: /(.{1}|.{0})union([^\n]*?)\n/gm, type: 'union' }
38 |
39 | const carrReturnEsc = '░'
40 | const tabEsc = '_t_'
41 |
42 | let _s = {}
43 | const escapeGraphQlSchemaPlus = (sch, cr, t) => {
44 | if (!sch)
45 | return sch
46 |
47 | if (!_s[sch])
48 | _s[sch] = escapeGraphQlSchema(sch, cr, t)
49 |
50 | return _s[sch]
51 | }
52 |
53 | const escapeDirectives = (str, metadata) => {
54 | const directives = (metadata || [])
55 | .filter(({ directiveValues }) => directiveValues && directiveValues[0] && directiveValues[0].id && directiveValues[0].value)
56 | .reduce((acc, { directiveValues }) => {
57 | acc.push(...directiveValues)
58 | return acc
59 | }, [])
60 |
61 | return directives.reduce((acc,{ id, value }) => {
62 | acc[0] = acc[0].replace(value, id)
63 | acc[1].push({ id, value })
64 | return acc
65 | },[str, []])
66 | }
67 |
68 | /**
69 | * Gets a first rough breakdown of the string schema.
70 | *
71 | * @param {String} sch Original GraphQl Schema
72 | * @param {String} metadata[].name e.g., "cypher"
73 | * @param {String} metadata[].body
74 | * @param {Boolean} metadata[].directive
75 | * @param {String} metadata[].directiveValues[].id e.g., "_RghS1T9k5_"
76 | * @param {String} metadata[].directiveValues[].value e.g., "@cypher(statement: \"CREATE (a:Area {name: $name, creationDate: timestamp()}) RETURN a\")"
77 | * @return {Array} Using regex, the interfaces, types, inputs, enums and abstracts entities are isolated
78 | * e.g. [{
79 | * property: 'type Query { bars: [Bar]! }',
80 | * block: [ 'bars: [Bar]!' ],
81 | * extend: false
82 | * },{
83 | * property: 'type Bar { id: ID }',
84 | * block: [ 'id: ID' ],
85 | * extend: false
86 | * }]
87 | */
88 | const _getSchemaBits = (sch='', metadata) => {
89 | const escapedSchemaWithComments = escapeGraphQlSchemaPlus(sch, carrReturnEsc, tabEsc)
90 |
91 | const comments = [
92 | ...(escapedSchemaWithComments.match(/#(.*?)░/g) || []),
93 | ...(escapedSchemaWithComments.match(/"""░(.*?)░\s*"""░/g) || []),
94 | ...(escapedSchemaWithComments.match(/"([^"]+)"░/g) || []),
95 | ]
96 | const { schema:escSchemaWithEscComments, tokens } = comments.reduce((acc,m) => {
97 | const commentToken = `#${newShortId()}░`
98 | acc.schema = acc.schema.replace(m, commentToken)
99 | acc.tokens.push({ id: commentToken, value: m })
100 | return acc
101 | }, { schema: escapedSchemaWithComments, tokens: [] })
102 |
103 | // We append '\n' to help isolating the 'union'
104 | const schemaWithoutComments = ' ' + sch.replace(/#(.*?)\n/g, '').replace(/"""\s*\n([^]*?)\n\s*"""\s*\n/g, '').replace(/"([^"]+)"\n/g, '') + '\n'
105 | const escapedSchemaWithoutComments = escapeGraphQlSchemaPlus(schemaWithoutComments, carrReturnEsc, tabEsc)
106 | const [escapedSchemaWithEscCommentsAndDirectives, directives] = escapeDirectives(escSchemaWithEscComments, metadata)
107 |
108 | return _.flatten([TYPE_REGEX, INPUT_REGEX, ENUM_REGEX, INTERFACE_REGEX, ABSTRACT_REGEX, SCALAR_REGEX, UNION_REGEX]
109 | .map(rx => {
110 | // 1. Apply the regex matching
111 | return chain((
112 | rx.type == 'scalar' ? escapedSchemaWithoutComments :
113 | rx.type == 'union' ? schemaWithoutComments :
114 | escapedSchemaWithEscCommentsAndDirectives).match(rx.regex) || [])
115 | // 2. Filter the right matches
116 | .next(regexMatches =>
117 | rx.type == 'scalar' ? regexMatches.filter(m => m.indexOf('scalar') == 0 || m.match(/^(?![a-zA-Z0-9])/)) :
118 | rx.type == 'union' ? regexMatches.filter(m => m.indexOf('union') == 0 || m.match(/^(?![a-zA-Z0-9])/)) : regexMatches)
119 | // 3. Replace the escaped comments with their true value
120 | .next(regexMatches => regexMatches.map(b => (b.match(/#(.*?)░/g) || []).reduce((acc,m) => {
121 | const value = (tokens.find(t => t.id == m) || {}).value
122 | return value ? acc.replace(m, value) : acc
123 | }, b)))
124 | // 4. Breackdown each match into 'property', 'block' and 'extend'
125 | .next(regexMatches => {
126 | const transform =
127 | rx.type == 'scalar' ? _breakdownScalarBit :
128 | rx.type == 'union' ? _breakdownUnionBit : (str => _breakdownSchemabBit(str, directives))
129 | return regexMatches.map(str => transform(str))
130 | })
131 | .val()}))
132 | }
133 |
134 | const _breakdownSchemabBit = (str, directives) => {
135 | directives = directives || []
136 | const blockMatch = str.match(/{(.*?)░([^#]*?)}/)
137 | if (!blockMatch) {
138 | const msg = 'Schema error: Missing block'
139 | log(msg)
140 | throw new Error(msg)
141 | }
142 |
143 | const [blockWithDirectives, rawProperty] = directives.reduce((acc, { id, value }) => {
144 | acc[0] = acc[0].replace(id,value)
145 | acc[1] = acc[1].replace(id,value)
146 | return acc
147 | }, [blockMatch[0], str.split(carrReturnEsc).join(' ').split(tabEsc).join(' ').replace(/ +(?= )/g,'').trim()])
148 |
149 | const block = _.toArray(_(blockWithDirectives.replace(/_t_/g, '').replace(/^{/,'').replace(/}$/,'').split(carrReturnEsc).map(x => x.trim())).filter(x => x != ''))
150 |
151 | const { property, extend } = rawProperty.indexOf('extend') == 0
152 | ? { property: rawProperty.replace('extend ', ''), extend: true }
153 | : { property: rawProperty, extend: false }
154 | return { property, block, extend }
155 | }
156 |
157 | const _breakdownScalarBit = str => {
158 | const block = (str.split(' ').slice(-1) || [])[0]
159 | return { property: `scalar ${block}`, block: block, extend: false }
160 | }
161 |
162 | const _breakdownUnionBit = str => {
163 | const block = str.replace(/(^union\s|\sunion\s|\n)/g, '').trim()
164 | return { property: `union ${block}`, block: block, extend: false }
165 | }
166 |
167 | /**
168 | *
169 | * @param {String} firstLine First line of a code block (e.g., 'type Page {')
170 | * @return {String} output.type Valid values: 'TYPE', 'ENUM', 'INPUT', 'INTERFACE', 'UNION', 'SCALAR'
171 | * @return {String} output.name e.g., 'Page'
172 | */
173 | const _getSchemaEntity = firstLine =>
174 | firstLine.indexOf('type') == 0 ? { type: 'TYPE', name: firstLine.match(/type\s+(.*?)\s+.*/)[1].trim() } :
175 | firstLine.indexOf('enum') == 0 ? { type: 'ENUM', name: firstLine.match(/enum\s+(.*?)\s+.*/)[1].trim() } :
176 | firstLine.indexOf('input') == 0 ? { type: 'INPUT', name: firstLine.match(/input\s+(.*?)\s+.*/)[1].trim() } :
177 | firstLine.indexOf('interface') == 0 ? { type: 'INTERFACE', name: firstLine.match(/interface\s+(.*?)\s+.*/)[1].trim() } :
178 | firstLine.indexOf('union') == 0 ? { type: 'UNION', name: firstLine.match(/union\s+(.*?)\s+.*/)[1].trim() } :
179 | firstLine.indexOf('scalar') == 0 ? { type: 'SCALAR', name: firstLine.match(/scalar\s+(.*?)\s+.*/)[1].trim() } :
180 | { type: null, name: null }
181 |
182 | /**
183 | * Gets all the comments associated to the schema blocks.
184 | *
185 | * @param {String} sch Raw GraphQL schema.
186 | * @return {String} output[].text Comment
187 | * @return {String} output[].property.type Valid values: 'TYPE', 'ENUM', 'INPUT', 'INTERFACE', 'UNION', 'SCALAR'
188 | * @return {String} output[].property.name Property name (e.g., 'User' if the block started with 'type User {').
189 | */
190 | const _getCommentsBits = (sch) =>
191 | (escapeGraphQlSchemaPlus(sch, carrReturnEsc, tabEsc).match(/░\s*[#"](.*?)░([^#"]*?)({|}|:)/g) || [])
192 | .filter(x => x.match(/{$/))
193 | .map(c => {
194 | const parts = _(c.split(carrReturnEsc).map(l => l.replace(/_t_/g, ' ').trim())).filter(x => x != '')
195 | const hashCount = parts.reduce((a,b) => {
196 | a.count = a.count + (b.indexOf('#') == 0 || b.indexOf('"') == 0 || a.inComment ? 1 : 0)
197 | if (b.indexOf('"""') === 0) {
198 | a.inComment = !a.inComment
199 | }
200 | return a
201 | }, { count: 0, inComment: false }).count
202 | return { text: parts.initial(), property: _getSchemaEntity(parts.last()), comments: hashCount == parts.size() - 1 }
203 | })
204 | .filter(x => x.comments).map(x => ({ text: x.text.join('\n'), property: x.property }))
205 |
206 | /**
207 | * Gets the alias for a generic type (e.g. Paged -> PagedProduct)
208 | * @param {String} genName e.g. Paged
209 | * @return {String} e.g. PagedProduct
210 | */
211 | const _genericDefaultNameAlias = genName => {
212 | if (!genName)
213 | return ''
214 | const m = genName.match(GENERICTYPEREGEX)
215 | if (m) {
216 | const parts = genName.split(m[0])
217 | return `${parts[0]}${m[1].split(',').map(x => x.trim()).join('')}`
218 | } else
219 | return genName
220 | }
221 |
222 | /**
223 | * Example: [T] -> [User], or T -> User or Toy -> Toy
224 | * @param {string} genericType e.g. 'Toy', 'Toy'
225 | * @param {array} genericLetters e.g. ['T'], ['T','U']
226 | * @param {string} concreteType e.g. 'User', 'User,Product'
227 | * @return {string} e.g. 'Toy', 'Toy'
228 | */
229 | const _replaceGenericWithType = (genericType, genericLetters, concreteType) =>
230 | chain({ gType: genericType.replace(/\s/g, ''), gLetters: genericLetters.map(x => x.replace(/\s/g, '')), cTypes: concreteType.split(',').map(x => x.replace(/\s/g, '')) })
231 | .next(({ gType, gLetters, cTypes }) => {
232 | const cTypesLength = cTypes.length
233 | const genericTypeIsArray = gType.indexOf('[') == 0 && gType.indexOf(']') > 0
234 | const endingChar = gType.match(/!$/) ? '!' : ''
235 | if (gLetters.length != cTypesLength)
236 | throw new Error(`Invalid argument exception. Mismatch between the number of types in 'genericLetters' (${genericLetters.join(',')}) and 'concreteType' (${concreteType}).`)
237 | // e.g. genericType = 'T', genericLetters = ['T'], concreteType = 'User' -> resp = 'User'
238 | if (gLetters.length == 1 && gType.replace(/!$/, '') == gLetters[0])
239 | return `${cTypes[0]}${endingChar}`
240 | // e.g. genericType = 'Paged' or '[Paged]'
241 | else if (gType.indexOf('<') > 0 && gType.indexOf('>') > 0) {
242 | const type = genericTypeIsArray ? gType.match(/\[(.*?)\]/)[1] : gType
243 | const typeName = type.match(/.*)[0].replace(/<$/,'').trim() // e.g. 'Toy'
244 | const types = type.match(/<(.*?)>/)[1].split(',').map(x => x.trim())
245 | if (types.length != gLetters.length)
246 | throw new Error(`Invalid argument exception. Mismatch between the number of types in 'genericLetters' (${genericLetters.join(',')}) and 'genericType' (${genericType}).`)
247 |
248 | const matchingConcreteTypes = types.map(t => {
249 | for(let i=0;i`
256 |
257 | return genericTypeIsArray ? `[${result}]${endingChar}` : `${result}${endingChar}`
258 | } else { // e.g. genericType = 'T' or '[T]'
259 | const type = genericTypeIsArray ? gType.match(/\[(.*?)\]/)[1] : gType
260 | const matchingConcreteTypes = type.split(',').map(t => {
261 | const isRequired = /!$/.test(t)
262 | t = (isRequired ? t.replace(/!$/, '') : t).trim()
263 | for(let i=0;i {
277 | if (memoizedGenericNameAliases[genericType])
278 | return memoizedGenericNameAliases[genericType]
279 |
280 | const genericStart = genericType.match(/.*)[0]
281 | const aliasObj = Array.isArray(metadata)
282 | ? _getAllAliases(metadata).find(x => x.schemaName.indexOf(genericStart) == 0)
283 | : metadata && metadata.name == 'alias' ? metadata : null
284 | const alias = aliasObj && aliasObj.body ? getGenericAlias(aliasObj.body)(genericType) : _genericDefaultNameAlias(genericType)
285 | memoizedGenericNameAliases[genericType] = alias
286 |
287 | return alias
288 | }
289 |
290 | let memoizedAliases = null
291 | const _getAllAliases = metadata => memoizedAliases || chain((metadata || []).filter(x => x.name == 'alias')).next(aliases => {
292 | memoizedAliases = aliases
293 | return aliases
294 | }).val()
295 |
296 | let memoizedGenericSchemaObjects = {}
297 | /**
298 | * Get all the type details
299 | *
300 | * @param {String} t Type (e.g. Paged or Paged)
301 | * @param {Array} metadata Array of metadata objects
302 | * @param {Array} genericParentTypes Array of string representing the types (e.g. ['T', 'U']) of the generic parent type
303 | * of that type if that type was extracted from a block. If this array is null, that
304 | * means the parent type was not a generic type.
305 | * @return {String} result.originName 't'
306 | * @return {Boolean} result.isGen Indicates if 't' is a generic type
307 | * @return {Boolean} result.dependsOnParent Not null if 't' is a generic. Indicates if the generic type of 't' depends
308 | * on its parent's type (if true, then that means the parent is itself a generic)
309 | * @return {Array} result.metadata 'metadata'
310 | * @return {Array} result.genericParentTypes If the parent is a generic type, then ths array contains contain all the
311 | * underlying types.
312 | * @return {String} result.name If 't' is not a generic type then 't' otherwise determine what's new name.
313 | */
314 | const _getTypeDetails = (t, metadata, genericParentTypes) => chain((t.match(GENERICTYPEREGEX) || [])[1])
315 | .next(genTypes => {
316 | const isGen = genTypes ? true : false
317 | const genericTypes = isGen ? genTypes.split(',').map(x => x.trim()) : null
318 | const originName = t.replace(/@.+/, '').trim()
319 | const directive = (t.match(/@.+/) || [])[0]
320 | const endingChar = originName.match(/!$/) ? '!' : ''
321 | const dependsOnParent = isGen && genericParentTypes && genericParentTypes.length > 0 && genericTypes.some(x => genericParentTypes.some(y => x == y))
322 | return {
323 | originName,
324 | directive,
325 | isGen,
326 | dependsOnParent,
327 | metadata,
328 | genericParentTypes,
329 | name: isGen && !dependsOnParent ? `${_getAliasName(originName, metadata)}${endingChar}` : originName
330 | }
331 | })
332 | .next(result => {
333 | if (result.isGen && !memoizedGenericSchemaObjects[result.name])
334 | memoizedGenericSchemaObjects[result.name] = result
335 | return result
336 | })
337 | .val()
338 |
339 | /**
340 | * Transpile parameters if generic types are used in them
341 | *
342 | * @param {String} params Parameters (e.g. (filter: Filtered)
343 | * @param {Array} metadata Array of metadata objects
344 | * @param {Array} genericParentTypes Array of string representing the types (e.g. ['T', 'U']) of the generic parent type
345 | * of that type if that type was extracted from a block. If this array is null, that
346 | * means the parent type was not a generic type.
347 | * @return {String} transpiledParams The transpiled parameters
348 | */
349 | const _getTranspiledParams = (params, genericParentTypes) => chain(params.split(','))
350 | .next(genTypes => {
351 | const transpiledParams = []
352 | genTypes.forEach(genType => {
353 | const genericTypeMatches = genType.match(GENERICTYPEREGEX)
354 | const isGen = !!genericTypeMatches
355 | const genericTypes = isGen ? genTypes.map(x => x.trim()) : null
356 | if(!genType) return
357 | const [ paramName, originName ] = genType.split(':').map(item => item.trim())
358 | const endingChar = originName.match(/!$/) ? '!' : ''
359 | const dependsOnParent = isGen && genericParentTypes && genericParentTypes.length > 0 && genericTypes.some(x => genericParentTypes.some(y => x === y))
360 | const result = {
361 | paramName,
362 | originName,
363 | isGen,
364 | name: isGen && !dependsOnParent ? `${_getAliasName(originName)}${endingChar}` : originName
365 | }
366 | if (result.isGen && !memoizedGenericSchemaObjects[result.name])
367 | memoizedGenericSchemaObjects[result.name] = result
368 | transpiledParams.push(`${result.paramName}: ${result.name}`)
369 | })
370 | return transpiledParams
371 | })
372 | .next(result => {
373 | return result.join(', ')
374 | })
375 | .val()
376 |
377 | const _getPropertyValue = ({ name, params, result }, mapResultName) => {
378 | const leftPart = `${name}${params ? `(${params})` : ''}`
379 | let delimiter = ''
380 | let rightPart = ''
381 | if (result && result.name) {
382 | delimiter = ': '
383 | rightPart = mapResultName ? mapResultName(result.name) : result.name
384 | if (result.directive)
385 | rightPart = `${rightPart} ${result.directive}`
386 | }
387 | return `${leftPart}${delimiter}${rightPart}`
388 | }
389 |
390 | /**
391 | * Breaks down a string representing a block { ... } into its various parts.
392 | * @param {string} blockParts String representing your entire block (e.g. { users: User[], posts: Paged })
393 | * @param {object} baseObj
394 | * @param {string} baseObj.type Type of the object with blockParts (e.g. TYPE, ENUM, ...)
395 | * @param {string} baseObj.name Name of the object with blockParts
396 | * @param {array} baseObj.genericTypes Array of types if the 'baseObj' is a generic type.
397 | * @param {array} metadata Array of object. Each object represents a metadata. Example: { name: 'node', body: '(name:hello)', schemaType: 'PROPERTY', schemaName: 'rating: PostRating!', parent: { type: 'TYPE', name: 'PostUserRating', metadata: [Object] } }
398 | * @return [{
399 | * comments: string,
400 | * details: {
401 | * name: string,
402 | * metadata: {
403 | * name: string,
404 | * body: string,
405 | * schemaType: string,
406 | * schemaName: string,
407 | * parent: {
408 | * type: string,
409 | * name: string,
410 | * metadata: [Object]
411 | * }
412 | * },
413 | * params: string,
414 | * result: {
415 | * originName: string,
416 | * isGen: boolean,
417 | * name: string
418 | * }
419 | * },
420 | * value: string
421 | * }] Property breakdown
422 | */
423 | const _getBlockProperties = (blockParts, baseObj, metadata) =>
424 | chain(_(metadata).filter(m => m.schemaType == 'PROPERTY' && m.parent && m.parent.type == baseObj.type && m.parent.name == baseObj.name))
425 | .next(meta => _(blockParts).reduce((a, part) => {
426 | const p = part.trim()
427 | const mData = meta.filter(m => m.schemaName == p).first() || null
428 | if (p.indexOf('#') == 0 || p.indexOf('"') == 0 || a.insideComment) {
429 | if (p.indexOf('"""') === 0) {
430 | a.insideComment = !a.insideComment
431 | }
432 | a.comments.push(p)
433 | } else {
434 | const prop = p.replace(/ +(?= )/g,'').replace(/,$/, '')
435 | const paramsMatch = prop.replace(/@.+/, '').match(PROPERTYPARAMSREGEX)
436 | const propDetails = paramsMatch
437 | ? chain(prop.split(paramsMatch[0]))
438 | .next(parts => ({ name: parts[0].trim(), metadata: mData, params: _getTranspiledParams(paramsMatch[1], baseObj.genericTypes), result: _getTypeDetails((parts[1] || '').replace(':', '').trim(), metadata, baseObj.genericTypes) })).val()
439 | : chain(prop.split(':'))
440 | .next(parts => ({ name: parts[0].trim(), metadata: mData, params: null, result: _getTypeDetails(parts.slice(1).join(':').trim(), metadata, baseObj.genericTypes) })).val()
441 | a.props.push({
442 | comments: a.comments.join('\n '),
443 | details: propDetails,
444 | value: _getPropertyValue(propDetails)
445 | })
446 | a.comments = []
447 | }
448 | return a
449 | }, { insideComment: false, comments:[], props:[] }).props)
450 | .val()
451 |
452 | /**
453 | * [description]
454 | * @param {Array} definitions Array of objects ({ property:..., block: [...], extend: ... }) coming from the '_getSchemaBits' function
455 | * @param {String} typeName e.g. 'type' or 'input'
456 | * @param {RegExp} nameRegEx Regex that can extract the specific details of the schema bit (i.e. definitions)
457 | * @param {Array} metadata metadata coming from the 'extractGraphMetadata' method.
458 | * @return {Array} Array of objects: Example:
459 | * [{
460 | * type: 'TYPE',
461 | * extend: false,
462 | * name: 'Foo',
463 | * metadata: null,
464 | * genericType: null,
465 | * blockProps: [ { comments: '', details: [Object], value: 'id: String!' } ],
466 | * inherits: null,
467 | * implements: null },
468 | * {
469 | * type: 'TYPE',
470 | * extend: true,
471 | * name: 'Query',
472 | * metadata: null,
473 | * genericType: null,
474 | * blockProps: [ { comments: '', details: [Object], value: 'foos: [Foo]!' } ],
475 | * inherits: null,
476 | * implements: null
477 | * }]
478 | */
479 | const _getSchemaObject = (definitions, typeName, nameRegEx, metadata) =>
480 | _.toArray(_(definitions).filter(d => d.property.indexOf(typeName) == 0)
481 | .map(d => {
482 | if (typeName == 'scalar')
483 | return {
484 | type: 'SCALAR',
485 | extend: false,
486 | name: d.block,
487 | metadata: null,
488 | genericType: false,
489 | blockProps: [],
490 | inherits: null,
491 | implements: null
492 | }
493 | else if (typeName == 'union')
494 | return {
495 | type: 'UNION',
496 | extend: false,
497 | name: d.block,
498 | metadata: null,
499 | genericType: false,
500 | blockProps: [],
501 | inherits: null,
502 | implements: null
503 | }
504 | else {
505 | const typeDefMatch = d.property.match(/(.*?){/)
506 | if (!typeDefMatch || typeDefMatch[0].indexOf('#') >= 0) throw new Error(`Schema error: Syntax error in '${d.property}'. Cannot any find schema type definition.`)
507 | const typeDef = typeDefMatch[0]
508 | const nameMatch = typeDef.match(nameRegEx)
509 | if (!nameMatch) throw new Error(`Schema error: ${typeName} with missing name.`)
510 | const name = nameMatch[1].trim().split(' ')[0]
511 | const genericTypeMatch = name.match(GENERICTYPEREGEX)
512 | const isGenericType = genericTypeMatch ? genericTypeMatch[1] : null
513 | const inheritsMatch = typeDef.match(INHERITSREGEX)
514 | const superClass = inheritsMatch && inheritsMatch[0].replace('inherits', '').trim().split(',').map(v => v.trim()) || null
515 | const implementsMatch = typeDef.match(IMPLEMENTSREGEX)
516 | const directive = (typeDef.match(/@[a-zA-Z0-9_]+(.*?)$/) || [''])[0].trim().replace(/{$/, '').trim() || null
517 |
518 | const _interface = implementsMatch
519 | ? implementsMatch[0].replace('implements ', '').replace('{', '').split(',').map(x => x.trim().split(' ')[0])
520 | : null
521 |
522 | const objectType = typeName.toUpperCase()
523 | const metadat = metadata
524 | ? _(metadata).filter(m => m.schemaType == objectType && m.schemaName == name).first() || null
525 | : null
526 |
527 | const genericTypes = isGenericType ? isGenericType.split(',').map(x => x.trim()) : null
528 | const baseObj = { type: objectType, name: name, genericTypes: genericTypes }
529 |
530 | const result = {
531 | type: objectType,
532 | extend: d.extend,
533 | name: name,
534 | metadata: metadat,
535 | directive: directive,
536 | genericType: isGenericType,
537 | blockProps: _getBlockProperties(d.block, baseObj, metadata),
538 | inherits: superClass,
539 | implements: _interface
540 | }
541 | return result
542 | }
543 | }))
544 |
545 | const getGenericAlias = s => !s ? _genericDefaultNameAlias :
546 | genName => chain(genName.match(GENERICTYPEREGEX)).next(m => m
547 | ? chain(m[1].split(',').map(x => `"${x.trim()}"`).join(',')).next(genericTypeName => eval(s + '(' + genericTypeName + ')')).val()
548 | : genName).val()
549 |
550 | const _getInterfaces = (definitions, metadata) => _getSchemaObject(definitions, 'interface', INTERFACENAMEREGEX, metadata)
551 |
552 | const _getAbstracts = (definitions, metadata) => _getSchemaObject(definitions, 'abstract', ABSTRACTNAMEREGEX, metadata)
553 |
554 | const _getTypes = (definitions, metadata) => _getSchemaObject(definitions, 'type', TYPENAMEREGEX, metadata)
555 |
556 | const _getInputs = (definitions, metadata) => _getSchemaObject(definitions, 'input', INPUTNAMEREGEX, metadata)
557 |
558 | const _getEnums = (definitions, metadata) => _getSchemaObject(definitions, 'enum', ENUMNAMEREGEX, metadata)
559 |
560 | const _getScalars = (definitions, metadata) => _getSchemaObject(definitions, 'scalar', null, metadata)
561 |
562 | const _getUnions = (definitions, metadata) => _getSchemaObject(definitions, 'union', null, metadata)
563 |
564 | /**
565 | * [description]
566 | * @param {String} genericTypeName e.g., 'Page'
567 | * @return {String} e.g., 'Page<0,1>'
568 | */
569 | const _getCanonicalGenericType = genericTypeName => {
570 | const [,types] = (genericTypeName || '').match(/<(.*?)>/) || []
571 | if (!types)
572 | return ''
573 | const canon = types.split(',').map((_,idx) => idx).join(',')
574 | return genericTypeName.replace(/<(.*?)>/, `<${canon}>`)
575 | }
576 |
577 | /**
578 | * Determines if a generic type is defined in the Schema.
579 | *
580 | * @param {String} schemaTypeName e.g., Page
581 | * @param {[SchemaType]} rawSchemaTypes All the available Schema Types. For example, if there is a { name: 'Page' }, then
582 | * this function returns true
583 | * @return {Boolean} output
584 | */
585 | const _isGenericTypeDefined = (schemaTypeName, rawSchemaTypes) => {
586 | if (!schemaTypeName || !rawSchemaTypes || rawSchemaTypes.length === 0)
587 | return false
588 |
589 | const canonicalSchemaTypeName = _getCanonicalGenericType(schemaTypeName)
590 | if (!canonicalSchemaTypeName)
591 | return false
592 |
593 | const canonicalGenericTypeNames = rawSchemaTypes.filter(({ genericType }) => genericType).map(({ name }) => _getCanonicalGenericType(name))
594 | return canonicalGenericTypeNames.some(name => name === canonicalSchemaTypeName)
595 | }
596 |
597 | const _getDefaultGenericName = concreteGenericTypeName => (concreteGenericTypeName || '').replace(/[<>,\s]/g,'')
598 |
599 | let _memoizedConcreteGenericTypes = {}
600 | /**
601 | * [description]
602 | * @param {String} concreteGenericTypeName Generic type name (e.g., 'Paged')
603 | * @param {[SchemaType]} rawSchemaTypes Array of not fully compiled Schema type objects.
604 | * @param {[Comments]} comments comments[].text, comments[].property.type, comments[].property.name
605 | * @param {String} aliasName Overides the default name. For example. If 'concreteGenericTypeName' is 'Paged'
606 | * its default name is 'PageUser'.
607 | * @return {SchemaType} Resolved Schema Type object.
608 | */
609 | const _resolveGenericType = ({ concreteGenericTypeName, rawSchemaTypes, comments, aliasName }) => {
610 | // 1. Returns if the result was already memoized before.
611 | const defaultConcreteName = aliasName || _getDefaultGenericName(concreteGenericTypeName)
612 | if (_memoizedConcreteGenericTypes[defaultConcreteName])
613 | return _memoizedConcreteGenericTypes[defaultConcreteName]
614 |
615 | // 2. Find the Generic definition type in the 'rawSchemaTypes'
616 | const genericTypePrefix = ((concreteGenericTypeName.match(/.+) || [])[0] || '').replace(/\[/g,'') // e.g., Paged<
617 |
618 | if (!genericTypePrefix)
619 | throw new Error(`Schema error: Cannot find type in generic object ${concreteGenericTypeName}`)
620 |
621 | const genericDefType = rawSchemaTypes.find(({ name }) => name.indexOf(genericTypePrefix) == 0)
622 |
623 | if (!genericDefType)
624 | throw new Error(`Schema error: Cannot find any definition for generic type starting with ${genericTypePrefix}`)
625 | else if (!genericDefType.genericType)
626 | throw new Error(`Schema error: Schema object ${genericDefType.name} is not generic!`)
627 |
628 | // 3. Resolve the types and the inherited types
629 | // 3.1. Resolve the types (e.g., if concreteGenericTypeName is 'Paged', typeNames is ['User', 'Product'])
630 | const typeNames = ((concreteGenericTypeName.match(/<(.*?)>/) || [])[1] || '').split(',').map(x => x.trim()).filter(x => x)
631 | // 3.1.1. WARNING: This code creates side-effects by mutating '_memoizedConcreteGenericTypes'.
632 | // This is the intended goal as '_memoizedConcreteGenericTypes' is used to in '_getSchemaBits' to get the new generic ASTs.
633 | typeNames.map(typeName => {
634 | if (isScalarType(typeName))
635 | return
636 | _getType(typeName, rawSchemaTypes, comments)
637 | })
638 |
639 | // 3.2. Resolve the inherited types
640 | const superClasses = (genericDefType.inherits || []).map(superClassName => _getType(superClassName, rawSchemaTypes, comments))
641 | // 3.2.1. WARNING: This code creates side-effects by mutating '_memoizedConcreteGenericTypes'.
642 | // This is the intended goal as '_memoizedConcreteGenericTypes' is used to in '_getSchemaBits' to get the new generic ASTs.
643 | superClasses.map((superClass) => {
644 | if (!_inheritingIsAllowed(genericDefType, superClass)){
645 | throw new Error('Schema error: ' + genericDefType.type.toLowerCase() + ' ' + genericDefType.name + ' cannot inherit from ' + superClass.type + ' ' + superClass.name + '.')
646 | }
647 | return _resolveSchemaType(superClass, rawSchemaTypes, comments)
648 | })
649 |
650 | // 4. Resolving each property of the generic type definition based on the concrete type.
651 | const blockProps = genericDefType.blockProps.map(prop => {
652 | let p = prop
653 | const concreteType = typeNames.join(',')
654 | if (isTypeGeneric(prop.details.result.name, genericDefType.genericType)) {
655 | let details = {
656 | name: prop.details.name,
657 | params: prop.params,
658 | result: {
659 | originName: prop.details.originName,
660 | isGen: prop.details.isGen,
661 | name: _replaceGenericWithType(prop.details.result.name, genericDefType.genericType.split(','), concreteType)
662 | }
663 | }
664 |
665 | // 4.1. This is a case where this property is from a generic type similar to type Paged { data:User }. The property
666 | // 'data' depends on parent.
667 | if (prop.details.result.dependsOnParent) {
668 | const propTypeIsRequired = prop.details.result.name.match(/!$/)
669 | const propTypeName = propTypeIsRequired ? prop.details.result.name.replace(/!$/,'') : prop.details.result.name // e.g. [Paged]
670 | const propTypeIsArray = propTypeName.match(/^\[.*\]$/)
671 | const originalConcretePropType = _replaceGenericWithType(propTypeName, prop.details.result.genericParentTypes, concreteType) // e.g. [Paged]
672 | const concretePropType = propTypeIsArray ? originalConcretePropType.replace(/^\[|\]$/g,'') : originalConcretePropType // e.g. Paged
673 | const concreteGenProp = _getTypeDetails(concretePropType, prop.details.result.metadata)
674 | const concreteGenPropName = concreteGenProp.name || _getDefaultGenericName(concretePropType) // e.g. PagedProduct
675 | let originalConcretePropTypeName = propTypeIsArray ? `[${concreteGenPropName}]` : concreteGenPropName // e.g. [PagedProduct]
676 | originalConcretePropTypeName = originalConcretePropTypeName + (propTypeIsRequired ? '!' : '') // e.g. [PagedProduct]!
677 | originalConcretePropTypeName = prop.details.result.directive ? `${originalConcretePropTypeName} ${prop.details.result.directive}` : originalConcretePropTypeName // e.g. [PagedProduct]! @isAuthenticated
678 | details.result = {
679 | originName: prop.details.result.directive ? `${prop.details.result.name} ${prop.details.result.directive}` : prop.details.result.name,
680 | isGen: true,
681 | name: originalConcretePropTypeName
682 | }
683 |
684 | // 4.1.1. Make sure this new generic type is memoized. WARNING: This code creates side-effects by mutating '_memoizedConcreteGenericTypes'.
685 | // This is the intended goal as '_memoizedConcreteGenericTypes' is used to in '_getSchemaBits' to get the new generic ASTs.
686 | _resolveGenericType({ concreteGenericTypeName:concretePropType, rawSchemaTypes, comments, aliasName:concreteGenPropName })
687 | }
688 |
689 | p = {
690 | comments: prop.comments,
691 | details: details,
692 | value: _getPropertyValue(details)
693 | }
694 | }
695 |
696 | return p
697 | })
698 |
699 | const result = {
700 | comments: _getPropertyComments(genericDefType, comments),
701 | type: genericDefType.type,
702 | name:defaultConcreteName,
703 | implements: genericDefType.implements,
704 | blockProps: blockProps,
705 | genericType: null
706 | }
707 |
708 | _memoizedConcreteGenericTypes[defaultConcreteName] = result
709 |
710 | return result
711 | }
712 |
713 | /**
714 | * Gets the type from 'rawSchemaTypes'.
715 | *
716 | * @param {String} typeName e.g., 'User', or 'Paged'
717 | * @param {[SchemaType]} rawSchemaTypes Array of not fully compiled Schema type objects.
718 | * @param {[Comments]} comments comments[].text, comments[].property.type, comments[].property.name
719 | * @return {SchemaType} Schema type from 'rawSchemaTypes' that matches 'typeName'. If 'typeName' is
720 | * a generic type (e.g., 'Paged'), the the returned type is fully compiled.
721 | */
722 | const _getType = (typeName, rawSchemaTypes, comments) => {
723 | let type = rawSchemaTypes.find(({ name }) => name == typeName)
724 | // 3.1. Double-check that the missing super class is not a generic type.
725 | if (!type) {
726 | if (!_isGenericTypeDefined(typeName, rawSchemaTypes))
727 | throw new Error(`Schema error: Type '${typeName}' cannot be found in the schema.`)
728 |
729 | type = _resolveGenericType({
730 | concreteGenericTypeName:typeName,
731 | rawSchemaTypes,
732 | comments
733 | })
734 | }
735 |
736 | return type
737 | }
738 |
739 | const _resolveGenericBlockProperies = (blockProperties,rawSchemaTypes,comments) => (blockProperties || []).forEach(prop => {
740 | if (prop && prop.details && prop.details.result && prop.details.result.isGen && !prop.details.result.dependsOnParent)
741 | _resolveGenericType({ concreteGenericTypeName:prop.details.result.originName, rawSchemaTypes, comments, aliasName:prop.details.result.name })
742 | })
743 |
744 | let memoizedExtendedObject = {}
745 | /**
746 | * [description]
747 | * @param {SchemaType} schemaType Not fully compiled Schema type object.
748 | * @param {[SchemaType]} rawSchemaTypes Array of not fully compiled Schema type objects.
749 | * @param {[Comments]} comments comments[].text, comments[].property.type, comments[].property.name
750 | * @return {SchemaType} Resolved Schema Type object.
751 | */
752 | const _resolveSchemaType = (schemaType, rawSchemaTypes, comments) => {
753 | const resolvedType = (() => {
754 | // 1. Use the trivial resolution method if the schema type does not need advanced resolution (i.e., it does not
755 | // inherits from complex types, or is not a generic type).
756 | if (!schemaType || !rawSchemaTypes || !schemaType.inherits)
757 | return _resolveUsingTrivialMethod(schemaType, rawSchemaTypes, comments)
758 |
759 | // 2. Returns immediately if the schema type has already been resolved.
760 | const key = `${schemaType.type}_${schemaType.name}_${schemaType.genericType}`
761 | if (memoizedExtendedObject[key])
762 | return memoizedExtendedObject[key]
763 |
764 | // 3. Resolve the inherited types first.
765 | const superClasses = schemaType.inherits.map(superClassName => _getType(superClassName, rawSchemaTypes, comments))
766 |
767 | const superClassesWithInheritance = superClasses.map((superClass) => {
768 | if (!_inheritingIsAllowed(schemaType, superClass)){
769 | throw new Error('Schema error: ' + schemaType.type.toLowerCase() + ' ' + schemaType.name + ' cannot inherit from ' + superClass.type + ' ' + superClass.name + '.')
770 | }
771 | return _resolveSchemaType(superClass, rawSchemaTypes, comments)
772 | })
773 |
774 | // 4. Merge the super classes properties with the current schema type properties.
775 | const schemaTypeBlockProps = superClassesWithInheritance.length
776 | ? superClassesWithInheritance.reduce((acc,superClass) => {
777 | const propertiesNotAlreadyIncluded = superClass.blockProps.filter(prop=> !acc.some(originalProp => originalProp.details.name == prop.details.name))
778 | acc.push(...propertiesNotAlreadyIncluded)
779 | return acc
780 | }, schemaType.blockProps)
781 | : schemaType.blockProps
782 |
783 | // 5. Resolve all generic properties. WARNING: This code creates side-effects by mutating '_memoizedConcreteGenericTypes'.
784 | // This is the intended goal as '_memoizedConcreteGenericTypes' is used to in '_getSchemaBits' to get the new generic ASTs.
785 | _resolveGenericBlockProperies(schemaTypeBlockProps,rawSchemaTypes, comments)
786 |
787 | const objWithInheritance = {
788 | type: schemaType.type,
789 | name: schemaType.name,
790 | genericType: schemaType.genericType,
791 | originalBlockProps: schemaType.blockProps,
792 | metadata: schemaType.metadata || _.last(superClassesWithInheritance).metadata || null,
793 | directive: schemaType.directive,
794 | implements: _.toArray(_.uniq(_.concat(schemaType.implements, superClassesWithInheritance.implements).filter(function(x) {
795 | return x
796 | }))),
797 | inherits: superClassesWithInheritance,
798 | blockProps: schemaTypeBlockProps
799 | }
800 |
801 | memoizedExtendedObject[key] = objWithInheritance
802 | return _resolveUsingTrivialMethod(objWithInheritance, rawSchemaTypes, comments)
803 | })()
804 |
805 | // 4. Add comments
806 | return _addComments(resolvedType, comments)
807 | }
808 |
809 |
810 | // const _getObjWithExtensions = (schemaType, rawSchemaTypes) => {
811 | // if (schemaType && rawSchemaTypes && schemaType.inherits) {
812 |
813 | // const key = `${schemaType.type}_${schemaType.name}_${schemaType.genericType}`
814 | // if (memoizedExtendedObject[key]) return memoizedExtendedObject[key]
815 |
816 | // const superClass = rawSchemaTypes.filter(function(x) {
817 | // return schemaType.inherits.indexOf(x.name) > -1
818 | // }).value()
819 | // const superClassNames = rawSchemaTypes.map(function(x) {
820 | // return x.name
821 | // }).value()
822 | // //find missing classes
823 | // const missingClasses = _.difference(schemaType.inherits, superClassNames)
824 |
825 | // missingClasses.forEach(function(c){
826 | // throw new Error('Schema error: ' + schemaType.type.toLowerCase() + ' ' + schemaType.name + ' cannot find inherited ' + schemaType.type.toLowerCase() + ' ' + c)
827 | // })
828 |
829 | // const superClassesWithInheritance = superClass.map(function(subClass){
830 |
831 | // if (!_inheritingIsAllowed(schemaType, subClass)){
832 | // throw new Error('Schema error: ' + schemaType.type.toLowerCase() + ' ' + schemaType.name + ' cannot inherit from ' + subClass.type + ' ' + subClass.name + '.')
833 | // }
834 | // return _getObjWithExtensions(subClass, rawSchemaTypes)
835 | // })
836 |
837 | // const objWithInheritance = {
838 | // type: schemaType.type,
839 | // name: schemaType.name,
840 | // genericType: schemaType.genericType,
841 | // originalBlockProps: schemaType.blockProps,
842 | // metadata: schemaType.metadata || _.last(superClassesWithInheritance).metadata || null,
843 | // directive: schemaType.directive,
844 | // implements: _.toArray(_.uniq(_.concat(schemaType.implements, superClassesWithInheritance.implements).filter(function(x) {
845 | // return x
846 | // }))),
847 | // inherits: superClassesWithInheritance,
848 | // blockProps: (superClassesWithInheritance instanceof Array ?
849 | // _.toArray(_.flatten(_.concat(_.flatten(superClassesWithInheritance.map(function(subClass){
850 | // return subClass.blockProps.filter(prop=>!schemaType.blockProps.find(originalProp=>originalProp.details.name==prop.details.name))
851 | // })), schemaType.blockProps))):
852 | // _.toArray(_.flatten(_.concat(superClassesWithInheritance.blockProps, schemaType.blockProps)))
853 | // )
854 | // }
855 |
856 | // memoizedExtendedObject[key] = objWithInheritance
857 | // return _resolveUsingTrivialMethod(objWithInheritance, rawSchemaTypes)
858 | // }
859 | // else
860 | // return _resolveUsingTrivialMethod(schemaType)
861 | // }
862 |
863 | const _inheritingIsAllowed = (obj, subClass) => {
864 | if (obj.type === 'TYPE')
865 | return subClass.type === 'TYPE' || subClass.type === 'INTERFACE'
866 | else
867 | return obj.type === subClass.type
868 | }
869 |
870 | const _resolveUsingTrivialMethod = (obj, rawSchemaTypes, comments) => {
871 | if (obj && obj.blockProps)
872 | _resolveGenericBlockProperies(obj.blockProps, rawSchemaTypes, comments)
873 | if (obj && rawSchemaTypes && obj.implements && obj.implements.length > 0) {
874 | const interfaceWithAncestors = _.toArray(_.uniq(_.flatten(_.concat(obj.implements.map(i => _getInterfaceWithAncestors(i, rawSchemaTypes))))))
875 | return {
876 | type: obj.type,
877 | name: obj.name,
878 | genericType: obj.genericType,
879 | originalBlockProps: obj.blockProps,
880 | metadata: obj.metadata,
881 | implements: interfaceWithAncestors,
882 | inherits: obj.inherits,
883 | blockProps: obj.blockProps
884 | }
885 | }
886 | else
887 | return obj
888 | }
889 |
890 | let memoizedInterfaceWithAncestors = {}
891 | const _getInterfaceWithAncestors = (_interface, schemaObjects) => {
892 | if (memoizedInterfaceWithAncestors[_interface]) return memoizedInterfaceWithAncestors[_interface]
893 | const interfaceObj = schemaObjects.filter(x => x.name == _interface)[0]
894 | if (!interfaceObj) throw new Error(`Schema error: interface ${_interface} is not defined.`)
895 | if (interfaceObj.type != 'INTERFACE') throw new Error(`Schema error: Schema property ${_interface} is not an interface. It cannot be implemented.`)
896 |
897 | const interfaceWithAncestors = interfaceObj.implements && interfaceObj.implements.length > 0
898 | ? _.toArray(_.uniq(_.flatten(_.concat(
899 | [_interface],
900 | interfaceObj.implements,
901 | interfaceObj.implements.map(i => _getInterfaceWithAncestors(i, schemaObjects))))))
902 | : [_interface]
903 |
904 | memoizedInterfaceWithAncestors[_interface] = interfaceWithAncestors
905 | return interfaceWithAncestors
906 | }
907 |
908 | /**
909 | * Gets the text comment for a specific property.
910 | *
911 | * @param {String} property.type Valid values: 'TYPE', 'ENUM', 'INPUT', 'INTERFACE', 'UNION', 'SCALAR'
912 | * @param {String} property.name e.g., 'User'
913 | * @param {[Comments]} comments comments[].text, comments[].property.type, comments[].property.name
914 | * @return {String} output Text.
915 | */
916 | const _getPropertyComments = (property, comments) => {
917 | const { type, name } = property || {}
918 | if (!type || !name)
919 | return ''
920 | return ((comments || []).filter(c => c.property.type == type && c.property.name == name)[0] || {}).text || ''
921 | }
922 |
923 | const _addComments = (obj, comments) => {
924 | obj.comments = _getPropertyComments(obj, comments)
925 | return obj
926 | }
927 |
928 | const _parseSchemaObjToString = (comments, type, name, _implements, blockProps, extend=false, directive) =>
929 | [
930 | `${comments && comments != '' ? `\n${comments}` : ''}`,
931 | `${extend ? 'extend ' : ''}${type.toLowerCase()} ${name.replace('!', '')}${_implements && _implements.length > 0 ? ` implements ${_implements.join(', ')}` : ''} ${blockProps.some(x => x) ? `${directive ? ` ${directive} ` : ''}{`: ''} `,
932 | blockProps.map(prop => ` ${prop.comments != '' ? `${prop.comments}\n ` : ''}${prop.value}`).join('\n'),
933 | blockProps.some(x => x) ? '}': ''
934 | ].filter(x => x).join('\n')
935 |
936 | /**
937 | * Tests if the type is a generic type based on the value of genericLetter
938 | *
939 | * @param {String} type e.g. 'Paged', '[T]', 'T', 'T!'
940 | * @param {String} genericLetter e.g. 'T', 'T,U'
941 | * @return {Boolean} e.g. if type equals 'Paged' or '[T]' and genericLetter equals 'T' then true.
942 | */
943 | const SANITIZE_GEN_TYPE_REGEX = /^\[|\s|\](\s*)(!*)(\s*)$|!/g
944 | const isTypeGeneric = (type, genericLetter) => {
945 | const sanitizedType = type ? type.replace(SANITIZE_GEN_TYPE_REGEX, '') : type
946 | const sanitizedgenericLetter = genericLetter ? genericLetter.replace(SANITIZE_GEN_TYPE_REGEX, '') : genericLetter
947 | if (!sanitizedType || !sanitizedgenericLetter)
948 | return false
949 | else if (sanitizedType == sanitizedgenericLetter)
950 | return true
951 | else if (sanitizedType.indexOf('<') > 0 && sanitizedType.indexOf('>') > 0) {
952 | const genericLetters = sanitizedgenericLetter.split(',')
953 | return (sanitizedType.match(/<(.*?)>/) || [null, ''])[1].split(',').some(x => genericLetters.some(y => y == x.trim()))
954 | }
955 | else
956 | return sanitizedgenericLetter.split(',').some(x => x.trim() == sanitizedType)
957 | }
958 |
959 |
960 | /**
961 | * [description]
962 | * @param {String} originName e.g., 'Paged'. Original name from the non-compiled GraphQL schema.
963 | * @param {Noolean} isGen Determines whether that type is generic or not.
964 | * @param {String} name e.g., 'PagedQuestion'. Name of the new type once it has been compiled.
965 | * @param {[Object]} schemaBreakDown Array of schema AST objects. This contains all the compiled types so far.
966 | * @param {Object} memoizedNewSchemaObjectFromGeneric Memoized 'newGenericType' to speed up this function (e.g., { 'PagedQuestion': { obj:{...}, stringObj:'...' } })
967 | *
968 | * @return {Object} newGenericType
969 | * @return {Object} newGenericType.obj New generic type object's AST.
970 | * @return {String} newGenericType.stringObj New generic type schema string definition (e.g., 'type PagedQuestion {\n data: [Question!]!\n cursor: ID\n }')
971 | */
972 | // const _createNewSchemaObjectFromGeneric = ({ originName, isGen, name }, schemaBreakDown, memoizedNewSchemaObjectFromGeneric) => {
973 | // if (!memoizedNewSchemaObjectFromGeneric)
974 | // throw new Error('Missing required argument. \'memoizedNewSchemaObjectFromGeneric\' is required.')
975 | // if (isGen && memoizedNewSchemaObjectFromGeneric[name])
976 | // return memoizedNewSchemaObjectFromGeneric[name]
977 | // else if (isGen) {
978 | // const genObjName = chain(originName.split('<')).next(parts => `${parts[0]}<`).val()
979 | // const concreteType = (originName.match(/<(.*?)>/) || [null, null])[1]
980 | // if (!concreteType) throw new Error(`Schema error: Cannot find generic type in object ${originName}`)
981 | // const baseGenObj = schemaBreakDown.find(x => x.name.indexOf(genObjName) == 0)
982 | // if (!baseGenObj) throw new Error(`Schema error: Cannot find any definition for generic type starting with ${genObjName}`)
983 | // if (!baseGenObj.genericType) throw new Error(`Schema error: Schema object ${baseGenObj.name} is not generic!`)
984 |
985 | // const blockProps = baseGenObj.blockProps.map(prop => {
986 | // let p = prop
987 | // if (isTypeGeneric(prop.details.result.name, baseGenObj.genericType)) {
988 | // let details = {
989 | // name: prop.details.name,
990 | // params: prop.params,
991 | // result: {
992 | // originName: prop.details.originName,
993 | // isGen: prop.details.isGen,
994 | // name: _replaceGenericWithType(prop.details.result.name, baseGenObj.genericType.split(','), concreteType)
995 | // }
996 | // }
997 | // if (prop.details.result.dependsOnParent) {
998 | // const propTypeIsRequired = prop.details.result.name.match(/!$/)
999 | // // e.g. [Paged]
1000 | // const propTypeName = propTypeIsRequired ? prop.details.result.name.replace(/!$/,'') : prop.details.result.name
1001 | // const propTypeIsArray = propTypeName.match(/^\[.*\]$/)
1002 | // // e.g. [Paged]
1003 | // const originalConcretePropType = _replaceGenericWithType(propTypeName, prop.details.result.genericParentTypes, concreteType)
1004 | // // e.g. Paged
1005 | // const concretePropType = propTypeIsArray ? originalConcretePropType.replace(/^\[|\]$/g,'') : originalConcretePropType
1006 | // const concreteGenProp = _getTypeDetails(concretePropType, prop.details.result.metadata)
1007 | // // e.g. PagedProduct
1008 | // const concreteGenPropName = _createNewSchemaObjectFromGeneric(concreteGenProp, schemaBreakDown, memoizedNewSchemaObjectFromGeneric).obj.name
1009 | // // e.g. [PagedProduct]
1010 | // let originalConcretePropTypeName = propTypeIsArray ? `[${concreteGenPropName}]` : concreteGenPropName
1011 | // // e.g. [PagedProduct]!
1012 | // originalConcretePropTypeName = originalConcretePropTypeName + (propTypeIsRequired ? '!' : '')
1013 | // // e.g. [PagedProduct]! @isAuthenticated
1014 | // originalConcretePropTypeName = prop.details.result.directive ? `${originalConcretePropTypeName} ${prop.details.result.directive}` : originalConcretePropTypeName
1015 | // details.result = {
1016 | // originName: prop.details.result.directive ? `${prop.details.result.name} ${prop.details.result.directive}` : prop.details.result.name,
1017 | // isGen: true,
1018 | // name: originalConcretePropTypeName
1019 | // }
1020 | // }
1021 |
1022 | // p = {
1023 | // comments: prop.comments,
1024 | // details: details,
1025 | // value: _getPropertyValue(details)
1026 | // }
1027 | // }
1028 |
1029 | // return p
1030 | // })
1031 |
1032 | // const newSchemaObjStr = _parseSchemaObjToString(baseGenObj.comments, baseGenObj.type, name, baseGenObj.implements, blockProps)
1033 | // const result = {
1034 | // obj: {
1035 | // comments: baseGenObj.comments,
1036 | // type: baseGenObj.type,
1037 | // name,
1038 | // implements: baseGenObj.implements,
1039 | // blockProps: blockProps,
1040 | // genericType: null
1041 | // },
1042 | // stringObj: newSchemaObjStr
1043 | // }
1044 | // memoizedNewSchemaObjectFromGeneric[name] = result
1045 | // return result
1046 | // }
1047 | // else return { obj: null, stringObj: null }
1048 | // }
1049 |
1050 | /**
1051 | * Breaks down a schema into its bits and pieces.
1052 | * @param {String} graphQlSchema
1053 | * @param {Array} metadata
1054 | * @param {Boolean} includeNewGenTypes
1055 | * @return {String} result.type e.g. 'TYPE', 'INTERFACE'
1056 | * @return {Boolean} result.raw
1057 | * @return {Boolean} result.extend
1058 | * @return {String} result.name
1059 | * @return {String} result.metadata
1060 | * @return {Boolean} result.genericType
1061 | * @return {String} result.blockProps
1062 | * @return {Boolean} result.inherits
1063 | * @return {String} result.implements
1064 | * @return {String} result.comments
1065 | */
1066 | const getSchemaParts = (graphQlSchema, metadata) => {
1067 | metadata = metadata || []
1068 | // 1. Extract all the comments
1069 | const comments = _getCommentsBits(graphQlSchema)
1070 | // 2. Extract all the blocks stripped out of all the comments.
1071 | const schemaBits = _getSchemaBits(graphQlSchema, metadata)
1072 | // 3. Classify the blocks in AST objects
1073 | const rawSchemaTypes = [_getInterfaces, _getAbstracts, _getTypes, _getInputs, _getEnums, _getScalars, _getUnions].reduce((acc, getObjects) => {
1074 | // 3.1. Creates the type
1075 | const schemaTypes = getObjects(schemaBits,metadata) || []
1076 | acc.push(...schemaTypes)
1077 | return acc
1078 | },[])
1079 |
1080 | // 4. Resolve all generic params names and memoize them.
1081 | const rawParamGenericTypes = Object.keys(memoizedGenericSchemaObjects)
1082 | .map(key => memoizedGenericSchemaObjects[key])
1083 | .filter(({ paramName, isGen }) => paramName && isGen)
1084 |
1085 | rawParamGenericTypes.map(({ originName, name }) =>
1086 | _resolveGenericType({ concreteGenericTypeName:originName, rawSchemaTypes, comments, aliasName:name }))
1087 |
1088 | // 5. Resolve all types
1089 | const resolvedTypes = rawSchemaTypes.map(schemaType => {
1090 | const resolvedSchemaType = _resolveSchemaType(schemaType, rawSchemaTypes, comments)
1091 | return resolvedSchemaType
1092 | })
1093 |
1094 | // 6. Include the generic types that were resolved as a side-effect of resolving the other types in step #4.
1095 | const resolvedGenericTypes = Object.keys(_memoizedConcreteGenericTypes).map(key => _memoizedConcreteGenericTypes[key])
1096 | const allTypes = [...resolvedTypes,...resolvedGenericTypes]
1097 |
1098 | // 7. Include directives.
1099 | const directives = (metadata || []).filter(m => m.directive)
1100 | if (directives.length > 0) {
1101 | allTypes.push(...directives.map(d => ({
1102 | type: 'DIRECTIVE',
1103 | name: d.name,
1104 | raw: (d.body || '').replace(/░/g, '\n'),
1105 | extend: false,
1106 | metadata: null,
1107 | genericType: null,
1108 | blockProps: [],
1109 | inherits: null,
1110 | implements: null,
1111 | comments: undefined
1112 | })))
1113 | }
1114 |
1115 | return allTypes
1116 | }
1117 |
1118 | const resetMemory = () => {
1119 | _s = {}
1120 | _memoizedConcreteGenericTypes = null
1121 | _memoizedConcreteGenericTypes = {}
1122 | memoizedGenericSchemaObjects = null
1123 | memoizedGenericSchemaObjects = {}
1124 | memoizedExtendedObject = null
1125 | memoizedExtendedObject = {}
1126 | memoizedInterfaceWithAncestors = null
1127 | memoizedInterfaceWithAncestors = {}
1128 | memoizedGenericNameAliases = null
1129 | memoizedGenericNameAliases = {}
1130 | memoizedAliases = null
1131 | return 1
1132 | }
1133 |
1134 | const _buildASTs = (ASTs=[]) => {
1135 | const part_01 = ASTs
1136 | .filter(x => !x.genericType && x.type != 'ABSTRACT' && x.type != 'DIRECTIVE')
1137 | .map(obj => _parseSchemaObjToString(obj.comments, obj.type, obj.name, obj.implements, obj.blockProps, obj.extend, obj.directive))
1138 | .join('\n')
1139 |
1140 | const directives = ASTs.filter(x => x.type == 'DIRECTIVE' && x.raw).map(x => x.raw).join('')
1141 |
1142 | return directives + '\n' + part_01
1143 | }
1144 |
1145 | const getSchemaAST = graphQlSchema => {
1146 | resetMemory()
1147 | const { stdSchema, metadata } = removeGraphMetadata(graphQlSchema)
1148 | const ASTs = getSchemaParts(stdSchema, metadata, true)
1149 | resetMemory()
1150 | return ASTs
1151 | }
1152 |
1153 | const transpile = graphQlSchema => {
1154 | const ASTs = getSchemaAST(graphQlSchema)
1155 | const build = _buildASTs(ASTs)
1156 | return build
1157 | }
1158 |
1159 | let graphqls2s = {
1160 | getSchemaAST,
1161 | transpileSchema: transpile,
1162 | extractGraphMetadata,
1163 | getGenericAlias,
1164 | getQueryAST,
1165 | buildQuery,
1166 | isTypeGeneric
1167 | }
1168 |
1169 | if (typeof(window) != 'undefined') window.graphqls2s = graphqls2s
1170 |
1171 | module.exports.graphqls2s = graphqls2s
1172 |
1173 |
--------------------------------------------------------------------------------
/src/utilities.js:
--------------------------------------------------------------------------------
1 | /** * Copyright (c) 2018, Neap Pty Ltd.
2 | * All rights reserved.
3 | *
4 | * This source code is licensed under the BSD-style license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 | const { parse } = require('graphql')
8 | const shortid = require('shortid')
9 |
10 | let _start
11 | const startTime = anything => {
12 | _start = Date.now()
13 | return anything
14 | }
15 | const logTime = (anything, label) => {
16 | if (!_start)
17 | _start = Date.now()
18 | console.log(label ? `${label}: ${Date.now() - _start} ms`: `${Date.now() - _start} ms`)
19 | return anything
20 | }
21 |
22 | const chain = value => ({ next: fn => chain(fn(value)), val: () => value })
23 | /*eslint-disable */
24 | const log = (msg, name, transformFn) => chain(name ? `${name}: ${typeof(msg) != 'object' ? msg : JSON.stringify(msg)}` : msg)
25 | /*eslint-disable */
26 | .next(v => transformFn ? console.log(chain(transformFn(msg)).next(v => name ? `${name}: ${v}` : v).val()) : console.log(v))
27 | /*eslint-enable */
28 | .next(() => msg)
29 | .val()
30 | /*eslint-enable */
31 | /**
32 | * Removes all multi-spaces with a single space + replace carriage returns with 'cr' and tabs with 't'
33 | * @param {String} sch Text input
34 | * @param {String} cr Carriage return replacement
35 | * @param {String} t Tab replacement
36 | * @return {String} Escaped text
37 | */
38 | const escapeGraphQlSchema = (sch, cr='░', t=' ') => sch.replace(/[\n\r]+/g, cr).replace(/[\t\r]+/g, t).replace(/\s+/g, ' ')
39 | const removeMultiSpaces = s => s.replace(/ +(?= )/g,'')
40 | const matchLeftNonGreedy = (str, startChar, endChar) => chain(str.match(new RegExp(`${startChar}(.*?)${endChar}`)))
41 | .next(m => m && m.length > 0
42 | ? chain(matchLeftNonGreedy(`${m[m.length-1]}${endChar}`, startChar, endChar)).next(v => v ? v : m).val()
43 | : m
44 | )
45 | .val()
46 |
47 | const throwError = (v, msg) => v ? (() => {throw new Error(msg)})() : true
48 |
49 | const GRAPHQLSCALARTYPES = { 'ID': true, 'String': true, 'Float': true, 'Int': true, 'Boolean': true }
50 | const isScalarType = type => GRAPHQLSCALARTYPES[type]
51 |
52 | /**
53 | * Check whether or not the 'type' that is defined in the 'schemaAST' is of type node.
54 | *
55 | * @param {String} type Type name
56 | * @param {Array} schemaAST Array of schema objects
57 | * @return {Boolean} Result
58 | */
59 | const isNodeType = (type, schemaAST) =>
60 | chain(throwError(!type, 'Error in method \'isNodeType\': Argument \'type\' is required.'))
61 | .next(() => type.replace(/!$/, ''))
62 | .next(type => (type.match(/^\[(.*?)\]$/) || [null, type])[1])
63 | .next(type => isScalarType(type)
64 | ? false
65 | : chain({ type, typeAST: schemaAST.find(x => x.name == type)})
66 | .next(({type, typeAST}) => !typeAST
67 | ? throwError(true, `Error in method 'isNodeType': Type '${type}' does not exist in the GraphQL schema.`)
68 | : (typeAST.type == 'TYPE' && typeAST.metadata && typeAST.metadata.name == 'node') ? true : false)
69 | .val())
70 | .val()
71 |
72 | /**
73 | * If the schemaAST's metadata is of type 'edge', it extracts its body.
74 | *
75 | * @param {Object} metadata SchemaAST's metadata
76 | * @return {String} SchemaAST's metadata's body
77 | */
78 | const getEdgeDesc = metadata => (!metadata || metadata.name != 'edge') ? null : metadata.body.replace(/(^\(|\)$)/g, '')
79 |
80 | /**
81 | * Remove potential alias in queries similor to 'users:persons'
82 | * @param {String} query e.g. 'users:persons'
83 | * @return {String} e.g. 'persons'
84 | */
85 | const removeAlias = (query='') => query.split(':').slice(-1).join('')
86 |
87 | /**
88 | * [description]
89 | *
90 | * @param {Object} queryProp Property object from the QueryAST
91 | * @param {Object} parentTypeAST Schema type object from the SchemaAST that is assumed to contain the queryProp
92 | * @param {Array} schemaAST Entire SchemaAST
93 | * @return {Object} Query prop's AST enriched with all metadata from the schemaAST
94 | */
95 | const addMetadataToProperty = (queryProp, parentTypeAST, schemaAST) =>
96 | chain(parentTypeAST.blockProps.find(x => x.details.name == removeAlias(queryProp.name)))
97 | .next(schemaProp => {
98 | if (schemaProp)
99 | return {
100 | name: queryProp.name,
101 | kind: queryProp.kind,
102 | type: schemaProp.details.result.name,
103 | metadata: schemaProp.details.metadata,
104 | isNode: isNodeType(schemaProp.details.result.name, schemaAST),
105 | edge: getEdgeDesc(schemaProp.details.metadata),
106 | args: queryProp.args,
107 | properties: queryProp.properties && queryProp.properties.length > 0
108 | ? chain(schemaProp.details.result.name)
109 | .next(typename => ((typename.match(/^\[(.*?)\]$/) || [null, typename])[1]).replace(/!$/, ''))
110 | .next(typename => schemaAST.find(x => x.type == 'TYPE' && x.name == typename))
111 | .next(parentTypeAST => parentTypeAST
112 | ? queryProp.properties.map(queryProp => addMetadataToProperty(queryProp, parentTypeAST, schemaAST))
113 | : throwError(true, `Error in method 'addMetadataToProperty': Cannot find type '${schemaProp.details.result.name}' in the GraphQL Schema.`))
114 | .val()
115 | : null
116 | }
117 | else
118 | return {
119 | name: queryProp.name,
120 | kind: queryProp.kind,
121 | type: null,
122 | metadata: null,
123 | isNode: null,
124 | edge: null,
125 | args: queryProp.args,
126 | properties: queryProp.properties,
127 | error: schemaProp ? null : `Error in method 'addMetadataToProperty': Query function '${queryProp.name}' is not defined in the GraphQL schema (specifically in the 'parentTypeAST' argument).`
128 | }
129 | })
130 | .val()
131 |
132 | /**
133 | * Parses a string GraphQL query to an AST enriched with metadata from the GraphQL Schema AST.
134 | *
135 | * @param {String} query Raw string GraphQL query (e.g. query Hello($person: String, $animal: String) { ... })
136 | * @param {Array} schemaAST Array of schema objects. Use 'graphql-s2s' npm package('getSchemaParts' method) to get that AST.
137 | * @return {Array} output Array represent all query's AST.
138 | * @return {String} output.head Head of the original query (e.g. Hello($person: String, $animal: String))
139 | * @return {String} output.type Query type (e.g. query || mutation || subscription)
140 | */
141 | const addMetadataToAST = (operation, schemaAST, queryType='Query') =>
142 | chain(
143 | // If that object has already been processed, then get it.
144 | schemaAST[`get${queryType}`] ||
145 | // If this is the first time we access that object, then compute it and save it for later.
146 | chain(schemaAST[`get${queryType}`] = schemaAST.find(x => x.type == 'TYPE' && x.name == queryType)).next(() => schemaAST[`get${queryType}`]).val())
147 | .next(parentTypeAST =>
148 | chain(operation && operation.properties
149 | ? operation.properties.map(prop => addMetadataToProperty(prop, parentTypeAST, schemaAST))
150 | : [])
151 | .next(body => {
152 | operation.properties = body
153 | return operation
154 | })
155 | .val())
156 | .val()
157 |
158 | const parseProperties = selectionSet => !selectionSet ? null : (selectionSet.selections || []).map(x => ({
159 | name: `${x.alias ? x.alias.value + ':' : ''}${x.name.value}`,
160 | args: parseArguments(x.arguments),
161 | properties: parseProperties(x.selectionSet),
162 | kind: x.kind
163 | }))
164 |
165 | const parseKeyValue = ({ kind, name, value }) => {
166 | return {
167 | name: name ? name.value : null,
168 | value: !name && !value.kind ? { kind, value } : {
169 | kind: value.kind,
170 | value:
171 | value.name ? value.name.value :
172 | value.fields ? value.fields.map(f => parseKeyValue(f)) :
173 | value.values ? value.values.map(v => v.value ? parseKeyValue(v) : v.fields.map(f => parseKeyValue(f))) : value.value
174 | }
175 | }
176 | }
177 |
178 | const parseArguments = astArgs => !astArgs || !astArgs.length
179 | ? null
180 | : astArgs.map(a => parseKeyValue(a))
181 |
182 | const parseFragments = (fragments = []) => fragments.length == 0 ? null : fragments.map(fragment => ({
183 | name: (fragment.name || {}).value,
184 | type: ((fragment.typeCondition || {}).name || {}).value,
185 | properties: parseProperties(fragment.selectionSet)
186 | }))
187 |
188 | const _graphQlQueryTypes = { 'query': 'Query', 'mutation': 'Mutation', 'subscription': 'Subscription' }
189 | /**
190 | * [description]
191 | * @param {[type]} query [description]
192 | * @param {[type]} schemaAST [description]
193 | * @param {Boolean} options.defrag [description]
194 | * @return {[type]} [description]
195 | */
196 | const getQueryAST = (query, operationName, schemaAST, options={}) => {
197 | const parsedQuery = (parse(query) || {}).definitions || []
198 | const ast = parsedQuery.find(x => x.kind == 'OperationDefinition' && (!operationName || x.name.value == operationName))
199 | if (!ast) {
200 | if (operationName)
201 | throw new Error(`Invalid Graphql query. Operation name '${operationName}' is not defined in the query.`)
202 | else
203 | throw new Error('Invalid Graphql query. No \'OperationDefinition\' defined in the query.')
204 | }
205 | const fragments = parsedQuery.filter(x => x.kind == 'FragmentDefinition')
206 | if (ast) {
207 | const operation = {
208 | type: ast.operation,
209 | name: ast.name ? ast.name.value : null,
210 | variables: ast.variableDefinitions
211 | ? ast.variableDefinitions.map(({ variable:v, type:t }) => {
212 | const nonNullType = t.kind == 'NonNullType'
213 | const exclPoint = nonNullType ? '!' : ''
214 | const typ = nonNullType ? t.type : t
215 | return {
216 | name: v.name.value,
217 | type: typ.kind == 'ListType' ? `[${typ.type.name.value}]${exclPoint}` : `${typ.name.value}${exclPoint}` }
218 | })
219 | : null,
220 | properties: parseProperties(ast.selectionSet),
221 | fragments: parseFragments(fragments)
222 | }
223 | const postProcess = options.defrag ? o => addMetadataToAST(defrag(o), schemaAST, _graphQlQueryTypes[ast.operation]) : o => o
224 | let output = postProcess(addMetadataToAST(operation, schemaAST, _graphQlQueryTypes[ast.operation] ))
225 | Object.assign(output, {
226 | filter: fn => filterQueryAST(output, fn),
227 | some: fn => detectQueryAST(output, fn),
228 | propertyPaths: fn => getQueryASTPropertyPaths(output, fn),
229 | containsProp: propPath => {
230 | if (!propPath)
231 | return false
232 |
233 | const matchFn = propPath instanceof RegExp ? (p => p.match(propPath)) : (p => p.indexOf(propPath) >= 0)
234 | return (getQueryASTPropertyPaths(output, ast => ast && ast.name) || []).some(({ property }) => {
235 | const propWithNoAliases = (property || '').split('.').map(part => part.split(':').slice(-1)[0]).join('.')
236 | return matchFn(propWithNoAliases)
237 | })
238 | }
239 | })
240 | return output
241 | }
242 | else
243 | return null
244 | }
245 |
246 | const stringifyOperation = (operation={}) => {
247 | const acc = []
248 | acc.push(operation.type || 'query')
249 | if (operation.name)
250 | acc.push(operation.name)
251 | if (operation.variables && operation.variables.length > 0)
252 | acc.push(`(${operation.variables.map(v => `$${v.name}: ${v.type}`).join(', ')})`)
253 |
254 | return acc.join(' ')
255 | }
256 |
257 | const filterQueryAST = (operation={}, predicate, onlyReturnBody=false) => {
258 | if (operation.properties && predicate) {
259 | const filteredBody = operation.properties
260 | .filter(x => predicate(x))
261 | .map(x => x.properties && x.properties.length > 0
262 | ? Object.assign({}, x, { properties: filterQueryAST(x, predicate, true) })
263 | : x)
264 |
265 | return onlyReturnBody ? filteredBody : Object.assign({}, operation, { properties: filteredBody })
266 | }
267 | else
268 | return onlyReturnBody ? null : operation
269 | }
270 |
271 | const detectQueryAST = (operation={}, predicate) =>
272 | operation.properties &&
273 | predicate &&
274 | (operation.properties.some(x => predicate(x)) || operation.properties.some(x => detectQueryAST(x, predicate)))
275 |
276 | const getQueryASTPropertyPaths = (operation={}, predicate, parent='') => {
277 | const prefix = parent ? parent + '.' : parent
278 | if (operation.properties && predicate)
279 | return operation.properties.reduce((acc, p) => {
280 | if (predicate(p))
281 | acc.push({ property: prefix + p.name, type: p.type })
282 | if (p.properties)
283 | acc.push(...getQueryASTPropertyPaths(p, predicate, prefix + p.name))
284 | return acc
285 | }, [])
286 | else
287 | return []
288 | }
289 |
290 | /**
291 | * Rebuild a string GraphQL query from the query AST
292 | * @param {Object} operation Query AST
293 | * @return {String} String GraphQL query
294 | */
295 | const buildQuery = (operation={}, skipOperationParsing=false) =>
296 | chain((operation.properties || []).map(a => buildSingleQuery(a)).join('\n'))
297 | .next(body => `${skipOperationParsing ? '' : stringifyOperation(operation)}{\n${body}\n}`)
298 | .next(op => operation.fragments && operation.fragments.length > 0
299 | ? `${op}\n${stringifyFragments(operation.fragments)}`
300 | : op)
301 | .val()
302 |
303 | const stringifyFragments = (fragments=[]) =>
304 | fragments.map(f => `fragment ${f.name} on ${f.type} ${buildQuery(f, true)}`).join('\n')
305 |
306 | const buildSingleQuery = AST => {
307 | if (AST && AST.name) {
308 | const fnName = AST.name
309 | const args = AST.args ? stringifyArgs(AST.args).trim() : ''
310 | const fields = AST.properties && AST.properties.length > 0 ? buildQuery(AST, true) : ''
311 | return AST.kind == 'FragmentSpread'
312 | ? `...${fnName}`
313 | : `${fnName}${args ? `(${args})` : ''}${fields}`
314 | }
315 | else
316 | return ''
317 | }
318 |
319 | const stringifyValue = ({kind, value}) => {
320 | if (Array.isArray(value))
321 | return kind == 'ListValue' ? `[${stringifyArgs(value)}]` : `{${stringifyArgs(value)}}`
322 | else
323 | return kind == 'Variable' ? `$${value}` :
324 | kind == 'StringValue' ? `"${value}"` : value
325 | }
326 |
327 | const stringifyArgs = (args=[]) =>
328 | `${args.map(arg => Array.isArray(arg) ? `{${stringifyArgs(arg)}}` : `${arg.name ? arg.name + ':' : '' }${stringifyValue(arg.value)}`).join(',')}`
329 |
330 | let _defragCache = {}
331 | const defrag = operation => {
332 | if (operation && operation.fragments && operation.fragments.length > 0) {
333 | // reset cache
334 | _defragCache = {}
335 | const properties = replaceFragmentsInProperties(operation.properties, operation.fragments)
336 | // reset cache
337 | _defragCache = {}
338 | return Object.assign({}, operation, { properties, fragments: null })
339 | }
340 | else
341 | return operation
342 | }
343 |
344 | const replaceFragmentsInProperty = (prop, fragments=[]) => {
345 | if (prop.kind == 'FragmentSpread') {
346 | const fragmentName = prop.name
347 | const fragment = fragments.find(f => f.name == fragmentName)
348 | if (!fragment)
349 | throw new Error(`Invalid GraphQL query. Fragment '${fragmentName}' does not exist.`)
350 |
351 | if (!_defragCache[fragmentName])
352 | _defragCache[fragmentName] = replaceFragmentsInProperties(fragment.properties, fragments)
353 |
354 | return _defragCache[fragmentName]
355 | }
356 | else if (prop.properties && prop.properties.length > 0) {
357 | const properties = replaceFragmentsInProperties(prop.properties, fragments)
358 | return Object.assign({}, prop, { properties })
359 | }
360 | else
361 | return prop
362 | }
363 |
364 | const replaceFragmentsInProperties = (properties, fragments=[]) => {
365 | if (properties && properties.length > 0) {
366 | const propertiesObj = properties.reduce((props, p) => {
367 | const _p = replaceFragmentsInProperty(p, fragments)
368 | if (Array.isArray(_p)) {
369 | _p.forEach(property => {
370 | const existingProp = props[property.name]
371 | // Save it if this property is new or if the existing property does not have a metadata property
372 | // WARNING: metadata === undefined is better than metadata == null as it really proves that metadata
373 | // has never been set.
374 | if (!existingProp || existingProp.metadata === undefined)
375 | props[property.name] = property
376 | })
377 | }
378 | else {
379 | const existingProp = props[_p.name]
380 | if (!existingProp || existingProp.metadata === undefined)
381 | props[_p.name] = _p
382 | }
383 | return props
384 | }, {})
385 |
386 | let results = []
387 | for(let key in propertiesObj)
388 | results.push(propertiesObj[key])
389 |
390 | return results
391 | }
392 | else
393 | return null
394 | }
395 |
396 | const newShortId = () => shortid.generate().replace(/-/g, 'r').replace(/_/g, '9')
397 |
398 | module.exports = {
399 | chain,
400 | log,
401 | escapeGraphQlSchema,
402 | removeMultiSpaces,
403 | matchLeftNonGreedy,
404 | getQueryAST,
405 | buildQuery,
406 | time: {
407 | start: startTime,
408 | log: logTime
409 | },
410 | newShortId,
411 | isScalarType
412 | }
--------------------------------------------------------------------------------
/test/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nicolasdao/graphql-s2s/eb7475832d9c88020fba235df705d646a9801c96/test/.DS_Store
--------------------------------------------------------------------------------
/test/browser/graphqls2s.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017, Neap Pty Ltd.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | /* global it */
9 | /* global describe */
10 | /* global chai */
11 | /* global graphqls2s */
12 | var browserctxt = typeof(chai) != 'undefined'
13 | var assert = browserctxt ? chai.assert : null
14 |
15 | function normalizeString(s) {
16 | if (s) {
17 | return s.replace(/\n|\t|\s/g, '')
18 | }
19 | else
20 | return ''
21 | }
22 |
23 | var runtest = function(s2s, assert) {
24 | var compressString = function(s) { return s.replace(/[\n\r]+/g, '').replace(/[\t\r]+/g, '').replace(/ /g,'') }
25 | var getSchemaAST = s2s.getSchemaAST
26 | var transpileSchema = s2s.transpileSchema
27 | var extractGraphMetadata = s2s.extractGraphMetadata
28 | var getQueryAST = s2s.getQueryAST
29 | var buildQuery = s2s.buildQuery
30 | var isTypeGeneric = s2s.isTypeGeneric
31 |
32 | describe('graphqls2s', () => {
33 | describe('#transpileSchema', () => {
34 | describe('BASIC', () => {
35 | it('01 - Should not affect a standard schema after transpilation.', () => {
36 | var output = transpileSchema(`
37 | # This is some description of
38 | # what a Post object is.
39 | type Post {
40 | id: ID!
41 | # A name is a property.
42 | name: String!
43 | }
44 |
45 | type PostUserRating {
46 | # Rating indicates the rating a user gave
47 | # to a post.
48 | rating: PostRating!
49 | }`)
50 | var answer = compressString(output)
51 | var correct = compressString(`
52 | # This is some description of
53 | # what a Post object is.
54 | type Post {
55 | id: ID!
56 | # A name is a property.
57 | name: String!
58 | }
59 |
60 | type PostUserRating {
61 | # Rating indicates the rating a user gave
62 | # to a post.
63 | rating: PostRating!
64 | }`)
65 | assert.equal(answer,correct)
66 | })
67 | })
68 | describe('SPECIAL KEYWORDS', () => {
69 | it('01 - Should support extending schema using the \'extend\' keyword.', () => {
70 | var output = transpileSchema(`
71 | type Query {
72 | bars: [Bar]!
73 | }
74 | type Bar {
75 | id: ID
76 | }
77 | type Foo {
78 | id: String!
79 | }
80 | extend type Query {
81 | foos: [Foo]!
82 | }`)
83 | var answer = compressString(output)
84 | var correct = compressString(`
85 | type Query {
86 | bars: [Bar]!
87 | }
88 | type Bar {
89 | id: ID
90 | }
91 | type Foo {
92 | id: String!
93 | }
94 | extend type Query {
95 | foos: [Foo]!
96 | }`)
97 | assert.equal(answer,correct)
98 | })
99 | it('02 - Should allow to define custom names in generic types using the @alias keyword.', () => {
100 | var schema = `
101 | type Post {
102 | code: String
103 | }
104 |
105 | type Brand {
106 | id: ID!
107 | name: String
108 | posts: Page
109 | }
110 |
111 | @alias((T) => T + 's')
112 | type Page {
113 | data: [T]
114 | }
115 | `
116 | var schema_output = `
117 | type Post {
118 | code: String
119 | }
120 |
121 | type Brand {
122 | id: ID!
123 | name: String
124 | posts: Posts
125 | }
126 |
127 | type Posts {
128 | data: [Post]
129 | }
130 | `
131 | var output = transpileSchema(schema)
132 | var answer = compressString(output)
133 | var correct = compressString(schema_output)
134 | assert.equal(answer,correct)
135 | })
136 | it('03 - Should support custom scalar types.', () => {
137 |
138 | var schema_input = `
139 | scalar Date
140 | scalar Like
141 |
142 | # This is some description of
143 | # what a Post object is plus an attemp to fool the scalar type.
144 | type Post {
145 | id: ID!
146 | # A name is a property.
147 | name: String!
148 | creationDate: Date
149 | likeRate: Like
150 | }
151 |
152 | scalar Strength
153 |
154 | type Test { id: ID }
155 |
156 | type PostUserRating {
157 | # Rating indicates the rating a user gave
158 | # to a post. Fooling test: type Test { id: ID }
159 | rating: Strength!
160 | }
161 | `
162 |
163 | var schema_output = `
164 | # This is some description of
165 | # what a Post object is plus an attemp to fool the scalar type.
166 | type Post {
167 | id: ID!
168 | # A name is a property.
169 | name: String!
170 | creationDate: Date
171 | likeRate: Like
172 | }
173 |
174 | type Test { id: ID }
175 |
176 | type PostUserRating {
177 | # Rating indicates the rating a user gave
178 | # to a post. Fooling test: type Test { id: ID }
179 | rating: Strength!
180 | }
181 |
182 |
183 | scalar Date
184 | scalar Like
185 | scalar Strength`
186 | var output = transpileSchema(schema_input)
187 | var answer = compressString(output)
188 | var correct = compressString(schema_output)
189 | assert.equal(answer,correct)
190 | })
191 | it('04 - Should support union types.', () => {
192 | var schema = `
193 | scalar Date
194 | scalar Like
195 |
196 | union Product = Bicycle | Racket
197 | union Details = PriceDetails | RacketDetails
198 |
199 | # This is some description of
200 | # what a Post object is plus an attemp to fool the union type.
201 | type Post {
202 | id: ID!
203 | # A name is a property.
204 | name: String!
205 | creationDate: Date
206 | likeRate: Like
207 | }
208 |
209 | scalar Strength
210 |
211 | type Test { id: ID }
212 |
213 | type PostUserRating {
214 | # Rating indicates the rating a user gave
215 | # to a post. Fooling test: type Test { id: ID }
216 | rating: Strength!
217 | }`
218 |
219 | var schema_output = `
220 | # This is some description of
221 | # what a Post object is plus an attemp to fool the union type.
222 | type Post {
223 | id: ID!
224 | # A name is a property.
225 | name: String!
226 | creationDate: Date
227 | likeRate: Like
228 | }
229 |
230 | type Test { id: ID }
231 |
232 | type PostUserRating {
233 | # Rating indicates the rating a user gave
234 | # to a post. Fooling test: type Test { id: ID }
235 | rating: Strength!
236 | }
237 |
238 |
239 | scalar Date
240 | scalar Like
241 | scalar Strength
242 | union Product = Bicycle | Racket
243 | union Details = PriceDetails | RacketDetails`
244 | var output = transpileSchema(schema)
245 | var answer = compressString(output)
246 | var correct = compressString(schema_output)
247 | assert.equal(answer,correct)
248 | })
249 | it('05 - Should allow to define custom names in nested generic types', () => {
250 | var schema = `
251 | @alias((T) => 'Standard' + T)
252 | type StandardData {
253 | id: ID!
254 | value: T
255 | }
256 |
257 | @alias((T) => T + 's')
258 | type Paged {
259 | data: [StandardData]
260 | cursor: ID
261 | }
262 |
263 | type Post {
264 | code: String
265 | }
266 |
267 | type User {
268 | posts: Paged
269 | }
270 | `
271 | var schema_output = `
272 | type Post {
273 | code: String
274 | }
275 |
276 | type User {
277 | posts: Posts
278 | }
279 |
280 | type StandardPost {
281 | id: ID!
282 | value: Post
283 | }
284 |
285 | type Posts {
286 | data: [StandardPost]
287 | cursor: ID
288 | }
289 | `
290 |
291 | var output = transpileSchema(schema)
292 | var answer = compressString(output)
293 | var correct = compressString(schema_output)
294 | assert.equal(answer,correct)
295 | })
296 | })
297 | describe('GENERIC TYPES', () => {
298 | it('01 - Should create a new type for each instance of a generic type, as well as removing the original generic type definition.', () => {
299 | // Basic
300 | var output_01 = transpileSchema(`
301 | type Post {
302 | code: String
303 | }
304 |
305 | type Paged {
306 | data: [T]
307 | cursor: ID
308 | }
309 |
310 | type User {
311 | posts: Paged
312 | }
313 | `)
314 | var answer_01 = compressString(output_01)
315 | var correct_01 = compressString(`
316 | type Post {
317 | code: String
318 | }
319 |
320 | type User {
321 | posts: PagedPost
322 | }
323 | type PagedPost {
324 | data: [Post]
325 | cursor: ID
326 | }
327 | `)
328 | assert.equal(answer_01,correct_01)
329 |
330 | // Medium complexity
331 | var schema_02 = `
332 | type Post {
333 | code: String
334 | }
335 |
336 | type StandardData {
337 | id: ID!
338 | value: T
339 | }
340 |
341 | type Paged {
342 | data: [StandardData]
343 | cursor: ID
344 | }
345 |
346 | type User {
347 | posts: Paged
348 | }
349 | `
350 | var schema_output_02 = `
351 | type Post {
352 | code: String
353 | }
354 |
355 | type User {
356 | posts: PagedPost
357 | }
358 |
359 | type StandardDataPost {
360 | id: ID!
361 | value: Post
362 | }
363 |
364 | type PagedPost {
365 | data: [StandardDataPost]
366 | cursor: ID
367 | }
368 | `
369 |
370 | var output_02 = transpileSchema(schema_02)
371 | var answer_02 = compressString(output_02)
372 | var correct_02 = compressString(schema_output_02)
373 | assert.equal(answer_02,correct_02)
374 |
375 | // More complicated test
376 |
377 | var schema_03 = `
378 | type Post {
379 | code: String
380 | }
381 |
382 | type AnotherDeeperGeneric {
383 | data: T
384 | }
385 |
386 | type StandardData {
387 | id: ID!
388 | value: T
389 | magic: AnotherDeeperGeneric
390 | }
391 |
392 | type Paged {
393 | data: [StandardData]
394 | cursor: ID
395 | }
396 |
397 | type User {
398 | posts: Paged
399 | }
400 | `
401 | var schema_output_03 = `
402 | type Post {
403 | code: String
404 | }
405 |
406 | type User {
407 | posts: PagedPost
408 | }
409 |
410 | type AnotherDeeperGenericPost {
411 | data: Post
412 | }
413 |
414 | type StandardDataPost {
415 | id: ID!
416 | value: Post
417 | magic: AnotherDeeperGenericPost
418 | }
419 |
420 | type PagedPost {
421 | data: [StandardDataPost]
422 | cursor: ID
423 | }`
424 |
425 | var output_03 = transpileSchema(schema_03)
426 | var answer_03 = compressString(output_03)
427 | var correct_03 = compressString(schema_output_03)
428 | assert.equal(answer_03,correct_03)
429 |
430 | // Support for the required symbol
431 | var schema_04 = `
432 | type Post {
433 | code: String
434 | }
435 |
436 | type StandardData {
437 | id: ID!
438 | value: T!
439 | }
440 |
441 | type Paged {
442 | data: [StandardData]!
443 | cursor: ID
444 | }
445 |
446 | type User {
447 | posts: Paged!
448 | }
449 | `
450 | var schema_output_04 = `
451 | type Post {
452 | code: String
453 | }
454 |
455 | type User {
456 | posts: PagedPost!
457 | }
458 |
459 | type StandardDataPost {
460 | id: ID!
461 | value: Post!
462 | }
463 |
464 | type PagedPost {
465 | data: [StandardDataPost]!
466 | cursor: ID
467 | }
468 | `
469 |
470 | var output_04 = transpileSchema(schema_04)
471 | var answer_04 = compressString(output_04)
472 | var correct_04 = compressString(schema_output_04)
473 | assert.equal(answer_04,correct_04)
474 | })
475 | it('02 - Should create a new type for each instance of a generic type, even for generic with multi-types, as well as removing the original generic type definition.', () => {
476 |
477 | var schema = `
478 | type Post {
479 | code: String
480 | }
481 |
482 | type Date {
483 | time: String
484 | }
485 |
486 | type StandardData {
487 | id: ID!
488 | value: T
489 | Dimension: U
490 | }
491 |
492 | type Paged {
493 | data: [StandardData< T, U>]
494 | cursor: ID
495 | }
496 |
497 | type User {
498 | posts: Paged
499 | }
500 | `
501 | var schema_output = `
502 | type Post {
503 | code: String
504 | }
505 |
506 | type Date {
507 | time: String
508 | }
509 |
510 | type User {
511 | posts: PagedPostDate
512 | }
513 |
514 | type StandardDataPostDate {
515 | id: ID!
516 | value: Post
517 | Dimension: Date
518 | }
519 |
520 | type PagedPostDate {
521 | data: [StandardDataPostDate]
522 | cursor: ID
523 | }
524 | `
525 |
526 | var output = transpileSchema(schema)
527 | var answer = compressString(output)
528 | var correct = compressString(schema_output)
529 | assert.equal(answer,correct)
530 | })
531 | it('03 - Should create a new type or input for each instance of a generic type, even for generic with multi-types and inputs, as well as removing the original generic input definition.', () => {
532 |
533 | var schema = `
534 | type Post {
535 | code: String
536 | }
537 |
538 | type Date {
539 | time: String
540 | }
541 |
542 | type Query {
543 | user(identifier:ID!):User,
544 | users(filter: Filter):User
545 | }
546 |
547 | input Filter {
548 | field: FilterFields!,
549 | value: String!
550 | }
551 |
552 | enum UserFilterFields {
553 | firstName
554 | lastName
555 | }
556 |
557 | type StandardData {
558 | id: ID!
559 | value: T
560 | Dimension: U
561 | }
562 |
563 | type Paged {
564 | data: [StandardData< T, U>]
565 | cursor: ID
566 | }
567 |
568 | type User {
569 | firstName: String,
570 | lastName: String,
571 | posts: Paged
572 | }
573 | `
574 | var schema_output = `
575 | type Post {
576 | code: String
577 | }
578 |
579 | type Date {
580 | time: String
581 | }
582 |
583 | type Query {
584 | user(identifier: ID!): User
585 | users(filter: FilterUserFilterFields): User
586 | }
587 | type User {
588 | firstName: String
589 | lastName: String
590 | posts: PagedPostDate
591 | }
592 | enum UserFilterFields {
593 | firstName
594 | lastName
595 | }
596 | input FilterUserFilterFields {
597 | field: UserFilterFields!
598 | value: String!
599 | }
600 | type StandardDataPostDate {
601 | id: ID!
602 | value: Post
603 | Dimension: Date
604 | }
605 | type PagedPostDate {
606 | data: [StandardDataPostDate]
607 | cursor: ID
608 | }
609 | `
610 |
611 | var output = transpileSchema(schema)
612 | var answer = compressString(output)
613 | var correct = compressString(schema_output)
614 | assert.equal(answer,correct)
615 | })
616 | it('04 - Should support non-nullable typed array (issue #23).', () => {
617 |
618 | var schema = `
619 | type Question {
620 | value: String
621 | }
622 |
623 | type Paged {
624 | data: [T!]!
625 | cursor: ID
626 | }
627 |
628 | type Student {
629 | name: String
630 | questions: Paged
631 | }
632 | `
633 |
634 | var schema_output = `
635 | type Question {
636 | value: String
637 | }
638 |
639 | type Student {
640 | name: String
641 | questions: PagedQuestion
642 | }
643 | type PagedQuestion {
644 | data: [Question!]!
645 | cursor: ID
646 | }
647 | `
648 |
649 | var output = transpileSchema(schema)
650 | var answer = compressString(output)
651 | var correct = compressString(schema_output)
652 | assert.equal(answer,correct)
653 | })
654 | })
655 | describe('METADATA', () => {
656 | it('01 - Should remove any metadata from the GraphQL schema so it can be compiled by Graphql.js.', () => {
657 | var output = transpileSchema(`
658 | @node
659 | type Brand {
660 | id: ID!
661 | name: String
662 | @edge('<-[ABOUT]-')
663 | posts: [Post]
664 | }
665 |
666 | @miracle
667 | input User {
668 | posts: [Post]
669 | }
670 | `)
671 | var answer = compressString(output)
672 | var correct = compressString(`
673 | type Brand {
674 | id: ID!
675 | name: String
676 | posts: [Post]
677 | }
678 |
679 | input User {
680 | posts: [Post]
681 | }
682 | `)
683 | assert.equal(answer,correct)
684 | })
685 | })
686 | describe('COMMENTS', () => {
687 | it('01 - Should successfully transpile the schema even when there are complex markdown comments containing code blocks.', () => {
688 | var schema = `
689 | # ### Page - Pagination Metadata
690 | # The Page object represents metadata about the size of the dataset returned. It helps with pagination.
691 | # Example:
692 | #
693 | # \`\`\`js
694 | # getData(first: 100, skip: 200)
695 | # \`\`\`
696 | # Skips the first 200 items, and gets the next 100.
697 | #
698 | # To help represent this query using pages, GraphHub adds properties like _current_ and _total_. In the
699 | # example above, the returned Page object could be:
700 | #
701 | # \`\`\`js
702 | # {
703 | # first: 100,
704 | # skip: 200,
705 | # current: 3,
706 | # total: {
707 | # size: 1000,
708 | # pages: 10
709 | # }
710 | # }
711 | # \`\`\`
712 | type Page {
713 | # The pagination parameter sent in the query (type, input {})
714 | first: Int!
715 |
716 | # The pagination parameter sent in the query
717 | skip: Int!
718 |
719 | # The convertion from 'first' and 'after' in terms of the current page
720 | # (e.g. { first: 100, after: 200 } -> current: 3).
721 | current: Int!
722 |
723 | # Inspect the total size of your dataset ignoring pagination.
724 | total: DatasetSize
725 | }
726 |
727 | # ### DatasetSize - Pagination Metadata
728 | # Used in the Page object to describe the total number of pages available.
729 | type DatasetSize {
730 | size: Int!
731 | pages: Int!
732 | }}
733 | `
734 | var schema_output = `
735 | # ### Page - Pagination Metadata
736 | # The Page object represents metadata about the size of the dataset returned. It helps with pagination.
737 | # Example:
738 | #
739 | # \`\`\`js
740 | # getData(first: 100, skip: 200)
741 | # \`\`\`
742 | # Skips the first 200 items, and gets the next 100.
743 | #
744 | # To help represent this query using pages, GraphHub adds properties like _current_ and _total_. In the
745 | # example above, the returned Page object could be:
746 | #
747 | # \`\`\`js
748 | # {
749 | # first: 100,
750 | # skip: 200,
751 | # current: 3,
752 | # total: {
753 | # size: 1000,
754 | # pages: 10
755 | # }
756 | # }
757 | # \`\`\`
758 | type Page {
759 | # The pagination parameter sent in the query (type, input {})
760 | first: Int!
761 | # The pagination parameter sent in the query
762 | skip: Int!
763 | # The convertion from 'first' and 'after' in terms of the current page
764 | # (e.g. { first: 100, after: 200 } -> current: 3).
765 | current: Int!
766 | # Inspect the total size of your dataset ignoring pagination.
767 | total: DatasetSize
768 | }
769 |
770 | # ### DatasetSize - Pagination Metadata
771 | # Used in the Page object to describe the total number of pages available.
772 | type DatasetSize {
773 | size: Int!
774 | pages: Int!
775 | }`
776 | var output = transpileSchema(schema)
777 | var answer = compressString(output)
778 | var correct = compressString(schema_output)
779 | assert.equal(answer,correct)
780 | })
781 | it('02 - Should successfully transpile the schema even when there are complex markdown comments containing code blocks from inherited types (bug #15).', () => {
782 | var schema = `
783 | # The most generic type of item. See also: schema.org/Thing
784 | type Thing {
785 | description: String
786 | identifier: ID!
787 | name: String
788 | url: String
789 | }
790 |
791 | # A person (alive, dead, undead, or fictional). See also: schema.org/Person
792 | type Person inherits Thing {
793 | # Person Blabla
794 | email: String
795 | familyName: String
796 | givenName: String
797 | }
798 | `
799 |
800 | var schema_output = `
801 | # The most generic type of item. See also: schema.org/Thing
802 | type Thing {
803 | description: String
804 | identifier: ID!
805 | name: String
806 | url: String
807 | }
808 |
809 | # A person (alive, dead, undead, or fictional). See also: schema.org/Person
810 | type Person {
811 | # Person Blabla
812 | email: String
813 | familyName: String
814 | givenName: String
815 | description: String
816 | identifier: ID!
817 | name: String
818 | url: String
819 | }`
820 | var output = transpileSchema(schema)
821 | //console.log(output)
822 | var answer = compressString(output)
823 | var correct = compressString(schema_output)
824 | assert.equal(answer,correct)
825 | })
826 | it('03 - Add management of description', () => {
827 | var schema = `
828 | # My comment
829 |
830 | """
831 | Description with multiple
832 | lines of my interface
833 | """
834 | interface Name {
835 | "Description of a field"
836 | name: String!
837 | }
838 |
839 | "Single line comment of the type"
840 | type PostUserRating inherits Name implements Name {
841 | """
842 | Multi-line comment of member
843 | """
844 | rating: String!
845 | # FIXME: Just a comment
846 | other: Int
847 | }
848 |
849 | "Test on enum"
850 | enum SolutionModeleEnum {
851 | "Comment 1"
852 | P1
853 |
854 | """
855 | Comment 2
856 | multiline
857 | """
858 | P2
859 |
860 | # Comment
861 | P3
862 | }
863 |
864 | # My comment
865 | # multi-line
866 | type AutreType {
867 | x: Boolean
868 | }
869 |
870 | `
871 |
872 | var schema_output = `
873 | # My comment
874 | """
875 | Description with multiple
876 | lines of my interface
877 | """
878 | interface Name {
879 | "Description of a field"
880 | name: String!
881 | }
882 |
883 | "Single line comment of the type"
884 | type PostUserRating implements Name {
885 | """
886 | Multi-line comment of member
887 | """
888 | rating: String!
889 | # FIXME: Just a comment
890 | other: Int
891 | "Description of a field"
892 | name: String!
893 | }
894 |
895 | # My comment
896 | # multi-line
897 | type AutreType {
898 | x: Boolean
899 | }
900 |
901 | "Test on enum"
902 | enum SolutionModeleEnum {
903 | "Comment 1"
904 | P1
905 | """
906 | Comment 2
907 | multiline
908 | """
909 | P2
910 | # Comment
911 | P3
912 | }`
913 |
914 |
915 | var output = transpileSchema(schema)
916 | var answer = compressString(output)
917 | var correct = compressString(schema_output)
918 | assert.equal(answer,correct)
919 | })
920 | })
921 | describe('DIRECTIVES', () => {
922 | it('01 - Should support directives.', () => {
923 |
924 | var schema = `
925 | directive @isAuthenticated on QUERY | FIELD
926 | directive @deprecated
927 | (
928 | reason: String = "No longer on supported"
929 | ) on FIELD_DEFINITION | ENUM_VALUE
930 |
931 | type Post {
932 | code: String
933 | }
934 |
935 | type Date {
936 | code: String
937 | }
938 |
939 | type ExampleType {
940 | newField: String
941 | oldField: String @deprecated(reason: "Use 'newField'.")
942 | }
943 |
944 | type StandardData {
945 | @auth
946 | id: ID!
947 | value: T
948 | Dimension: U
949 | }
950 |
951 | type Paged {
952 | data: [StandardData< T, U>]
953 | cursor: ID @isAuthenticated
954 | }
955 |
956 | type User {
957 | posts: Paged
958 | }
959 | `
960 | var schema_output = `
961 | directive @isAuthenticated on QUERY | FIELD
962 | directive @deprecated
963 | (
964 | reason: String = "No longer on supported"
965 | ) on FIELD_DEFINITION | ENUM_VALUE
966 |
967 | type Post {
968 | code: String
969 | }
970 |
971 | type Date {
972 | code: String
973 | }
974 |
975 | type ExampleType {
976 | newField: String
977 | oldField: String @deprecated(reason: "Use 'newField'.")
978 | }
979 |
980 | type User {
981 | posts: PagedPostDate
982 | }
983 |
984 | type StandardDataPostDate {
985 | id: ID!
986 | value: Post
987 | Dimension: Date
988 | }
989 |
990 | type PagedPostDate {
991 | data: [StandardDataPostDate]
992 | cursor: ID @isAuthenticated
993 | }
994 | `
995 |
996 | var output = transpileSchema(schema)
997 | var answer = compressString(output)
998 | var correct = compressString(schema_output)
999 | assert.equal(answer,correct)
1000 | })
1001 | it('02 - Should support directive after generic type.', () => {
1002 | var schema = `
1003 | directive @isAuthenticated on QUERY | FIELD
1004 | directive @deprecated(reason: String = "No longer on supported") on FIELD_DEFINITION | ENUM_VALUE
1005 |
1006 | type Post {
1007 | code: String
1008 | }
1009 |
1010 | type Date {
1011 | code: String
1012 | }
1013 |
1014 | type ExampleType {
1015 | newField: String
1016 | oldField: String @deprecated(reason: "Use 'newField'.")
1017 | }
1018 |
1019 | @alias((T,U) => T + U)
1020 | type StandardData {
1021 | @auth
1022 | id: ID!
1023 | value: T
1024 | Dimension: U
1025 | }
1026 |
1027 | type Paged {
1028 | data: [StandardData< T, U>] @isAuthenticated
1029 | cursor: ID @isAuthenticated
1030 | }
1031 |
1032 | type User {
1033 | posts: Paged @isAuthenticated
1034 | examples: Paged @deprecated(reason: "Use 'newField'.")
1035 | }
1036 | `
1037 | var schema_output = `
1038 | directive @isAuthenticated on QUERY | FIELD
1039 | directive @deprecated
1040 | (
1041 | reason: String = "No longer on supported"
1042 | ) on FIELD_DEFINITION | ENUM_VALUE
1043 |
1044 | type Post {
1045 | code: String
1046 | }
1047 |
1048 | type Date {
1049 | code: String
1050 | }
1051 |
1052 | type ExampleType {
1053 | newField: String
1054 | oldField: String @deprecated(reason: "Use 'newField'.")
1055 | }
1056 |
1057 | type User {
1058 | posts: PagedPostDate @isAuthenticated
1059 | examples: PagedExampleTypeDate @deprecated(reason: "Use 'newField'.")
1060 | }
1061 |
1062 | type PostDate {
1063 | id: ID!
1064 | value: Post
1065 | Dimension: Date
1066 | }
1067 |
1068 | type PagedPostDate {
1069 | data: [PostDate] @isAuthenticated
1070 | cursor: ID @isAuthenticated
1071 | }
1072 |
1073 | type ExampleTypeDate {
1074 | id: ID!
1075 | value: ExampleType
1076 | Dimension: Date
1077 | }
1078 |
1079 | type PagedExampleTypeDate {
1080 | data: [ExampleTypeDate] @isAuthenticated
1081 | cursor: ID @isAuthenticated
1082 | }
1083 | `
1084 |
1085 | var output = transpileSchema(schema)
1086 | var answer = compressString(output)
1087 | var correct = compressString(schema_output)
1088 | assert.equal(answer,correct)
1089 | })
1090 | it('03 - Should support rogue native directives, i.e., native directive without explicit definitions (fix #14).', () => {
1091 | var schema = `
1092 | # Mutation
1093 | type Mutation {
1094 | createName(name: String!): Name
1095 | }
1096 |
1097 | type Subscription {
1098 | nameCreated: Name @aws_subscribe(mutations:["createName"])
1099 | }
1100 |
1101 | type Name @cacheControl(maxAge: 240) {
1102 | @node
1103 | name: String!
1104 | }
1105 |
1106 | type Surname inherits Name @cacheControl(maxAge: 240) {
1107 | alias: String
1108 | }`
1109 |
1110 | var schema_output = `
1111 | # Mutation
1112 | type Mutation {
1113 | createName(name: String!): Name
1114 | }
1115 |
1116 | type Subscription {
1117 | nameCreated: Name @aws_subscribe(mutations:["createName"])
1118 | }
1119 |
1120 | type Name @cacheControl(maxAge: 240) {
1121 | name: String!
1122 | }
1123 |
1124 | type Surname @cacheControl(maxAge: 240) {
1125 | alias: String
1126 | name: String!
1127 | }`
1128 | var output = transpileSchema(schema)
1129 | var answer = compressString(output)
1130 | var correct = compressString(schema_output)
1131 | assert.equal(answer,correct, '01')
1132 |
1133 | var schema_02 = `
1134 | enum OperationType {
1135 | INVEST
1136 | WITHDRAW
1137 | }
1138 | type Transaction {
1139 | id: ID! @unique
1140 | user: User!
1141 | date: DateTime!
1142 | operationType: OperationType!
1143 | amount: Float!
1144 | tx: String
1145 | notes: String
1146 | }
1147 |
1148 | enum Role {
1149 | ADMIN
1150 | USER
1151 | }
1152 |
1153 | type User {
1154 | id: ID! @unique
1155 | email: String! @unique
1156 | name: String!
1157 | roles: [Role!]!
1158 | referrer: User @relation(name: "UserReferrerRelation")
1159 | referrals: [User!]! @relation(name: "UserReferralsRelation")
1160 | password: String!
1161 | rate: Float!
1162 | }`
1163 |
1164 | var schema_output_02 = `
1165 | type Transaction {
1166 | id: ID! @unique
1167 | user: User!
1168 | date: DateTime!
1169 | operationType: OperationType!
1170 | amount: Float!
1171 | tx: String
1172 | notes: String
1173 | }
1174 | type User {
1175 | id: ID! @unique
1176 | email: String! @unique
1177 | name: String!
1178 | roles: [Role!]!
1179 | referrer: User @relation(name: "UserReferrerRelation")
1180 | referrals: [User!]! @relation(name: "UserReferralsRelation")
1181 | password: String!
1182 | rate: Float!
1183 | }
1184 | enum OperationType {
1185 | INVEST
1186 | WITHDRAW
1187 | }
1188 | enum Role {
1189 | ADMIN
1190 | USER
1191 | }`
1192 |
1193 | var output_02 = transpileSchema(schema_02)
1194 | var answer_02 = compressString(output_02)
1195 | var correct_02 = compressString(schema_output_02)
1196 | assert.equal(answer_02,correct_02, '02')
1197 | })
1198 | it('04 - Should support directives with complex body (fix #37).', () => {
1199 | var schema = `
1200 | # Mutation
1201 | type Mutation {
1202 | CreateArea(name: String): Area @cypher(statement: "CREATE (a:Area {name: $name, creationDate: timestamp()}) RETURN a")
1203 | }`
1204 |
1205 | var schema_output = `
1206 | # Mutation
1207 | type Mutation {
1208 | CreateArea(name: String): Area @cypher(statement: "CREATE (a:Area {name: $name, creationDate: timestamp()}) RETURN a")
1209 | }`
1210 |
1211 | var output = transpileSchema(schema)
1212 | var answer = compressString(output)
1213 | var correct = compressString(schema_output)
1214 | assert.equal(answer,correct, '01')
1215 | })
1216 | })
1217 | describe('INHERITANCE', () => {
1218 | it('01 - Should not let a type inherits from a super type when the \'inherits\' keyword has been commented out on the same line (e.g. \'type User { #inherits Person {\').', () => {
1219 | var output = transpileSchema(`
1220 | type Person {
1221 | firstname: String
1222 | lastname: String
1223 | }
1224 |
1225 | type User { #inherits Person {
1226 | username: String!
1227 | posts: [Post]
1228 | }
1229 | `)
1230 | var answer = compressString(output)
1231 | var correct = compressString(`
1232 | type Person {
1233 | firstname: String
1234 | lastname: String
1235 | }
1236 |
1237 | type User {
1238 | #inherits Person {
1239 | username: String!
1240 | posts: [Post]
1241 | }
1242 | `)
1243 | assert.equal(answer,correct)
1244 | })
1245 | it('02 - Should add properties from the super type to the sub type.', () => {
1246 | var output = transpileSchema(`
1247 | type Post {
1248 | id: ID!
1249 | name: String!
1250 | }
1251 | type PostUserRating inherits Post {
1252 | rating: PostRating!
1253 | }
1254 | `)
1255 | var answer = compressString(output)
1256 | var correct = compressString(`
1257 | type Post {
1258 | id: ID!
1259 | name: String!
1260 | }
1261 |
1262 | type PostUserRating {
1263 | rating: PostRating!
1264 | id: ID!
1265 | name: String!
1266 | }
1267 | `)
1268 | assert.equal(answer,correct)
1269 | })
1270 | it('03 - Should support multiple inheritance type.', () => {
1271 | var output = transpileSchema(`
1272 | type Name {
1273 | name: String!
1274 | }
1275 | type Author {
1276 | author: String!
1277 | }
1278 | type PostUserRating inherits Name,Author {
1279 | rating: String!
1280 | }`)
1281 | var answer = compressString(output)
1282 | var correct = compressString(`
1283 | type Name {
1284 | name: String!
1285 | }
1286 | type Author {
1287 | author: String!
1288 | }
1289 | type PostUserRating {
1290 | rating: String!
1291 | name: String!
1292 | author: String!
1293 | }`)
1294 | assert.equal(answer,correct)
1295 | })
1296 | it('04 - Should support multiple inheritance type with implements interface.', () => {
1297 | var output = transpileSchema(`
1298 | interface Node {
1299 | id: Int!
1300 | }
1301 | type Name {
1302 | name: String!
1303 | }
1304 | type Author {
1305 | author: String!
1306 | }
1307 | type PostUserRating inherits Name,Author implements Node {
1308 | id: Int!
1309 | rating: String!
1310 | }`)
1311 | var answer = compressString(output)
1312 | var correct = compressString(`
1313 | interface Node {
1314 | id:Int!
1315 | }
1316 | type Name {
1317 | name: String!
1318 | }
1319 | type Author {
1320 | author: String!
1321 | }
1322 | type PostUserRating implements Node {
1323 | id: Int!
1324 | rating: String!
1325 | name: String!
1326 | author: String!
1327 | }`)
1328 | assert.equal(answer,correct)
1329 | })
1330 | it('05 - Should throw an error if inherited type is missing.', () => {
1331 | assert.throws(() =>
1332 | transpileSchema(`
1333 | type Name {
1334 | name: String!
1335 | }
1336 | type PostUserRating inherits Name,Author {
1337 | rating: String!
1338 | }`),
1339 | 'Schema error: Type \'Author\' cannot be found in the schema.'
1340 | )
1341 | })
1342 | it('06 - Should throw an error if inherits from wrong type, it should be of "type=\'TYPE\'" or "type=\'INTERFACE\'".', () => {
1343 | assert.throws(() =>
1344 | transpileSchema(`
1345 | input Name {
1346 | name: String!
1347 | }
1348 | type PostUserRating inherits Name {
1349 | rating: String!
1350 | }`),
1351 | 'Schema error: type PostUserRating cannot inherit from INPUT Name.'
1352 | )
1353 | })
1354 | it('07 - Should support inheriting from an INTERFACE.', () => {
1355 | var schema = `
1356 | interface Name {
1357 | name: String!
1358 | }
1359 | type PostUserRating inherits Name {
1360 | rating: String!
1361 | }`
1362 |
1363 | var schema_output = `
1364 | interface Name {
1365 | name: String!
1366 | }
1367 | type PostUserRating {
1368 | rating: String!
1369 | name: String!
1370 | }`
1371 |
1372 |
1373 | var output = transpileSchema(schema)
1374 | var answer = compressString(output)
1375 | var correct = compressString(schema_output)
1376 | assert.equal(answer,correct)
1377 | })
1378 | it('08 - Should support inheriting from an INTERFACE and implementing it.', () => {
1379 | var schema = `
1380 | interface Name {
1381 | name: String!
1382 | }
1383 | type PostUserRating inherits Name implements Name {
1384 | rating: String!
1385 | }`
1386 |
1387 | var schema_output = `
1388 | interface Name {
1389 | name: String!
1390 | }
1391 | type PostUserRating implements Name{
1392 | rating: String!
1393 | name: String!
1394 | }`
1395 |
1396 |
1397 | var output = transpileSchema(schema)
1398 | var answer = compressString(output)
1399 | var correct = compressString(schema_output)
1400 | assert.equal(answer,correct)
1401 | })
1402 | it('09 - Should support inheriting from an generic types.', () => {
1403 | var schema_01 = `
1404 | enum MyEnum {
1405 | option1
1406 | option2
1407 | }
1408 |
1409 | type BaseGeneric {
1410 | enumeratedThing: T!
1411 | otherProperty: String
1412 | }
1413 |
1414 | type FinalType inherits BaseGeneric {
1415 | someOtherProperty: String!
1416 | }`
1417 |
1418 | var schema_output_01 = `
1419 | type FinalType {
1420 | someOtherProperty: String!
1421 | enumeratedThing: MyEnum!
1422 | otherProperty: String
1423 | }
1424 |
1425 | enum MyEnum {
1426 | option1
1427 | option2
1428 | }
1429 |
1430 | type BaseGenericMyEnum {
1431 | enumeratedThing: MyEnum!
1432 | otherProperty: String
1433 | }`
1434 |
1435 |
1436 | var output_01 = transpileSchema(schema_01)
1437 | var answer_01 = compressString(output_01)
1438 | var correct_01 = compressString(schema_output_01)
1439 | assert.equal(answer_01,correct_01)
1440 |
1441 | var schema_02 = `
1442 | enum MyEnum {
1443 | option1
1444 | option2
1445 | }
1446 |
1447 | type Node {
1448 | id: ID
1449 | }
1450 |
1451 | type BaseGeneric inherits Node {
1452 | enumeratedThing: T!
1453 | otherProperty: String
1454 | }
1455 |
1456 | type Person {
1457 | name: String
1458 | }
1459 |
1460 | type SomethingElse inherits Person {
1461 | data: BaseGeneric
1462 | }
1463 |
1464 | type FinalType inherits BaseGeneric {
1465 | someOtherProperty: String!
1466 | }`
1467 |
1468 | var schema_output_02 = `
1469 | type Node {
1470 | id: ID
1471 | }
1472 |
1473 | type Person {
1474 | name: String
1475 | }
1476 |
1477 | type SomethingElse {
1478 | data: BaseGenericInt
1479 | name: String
1480 | }
1481 |
1482 | type FinalType {
1483 | someOtherProperty: String!
1484 | enumeratedThing: MyEnum!
1485 | otherProperty: String
1486 | id: ID
1487 | }
1488 |
1489 | enum MyEnum {
1490 | option1
1491 | option2
1492 | }
1493 |
1494 | type BaseGenericInt {
1495 | enumeratedThing: Int!
1496 | otherProperty: String
1497 | id: ID
1498 | }
1499 |
1500 | type BaseGenericMyEnum {
1501 | enumeratedThing: MyEnum!
1502 | otherProperty: String
1503 | id: ID
1504 | }`
1505 |
1506 |
1507 | var output_02 = transpileSchema(schema_02)
1508 | var answer_02 = compressString(output_02)
1509 | var correct_02 = compressString(schema_output_02)
1510 | assert.equal(answer_02,correct_02)
1511 | })
1512 | })
1513 | describe('BUG FIXES', () => {
1514 | it('01 - Should not duplicate properties', () => {
1515 |
1516 | var schema = `
1517 | type Organism {
1518 | uuid:ID!
1519 | }
1520 |
1521 | type People inherits Organism{
1522 | name:String
1523 | }
1524 |
1525 | type Man inherits People{
1526 | name:String
1527 | bearded:Boolean
1528 | }
1529 | type Boy inherits Man,People,Organism{
1530 | uuid:ID!
1531 | name:String
1532 | bearded:Boolean
1533 | }
1534 | `
1535 |
1536 | var schema_output = `
1537 | type Organism{
1538 | uuid:ID!
1539 | }
1540 |
1541 | type People{
1542 | name:String
1543 | uuid:ID!
1544 | }
1545 |
1546 | type Man{
1547 | name:String
1548 | bearded:Boolean
1549 | uuid:ID!
1550 | }
1551 |
1552 | type Boy{
1553 | uuid:ID!
1554 | name:String
1555 | bearded:Boolean
1556 | }
1557 | `
1558 | var output = transpileSchema(schema)
1559 | var answer = compressString(output)
1560 | var correct = compressString(schema_output)
1561 | assert.equal(answer,correct)
1562 | })
1563 | it('02 - Override properties should not be a property of the parent class', () => {
1564 | var schema = `
1565 | type Organism {
1566 | uuid:ID!
1567 | age:Int
1568 | }
1569 |
1570 | type People inherits Organism {
1571 | name:String
1572 | age:String
1573 | }
1574 | type Person inherits People {
1575 | age:Int
1576 | }
1577 | `
1578 | var schema_output = `
1579 | type Organism {
1580 | uuid:ID!
1581 | age:Int
1582 | }
1583 |
1584 | type People{
1585 | name:String
1586 | age:String
1587 | uuid:ID!
1588 | }
1589 |
1590 | type Person{
1591 | age:Int
1592 | name:String
1593 | uuid:ID!
1594 | }
1595 | `
1596 | var output = transpileSchema(schema)
1597 | var answer = compressString(output)
1598 | var correct = compressString(schema_output)
1599 | assert.equal(answer,correct)
1600 | })
1601 | })
1602 | })
1603 |
1604 | describe('#isTypeGeneric', () =>
1605 | it('Should test whether or not a type is a generic type based on predefined type constraints.', () => {
1606 |
1607 | assert.isOk(isTypeGeneric('T', 'T'), '\'T\', \'T\' should work.')
1608 | assert.isOk(isTypeGeneric('T', 'T,U'), '\'T\', \'T,U\' should work.')
1609 | assert.isOk(isTypeGeneric('Paged', 'T'), '\'Paged\', \'T\' should work.')
1610 | assert.isOk(isTypeGeneric('[T]', 'T'), '\'[T]\', \'T\' should work.')
1611 | assert.isOk(isTypeGeneric('[Paged]', 'T'), '\'[Paged]\', \'T\' should work.')
1612 | assert.isOk(!isTypeGeneric('Product', 'T'), '\'Product\', \'T\' should NOT work.')
1613 | assert.isOk(!isTypeGeneric('Paged', 'T'), '\'Paged\', \'T\' should NOT work.')
1614 | assert.isOk(!isTypeGeneric('[Paged]', 'T'), '\'[Paged]\', \'T\' should NOT work.')
1615 | }))
1616 |
1617 | describe('#extractGraphMetadata: EXTRACT METADATA', () =>
1618 | it('Should extract all metadata (i.e. data starting with \'@\') located on top of schema types of properties.', () => {
1619 | var output = extractGraphMetadata(`
1620 | @node
1621 | type Brand {
1622 | id: ID!
1623 | name: String
1624 | @edge('<-[ABOUT]-')
1625 | posts: [Post]
1626 | }
1627 |
1628 | @miracle
1629 | input User {
1630 | posts: [Post]
1631 | }
1632 | `)
1633 | //console.log(inspect(output));
1634 | assert.isOk(output)
1635 | assert.isOk(output.length)
1636 | assert.equal(output.length, 3)
1637 | var meta1 = output[0]
1638 | var meta2 = output[1]
1639 | var meta3 = output[2]
1640 | assert.equal(meta1.name, 'node')
1641 | assert.equal(meta2.name, 'edge')
1642 | assert.equal(meta3.name, 'miracle')
1643 | assert.equal(meta1.body, '')
1644 | assert.equal(meta2.body, '(\'<-[ABOUT]-\')')
1645 | assert.equal(meta3.body, '')
1646 | assert.equal(meta1.schemaType, 'TYPE')
1647 | assert.equal(meta2.schemaType, 'PROPERTY')
1648 | assert.equal(meta3.schemaType, 'INPUT')
1649 | assert.equal(meta1.schemaName, 'Brand')
1650 | assert.equal(meta2.schemaName, 'posts: [Post]')
1651 | assert.equal(meta3.schemaName, 'User')
1652 | assert.equal(meta1.parent, null)
1653 | assert.isOk(meta2.parent)
1654 | assert.equal(meta3.parent, null)
1655 | assert.equal(meta2.parent.type, 'TYPE')
1656 | assert.equal(meta2.parent.name, 'Brand')
1657 | assert.isOk(meta2.parent.metadata)
1658 | assert.equal(meta2.parent.metadata.type, 'TYPE')
1659 | assert.equal(meta2.parent.metadata.name, 'node')
1660 | }))
1661 |
1662 | describe('#getSchemaAST', () => {
1663 | it('01 - BASICS: Should extract all types and their properties including their respective comments.', () => {
1664 | var schema = `
1665 | # This is some description of
1666 | # what a Post object is.
1667 | type Post {
1668 | id: ID!
1669 | # A name is a property.
1670 | name: String!
1671 | }
1672 |
1673 | input PostUserRating {
1674 | # Rating indicates the rating a user gave
1675 | # to a post.
1676 | rating: PostRating!
1677 | }
1678 | `
1679 | var schemaParts = getSchemaAST(schema)
1680 | //console.log(schemaParts);
1681 | assert.isOk(schemaParts, 'schemaParts should exist.')
1682 | assert.equal(schemaParts.length, 2)
1683 |
1684 | var type1 = schemaParts[0]
1685 | assert.equal(type1.type, 'TYPE')
1686 | assert.equal(type1.name, 'Post')
1687 | assert.equal(type1.genericType, null)
1688 | assert.equal(type1.inherits, null)
1689 | assert.equal(type1.implements, null)
1690 | assert.equal(compressString(type1.comments), compressString('# This is some description of\n# what a Post object is.'))
1691 | assert.isOk(type1.blockProps, 'type1.blockProps should exist.')
1692 | assert.equal(type1.blockProps.length, 2)
1693 | var type1Prop1 = type1.blockProps[0]
1694 | var type1Prop2 = type1.blockProps[1]
1695 | assert.equal(!type1Prop1.comments, true)
1696 | assert.isOk(type1Prop1.details, 'type1Prop1.details should exist.')
1697 | assert.equal(type1Prop1.details.name, 'id')
1698 | assert.equal(type1Prop1.details.params, null)
1699 | assert.isOk(type1Prop1.details.result, 'type1Prop1.details.result should exist.')
1700 | assert.equal(type1Prop1.details.result.originName, 'ID!')
1701 | assert.equal(type1Prop1.details.result.isGen, false)
1702 | assert.equal(type1Prop1.details.result.name, 'ID!')
1703 | assert.equal(compressString(type1Prop2.comments), compressString('# A name is a property.'))
1704 | assert.isOk(type1Prop2.details, 'type1Prop2.details should exist.')
1705 | assert.equal(type1Prop2.details.name, 'name')
1706 | assert.equal(type1Prop2.details.params, null)
1707 | assert.isOk(type1Prop2.details.result, 'type1Prop2.details.result should exist.')
1708 | assert.equal(type1Prop2.details.result.originName, 'String!')
1709 | assert.equal(type1Prop2.details.result.isGen, false)
1710 | assert.equal(type1Prop2.details.result.name, 'String!')
1711 |
1712 | var type2 = schemaParts[1]
1713 | assert.equal(type2.type, 'INPUT')
1714 | assert.equal(type2.name, 'PostUserRating')
1715 | assert.equal(type2.genericType, null)
1716 | assert.equal(type2.inherits, null)
1717 | assert.equal(type2.implements, null)
1718 | assert.equal(!type2.comments, true)
1719 | assert.isOk(type2.blockProps, 'type2.blockProps should exist.')
1720 | assert.equal(type2.blockProps.length, 1)
1721 | var type2Prop1 = type2.blockProps[0]
1722 | assert.equal(compressString(type2Prop1.comments), compressString('# Rating indicates the rating a user gave\n# to a post.'))
1723 | assert.isOk(type2Prop1.details, 'type2Prop1.details should exist.')
1724 | assert.equal(type2Prop1.details.name, 'rating')
1725 | assert.equal(type2Prop1.details.params, null)
1726 | assert.isOk(type2Prop1.details.result, 'type2Prop1.details.result should exist.')
1727 | assert.equal(type2Prop1.details.result.originName, 'PostRating!')
1728 | assert.equal(type2Prop1.details.result.isGen, false)
1729 | assert.equal(type2Prop1.details.result.name, 'PostRating!')
1730 | })
1731 | it('02 - GENERIC TYPES: Should create new types for each instance of a generic type.', () => {
1732 | var schema = `
1733 | type Paged {
1734 | data: [T]
1735 | cursor: ID
1736 | }
1737 | type Post {
1738 | name
1739 | }
1740 | type User {
1741 | username: String!
1742 | posts: Paged
1743 | }
1744 | `
1745 | var schemaParts = getSchemaAST(schema)
1746 | assert.isOk(schemaParts)
1747 | assert.equal(schemaParts.length, 4)
1748 | var genObj = (schemaParts || []).filter(s => s.type == 'TYPE' && s.name == 'PagedPost')[0]
1749 | assert.isOk(genObj, 'The object \'PagedPost\' that should have been auto-generated from Paged has not been created.')
1750 | })
1751 | it('03 - INHERITED METADATA: Should add properties from the super type to the sub type.', () => {
1752 | var schema = `
1753 | @supertype(() => { return 1*2; })
1754 | type PostUserRating inherits Post {
1755 | @brendan((args) => { return 'hello world'; })
1756 | rating: PostRating!
1757 | creationDate: String
1758 | }
1759 |
1760 | @node
1761 | type Node {
1762 | @primaryKey
1763 | id: ID!
1764 | }
1765 |
1766 | type Post inherits Node {
1767 | @boris
1768 | name: String!
1769 | }
1770 | `
1771 | var schemaParts = getSchemaAST(schema)
1772 |
1773 | assert.isOk(schemaParts)
1774 | assert.equal(schemaParts.length, 3)
1775 | // PostUserRating
1776 | var schemaPart1 = schemaParts[0]
1777 | var typeMeta1 = schemaPart1.metadata
1778 | assert.isOk(typeMeta1)
1779 | assert.equal(typeMeta1.name, 'supertype')
1780 | assert.equal(typeMeta1.body, '(() => { return 1*2; })')
1781 | assert.isOk(schemaPart1.blockProps)
1782 | assert.equal(schemaPart1.blockProps.length, 4)
1783 | var typeMeta1Prop1 = schemaPart1.blockProps[3]
1784 | assert.equal(typeMeta1Prop1.details.name, 'id')
1785 | assert.isOk(typeMeta1Prop1.details.metadata)
1786 | assert.equal(!typeMeta1Prop1.details.metadata.body, true)
1787 | assert.equal(typeMeta1Prop1.details.metadata.name, 'primaryKey')
1788 | var typeMeta1Prop2 = schemaPart1.blockProps[2]
1789 | assert.equal(typeMeta1Prop2.details.name, 'name')
1790 | assert.isOk(typeMeta1Prop2.details.metadata)
1791 | assert.equal(!typeMeta1Prop2.details.metadata.body, true)
1792 | assert.equal(typeMeta1Prop2.details.metadata.name, 'boris')
1793 | var typeMeta1Prop3 = schemaPart1.blockProps[0]
1794 | assert.equal(typeMeta1Prop3.details.name, 'rating')
1795 | assert.isOk(typeMeta1Prop3.details.metadata)
1796 | assert.equal(typeMeta1Prop3.details.metadata.body, '((args) => { return \'hello world\'; })')
1797 | assert.equal(typeMeta1Prop3.details.metadata.name, 'brendan')
1798 | var typeMeta1Prop4 = schemaPart1.blockProps[1]
1799 | assert.equal(typeMeta1Prop4.details.name, 'creationDate')
1800 | assert.isOk(!typeMeta1Prop4.details.metadata)
1801 |
1802 | // Node
1803 | var schemaPart2 = schemaParts[1]
1804 | var typeMeta2 = schemaPart2.metadata
1805 | assert.isOk(typeMeta2)
1806 | assert.equal(typeMeta2.name, 'node')
1807 | assert.equal(!typeMeta2.body, true)
1808 | assert.isOk(schemaPart2.blockProps)
1809 | assert.equal(schemaPart2.blockProps.length, 1)
1810 | var typeMeta2Prop1 = schemaPart2.blockProps[0]
1811 | assert.equal(typeMeta2Prop1.details.name, 'id')
1812 | assert.isOk(typeMeta2Prop1.details.metadata)
1813 | assert.equal(!typeMeta2Prop1.details.metadata.body, true)
1814 | assert.equal(typeMeta2Prop1.details.metadata.name, 'primaryKey')
1815 |
1816 | // Post
1817 | var schemaPart3 = schemaParts[2]
1818 | var typeMeta3 = schemaPart3.metadata
1819 | assert.isOk(typeMeta3)
1820 | assert.equal(typeMeta3.name, 'node')
1821 | assert.equal(!typeMeta3.body, true)
1822 | assert.isOk(schemaPart3.blockProps)
1823 | assert.equal(schemaPart3.blockProps.length, 2)
1824 | var typeMeta3Prop1 = schemaPart3.blockProps[1]
1825 | assert.equal(typeMeta3Prop1.details.name, 'id')
1826 | assert.isOk(typeMeta3Prop1.details.metadata)
1827 | assert.equal(!typeMeta3Prop1.details.metadata.body, true)
1828 | assert.equal(typeMeta3Prop1.details.metadata.name, 'primaryKey')
1829 | var typeMeta3Prop2 = schemaPart3.blockProps[0]
1830 | assert.equal(typeMeta3Prop2.details.name, 'name')
1831 | assert.isOk(typeMeta3Prop2.details.metadata)
1832 | assert.equal(!typeMeta3Prop2.details.metadata.body, true)
1833 | assert.equal(typeMeta3Prop2.details.metadata.name, 'boris')
1834 | })
1835 | it('04 - REQUIRED PARAMS: Should deal with required params', () => {
1836 | var schema = `
1837 | type Page {
1838 | cursor: ID
1839 | data: [T]
1840 | }
1841 |
1842 | type Location {
1843 | lat: Float!
1844 | long: Float!
1845 | }
1846 |
1847 | type Event {
1848 | location: Location!
1849 | }
1850 |
1851 | type Query {
1852 | events: Page
1853 | }`
1854 |
1855 | var query = `{
1856 | events{
1857 | data{
1858 | location {
1859 | lat
1860 | long
1861 | }
1862 | }
1863 | }
1864 | }`
1865 |
1866 | var schemaAST = getSchemaAST(schema)
1867 | var queryAST = getQueryAST(query, null, schemaAST)
1868 | assert.isOk(queryAST, '01')
1869 | })
1870 | })
1871 |
1872 | describe('#getQueryAST', () => {
1873 | it('01 - GET METADATA: Should retrieve all metadata associated to the query.', () => {
1874 |
1875 | var schema = `
1876 | type User {
1877 | id: ID!
1878 | username: String!
1879 | }
1880 |
1881 | type Query {
1882 | @auth
1883 | users: [User]
1884 | }
1885 |
1886 | input UserInput {
1887 | name: String
1888 | kind: String
1889 | }
1890 |
1891 | type Mutation {
1892 | @auth
1893 | insert(input: UserInput): User
1894 |
1895 | @author
1896 | update(input: UserInput): User
1897 | }
1898 | `
1899 | var query = `
1900 | query Hello($person: String, $animal: String) {
1901 | hello:users(where:{name:$person, kind: $animal}){
1902 | id
1903 | username
1904 | }
1905 | users{
1906 | id
1907 | }
1908 | }`
1909 |
1910 | var mutation = `
1911 | mutation World($person: String, $animal: String) {
1912 | hello:insert(input:{name:$person, kind: $animal}){
1913 | id
1914 | username
1915 | }
1916 | update(input: { name: "fred" }){
1917 | id
1918 | }
1919 | }`
1920 |
1921 | var schemaAST = getSchemaAST(schema)
1922 | var queryOpAST = getQueryAST(query, null, schemaAST)
1923 | var mutationOpAST = getQueryAST(mutation, null, schemaAST)
1924 |
1925 | var queryAST = queryOpAST.properties
1926 | var mutationAST = mutationOpAST.properties
1927 |
1928 | assert.equal(queryOpAST.type, 'query', 'Operation type should be \'query\'')
1929 | assert.equal(queryOpAST.name, 'Hello', 'Operation name should be \'Hello\'')
1930 | assert.isOk(queryOpAST.variables, 'Operation variable should exist')
1931 | assert.equal(queryOpAST.variables.length, 2, 'There should be 2 variables for the query operation.')
1932 | assert.equal(queryOpAST.variables[0].name, 'person', 'The 1st query variable should be \'person\'.')
1933 | assert.equal(queryOpAST.variables[0].type, 'String', 'The 1st query variable should be a \'String\' type.')
1934 | assert.equal(queryOpAST.variables[1].name, 'animal', 'The 2nd query variable should be \'animal\'.')
1935 | assert.equal(queryOpAST.variables[1].type, 'String', 'The 2nd query variable should be a \'String\' type.')
1936 | assert.isOk(queryAST, 'An query AST should exist.')
1937 | assert.equal(queryAST.length, 2, 'There should be 2 AST found for the query.')
1938 | assert.equal(queryAST[1].name, 'users','The 2nd AST should be named \'users\'.')
1939 | assert.isOk(queryAST[1].metadata,'metadata should be defined on the \'users\' query.')
1940 | assert.equal(queryAST[1].metadata.name, 'auth','There should be an \'auth\' metadata on the \'users\' query.')
1941 | assert.equal(queryAST[0].name, 'hello:users','The 1st AST should be named \'hello:users\'.')
1942 | assert.isOk(queryAST[0].metadata,'metadata should be defined on the \'users\' query.')
1943 | assert.equal(queryAST[0].metadata.name, 'auth','There should be an \'auth\' metadata on the \'users\' query.')
1944 |
1945 |
1946 | assert.equal(mutationOpAST.type, 'mutation', 'Operation type should be \'mutation\'')
1947 | assert.equal(mutationOpAST.name, 'World', 'Operation name should be \'World\'')
1948 | assert.isOk(mutationOpAST.variables, 'Operation variable should exist')
1949 | assert.equal(mutationOpAST.variables.length, 2, 'There should be 2 variables for the mutation operation.')
1950 | assert.equal(mutationOpAST.variables[0].name, 'person', 'The 1st query variable should be \'person\'.')
1951 | assert.equal(mutationOpAST.variables[0].type, 'String', 'The 1st query variable should be a \'String\' type.')
1952 | assert.equal(mutationOpAST.variables[1].name, 'animal', 'The 2nd query variable should be \'animal\'.')
1953 | assert.equal(mutationOpAST.variables[1].type, 'String', 'The 2nd query variable should be a \'String\' type.')
1954 | assert.isOk(mutationAST, 'An mutation AST should exist.')
1955 | assert.equal(mutationAST.length, 2, 'There should be 2 AST found for the mutation.')
1956 | assert.equal(mutationAST[0].name, 'hello:insert','The 1st AST should be named \'hello:insert\'.')
1957 | assert.isOk(mutationAST[0].metadata,'metadata should be defined on the \'users\' mutation.')
1958 | assert.equal(mutationAST[0].metadata.name, 'auth','There should be an \'auth\' metadata on the \'users\' mutation.')
1959 | assert.equal(mutationAST[1].name, 'update','The 2nd AST should be named \'update\'.')
1960 | assert.isOk(mutationAST[1].metadata,'metadata should be defined on the \'users\' mutation.')
1961 | assert.equal(mutationAST[1].metadata.name, 'author','There should be an \'auth\' metadata on the \'users\' mutation.')
1962 | })
1963 | it('02 - DETECT AST: Should detect if any query AST match a specific predicate.', () => {
1964 | var schema = `
1965 | type User {
1966 | id: ID!
1967 | username: String!
1968 | details: UserDetails
1969 | }
1970 |
1971 | type UserDetails {
1972 | gender: String
1973 | bankDetails: BankDetail
1974 | }
1975 |
1976 | type BankDetail {
1977 | name: String
1978 | @auth
1979 | account: String
1980 | }
1981 |
1982 | type Query {
1983 | users: [User]
1984 | }
1985 | `
1986 | var query = `
1987 | query Hello($person: String, $animal: String) {
1988 | hello:users(where:{name:$person, kind: $animal}){
1989 | id
1990 | username
1991 | details {
1992 | gender
1993 | bankDetails{
1994 | account
1995 | }
1996 | }
1997 | }
1998 | users{
1999 | id
2000 | }
2001 | }`
2002 | var schemaAST = getSchemaAST(schema)
2003 | var queryOpAST = getQueryAST(query, null, schemaAST).some(x => x.metadata && x.metadata.name == 'auth')
2004 | assert.isOk(queryOpAST)
2005 | })
2006 | it('03 - FIND ALL AST PATHS: Should return the details of all the AST property that match a predicate.', () => {
2007 | var schema = `
2008 | type User {
2009 | id: ID!
2010 | username: String!
2011 | details: UserDetails
2012 | }
2013 |
2014 | type Address {
2015 | street: String
2016 | }
2017 |
2018 | type UserDetails {
2019 | gender: String
2020 | bankDetails: BankDetail
2021 | }
2022 |
2023 | type BankDetail {
2024 | name: String
2025 | @auth
2026 | account: String!
2027 | }
2028 |
2029 | type Query {
2030 | users: [User]
2031 | @auth
2032 | addresses: [Address]
2033 | }
2034 | `
2035 | var query = `
2036 | query Hello($person: String, $animal: String) {
2037 | hello:users(where:{name:$person, kind: $animal}){
2038 | id
2039 | username
2040 | details {
2041 | gender
2042 | bankDetails{
2043 | account
2044 | }
2045 | }
2046 | }
2047 | users{
2048 | id
2049 | }
2050 | addresses {
2051 | street
2052 | }
2053 | }`
2054 | var schemaAST = getSchemaAST(schema)
2055 | var paths = getQueryAST(query, null, schemaAST).propertyPaths(x => x.metadata && x.metadata.name == 'auth')
2056 | assert.equal(paths.length, 2, 'There should be 2 fields with the \'auth\' metadata.')
2057 | assert.equal(paths[0].property, 'hello:users.details.bankDetails.account', '1st \'auth\' path does not match.')
2058 | assert.equal(paths[0].type, 'String!')
2059 | assert.equal(paths[1].property, 'addresses', '2nd \'auth\' path does not match.')
2060 | assert.equal(paths[1].type, '[Address]')
2061 | })
2062 | it('04 - BASIC TYPES SUPPORT: Should support queries with for basic types (id, string, int, boolean, float).', () => {
2063 |
2064 | var schema = `
2065 | type Property {
2066 | inspectionSchedule: InspectionSchedule
2067 | }
2068 |
2069 | input PagingInput {
2070 | after: ID
2071 | limit: Int
2072 | }
2073 |
2074 | type InspectionSchedule {
2075 | id: ID
2076 | nbrOfVisits: Int
2077 | byAppointment: Boolean
2078 | recurring: Boolean
2079 | description: String
2080 | price: Float
2081 | }
2082 |
2083 | input DirectionalPagingInput inherits PagingInput {
2084 | before: ID
2085 | direction: SortDirection
2086 | }
2087 |
2088 | type Query {
2089 | properties(paging: DirectionalPagingInput): [Property]
2090 | }
2091 | `
2092 |
2093 | var query_input = `
2094 | query {
2095 | properties (paging: { limit: 10 }) {
2096 | inspectionSchedule {
2097 | id
2098 | nbrOfVisits
2099 | byAppointment
2100 | recurring
2101 | description
2102 | price
2103 | }
2104 | }
2105 | }
2106 | `
2107 | var schemaAST = getSchemaAST(schema)
2108 | var queryOpASTIntrospec = getQueryAST(query_input, null, schemaAST, { defrag: true })
2109 |
2110 | var query = normalizeString(query_input)
2111 | var queryAnswer = normalizeString(buildQuery(queryOpASTIntrospec))
2112 |
2113 | assert.equal(queryAnswer, query, 'The rebuild query should match the filtered mock.')
2114 | })
2115 | it('05 - DETECT STRING PROPS IN QUERY: Should return a boolean indicating whether a property is present in the query or not.', () => {
2116 | var schema = `
2117 | type User {
2118 | id: ID!
2119 | username: String!
2120 | details: UserDetails
2121 | }
2122 |
2123 | type Address {
2124 | street: String
2125 | }
2126 |
2127 | type UserDetails {
2128 | gender: String
2129 | bankDetails: BankDetail
2130 | }
2131 |
2132 | type BankDetail {
2133 | name: String
2134 | @auth
2135 | account: String!
2136 | }
2137 |
2138 | type Query {
2139 | users: [User]
2140 | @auth
2141 | addresses: [Address]
2142 | }
2143 | `
2144 | var query = `
2145 | query Hello($person: String, $animal: String) {
2146 | hello:users(where:{name:$person, kind: $animal}){
2147 | id
2148 | username
2149 | details {
2150 | gender
2151 | bankDetails{
2152 | account
2153 | }
2154 | }
2155 | }
2156 | users{
2157 | id
2158 | }
2159 | addresses {
2160 | street
2161 | }
2162 | }`
2163 | var schemaAST = getSchemaAST(schema)
2164 | var queryAST = getQueryAST(query, null, schemaAST)
2165 | assert.isOk(queryAST.containsProp('users.id'), '01')
2166 | assert.isOk(queryAST.containsProp('users.details'), '02')
2167 | assert.isOk(queryAST.containsProp('users.details.gender'), '03')
2168 | assert.isOk(queryAST.containsProp('users.details.bankDetails'), '04')
2169 | assert.isOk(queryAST.containsProp('users.details.bankDetails.account'), '05')
2170 | assert.isOk(queryAST.containsProp('bankDetails'), '06')
2171 | assert.isOk(queryAST.containsProp('bankDetails.account'), '07')
2172 | assert.isOk(queryAST.containsProp('addresses.street'), '08')
2173 | assert.isNotOk(queryAST.containsProp('users.name'), '09')
2174 | assert.isNotOk(queryAST.containsProp('bankDetails.account.id'), '10')
2175 | })
2176 | it('06 - DETECT REGEX PROPS IN QUERY: Should return a boolean indicating whether a property is present in the query or not.', () => {
2177 | var schema = `
2178 | type User {
2179 | id: ID!
2180 | username: String!
2181 | details: UserDetails
2182 | }
2183 |
2184 | type Address {
2185 | street: String
2186 | }
2187 |
2188 | type UserDetails {
2189 | gender: String
2190 | bankDetails: BankDetail
2191 | }
2192 |
2193 | type BankDetail {
2194 | name: String
2195 | @auth
2196 | account: String!
2197 | }
2198 |
2199 | type Query {
2200 | users: [User]
2201 | @auth
2202 | addresses: [Address]
2203 | }
2204 | `
2205 | var query = `
2206 | query Hello($person: String, $animal: String) {
2207 | hello:users(where:{name:$person, kind: $animal}){
2208 | id
2209 | username
2210 | details {
2211 | gender
2212 | bankDetails{
2213 | account
2214 | }
2215 | }
2216 | }
2217 | users{
2218 | id
2219 | }
2220 | addresses {
2221 | street
2222 | }
2223 | }`
2224 | var schemaAST = getSchemaAST(schema)
2225 | var queryAST = getQueryAST(query, null, schemaAST)
2226 | assert.isOk(queryAST.containsProp(/users.*/), '01')
2227 | assert.isOk(queryAST.containsProp(/users\.details\.(gender|bankDetails)/), '02')
2228 | assert.isOk(queryAST.containsProp(/users\.details\.(?!id)/), '03')
2229 | assert.isOk(queryAST.containsProp(/users\.(?!username)/), '05')
2230 | })
2231 | })
2232 |
2233 | describe('#buildQuery', () => {
2234 | it('01 - REBUILD QUERY FROM QUERY AST: Should rebuild the query exactly similar to its original based on the query AST.', () => {
2235 | var schema = `
2236 | type User {
2237 | id: ID!
2238 | username: String!
2239 | addesses(where: AddressWhere): [Address]
2240 | kind: String
2241 | gender: Gender
2242 | }
2243 |
2244 | type Address {
2245 | street: String
2246 | streetType: StreetType
2247 | postcode: String
2248 | country: Country
2249 | }
2250 |
2251 | type Country {
2252 | @auth
2253 | id: ID
2254 | name: String
2255 | }
2256 |
2257 | input AddressWhere {
2258 | postcode: String
2259 | streetType: [StreetType]
2260 | }
2261 |
2262 | type Query {
2263 | @auth
2264 | users(where: UserWhere, kind: String, street: [StreetType]): [User]
2265 | addresses: [Address]
2266 | }
2267 |
2268 | input UserWhere {
2269 | id: ID
2270 | name: String
2271 | kind: String
2272 | gender: Gender
2273 | }
2274 |
2275 | input UserInput {
2276 | name: String
2277 | kind: String
2278 | }
2279 |
2280 | type Mutation {
2281 | @auth
2282 | insert(input: UserInput): User
2283 |
2284 | @author
2285 | update(input: UserInput): User
2286 | }
2287 |
2288 | enum StreetType {
2289 | STREET
2290 | ROAD
2291 | PLACE
2292 | }
2293 |
2294 | enum Gender {
2295 | MALE
2296 | FEMALE
2297 | }
2298 | `
2299 | var query_input = `
2300 | query Hello($person: String, $animal: String) {
2301 | hello:users(where:{name:$person, kind: $animal}, kind: $animal){
2302 | id
2303 | username
2304 | }
2305 | users(where: { gender: MALE, id: 1, name: "Nic" }){
2306 | id
2307 | addesses(where: { streetType: [STREET, ROAD] }) {
2308 | street
2309 | }
2310 | }
2311 | test:users(street:[STREET, ROAD]){
2312 | id
2313 | }
2314 | addresses {
2315 | street
2316 | country {
2317 | id
2318 | name
2319 | }
2320 | }
2321 | }`
2322 |
2323 | var mutation_input = `
2324 | mutation World($person: String, $animal: String) {
2325 | hello:insert(input:{name:$person, kind: $animal}){
2326 | id
2327 | username
2328 | }
2329 | update(input: { name: "fred" }){
2330 | id
2331 | }
2332 | }`
2333 | var schemaAST = getSchemaAST(schema)
2334 | var queryAST = getQueryAST(query_input, null, schemaAST)
2335 | var mutationAST = getQueryAST(mutation_input, null, schemaAST)
2336 |
2337 | var query = normalizeString(query_input)
2338 | var mutation = normalizeString(mutation_input)
2339 |
2340 | var queryAnswer = normalizeString(buildQuery(queryAST))
2341 | var mutationAnswer = normalizeString(buildQuery(mutationAST))
2342 |
2343 | assert.equal(queryAnswer, query, 'The rebuild query should match the original.')
2344 | assert.equal(mutationAnswer, mutation, 'The rebuild mutation should match the original.')
2345 | })
2346 | it('02 - REBUILD QUERY FOR QUERIES WITH VARIABLES WITH ARRAYS: Should support queries with variables of type array.', () => {
2347 | var schema = `
2348 | type User {
2349 | id: ID!
2350 | username: String!
2351 | details: UserDetails
2352 | }
2353 |
2354 | type Address {
2355 | street: String
2356 | }
2357 |
2358 | type UserDetails {
2359 | gender: String
2360 | bankDetails: BankDetail
2361 | }
2362 |
2363 | type BankDetail {
2364 | name: String
2365 | @auth
2366 | account: String
2367 | }
2368 |
2369 | type Query {
2370 | users: [User]
2371 | @auth
2372 | addresses: [Address]
2373 | }
2374 | `
2375 | var query_input = `
2376 | query queryProperties($id: ID, $tags: [String], $before: ID, $limit: Int) {
2377 | properties(where: {id: $id, tags: $tags}, paging: {before: $before, limit: $limit, direction: DESC}) {
2378 | id
2379 | images
2380 | tags
2381 | bathrooms
2382 | carspaces
2383 | bedrooms
2384 | headline
2385 | displayableAddress
2386 | streetNumber
2387 | suburb
2388 | postcode
2389 | state
2390 | __typename
2391 | }
2392 | }`
2393 | var schemaAST = getSchemaAST(schema)
2394 | var queryOpAST = getQueryAST(query_input, null, schemaAST)
2395 |
2396 | var query = normalizeString(query_input)
2397 | var queryAnswer = normalizeString(buildQuery(queryOpAST))
2398 |
2399 | assert.equal(queryAnswer, query, 'The rebuild query should match the original with variables of type array.')
2400 | })
2401 | it('03 - FILTER QUERY BASED ON METADATA: Should rebuild a query different from its origin if some fields have been filtered from the orginal query.', () => {
2402 | var schema = `
2403 | type User {
2404 | id: ID!
2405 | username: String!
2406 | addesses(where: AddressWhere): [Address]
2407 | kind: String
2408 | gender: Gender
2409 | }
2410 |
2411 | type Address {
2412 | street: String
2413 | streetType: StreetType
2414 | postcode: String
2415 | country: Country
2416 | }
2417 |
2418 | type Country {
2419 | @auth
2420 | id: ID
2421 | name: String
2422 | }
2423 |
2424 | input AddressWhere {
2425 | postcode: String
2426 | streetType: [StreetType]
2427 | }
2428 |
2429 | type Query {
2430 | @auth
2431 | users(where: UserWhere, kind: String, street: [StreetType]): [User]
2432 | addresses: [Address]
2433 | }
2434 |
2435 | input UserWhere {
2436 | id: ID
2437 | name: String
2438 | kind: String
2439 | gender: Gender
2440 | }
2441 |
2442 | input UserInput {
2443 | name: String
2444 | kind: String
2445 | }
2446 |
2447 | type Mutation {
2448 | @auth
2449 | insert(input: UserInput): User
2450 |
2451 | @author
2452 | update(input: UserInput): User
2453 | }
2454 |
2455 | enum StreetType {
2456 | STREET
2457 | ROAD
2458 | PLACE
2459 | }
2460 |
2461 | enum Gender {
2462 | MALE
2463 | FEMALE
2464 | }
2465 | `
2466 | var query_input = `
2467 | query Hello($person: String, $animal: String) {
2468 | hello:users(where:{name:$person, kind: $animal}, kind: $animal){
2469 | id
2470 | username
2471 | }
2472 | users(where: { gender: MALE, id: 1, name: "Nic" }){
2473 | id
2474 | addesses(where: { streetType: [STREET, ROAD] }) {
2475 | street
2476 | }
2477 | }
2478 | test:users(street:[STREET, ROAD]){
2479 | id
2480 | }
2481 | addresses {
2482 | street
2483 | country {
2484 | id
2485 | name
2486 | }
2487 | }
2488 | }`
2489 |
2490 | var query_filtered = `
2491 | query Hello($person: String, $animal: String) {
2492 | addresses {
2493 | street
2494 | country {
2495 | name
2496 | }
2497 | }
2498 | }`
2499 | var schemaAST = getSchemaAST(schema)
2500 | var queryAST = getQueryAST(query_input, null, schemaAST)
2501 |
2502 | var filteredQueryAST = queryAST.filter(a => !a.metadata || a.metadata.name != 'auth')
2503 |
2504 | var query = normalizeString(query_filtered)
2505 |
2506 | var queryAnswer = normalizeString(buildQuery(filteredQueryAST))
2507 |
2508 | assert.equal(queryAnswer, query, 'The rebuild query should match the filtered mock.')
2509 | })
2510 | it('04 - FRAGMENTS #1: Should support queries with fragments.', () => {
2511 | var schema = `
2512 | type User {
2513 | id: ID!
2514 | username: String!
2515 | }
2516 |
2517 | type Query {
2518 | @auth
2519 | users: [User]
2520 | }
2521 |
2522 | input UserInput {
2523 | name: String
2524 | kind: String
2525 | }
2526 |
2527 | type Mutation {
2528 | @auth
2529 | insert(input: UserInput): User
2530 |
2531 | @author
2532 | update(input: UserInput): User
2533 | }
2534 | `
2535 |
2536 | var query_input = `
2537 | query IntrospectionQuery {
2538 | __schema {
2539 | queryType { name }
2540 | mutationType { name }
2541 | subscriptionType { name }
2542 | types {
2543 | ...FullType
2544 | }
2545 | directives {
2546 | name
2547 | description
2548 | locations
2549 | args {
2550 | ...InputValue
2551 | }
2552 | }
2553 | }
2554 | }
2555 |
2556 | fragment FullType on __Type {
2557 | kind
2558 | name
2559 | description
2560 | fields(includeDeprecated: true) {
2561 | name
2562 | description
2563 | args {
2564 | ...InputValue
2565 | }
2566 | type {
2567 | ...TypeRef
2568 | }
2569 | isDeprecated
2570 | deprecationReason
2571 | }
2572 | inputFields {
2573 | ...InputValue
2574 | }
2575 | interfaces {
2576 | ...TypeRef
2577 | }
2578 | enumValues(includeDeprecated: true) {
2579 | name
2580 | description
2581 | isDeprecated
2582 | deprecationReason
2583 | }
2584 | possibleTypes {
2585 | ...TypeRef
2586 | }
2587 | }
2588 |
2589 | fragment InputValue on __InputValue {
2590 | name
2591 | description
2592 | type { ...TypeRef }
2593 | defaultValue
2594 | }
2595 |
2596 | fragment TypeRef on __Type {
2597 | kind
2598 | name
2599 | ofType {
2600 | kind
2601 | name
2602 | ofType {
2603 | kind
2604 | name
2605 | ofType {
2606 | kind
2607 | name
2608 | ofType {
2609 | kind
2610 | name
2611 | ofType {
2612 | kind
2613 | name
2614 | ofType {
2615 | kind
2616 | name
2617 | ofType {
2618 | kind
2619 | name
2620 | }
2621 | }
2622 | }
2623 | }
2624 | }
2625 | }
2626 | }
2627 | }
2628 | `
2629 | var schemaAST = getSchemaAST(schema)
2630 | var queryOpAST = getQueryAST(query_input, null, schemaAST)
2631 | var rebuiltQuery = buildQuery(queryOpAST)
2632 |
2633 | var query = normalizeString(query_input)
2634 | var queryAnswer = normalizeString(rebuiltQuery)
2635 |
2636 | assert.equal(queryAnswer, query, 'The rebuild query for the schema request should match the original with fragments.')
2637 | })
2638 | it('05 - FRAGMENTS #2: Should support merging fragments (DEFRAG).', () => {
2639 | var schema = `
2640 | type User {
2641 | @auth
2642 | id: ID!
2643 | @auth
2644 | password: String
2645 | username: String!
2646 | }
2647 |
2648 | type Query {
2649 | users: [User]
2650 | }
2651 |
2652 | input UserInput {
2653 | name: String
2654 | kind: String
2655 | }
2656 |
2657 | type Mutation {
2658 | @auth
2659 | insert(input: UserInput): User
2660 |
2661 | @author
2662 | update(input: UserInput): User
2663 | }
2664 | `
2665 |
2666 | var query_input = `
2667 | query IntrospectionQuery {
2668 | __schema {
2669 | queryType { name }
2670 | mutationType { name }
2671 | subscriptionType { name }
2672 | types {
2673 | ...FullType
2674 | }
2675 | directives {
2676 | name
2677 | description
2678 | locations
2679 | args {
2680 | ...InputValue
2681 | }
2682 | }
2683 | }
2684 | users{
2685 | ...UserConfidential
2686 | username
2687 | }
2688 | }
2689 |
2690 | fragment UserConfidential on User {
2691 | id
2692 | password
2693 | }
2694 |
2695 | fragment FullType on __Type {
2696 | fields(includeDeprecated: true) {
2697 | name
2698 | description
2699 | }
2700 | inputFields {
2701 | ...InputValue
2702 | }
2703 | possibleTypes {
2704 | ...TypeRef
2705 | }
2706 | }
2707 |
2708 | fragment InputValue on __InputValue {
2709 | name
2710 | type { ...TypeRef }
2711 | }
2712 |
2713 | fragment TypeRef on __Type {
2714 | kind
2715 | name
2716 | }
2717 | `
2718 |
2719 | var query_defragged = `
2720 | query IntrospectionQuery {
2721 | __schema {
2722 | queryType { name }
2723 | mutationType { name }
2724 | subscriptionType { name }
2725 | types {
2726 | fields(includeDeprecated: true) {
2727 | name
2728 | description
2729 | }
2730 | inputFields {
2731 | name
2732 | type {
2733 | kind
2734 | name
2735 | }
2736 | }
2737 | possibleTypes {
2738 | kind
2739 | name
2740 | }
2741 | }
2742 | directives {
2743 | name
2744 | description
2745 | locations
2746 | args {
2747 | name
2748 | type {
2749 | kind
2750 | name
2751 | }
2752 | }
2753 | }
2754 | }
2755 | users{
2756 | username
2757 | }
2758 | }
2759 | `
2760 | var schemaAST = getSchemaAST(schema)
2761 | var queryOpAST = getQueryAST(query_input, null, schemaAST, { defrag: true })
2762 | var filteredOpAST = queryOpAST.filter(x => !x.metadata || x.metadata.name != 'auth')
2763 | var rebuiltQuery = buildQuery(filteredOpAST)
2764 |
2765 | var query = normalizeString(query_defragged)
2766 | var queryAnswer = normalizeString(rebuiltQuery)
2767 |
2768 | assert.equal(queryAnswer, query, 'The rebuild query for the schema request should match the original with fragments.')
2769 | })
2770 | it('06 - FRAGMENTS #2: Should support queries with multiple queries.', () => {
2771 | var schema = `
2772 | type User {
2773 | @auth
2774 | id: ID!
2775 | @auth
2776 | password: String
2777 | username: String!
2778 | }
2779 |
2780 | type Query {
2781 | users: [User]
2782 | }
2783 |
2784 | input UserInput {
2785 | name: String
2786 | kind: String
2787 | }
2788 |
2789 | type Mutation {
2790 | @auth
2791 | insert(input: UserInput): User
2792 |
2793 | @author
2794 | update(input: UserInput): User
2795 | }
2796 | `
2797 |
2798 | var query_input = `
2799 | query IntrospectionQuery {
2800 | __schema {
2801 | queryType { name }
2802 | mutationType { name }
2803 | subscriptionType { name }
2804 | types {
2805 | ...FullType
2806 | }
2807 | directives {
2808 | name
2809 | description
2810 | locations
2811 | args {
2812 | ...InputValue
2813 | }
2814 | }
2815 | }
2816 | users{
2817 | ...UserConfidential
2818 | username
2819 | }
2820 | }
2821 |
2822 | query Test {
2823 | users{
2824 | ...UserConfidential
2825 | username
2826 | }
2827 | }
2828 |
2829 | fragment UserConfidential on User {
2830 | id
2831 | password
2832 | }
2833 |
2834 | fragment FullType on __Type {
2835 | fields(includeDeprecated: true) {
2836 | name
2837 | description
2838 | }
2839 | inputFields {
2840 | ...InputValue
2841 | }
2842 | possibleTypes {
2843 | ...TypeRef
2844 | }
2845 | }
2846 |
2847 | fragment InputValue on __InputValue {
2848 | name
2849 | type { ...TypeRef }
2850 | }
2851 |
2852 | fragment TypeRef on __Type {
2853 | kind
2854 | name
2855 | }
2856 | `
2857 |
2858 | var query_defragged_introspection = `
2859 | query IntrospectionQuery {
2860 | __schema {
2861 | queryType { name }
2862 | mutationType { name }
2863 | subscriptionType { name }
2864 | types {
2865 | fields(includeDeprecated: true) {
2866 | name
2867 | description
2868 | }
2869 | inputFields {
2870 | name
2871 | type {
2872 | kind
2873 | name
2874 | }
2875 | }
2876 | possibleTypes {
2877 | kind
2878 | name
2879 | }
2880 | }
2881 | directives {
2882 | name
2883 | description
2884 | locations
2885 | args {
2886 | name
2887 | type {
2888 | kind
2889 | name
2890 | }
2891 | }
2892 | }
2893 | }
2894 | users{
2895 | username
2896 | }
2897 | }
2898 | `
2899 |
2900 | var query_defragged = `
2901 | query Test {
2902 | users{
2903 | username
2904 | }
2905 | }
2906 | `
2907 | var schemaAST = getSchemaAST(schema)
2908 | var queryOpASTIntrospec = getQueryAST(query_input, 'IntrospectionQuery', schemaAST, { defrag: true })
2909 | var filteredOpASTIntrospec = queryOpASTIntrospec.filter(x => !x.metadata || x.metadata.name != 'auth')
2910 | var rebuiltQueryIntrospec = buildQuery(filteredOpASTIntrospec)
2911 | var queryOpASTTest = getQueryAST(query_input, 'Test', schemaAST, { defrag: true })
2912 | var filteredOpASTTest = queryOpASTTest.filter(x => !x.metadata || x.metadata.name != 'auth')
2913 | var rebuiltQueryTest = buildQuery(filteredOpASTTest)
2914 |
2915 | var query_introspec = normalizeString(query_defragged_introspection)
2916 | var query_test = normalizeString(query_defragged)
2917 | var queryAnswer_introspec = normalizeString(rebuiltQueryIntrospec)
2918 | var queryAnswer_test = normalizeString(rebuiltQueryTest)
2919 |
2920 | assert.equal(queryAnswer_introspec, query_introspec, 'The rebuild introspec query for the schema request should match the original with fragments.')
2921 | assert.equal(queryAnswer_test, query_test, 'The rebuild test query for the schema request should match the original with fragments.')
2922 | })
2923 | it('07 - SUPPORT NON-NULLABLE FIELDS: Should support queries with multiple queries.', () => {
2924 | var schema = `
2925 | type Message {
2926 | message: String
2927 | }
2928 |
2929 | input CredsInput {
2930 | token: String!
2931 | password: String!
2932 | }
2933 |
2934 | type Mutation {
2935 | resetPasswordMutation(creds: CredsInput): Message
2936 | }
2937 | `
2938 |
2939 | var query = `
2940 | mutation resetPasswordMutation($token: String!, $password: String!) {
2941 | userResetPassword(creds: {token: $token, password: $password}) {
2942 | message
2943 | __typename
2944 | }
2945 | }
2946 | `
2947 | var schemaAST = getSchemaAST(schema)
2948 | var queryOpAST = getQueryAST(query, null, schemaAST, { defrag: true })
2949 | var rebuiltQuery = buildQuery(queryOpAST)
2950 |
2951 | assert.equal(normalizeString(rebuiltQuery), normalizeString(query))
2952 | })
2953 | it('08 - SUPPORT INPUT WITH ARRAY VALUES: Should support input with array values.', () => {
2954 | var schema = `
2955 | type Message {
2956 | message: String
2957 | }
2958 |
2959 | input InputWhere {
2960 | name: String
2961 | locations: [LocationInput]
2962 | }
2963 |
2964 | input LocationInput {
2965 | type: String
2966 | value: String
2967 | }
2968 |
2969 | type Query {
2970 | properties(where: InputWhere): Message
2971 | }
2972 | `
2973 |
2974 | var query = `
2975 | query{
2976 | properties(where: { name: "Love", locations: [{ type: "house", value: "Bellevue hill" }] }){
2977 | message
2978 | }
2979 | }
2980 | `
2981 | var schemaAST = getSchemaAST(schema)
2982 | var queryOpAST = getQueryAST(query, null, schemaAST, { defrag: true })
2983 | var rebuiltQuery = buildQuery(queryOpAST)
2984 |
2985 | assert.equal(normalizeString(rebuiltQuery), normalizeString(query))
2986 | })
2987 | it('09 - ARGUMENTS. Should compile with empty arguments list', () => {
2988 |
2989 | var input = transpileSchema(`
2990 |
2991 | type Query {
2992 | ping(): String!
2993 | }
2994 | `)
2995 | var answer = compressString(input)
2996 | var correct = compressString(`
2997 | type Query {
2998 | ping: String!
2999 | }`)
3000 | assert.equal(answer,correct)
3001 | })
3002 | })
3003 | })
3004 | }
3005 |
3006 | if (browserctxt) runtest(graphqls2s, assert)
3007 |
3008 | if (typeof(module) != 'undefined')
3009 | module.exports = {
3010 | runtest
3011 | }
3012 |
--------------------------------------------------------------------------------
/test/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mocha Tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test/node/graphqls2s.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017, Neap Pty Ltd.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | /*eslint-disable */
9 | const env = process.env.MOCHA_ENV || 'prod'
10 | /*eslint-enable */
11 | const { assert } = require('chai')
12 | let graphqls2s = null
13 | if (env == 'prod')
14 | graphqls2s = require('../../lib/graphqls2s.min').graphqls2s
15 | else if (env == 'dev')
16 | graphqls2s = require('../../src/graphqls2s').graphqls2s
17 | else
18 | throw new Error(`Failed to test - Environment ${env} is unknown.`)
19 |
20 | const { runtest } = require('../browser/graphqls2s')
21 |
22 | runtest(graphqls2s, assert)
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017, Neap Pty Ltd.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | const path = require('path')
9 | const TerserPlugin = require("terser-webpack-plugin")
10 |
11 | const env = process.env.WEBPACK_ENV
12 | const outputfilename = "graphqls2s"
13 | const prod = env == "build"
14 |
15 | const { plugins, outputfile } = prod
16 | ? { plugins: [], outputfile: `${outputfilename}.min.js` }
17 | : { plugins: [], outputfile: `${outputfilename}.js` }
18 |
19 | module.exports = {
20 | mode: prod ? 'production' : 'development',
21 | entry: [
22 | 'core-js/stable',
23 | 'regenerator-runtime/runtime',
24 | './src/graphqls2s.js'
25 | ],
26 | output: {
27 | path: __dirname + '/lib',
28 | filename: outputfile,
29 | libraryTarget: 'umd',
30 | umdNamedDefine: true,
31 | globalObject: 'this'
32 | },
33 | module: {
34 | rules: [{
35 | loader: "babel-loader",
36 | exclude: [
37 | path.resolve(__dirname, "node_modules")
38 | ],
39 | // Only run `.js` and `.jsx` files through Babel
40 | test: /\.jsx?$/,
41 | // Options to configure babel with
42 | options: {
43 | // plugins: ['transform-runtime'],
44 | presets: [
45 | ['@babel/preset-env', {'modules': false}]
46 | ]
47 | }
48 | }, ]
49 | },
50 | devtool: 'source-map',
51 | plugins: plugins,
52 | optimization: {
53 | minimize: prod,
54 | minimizer: [new TerserPlugin()]
55 | }
56 | }
--------------------------------------------------------------------------------