├── .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 · [![NPM](https://img.shields.io/npm/v/graphql-s2s.svg?style=flat)](https://www.npmjs.com/package/graphql-s2s) [![Tests](https://travis-ci.org/nicolasdao/graphql-s2s.svg?branch=master)](https://travis-ci.org/nicolasdao/graphql-s2s) [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Neap](https://neap.co/img/made_by_neap.svg)](#this-is-what-we-re-up-to) [![npm downloads](https://img.shields.io/npm/dt/graphql-s2s.svg?style=flat)](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 |

Neap Pty Ltd logo

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(/.*/)[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(/.* 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(/.+ 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 | } --------------------------------------------------------------------------------