├── .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 | }
--------------------------------------------------------------------------------