├── .DS_Store ├── .gitignore ├── .npmignore ├── LICENSE.txt ├── README.md ├── __tests__ ├── compare.js ├── queryFormatter.js └── queryMapper.js ├── assets ├── .DS_Store ├── after.gif ├── banner.png ├── before.gif └── logo.png ├── package-lock.json ├── package.json ├── package ├── compare.js ├── compare.ts ├── errObjectParser.js ├── errObjectParser.ts ├── index.js ├── index.ts ├── queryFormatter.js ├── queryFormatter.ts ├── queryMapper.js ├── queryMapper.ts ├── types.js └── types.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/gql-error-handler/2aa32f0b4a58bba95247bb35956a20f445a1a156/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | --- 5 | 6 |
7 | 8 | [gql-error-handler](https://www.gql-error-handler.com) is an Apollo Server plugin that returns partial data upon validation errors in GraphQL. 9 | 10 | ## Features 11 | 12 | **partialDataPlugin:** A Javascript function that reformulates queries that would otherwise be invalidated by removing invalid fields and allows developers to use partial data returned. 13 | 14 | - Functionality is supported for queries and mutations with multiple root levels and nested fields up to 3 levels deep, including circular dependencies 15 | - Implement core functionality through utilization of a single plugin in an `ApolloServer` instance 16 | 17 | Before using our plugin, no data is returned due to validation errors: 18 | 19 |
20 | 21 |
22 | 23 | After using our plugin, partial data is returned for all valid fields and a custom error message is added indicating which fields were problematic: 24 | 25 |
26 | 27 |
28 | 29 | ## Setup 30 | 31 | - In your server file utilizing Apollo Server, import or require in `partialDataPlugin` 32 | - At initialization of your instance of `ApolloServer`, list `partialDataPlugin` as an element in the array value of the `plugins` property 33 | 34 | ```javascript 35 | const { ApolloServer } = require('apollo-server'); 36 | const partialDataPlugin = require('gql-error-handler'); 37 | 38 | const server = new ApolloServer({ 39 | typeDefs, 40 | resolvers, 41 | plugins: [partialDataPlugin], 42 | }); 43 | ``` 44 | 45 | ## Installation 46 | 47 | ```javascript 48 | npm i gql-error-handler 49 | ``` 50 | 51 | ## Future Considerations 52 | 53 | - Extend handling of nested queries beyond three levels of depth 54 | - Develop [GUI](https://github.com/gql-error-handler/gql-UI) to show logs of previous queries and server response 55 | - Add authentication and other security measures 56 | - Handle other types of errors in GraphQL 57 | 58 | ## Contributors 59 | 60 | - **Jeremy Buron-Yi** | [LinkedIn](https://www.linkedin.com/in/jeremy-buronyi/) | [GitHub](https://github.com/JEF-BY) 61 | - **Woobae Kim** | [LinkedIn](https://www.linkedin.com/in/woobaekim/) | [GitHub](https://github.com/woobaekim) 62 | - **Samuel Ryder** | [LinkedIn](https://www.linkedin.com/in/samuelRyder/) | [GitHub](https://github.com/samryderE) 63 | - **Tiffany Wong** | [LinkedIn](https://www.linkedin.com/in/tiffanywong149/) | [GitHub](https://github.com/twong-cs) 64 | - **Nancy Yang** | [LinkedIn](https://www.linkedin.com/in/naixinyang/) | [GitHub](https://github.com/nancyynx88) 65 | 66 | ## License 67 | 68 | _gql-error-handler is MIT licensed._ 69 | 70 | Thank you for using gql-error-handler. We hope that through the use of our plugin, your GraphQL user experience is enhanced. Should you encounter any issues during implementation or require further information, please reach out to us for assistance. 71 | 72 | -------------------------------------------------------------------------------- /__tests__/compare.js: -------------------------------------------------------------------------------- 1 | const compare = require('../package/compare.js'); 2 | 3 | describe('compare tests', () => { 4 | const testSchema = { 5 | query: { 6 | characters: { 7 | id: 'ID', 8 | name: 'String', 9 | height: 'Int', 10 | gender: 'String', 11 | birthYear: 'String', 12 | eyeColor: 'String', 13 | skinColor: 'String', 14 | hairColor: 'String', 15 | mass: 'String', 16 | films: '[Film]', 17 | }, 18 | films: { 19 | id: 'ID', 20 | title: 'String', 21 | director: 'String', 22 | releaseDate: 'String', 23 | characters: '[Character]', 24 | }, 25 | }, 26 | mutation: { 27 | createCharacter: { 28 | id: 'ID', 29 | name: 'String', 30 | height: 'Int', 31 | gender: 'String', 32 | birthYear: 'String', 33 | eyeColor: 'String', 34 | skinColor: 'String', 35 | hairColor: 'String', 36 | mass: 'String', 37 | films: '[Film]', 38 | }, 39 | }, 40 | }; 41 | 42 | it('The sample query is valid and the error object is an empty object', () => { 43 | const sampleQuery = { 44 | query: { 45 | characters: ['id', 'height', 'gender'], 46 | }, 47 | }; 48 | 49 | const errorObj = {}; 50 | 51 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 52 | }); 53 | 54 | it('Return error object with type properties whose value is an array with invalid field strings on query type', () => { 55 | const sampleQuery = { 56 | query: { 57 | characters: ['id', 'height', 'gender', 'woobae'], 58 | }, 59 | }; 60 | 61 | const errorObj = { characters: ['woobae'] }; 62 | 63 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 64 | }); 65 | 66 | it('Return error object when invalid fields occur in two different levels of query depth', () => { 67 | 68 | const sampleQuery = { 69 | query: { 70 | characters: ['id', 'height', 'gender', 'jeremy', { films: ['title', 'director', 'sam'] }] 71 | } 72 | }; 73 | 74 | const errorObj = { 75 | characters: ['jeremy'], 76 | films: ['sam'] 77 | }; 78 | 79 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 80 | }); 81 | 82 | it('Return error object when invalid fields occur in three different levels of query depth', () => { 83 | 84 | const sampleQuery = { 85 | query: { 86 | characters: ['id', 'height', 'gender', 'woobae', { films: ['title', 'director', 'sam', { characters: ['jeremy', 'height', 'name'] }] }] 87 | } 88 | }; 89 | 90 | const errorObj = { 91 | characters: ['woobae', 'jeremy'], 92 | films: ['sam'] 93 | }; 94 | 95 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 96 | }); 97 | 98 | it('Return error object when invalid fields occur only in first level of query depth, despite query continuing to three levels of depth', () => { 99 | 100 | const sampleQuery = { 101 | query: { 102 | characters: ['id', 'height', 'gender', 'woobae', { films: ['title', 'director', { characters: ['height', 'name'] }] }] 103 | } 104 | }; 105 | 106 | const errorObj = { characters: ['woobae'] }; 107 | 108 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 109 | }); 110 | 111 | it('Return error object when invalid fields occur only in second level of query depth, despite query continuing to three levels of depth', () => { 112 | 113 | const sampleQuery = { 114 | query: { 115 | characters: ['id', 'height', 'gender', { films: ['title', 'director', 'woobae', { characters: ['height', 'name'] }] }] 116 | } 117 | }; 118 | 119 | const errorObj = { films: ['woobae'] }; 120 | 121 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 122 | }); 123 | 124 | it('Return error object when invalid fields occur only in third level of query depth', () => { 125 | 126 | const sampleQuery = { 127 | query: { 128 | characters: ['id', 'height', 'gender', { films: ['title', 'director', { characters: ['height', 'jeremy'] }] }] 129 | } 130 | }; 131 | 132 | const errorObj = { characters: ['jeremy'] }; 133 | 134 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 135 | 136 | }); 137 | 138 | it('Return error object when invalid fields occur only in third level of query depth', () => { 139 | 140 | const sampleQuery = { 141 | query: { 142 | characters: ['id', 'height', 'gender', { films: ['title', 'director', 'height', { characters: ['height'] }] }] 143 | } 144 | }; 145 | 146 | const errorObj = { films: ['height'] }; 147 | 148 | expect(compare(testSchema, sampleQuery)).toEqual(errorObj); 149 | 150 | }); 151 | 152 | }); 153 | -------------------------------------------------------------------------------- /__tests__/queryFormatter.js: -------------------------------------------------------------------------------- 1 | const queryFormatter = require('../package/queryFormatter.js'); 2 | 3 | describe('queryFormatter tests', () => { 4 | 5 | // 1 - valid query 6 | 7 | it('1.1 - Return the input query if there aren\'t any errors in a single depth level query', () => { 8 | const testQuery = ` 9 | query { 10 | feed { 11 | id 12 | } 13 | } 14 | `; 15 | const testError = {}; 16 | const test = queryFormatter(testQuery); 17 | expect(test(testError)).toEqual(testQuery); 18 | }); 19 | 20 | it('1.2 - Return the input query if there aren\'t any errors in queries with 2 levels of depth', () => { 21 | const testQuery = ` 22 | query { 23 | feed { 24 | links { 25 | id 26 | description 27 | } 28 | } 29 | } 30 | `; 31 | const testError = {}; 32 | const test = queryFormatter(testQuery); 33 | expect(test(testError)).toEqual(testQuery); 34 | }); 35 | 36 | it('1.3 - Return the input query if there aren\'t any errors in queries with 3 levels of depth', () => { 37 | const testQuery = ` 38 | query { 39 | feed { 40 | links { 41 | id 42 | description 43 | text { 44 | content 45 | } 46 | } 47 | } 48 | } 49 | `; 50 | const testError = {}; 51 | const test = queryFormatter(testQuery); 52 | expect(test(testError)).toEqual(testQuery); 53 | }); 54 | 55 | 56 | // 2 - completely invalid query 57 | 58 | it('2.1 - Return original query where all fields are invalid at single depth', () => { 59 | const testQuery = ` 60 | query { 61 | feed { 62 | test 63 | } 64 | } 65 | `; 66 | const testError = { feed: ['test'] }; 67 | const test = queryFormatter(testQuery); 68 | expect(test(testError)).toEqual(testQuery); 69 | }); 70 | 71 | it('2.2 - Return original query where invalid fields are at 2nd level of depth', () => { 72 | const testQuery = ` 73 | query { 74 | feed { 75 | links { 76 | test 77 | } 78 | } 79 | } 80 | `; 81 | const testError = { links: ['test'] }; 82 | const test = queryFormatter(testQuery); 83 | expect(test(testError)).toEqual(testQuery); 84 | }); 85 | 86 | it('2.3 - Return original query where invalid fields are at 2nd & 3rd level of depth', () => { 87 | const testQuery = ` 88 | query { 89 | feed { 90 | links { 91 | test 92 | text { 93 | TIFFAAAANNNNYYYYYYYYYY 94 | } 95 | } 96 | } 97 | } 98 | `; 99 | const testError = { 100 | links: ['test'], 101 | text: ['TIFFAAAANNNNYYYYYYYYYY'] 102 | }; 103 | const test = queryFormatter(testQuery); 104 | expect(test(testError)).toEqual(testQuery); 105 | }); 106 | 107 | 108 | // 3 - 1 level of depth 109 | 110 | it('3.1 - Return reformatted query where invalid field is at 1 level of depth in 1 type', () => { 111 | const testQuery = ` 112 | query { 113 | feed { 114 | id 115 | test 116 | } 117 | } 118 | `; 119 | const testError = { feed: ['test'] }; 120 | const outputQuery = ` 121 | query { 122 | feed { 123 | id 124 | } 125 | } 126 | `; 127 | const test = queryFormatter(testQuery); 128 | expect(test(testError)).toEqual(outputQuery); 129 | }); 130 | 131 | it('3.2 - Return reformatted query where invalid fields are at 1 level of depth in 1 type', () => { 132 | const testQuery = ` 133 | query { 134 | feed { 135 | test 136 | id 137 | TIFFAAAANNNNYYYYYYYYYY 138 | } 139 | } 140 | `; 141 | const testError = { feed: ['test', 'TIFFAAAANNNNYYYYYYYYYY'] }; 142 | const outputQuery = ` 143 | query { 144 | feed { 145 | id 146 | } 147 | } 148 | `; 149 | const test = queryFormatter(testQuery); 150 | expect(test(testError)).toEqual(outputQuery); 151 | }); 152 | 153 | it('3.3 - Return reformatted query where invalid type on query', () => { 154 | const testQuery = ` 155 | query { 156 | fed { 157 | id 158 | } 159 | links { 160 | id 161 | } 162 | } 163 | `; 164 | const testError = { query: ['fed'] }; 165 | const outputQuery = ` 166 | query { 167 | links { 168 | id 169 | } 170 | } 171 | `; 172 | const test = queryFormatter(testQuery); 173 | expect(test(testError)).toEqual(outputQuery); 174 | }); 175 | 176 | it('3.4 - Return reformatted query where invalid fields are at 1 level of depth in 2 types', () => { 177 | const testQuery = ` 178 | query { 179 | feed { 180 | id 181 | TIFFFFFFAAAAAAANNNNNNNNYYYYYYYYYYYYY 182 | } 183 | links { 184 | id 185 | test 186 | } 187 | } 188 | `; 189 | const testError = { 190 | feed: ['TIFFFFFFAAAAAAANNNNNNNNYYYYYYYYYYYYY'], 191 | links: ['test'] 192 | }; 193 | const outputQuery = ` 194 | query { 195 | feed { 196 | id 197 | } 198 | links { 199 | id 200 | } 201 | } 202 | `; 203 | const test = queryFormatter(testQuery); 204 | expect(test(testError)).toEqual(outputQuery); 205 | }); 206 | 207 | 208 | // 4 - 2 levels of depth 209 | 210 | it('4.1 - Return reformatted query where invalid fields are at 1 level of depth in 1 type', () => { 211 | const testQuery = ` 212 | query { 213 | feed { 214 | links { 215 | id 216 | description 217 | TIFFFFFFFFFAAAAAAAAAANNNNNYYYYYYYYYYY 218 | } 219 | } 220 | } 221 | `; 222 | const testError = { links: ['TIFFFFFFFFFAAAAAAAAAANNNNNYYYYYYYYYYY'] }; 223 | const outputQuery = ` 224 | query { 225 | feed { 226 | links { 227 | id 228 | description 229 | } 230 | } 231 | } 232 | `; 233 | const test = queryFormatter(testQuery); 234 | expect(test(testError)).toEqual(outputQuery); 235 | }); 236 | 237 | it('4.2 - Return reformatted query where a field appears multiple times, in one case being valid and in the other, invalid', () => { 238 | const testQuery = ` 239 | query { 240 | feed { 241 | links { 242 | id 243 | description 244 | } 245 | text { 246 | id 247 | content 248 | } 249 | } 250 | } 251 | `; 252 | const testError = { text: ['id'] }; 253 | const outputQuery = ` 254 | query { 255 | feed { 256 | links { 257 | id 258 | description 259 | } 260 | text { 261 | content 262 | } 263 | } 264 | } 265 | `; 266 | const test = queryFormatter(testQuery); 267 | expect(test(testError)).toEqual(outputQuery); 268 | }); 269 | 270 | it('4.3 - Return reformatted query where invalid fields occur at same depth within multiple types', () => { 271 | const testQuery = ` 272 | query { 273 | feed { 274 | links { 275 | test 276 | id 277 | description 278 | } 279 | text { 280 | content 281 | TIFFANYYY 282 | } 283 | } 284 | } 285 | `; 286 | const testError = { links: ['test'], text: ['TIFFANYYY'] }; 287 | const outputQuery = ` 288 | query { 289 | feed { 290 | links { 291 | id 292 | description 293 | } 294 | text { 295 | content 296 | } 297 | } 298 | } 299 | `; 300 | const test = queryFormatter(testQuery); 301 | expect(test(testError)).toEqual(outputQuery); 302 | }); 303 | 304 | it('4.4 - Return reformatted query where invalid fields are at 1 level of depth in multiple types', () => { 305 | const testQuery = ` 306 | query { 307 | feed { 308 | links { 309 | id 310 | description 311 | test 312 | TIFFANYYY 313 | } 314 | text { 315 | content 316 | } 317 | } 318 | } 319 | `; 320 | const testError = { links: ['test', 'TIFFANYYY'], text: ['content'] }; 321 | const outputQuery = ` 322 | query { 323 | feed { 324 | links { 325 | id 326 | description 327 | } 328 | } 329 | } 330 | `; 331 | const test = queryFormatter(testQuery); 332 | expect(test(testError)).toEqual(outputQuery); 333 | }); 334 | 335 | it('4.5 - Return reformatted query where invalid fields are at 1st level of depth only', () => { 336 | const testQuery = ` 337 | query { 338 | TIFFANYYY { 339 | test 340 | } 341 | feed { 342 | links { 343 | id 344 | description 345 | } 346 | } 347 | } 348 | `; 349 | const testError = { query: ['TIFFANYYY'] }; 350 | const outputQuery = ` 351 | query { 352 | feed { 353 | links { 354 | id 355 | description 356 | } 357 | } 358 | } 359 | `; 360 | const test = queryFormatter(testQuery); 361 | expect(test(testError)).toEqual(outputQuery); 362 | }); 363 | 364 | 365 | // 5 - 3 levels of depth 366 | 367 | it('5.1 - Return reformatted query where invalid fields are only at 3rd level of depth at 1 type', () => { 368 | const testQuery = ` 369 | query { 370 | feed { 371 | links { 372 | id 373 | description 374 | text { 375 | content 376 | TIFFANYYYYY WONGGGGG 377 | } 378 | } 379 | } 380 | } 381 | `; 382 | const testError = { text: ['TIFFANYYYYY WONGGGGG'] }; 383 | const outputQuery = ` 384 | query { 385 | feed { 386 | links { 387 | id 388 | description 389 | text { 390 | content 391 | } 392 | } 393 | } 394 | } 395 | `; 396 | const test = queryFormatter(testQuery); 397 | expect(test(testError)).toEqual(outputQuery); 398 | }); 399 | 400 | it('5.2 - Return reformatted query where invalid fields are shallow compared to more deeply nested fields', () => { 401 | const testQuery = ` 402 | query { 403 | feed { 404 | links { 405 | id 406 | description 407 | test 408 | TIFFAAAANNNNYYYYYYYYYY 409 | text { 410 | content 411 | } 412 | } 413 | } 414 | } 415 | `; 416 | const testError = { links: ['test', 'TIFFAAAANNNNYYYYYYYYYY'] }; 417 | const outputQuery = ` 418 | query { 419 | feed { 420 | links { 421 | id 422 | description 423 | text { 424 | content 425 | } 426 | } 427 | } 428 | } 429 | `; 430 | const test = queryFormatter(testQuery); 431 | expect(test(testError)).toEqual(outputQuery); 432 | }); 433 | 434 | it('5.3 - Return reformatted query where invalid fields are at 1st level of depth only', () => { 435 | const testQuery = ` 436 | query { 437 | feed { 438 | jobs 439 | links { 440 | id 441 | description 442 | text { 443 | content 444 | } 445 | } 446 | } 447 | } 448 | `; 449 | const testError = { feed: ['jobs'] }; 450 | const outputQuery = ` 451 | query { 452 | feed { 453 | links { 454 | id 455 | description 456 | text { 457 | content 458 | } 459 | } 460 | } 461 | } 462 | `; 463 | const test = queryFormatter(testQuery); 464 | expect(test(testError)).toEqual(outputQuery); 465 | }); 466 | 467 | it('5.4 - Return reformatted query where invalid fields occur at three different nested levels of depth', () => { 468 | const testQuery = ` 469 | query { 470 | feed { 471 | TIFFAAAANNNNYYYYYYYYYY 472 | links { 473 | id 474 | description 475 | test 476 | TIFFAAAANNNNYYYYYYYYYY 477 | text { 478 | content 479 | } 480 | } 481 | } 482 | } 483 | `; 484 | const testError = { 485 | feed: ['TIFFAAAANNNNYYYYYYYYYY'], 486 | links: ['test', 'TIFFAAAANNNNYYYYYYYYYY'], 487 | text: ['content'] 488 | }; 489 | const outputQuery = ` 490 | query { 491 | feed { 492 | links { 493 | id 494 | description 495 | } 496 | } 497 | } 498 | `; 499 | const test = queryFormatter(testQuery); 500 | expect(test(testError)).toEqual(outputQuery); 501 | }); 502 | 503 | it('5.5 - Return reformatted query where invalid fields are at 2nd & 3rd levels of depth', () => { 504 | const testQuery = ` 505 | query { 506 | feed { 507 | links { 508 | id 509 | description 510 | test 511 | TIFFAAAANNNNYYYYYYYYYY 512 | text { 513 | content 514 | } 515 | } 516 | } 517 | } 518 | `; 519 | const testError = { links: ['test', 'TIFFAAAANNNNYYYYYYYYYY'], text: ['content'] }; 520 | const outputQuery = ` 521 | query { 522 | feed { 523 | links { 524 | id 525 | description 526 | } 527 | } 528 | } 529 | `; 530 | const test = queryFormatter(testQuery); 531 | expect(test(testError)).toEqual(outputQuery); 532 | }); 533 | 534 | it('5.6 - Return reformatted query where invalid fields are at 1st and 3rd levels of depth', () => { 535 | const testQuery = ` 536 | query { 537 | feed { 538 | TIFFAAAANNNNYYYYYYYYYY 539 | links { 540 | id 541 | description 542 | text { 543 | TIFFAAAANNNNYYYYYYYYYY 544 | } 545 | } 546 | } 547 | } 548 | `; 549 | const testError = { feed: ['TIFFAAAANNNNYYYYYYYYYY'], text: ['TIFFAAAANNNNYYYYYYYYYY'] }; 550 | const outputQuery = ` 551 | query { 552 | feed { 553 | links { 554 | id 555 | description 556 | } 557 | } 558 | } 559 | `; 560 | const test = queryFormatter(testQuery); 561 | expect(test(testError)).toEqual(outputQuery); 562 | }); 563 | 564 | it('5.7 - Return reformatted query where invalid fields are at 1st and 2nd levels of depth', () => { 565 | const testQuery = ` 566 | query { 567 | feed { 568 | TIFFANNYYYY 569 | links { 570 | TIFFANNYYYY 571 | id 572 | description 573 | text { 574 | id 575 | } 576 | } 577 | } 578 | } 579 | `; 580 | const testError = { 581 | feed: ['TIFFANNYYYY'], 582 | links: ['TIFFANNYYYY'] 583 | }; 584 | const outputQuery = ` 585 | query { 586 | feed { 587 | links { 588 | id 589 | description 590 | text { 591 | id 592 | } 593 | } 594 | } 595 | } 596 | `; 597 | const test = queryFormatter(testQuery); 598 | expect(test(testError)).toEqual(outputQuery); 599 | }); 600 | 601 | it('5.8 - Return reformatted query where invalid fields are at 2nd & 3rd levels of depth stemming from different types at equivalent depth', () => { 602 | const testQuery = ` 603 | query { 604 | feed { 605 | links { 606 | id 607 | description 608 | test { 609 | name 610 | nested 611 | } 612 | } 613 | text { 614 | content 615 | TIFFAAAANNNNYYYYYYYYYY 616 | } 617 | } 618 | } 619 | `; 620 | const testError = { test: ['name', 'nested'], text: ['TIFFAAAANNNNYYYYYYYYYY'] }; 621 | const outputQuery = ` 622 | query { 623 | feed { 624 | links { 625 | id 626 | description 627 | } 628 | text { 629 | content 630 | } 631 | } 632 | } 633 | `; 634 | const test = queryFormatter(testQuery); 635 | expect(test(testError)).toEqual(outputQuery); 636 | }); 637 | 638 | it('5.9 - Return reformatted query when invalid field occurs at all 3 levels of depth, but at least 1 valid field should remain in each level', () => { 639 | const testQuery = ` 640 | query { 641 | feed { 642 | id 643 | jobs 644 | links { 645 | id 646 | businesses 647 | description 648 | test { 649 | id 650 | name 651 | nested 652 | } 653 | } 654 | } 655 | } 656 | `; 657 | const testError = { 658 | feed: ['jobs'], 659 | links: ['businesses'], 660 | test: ['name', 'nested'] 661 | }; 662 | const outputQuery = ` 663 | query { 664 | feed { 665 | id 666 | links { 667 | id 668 | description 669 | test { 670 | id 671 | } 672 | } 673 | } 674 | } 675 | `; 676 | const test = queryFormatter(testQuery); 677 | expect(test(testError)).toEqual(outputQuery); 678 | }); 679 | 680 | }); 681 | 682 | -------------------------------------------------------------------------------- /__tests__/queryMapper.js: -------------------------------------------------------------------------------- 1 | const queryMapper = require('../package/queryMapper.js'); 2 | 3 | describe('queryMapper tests', () => { 4 | it('1.1 - Returns an object with invalid fields (1 level)', () => { 5 | const testQuery = ` 6 | query { 7 | feed { 8 | id 9 | tiffany 10 | } 11 | } 12 | `; 13 | const test = queryMapper(testQuery); 14 | expect(test).toEqual({ query: { feed: ['id', 'tiffany'] } }); 15 | }); 16 | 17 | it('1.2 - Returns an object with invalid fields (2 levels)', () => { 18 | const testQuery = ` 19 | query { 20 | feed { 21 | id 22 | tiffany 23 | links { 24 | id 25 | description 26 | tiffany2 27 | } 28 | } 29 | } 30 | `; 31 | const test = queryMapper(testQuery); 32 | expect(test).toEqual({ 33 | query: { 34 | feed: ['id', 'tiffany', { links: ['id', 'description', 'tiffany2'] }], 35 | }, 36 | }); 37 | }); 38 | 39 | it('1.3 - Returns an object with invalid fields (3 levels, circular)', () => { 40 | const testQuery = ` 41 | query { 42 | feed { 43 | id 44 | tiffany 45 | links { 46 | id 47 | description 48 | tiffany2 49 | feed { 50 | name 51 | tiffany3 52 | } 53 | } 54 | } 55 | } 56 | `; 57 | const test = queryMapper(testQuery); 58 | expect(test).toEqual({ 59 | query: { 60 | feed: [ 61 | 'id', 62 | 'tiffany', 63 | { 64 | links: [ 65 | 'id', 66 | 'description', 67 | 'tiffany2', 68 | { feed: ['name', 'tiffany3'] }, 69 | ], 70 | }, 71 | ], 72 | }, 73 | }); 74 | }); 75 | 76 | it('1.4 - Returns an object with only one invalid fields, and no valid field on 3rd level', () => { 77 | const testQuery = ` 78 | query { 79 | feed { 80 | id 81 | tiffany 82 | links { 83 | id 84 | description 85 | tiffany2 86 | feed { 87 | tiffany3 88 | } 89 | } 90 | } 91 | } 92 | `; 93 | const test = queryMapper(testQuery); 94 | expect(test).toEqual({ 95 | query: { 96 | feed: ['id', 'tiffany', { links: ['id', 'description', 'tiffany2', {feed: ['tiffany3']}] }], 97 | }, 98 | }); 99 | }); 100 | it('2.1 - has sibling on the 2nd level', () => { 101 | const testQuery = ` 102 | query { 103 | feed { 104 | id 105 | tiffany 106 | links { 107 | id 108 | description 109 | tiffany2 110 | } 111 | feed { 112 | tiffany3 113 | } 114 | } 115 | } 116 | `; 117 | const test = queryMapper(testQuery); 118 | expect(test).toEqual({ 119 | query: { 120 | feed: ['id', 'tiffany', { links: ['id', 'description', 'tiffany2'] }, { feed: ['tiffany3'] } ], 121 | }, 122 | }); 123 | }); 124 | it('2.2 - has sibling on the 3rd level', () => { 125 | const testQuery = ` 126 | query { 127 | feed { 128 | id 129 | tiffany 130 | links { 131 | id 132 | description 133 | tiffany2 134 | feed { 135 | tiffany3 136 | } 137 | links { 138 | id 139 | } 140 | } 141 | 142 | } 143 | } 144 | `; 145 | const test = queryMapper(testQuery); 146 | expect(test).toEqual({ 147 | query: { 148 | feed: ['id', 'tiffany', { links: ['id', 'description', 'tiffany2', {feed: ['tiffany3']}, {links: ['id']}] }], 149 | }, 150 | }); 151 | }); 152 | 153 | it('2.3 - has sibling on the 1st level', () => { 154 | const testQuery = ` 155 | query { 156 | test { 157 | id 158 | description 159 | } 160 | feed { 161 | id 162 | tiffany 163 | links { 164 | id 165 | description 166 | tiffany2 167 | feed { 168 | tiffany3 169 | } 170 | } 171 | } 172 | } 173 | `; 174 | const test = queryMapper(testQuery); 175 | expect(test).toEqual({ 176 | query: { 177 | test: ['id', 'description'], 178 | feed: ['id', 'tiffany', { links: ['id', 'description', 'tiffany2', { feed: ['tiffany3'] }] } ], 179 | }, 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/gql-error-handler/2aa32f0b4a58bba95247bb35956a20f445a1a156/assets/.DS_Store -------------------------------------------------------------------------------- /assets/after.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/gql-error-handler/2aa32f0b4a58bba95247bb35956a20f445a1a156/assets/after.gif -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/gql-error-handler/2aa32f0b4a58bba95247bb35956a20f445a1a156/assets/banner.png -------------------------------------------------------------------------------- /assets/before.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/gql-error-handler/2aa32f0b4a58bba95247bb35956a20f445a1a156/assets/before.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/gql-error-handler/2aa32f0b4a58bba95247bb35956a20f445a1a156/assets/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gql-error-handler", 3 | "version": "1.0.2", 4 | "description": "gql-error-handler is an Apollo Server plugin that returns partial data upon validation errors in GraphQL.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "keywords": ["JavaScript", "Typescript", "GraphQL", "Apollo Client", "Apollo Server", "Partial Data", 10 | "GraphQL Errors", "Error Handling", "Validation Errors", "Circular Dependencies", "Plugin", "Open Source", 11 | "npm package"], 12 | "author": "Nancy Yang | Tiffany Wong | Sam Ryder | Woobae Kim | Jeremy Buron-Yi", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@apollo/client": "^3.8.3", 16 | "@apollo/server": "^4.9.3", 17 | "graphql": "^16.8.0", 18 | "typescript": "^5.2.2" 19 | }, 20 | "devDependencies": { 21 | "jest": "^29.7.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package/compare.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | // function takes in cached schema and mapped query object 4 | // to generate object containing invalid fields 5 | function compare(schema, queryObj) { 6 | var errorObj = {}; 7 | function helper(object, type) { 8 | for (var key in object) { 9 | if (schema[type][key]) { 10 | for (var i = 0; i < object[key].length; i++) { 11 | if (typeof object[key][i] !== 'object') { 12 | if (!schema[type][key][object[key][i]]) { 13 | if (object[key][i][0] !== '_') { 14 | if (errorObj[key] && !errorObj[key].includes(object[key][i])) { 15 | errorObj[key].push(object[key][i]); 16 | } 17 | else { 18 | errorObj[key] = [object[key][i]]; 19 | } 20 | } 21 | } 22 | } 23 | else { 24 | // recursively call function if original query is nested 25 | helper(object[key][i], type); 26 | } 27 | } 28 | } 29 | } 30 | } 31 | helper(queryObj.query, 'query'); 32 | helper(queryObj.mutation, 'mutation'); 33 | return errorObj; 34 | } 35 | module.exports = compare; 36 | -------------------------------------------------------------------------------- /package/compare.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, CacheSchemaObject, queryObject } from './types'; 2 | 3 | // function takes in cached schema and mapped query object 4 | // to generate object containing invalid fields 5 | 6 | function compare(schema: CacheSchemaObject, queryObj: queryObject) { 7 | const errorObj: ErrorMessage = {}; 8 | function helper(object: any, type: string) { 9 | for (const key in object) { 10 | if (schema[type][key]) { 11 | for (let i = 0; i < object[key].length; i++) { 12 | if (typeof object[key][i] !== 'object') { 13 | if (!schema[type][key][object[key][i]]) { 14 | if (object[key][i][0] !== '_') { 15 | if (errorObj[key] && !errorObj[key].includes(object[key][i])) { 16 | errorObj[key].push(object[key][i]); 17 | } else { 18 | errorObj[key] = [object[key][i]]; 19 | } 20 | } 21 | } 22 | } else { 23 | // recursively call function if original query is nested 24 | helper(object[key][i], type); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | helper(queryObj.query, 'query'); 32 | helper(queryObj.mutation, 'mutation'); 33 | 34 | return errorObj; 35 | } 36 | 37 | module.exports = compare; 38 | -------------------------------------------------------------------------------- /package/errObjectParser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | // function takes in object containing invalid fields and object mapping custom types to their corresponding field names 4 | // and generates an array containing custom error message strings to be attached to returned partial data 5 | function errObjectParser(errorObj, typeFieldsCache) { 6 | var errorMessArr = []; 7 | for (var prop in errorObj) { 8 | for (var i = 0; i < errorObj[prop].length; i++) { 9 | errorMessArr.push("Cannot query field \"".concat(errorObj[prop][i], "\" on type \"").concat(typeFieldsCache[prop], "\".")); 10 | } 11 | } 12 | return errorMessArr; 13 | } 14 | module.exports = errObjectParser; 15 | -------------------------------------------------------------------------------- /package/errObjectParser.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, TypeFieldsCacheObject } from './types'; 2 | 3 | // function takes in object containing invalid fields and object mapping custom types to their corresponding field names 4 | // and generates an array containing custom error message strings to be attached to returned partial data 5 | 6 | function errObjectParser( 7 | errorObj: ErrorMessage, 8 | typeFieldsCache: TypeFieldsCacheObject 9 | ): string[] { 10 | const errorMessArr = []; 11 | 12 | for (const prop in errorObj) { 13 | for (let i = 0; i < errorObj[prop].length; i++) { 14 | errorMessArr.push( 15 | `Cannot query field "${errorObj[prop][i]}" on type "${typeFieldsCache[prop]}".` 16 | ); 17 | } 18 | } 19 | 20 | return errorMessArr; 21 | } 22 | 23 | module.exports = errObjectParser; 24 | -------------------------------------------------------------------------------- /package/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | var compare = require('./compare'); 40 | var queryMapper = require('./queryMapper'); 41 | var queryFormatter = require('./queryFormatter'); 42 | var errObjectParser = require('./errObjectParser'); 43 | // plugin to be utilized in an instance of Apollo Server 44 | var partialDataPlugin = { 45 | requestDidStart: function (requestContext) { 46 | var cacheSchema = { 47 | query: {}, 48 | mutation: {}, 49 | }; 50 | var customTypes = {}; 51 | var typeFieldsCache = {}; 52 | var scalarTypes = [ 53 | 'ID', 54 | 'ID!', 55 | 'String', 56 | 'String!', 57 | 'Int', 58 | 'Int!', 59 | 'Boolean', 60 | 'Boolean!', 61 | 'Float', 62 | 'Float!', 63 | ]; 64 | var schema = requestContext.schema; 65 | var allTypes = Object.values(schema.getTypeMap()); 66 | // Populating customTypes object to be utilized inside cacheSchema object 67 | allTypes.forEach(function (type) { 68 | if (type && type.constructor.name === 'GraphQLObjectType') { 69 | if (type.name[0] !== '_') { 70 | if (type.name !== 'Query' && type.name !== 'Mutation') { 71 | customTypes[type.name] = {}; 72 | var fields = type.getFields(); 73 | Object.values(fields).forEach(function (field) { 74 | customTypes[type.name][field.name] = field.type.toString(); 75 | }); 76 | } 77 | } 78 | } 79 | }); 80 | // Establishing deep copies of customTypes within cacheSchema, 81 | // effectively creating pointers from cacheSchema to differing properties within the customTypes object 82 | // in order to later on compare incoming nested queries with the cacheSchema object for reconciliation 83 | allTypes.forEach(function (type) { 84 | if (type.name === 'Query' || type.name === 'Mutation') { 85 | var fields = type.getFields(); 86 | Object.values(fields).forEach(function (field) { 87 | var fieldType = field.type.toString(); 88 | if (fieldType[0] === '[') { 89 | fieldType = fieldType.slice(1, fieldType.length - 1); 90 | typeFieldsCache[field.name] = fieldType; 91 | } 92 | cacheSchema[type.name.toLowerCase()][field.name] = 93 | customTypes[fieldType]; 94 | }); 95 | } 96 | }); 97 | // helper function to apply regex and create shallow copies of custom type reference 98 | function shallowCopy(prop) { 99 | if (!scalarTypes.includes(prop)) { 100 | var propRegEx = prop.replace(/[^a-zA-Z]/g, ''); 101 | return JSON.parse(JSON.stringify(customTypes[propRegEx])); 102 | } 103 | return prop; 104 | } 105 | // helper function to nest shallow copies 106 | function nest(nestedProps) { 107 | for (var nestedProp in nestedProps) { 108 | if (typeof nestedProps[nestedProp] === 'string' && 109 | !scalarTypes.includes(nestedProps[nestedProp])) { 110 | nestedProps[nestedProp] = shallowCopy(nestedProps[nestedProp]); 111 | } 112 | } 113 | return nestedProps; 114 | } 115 | // create nested levels of custom types to be referred to by cacheSchema for functionality regarding nested queries 116 | // using shallowCopy and nest helper procedures 117 | for (var customType in customTypes) { 118 | for (var prop in customTypes[customType]) { 119 | customTypes[customType][prop] = shallowCopy(customTypes[customType][prop]); 120 | if (typeof customTypes[customType][prop] === 'object') { 121 | customTypes[customType][prop] = nest(customTypes[customType][prop]); 122 | } 123 | } 124 | } 125 | var resultQueryMapper = queryMapper(requestContext.request.query); 126 | var errorObj = compare(cacheSchema, resultQueryMapper); 127 | var queryFunc = queryFormatter(requestContext.request.query); 128 | requestContext.request.query = queryFunc(errorObj); 129 | return { 130 | willSendResponse: function (requestContext) { 131 | return __awaiter(this, void 0, void 0, function () { 132 | var response, errArray; 133 | return __generator(this, function (_a) { 134 | response = requestContext.response; 135 | // add custom error message if invalid fields were present in original query 136 | if (!response || !response.errors) { 137 | errArray = errObjectParser(errorObj, typeFieldsCache); 138 | if (errArray.length > 0) 139 | response.errors = errArray; 140 | } 141 | return [2 /*return*/]; 142 | }); 143 | }); 144 | }, 145 | }; 146 | }, 147 | }; 148 | module.exports = partialDataPlugin; 149 | -------------------------------------------------------------------------------- /package/index.ts: -------------------------------------------------------------------------------- 1 | const compare = require('./compare'); 2 | const queryMapper = require('./queryMapper'); 3 | const queryFormatter = require('./queryFormatter'); 4 | const errObjectParser = require('./errObjectParser'); 5 | 6 | import { GraphQLSchema } from 'graphql'; 7 | import type { 8 | RequestContextType, 9 | CacheSchemaObject, 10 | CustomTypesObject, 11 | TypeFieldsCacheObject, 12 | } from './types'; 13 | 14 | // plugin to be utilized in an instance of Apollo Server 15 | 16 | const partialDataPlugin = { 17 | requestDidStart(requestContext: RequestContextType) { 18 | const cacheSchema: CacheSchemaObject = { 19 | query: {}, 20 | mutation: {}, 21 | }; 22 | const customTypes: CustomTypesObject = {}; 23 | const typeFieldsCache: TypeFieldsCacheObject = {}; 24 | const scalarTypes: string[] = [ 25 | 'ID', 26 | 'ID!', 27 | 'String', 28 | 'String!', 29 | 'Int', 30 | 'Int!', 31 | 'Boolean', 32 | 'Boolean!', 33 | 'Float', 34 | 'Float!', 35 | ]; 36 | const schema: GraphQLSchema = requestContext.schema as GraphQLSchema; 37 | 38 | const allTypes: any[] = Object.values(schema.getTypeMap()); 39 | 40 | // Populating customTypes object to be utilized inside cacheSchema object 41 | 42 | allTypes.forEach((type: { name: string; getFields: () => any }) => { 43 | if (type && type.constructor.name === 'GraphQLObjectType') { 44 | if (type.name[0] !== '_') { 45 | if (type.name !== 'Query' && type.name !== 'Mutation') { 46 | customTypes[type.name] = {}; 47 | const fields: object = type.getFields(); 48 | Object.values(fields).forEach( 49 | (field: { name: string; type: string }) => { 50 | customTypes[type.name][field.name] = field.type.toString(); 51 | } 52 | ); 53 | } 54 | } 55 | } 56 | }); 57 | 58 | // Establishing deep copies of customTypes within cacheSchema, 59 | // effectively creating pointers from cacheSchema to differing properties within the customTypes object 60 | // in order to later on compare incoming nested queries with the cacheSchema object for reconciliation 61 | 62 | allTypes.forEach((type: { name: string; getFields: () => any }) => { 63 | if (type.name === 'Query' || type.name === 'Mutation') { 64 | const fields: object = type.getFields(); 65 | Object.values(fields).forEach( 66 | (field: { name: string; type: string }) => { 67 | let fieldType = field.type.toString(); 68 | if (fieldType[0] === '[') { 69 | fieldType = fieldType.slice(1, fieldType.length - 1); 70 | typeFieldsCache[field.name] = fieldType; 71 | } 72 | cacheSchema[type.name.toLowerCase()][field.name] = 73 | customTypes[fieldType]; 74 | } 75 | ); 76 | } 77 | }); 78 | 79 | // helper function to apply regex and create shallow copies of custom type reference 80 | 81 | function shallowCopy(prop: string) { 82 | if (!scalarTypes.includes(prop)) { 83 | const propRegEx = prop.replace(/[^a-zA-Z]/g, ''); 84 | return JSON.parse(JSON.stringify(customTypes[propRegEx])); 85 | } 86 | return prop; 87 | } 88 | 89 | // helper function to nest shallow copies 90 | 91 | function nest(nestedProps: Record) { 92 | for (const nestedProp in nestedProps) { 93 | if ( 94 | typeof nestedProps[nestedProp] === 'string' && 95 | !scalarTypes.includes(nestedProps[nestedProp]) 96 | ) { 97 | nestedProps[nestedProp] = shallowCopy(nestedProps[nestedProp]); 98 | } 99 | } 100 | return nestedProps; 101 | } 102 | 103 | // create nested levels of custom types to be referred to by cacheSchema for functionality regarding nested queries 104 | // using shallowCopy and nest helper procedures 105 | 106 | for (const customType in customTypes) { 107 | for (const prop in customTypes[customType]) { 108 | customTypes[customType][prop] = shallowCopy( 109 | customTypes[customType][prop] 110 | ); 111 | if (typeof customTypes[customType][prop] === 'object') { 112 | customTypes[customType][prop] = nest(customTypes[customType][prop]); 113 | } 114 | } 115 | } 116 | 117 | const resultQueryMapper = queryMapper(requestContext.request.query); 118 | const errorObj = compare(cacheSchema, resultQueryMapper); 119 | const queryFunc = queryFormatter(requestContext.request.query); 120 | requestContext.request.query = queryFunc(errorObj); 121 | 122 | return { 123 | async willSendResponse(requestContext: RequestContextType) { 124 | const { response } = requestContext; 125 | 126 | // add custom error message if invalid fields were present in original query 127 | 128 | if (!response || !response.errors) { 129 | const errArray = errObjectParser(errorObj, typeFieldsCache); 130 | if (errArray.length > 0) response.errors = errArray; 131 | } 132 | }, 133 | }; 134 | }, 135 | }; 136 | 137 | module.exports = partialDataPlugin; 138 | -------------------------------------------------------------------------------- /package/queryFormatter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | // function takes in original query as a string 4 | // and returns a function that takes in object containing invalid fields 5 | // to remove invalid fields and produce a valid result query 6 | function queryFormatter(query) { 7 | var cache = query; 8 | return function (error) { 9 | var resultQuery = query; 10 | for (var keys in error) { 11 | for (var i = 0; i < error[keys].length; i++) { 12 | // If all fields are invalid, return the original query in order to use Apollo Server's validation error message 13 | if (remove(keys, error[keys][i], resultQuery) === resultQuery) { 14 | return query; 15 | } 16 | // create reformulated query as invalid fields are being removed 17 | resultQuery = remove(keys, error[keys][i], resultQuery); 18 | } 19 | } 20 | return resultQuery; 21 | }; 22 | } 23 | function remove(type, field, query) { 24 | if (type === 'query' || type === 'mutation') { 25 | var regexField = new RegExp("(\\s|\\n)*".concat(field, "(\\s|\\n)*\\{[^{}]*\\}")); 26 | if (regexField.test(query)) { 27 | query = query.replace(regexField, ''); 28 | } 29 | } 30 | var regex = new RegExp("".concat(type, "\\s*{[^]*?").concat(field, "(\\s|\\n)*")); 31 | var match = query.match(regex); 32 | if (match) { 33 | var extractedField = match[0]; 34 | var newQuery = ''; 35 | var regexExtract = new RegExp("\\{[(\\s|\\n)*".concat(field, "[ ]*\\}")); 36 | if (regexExtract.test(extractedField)) { 37 | newQuery = query.replace(extractedField, ''); 38 | return newQuery; 39 | } 40 | if (extractedField.includes(field)) { 41 | var regex1 = new RegExp("".concat(field, "\\s*\\{")); 42 | if (!regex1.test(extractedField)) { 43 | var regex2 = new RegExp("".concat(field, "\\s*")); 44 | newQuery = extractedField.replace(regex2, ''); 45 | } 46 | else { 47 | var regex_1 = new RegExp("(\\s|\\n)*".concat(field, "(\\s|\\n)*\\{[^{}]*\\}"), 'g'); 48 | newQuery = extractedField.replace(regex_1, ''); 49 | } 50 | var result = query.replace(extractedField, newQuery); 51 | var regexType = new RegExp("\\s*".concat(type, "\\s*\\{\\s*}")); 52 | if (regexType.test(result)) { 53 | result = result.replace(regexType, ''); 54 | } 55 | var regexEmpty = new RegExp("\\s*\\{\\s*}"); 56 | if (regexEmpty.test(result)) { 57 | return query; 58 | } 59 | return result; 60 | } 61 | else { 62 | return query; 63 | } 64 | } 65 | else { 66 | return query; 67 | } 68 | } 69 | module.exports = queryFormatter; 70 | -------------------------------------------------------------------------------- /package/queryFormatter.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from './types'; 2 | 3 | // function takes in original query as a string 4 | // and returns a function that takes in object containing invalid fields 5 | // to remove invalid fields and produce a valid result query 6 | 7 | function queryFormatter(query: string) { 8 | const cache = query; 9 | 10 | return function (error: ErrorMessage) { 11 | let resultQuery = query; 12 | 13 | for (let keys in error) { 14 | for (let i = 0; i < error[keys].length; i++) { 15 | // If all fields are invalid, return the original query in order to use Apollo Server's validation error message 16 | if (remove(keys, error[keys][i], resultQuery) === resultQuery) { 17 | return query; 18 | } 19 | // create reformulated query as invalid fields are being removed 20 | resultQuery = remove(keys, error[keys][i], resultQuery); 21 | } 22 | } 23 | return resultQuery; 24 | }; 25 | } 26 | 27 | function remove(type: string, field: string, query: string) { 28 | if (type === 'query' || type === 'mutation') { 29 | const regexField = new RegExp(`(\\s|\\n)*${field}(\\s|\\n)*\\{[^{}]*\\}`); 30 | if (regexField.test(query)) { 31 | query = query.replace(regexField, ''); 32 | } 33 | } 34 | const regex = new RegExp(`${type}\\s*{[^]*?${field}(\\s|\\n)*`); 35 | const match = query.match(regex); 36 | if (match) { 37 | const extractedField = match[0]; 38 | let newQuery = ''; 39 | const regexExtract = new RegExp(`\\{[(\\s|\\n)*${field}[ ]*\\}`); 40 | 41 | if (regexExtract.test(extractedField)) { 42 | newQuery = query.replace(extractedField, ''); 43 | return newQuery; 44 | } 45 | 46 | if (extractedField.includes(field)) { 47 | const regex1 = new RegExp(`${field}\\s*\\{`); 48 | if (!regex1.test(extractedField)) { 49 | const regex2 = new RegExp(`${field}\\s*`); 50 | newQuery = extractedField.replace(regex2, ''); 51 | } else { 52 | const regex = new RegExp( 53 | `(\\s|\\n)*${field}(\\s|\\n)*\\{[^{}]*\\}`, 54 | 'g' 55 | ); 56 | newQuery = extractedField.replace(regex, ''); 57 | } 58 | let result = query.replace(extractedField, newQuery); 59 | const regexType = new RegExp(`\\s*${type}\\s*\\{\\s*}`); 60 | if (regexType.test(result)) { 61 | result = result.replace(regexType, ''); 62 | } 63 | const regexEmpty = new RegExp(`\\s*\\{\\s*}`); 64 | if (regexEmpty.test(result)) { 65 | return query; 66 | } 67 | return result; 68 | } else { 69 | return query; 70 | } 71 | } else { 72 | return query; 73 | } 74 | } 75 | 76 | module.exports = queryFormatter; 77 | -------------------------------------------------------------------------------- /package/queryMapper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var graphql_1 = require("graphql"); 4 | // Parse the client's query and traverse the AST object 5 | // Construct a query object queryMap, using the same structure as the cacheSchema for comparison 6 | function queryMapper(query) { 7 | var ast = (0, graphql_1.parse)(query); 8 | var queryMap = {}; 9 | var operationType; 10 | // Traverse each individual nodes inside the AST object 11 | // Populate the query object with all the types and fields from the input query 12 | // Key/value pairs in the returned query object are a type (key) pointing to an array of fields (string) and types (object) 13 | var buildFieldArray = function (node) { 14 | var fieldArray = []; 15 | if (node.selectionSet) { 16 | node.selectionSet.selections.forEach(function (selection) { 17 | var _a; 18 | if (selection.kind === 'Field') { 19 | if (selection.selectionSet) { 20 | var fieldEntry = (_a = {}, _a[selection.name.value] = [], _a); 21 | fieldEntry[selection.name.value] = buildFieldArray(selection); 22 | fieldArray.push(fieldEntry); 23 | } 24 | else { 25 | fieldArray.push.apply(fieldArray, buildFieldArray(selection)); 26 | } 27 | } 28 | }); 29 | } 30 | var temp = fieldArray.length > 0 ? fieldArray : [node.name.value]; 31 | return temp; 32 | }; 33 | //graphQL visitor object, that contains callback functions, to traverse AST object 34 | //it divides the query with 'query' and 'mutation', and populate each type and field for nested types of field 35 | //by calling visit method on each individual node 36 | var visitor = { 37 | OperationDefinition: { 38 | enter: function (node) { 39 | operationType = node.operation; 40 | queryMap[operationType] = {}; 41 | var types = node.selectionSet.selections; 42 | var fieldVisitor = { 43 | Field: function (node) { 44 | // conditions that handle nested types/fields and sibling types 45 | Object.values(types).forEach(function (type) { 46 | if (!queryMap[operationType][type.name.value] && node.selectionSet) { 47 | if (Object.values(types).includes(node)) { 48 | queryMap[operationType][node.name.value] = buildFieldArray(node); 49 | } 50 | } 51 | }); 52 | }, 53 | }; 54 | (0, graphql_1.visit)(node, fieldVisitor); 55 | }, 56 | }, 57 | }; 58 | (0, graphql_1.visit)(ast, visitor); 59 | return queryMap; 60 | } 61 | module.exports = queryMapper; 62 | -------------------------------------------------------------------------------- /package/queryMapper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | parse, 4 | visit, 5 | OperationDefinitionNode, 6 | FieldNode, 7 | } from 'graphql'; 8 | 9 | 10 | // Parse the client's query and traverse the AST object 11 | // Construct a query object queryMap, using the same structure as the cacheSchema for comparison 12 | 13 | function queryMapper(query: string) { 14 | 15 | const ast: DocumentNode = parse(query); 16 | 17 | const queryMap: Record> = {}; 18 | 19 | let operationType: string; 20 | 21 | // Traverse each individual nodes inside the AST object 22 | // Populate the query object with all the types and fields from the input query 23 | // Key/value pairs in the returned query object are a type (key) pointing to an array of fields (string) and types (object) 24 | const buildFieldArray = (node: FieldNode): any => { 25 | const fieldArray: any[] = []; 26 | 27 | if (node.selectionSet) { 28 | node.selectionSet.selections.forEach((selection) => { 29 | if (selection.kind === 'Field') { 30 | if (selection.selectionSet) { 31 | const fieldEntry = { [selection.name.value]: []}; 32 | fieldEntry[selection.name.value] = buildFieldArray(selection); 33 | fieldArray.push(fieldEntry); 34 | } else { 35 | fieldArray.push(...buildFieldArray(selection)); 36 | } 37 | } 38 | }); 39 | } 40 | 41 | let temp = fieldArray.length > 0 ? fieldArray : [node.name.value]; 42 | return temp; 43 | }; 44 | 45 | //graphQL visitor object, that contains callback functions, to traverse AST object 46 | //it divides the query with 'query' and 'mutation', and populate each type and field for nested types of field 47 | //by calling visit method on each individual node 48 | const visitor = { 49 | OperationDefinition: { 50 | enter(node: OperationDefinitionNode) { 51 | operationType = node.operation; 52 | queryMap[operationType] = {}; 53 | 54 | const types = node.selectionSet.selections; 55 | 56 | const fieldVisitor = { 57 | Field(node: FieldNode) { 58 | 59 | // conditions that handle nested types/fields and sibling types 60 | Object.values(types).forEach((type: any) => { 61 | if (!queryMap[operationType][type.name.value] && node.selectionSet) { 62 | if (Object.values(types).includes(node)) { 63 | queryMap[operationType][node.name.value] = buildFieldArray(node); 64 | } 65 | } 66 | }) 67 | }, 68 | }; 69 | 70 | visit(node, fieldVisitor); 71 | }, 72 | }, 73 | }; 74 | 75 | visit(ast, visitor); 76 | 77 | return queryMap; 78 | } 79 | 80 | module.exports = queryMapper; 81 | -------------------------------------------------------------------------------- /package/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /package/types.ts: -------------------------------------------------------------------------------- 1 | // object type containing invalid fields 2 | export type ErrorMessage = { 3 | [key: string]: string[]; 4 | }; 5 | 6 | // object type representing original query where keys are types and values are an array of fields 7 | export type queryObject = { 8 | [key: string]: object | string[]; 9 | }; 10 | 11 | // object type representing requestContext parameter in Apollo Server events 12 | export type RequestContextType = { 13 | logger: object; 14 | schema: { getTypeMap: () => any }; 15 | schemaHash: string; 16 | request: { 17 | query: string; 18 | }; 19 | response: ResponseType; 20 | context: object; 21 | cache: object; 22 | debug: boolean; 23 | metrics: object; 24 | overallCachePolicy: object; 25 | requestIsBatched: boolean; 26 | }; 27 | 28 | type ResponseType = { 29 | errors: any[]; 30 | }; 31 | 32 | // object type representing schema as captured from requestContext object 33 | export type CacheSchemaObject = { 34 | [key: string]: { 35 | [field: string]: any; 36 | }; 37 | }; 38 | 39 | // object type representing custom types as captured from requestContext object 40 | // utilized in populating CacheSchemaObject 41 | export type CustomTypesObject = { 42 | [key: string]: { 43 | [field: string]: any; 44 | }; 45 | }; 46 | 47 | // object type mapping custom types to their corresponding field names as captured from requestContext object 48 | export type TypeFieldsCacheObject = { 49 | [key: string]: string; 50 | }; 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "strict": true, 5 | "module": "CommonJS", 6 | "esModuleInterop": true, 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "sourceMap": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": [ 13 | "package/**/*" 14 | ] 15 | } --------------------------------------------------------------------------------