From 4bf20192c36d764dc0a3630d1643cd9ce156a50e Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Thu, 12 Nov 2020 14:10:15 -0500 Subject: [PATCH] Ensure that arrays are properly supported --- src/RewriteHandler.ts | 6 +- src/ast.ts | 12 ++- src/rewriters/NestFieldOutputsRewriter.ts | 24 ++---- src/rewriters/Rewriter.ts | 52 ++++++++++- .../ScalarFieldToObjectFieldRewriter.ts | 20 ++--- src/rewriters/index.ts | 2 +- test/ast.test.ts | 8 +- .../rewriteNestFieldOutputs.test.ts | 56 +++++++++++- .../rewriteScalarFieldToObjectField.test.ts | 46 +++++++++- test/functional/rewriter.test.ts | 86 +++++++++++++++++++ 10 files changed, 268 insertions(+), 44 deletions(-) create mode 100644 test/functional/rewriter.test.ts diff --git a/src/RewriteHandler.ts b/src/RewriteHandler.ts index c1fa956..505448d 100644 --- a/src/RewriteHandler.ts +++ b/src/RewriteHandler.ts @@ -72,8 +72,10 @@ export default class RewriteHandler { let rewrittenResponse = response; this.matches.reverse().forEach(({ rewriter, paths }) => { paths.forEach(path => { - rewrittenResponse = rewriteResultsAtPath(rewrittenResponse, path, (parentResponse, key) => - rewriter.rewriteResponse(parentResponse, key) + rewrittenResponse = rewriteResultsAtPath( + rewrittenResponse, + path, + (parentResponse, key, index) => rewriter.rewriteResponse(parentResponse, key, index) ); }); }); diff --git a/src/ast.ts b/src/ast.ts index 94a3917..525cde9 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -261,7 +261,7 @@ interface ResultObj { export const rewriteResultsAtPath = ( results: ResultObj, path: ReadonlyArray, - callback: (parentResult: any, key: string | number) => any + callback: (parentResult: any, key: string, position?: number) => any ): ResultObj => { if (path.length === 0) return results; @@ -271,12 +271,10 @@ export const rewriteResultsAtPath = ( if (path.length === 1) { if (Array.isArray(curResults)) { - newResults[curPathElm] = curResults.map((_, index) => { - const newValue = callback(curResults, index); - return newValue; - }); - - return newResults; + return curResults.reduce( + (reducedResults, _, index) => callback(reducedResults, curPathElm, index), + results + ); } return callback(results, curPathElm); diff --git a/src/rewriters/NestFieldOutputsRewriter.ts b/src/rewriters/NestFieldOutputsRewriter.ts index 74e0273..84fcbe1 100644 --- a/src/rewriters/NestFieldOutputsRewriter.ts +++ b/src/rewriters/NestFieldOutputsRewriter.ts @@ -65,23 +65,17 @@ class NestFieldOutputsRewriter extends Rewriter { } as NodeAndVarDefs; } - public rewriteResponse(response: any, key: string | number) { - const pathResponse = response[key]; + public rewriteResponse(response: any, key: string, index?: number) { + // Extract the element we are working on + const element = super.extractReponseElement(response, key, index); + if (element === null || typeof element !== 'object') return response; - if (typeof pathResponse === 'object') { - // undo the nesting in the response so it matches the original query - if ( - pathResponse[this.newOutputName] && - typeof pathResponse[this.newOutputName] === 'object' - ) { - const rewrittenResponse = { ...pathResponse, ...pathResponse[this.newOutputName] }; - delete rewrittenResponse[this.newOutputName]; + // Undo the nesting in the response so it matches the original query + if (element[this.newOutputName] && typeof element[this.newOutputName] === 'object') { + const newElement = { ...element, ...element[this.newOutputName] }; + delete newElement[this.newOutputName]; - return { - ...response, - [key]: rewrittenResponse - }; - } + return super.rewriteResponseElement(response, newElement, key, index); } return response; diff --git a/src/rewriters/Rewriter.ts b/src/rewriters/Rewriter.ts index 9af1c79..70420d9 100644 --- a/src/rewriters/Rewriter.ts +++ b/src/rewriters/Rewriter.ts @@ -56,7 +56,57 @@ abstract class Rewriter { return variables; } - public rewriteResponse(response: any, key: string | number): any { + /* + * Receives the parent object of the matched field with the key of the matched field. + * For arrays, the index of the element is also present. + */ + public rewriteResponse(response: any, key: string, index?: number): any { + return response; + } + + /* + * Helper that extracts the element from the response if possible otherwise returns null. + */ + protected extractReponseElement(response: any, key: string, index?: number): any { + // Verify the response format + let element = null; + if (response === null || typeof response !== 'object') return element; + + // Extract the key + element = response[key] || null; + + // Extract the position + if (Array.isArray(element)) { + element = element[index!] || null; + } + + return element; + } + + /* + * Helper that rewrite the element from the response if possible and returns the response. + */ + protected rewriteResponseElement( + response: any, + newElement: any, + key: string, + index?: number + ): any { + // Verify the response format + if (response === null || typeof response !== 'object') return response; + + // Extract the key + let element = response[key]; + + // Extract the position + // NOTE: We might eventually want to create an array if one is not present at the key + // and we receive an index in input + if (Array.isArray(element)) { + element[index!] = newElement; + } else { + response[key] = newElement; + } + return response; } } diff --git a/src/rewriters/ScalarFieldToObjectFieldRewriter.ts b/src/rewriters/ScalarFieldToObjectFieldRewriter.ts index 0e909e2..2136091 100644 --- a/src/rewriters/ScalarFieldToObjectFieldRewriter.ts +++ b/src/rewriters/ScalarFieldToObjectFieldRewriter.ts @@ -7,7 +7,7 @@ interface ScalarFieldToObjectFieldRewriterOpts extends RewriterOpts { } /** - * Rewriter which nests output fields inside of a new output object + * Rewriter which nests a scalar field inside of a new output object * ex: change from `field { subField }` to `field { subField { objectfield } }` */ class ScalarFieldToObjectFieldRewriter extends Rewriter { @@ -48,18 +48,14 @@ class ScalarFieldToObjectFieldRewriter extends Rewriter { } as NodeAndVarDefs; } - public rewriteResponse(response: any, key: string | number) { - if (typeof response === 'object') { - const pathResponse = response[key]; + public rewriteResponse(response: any, key: string, index?: number) { + // Extract the element we are working on + const element = super.extractReponseElement(response, key, index); + if (element === null) return response; - // undo the nesting in the response so it matches the original query - return { - ...response, - [key]: pathResponse[this.objectFieldName] - }; - } - - return response; + // Undo the nesting in the response so it matches the original query + const newElement = element[this.objectFieldName]; + return super.rewriteResponseElement(response, newElement, key, index); } } diff --git a/src/rewriters/index.ts b/src/rewriters/index.ts index 585cdfa..35376e3 100644 --- a/src/rewriters/index.ts +++ b/src/rewriters/index.ts @@ -1,4 +1,4 @@ -export { default as Rewriter } from './Rewriter'; +export { default as Rewriter, RewriterOpts } from './Rewriter'; export { default as FieldArgNameRewriter } from './FieldArgNameRewriter'; export { default as FieldArgsToInputTypeRewriter } from './FieldArgsToInputTypeRewriter'; export { default as FieldArgTypeRewriter } from './FieldArgTypeRewriter'; diff --git a/test/ast.test.ts b/test/ast.test.ts index 43c952d..e7be20c 100644 --- a/test/ast.test.ts +++ b/test/ast.test.ts @@ -68,10 +68,10 @@ describe('ast utils', () => { ] }); expect( - rewriteResultsAtPath(obj, ['things', 'moreThings'], (elm, path) => ({ - ...elm[path], - meh: '7' - })) + rewriteResultsAtPath(obj, ['things', 'moreThings'], (elm, path, index) => { + elm[path][index!] = { ...elm[path][index!], meh: '7' }; + return elm; + }) ).toEqual({ things: [ { diff --git a/test/functional/rewriteNestFieldOutputs.test.ts b/test/functional/rewriteNestFieldOutputs.test.ts index 924919f..b1dcc2e 100644 --- a/test/functional/rewriteNestFieldOutputs.test.ts +++ b/test/functional/rewriteNestFieldOutputs.test.ts @@ -2,7 +2,7 @@ import RewriteHandler from '../../src/RewriteHandler'; import NestFieldOutputsRewriter from '../../src/rewriters/NestFieldOutputsRewriter'; import { gqlFmt } from '../testUtils'; -describe('Rewrite field args to input type', () => { +describe('Rewrite output fields inside of a new output object', () => { it('allows nesting the args provided into an input type', () => { const handler = new RewriteHandler([ new NestFieldOutputsRewriter({ @@ -102,4 +102,58 @@ describe('Rewrite field args to input type', () => { } }); }); + + it('allows nesting the args provided in an array', () => { + const handler = new RewriteHandler([ + new NestFieldOutputsRewriter({ + fieldName: 'createCats', + newOutputName: 'cat', + outputsToNest: ['name', 'color', 'id'] + }) + ]); + const query = gqlFmt` + mutation createManyCats { + createCats { + id + name + color + } + } + `; + const expectedRewritenQuery = gqlFmt` + mutation createManyCats { + createCats { + cat { + id + name + color + } + } + } + `; + expect(handler.rewriteRequest(query)).toEqual({ + query: expectedRewritenQuery + }); + expect( + handler.rewriteResponse({ + createCats: [ + { + cat: { + id: 1, + name: 'jack', + color: 'blue' + } + } + ] + }) + ).toEqual({ + createCats: [ + { + id: 1, + name: 'jack', + color: 'blue' + } + ] + }); + }); }); diff --git a/test/functional/rewriteScalarFieldToObjectField.test.ts b/test/functional/rewriteScalarFieldToObjectField.test.ts index ef271d7..fdbbb42 100644 --- a/test/functional/rewriteScalarFieldToObjectField.test.ts +++ b/test/functional/rewriteScalarFieldToObjectField.test.ts @@ -3,7 +3,7 @@ import ScalarFieldToObjectFieldRewriter from '../../src/rewriters/ScalarFieldToO import { gqlFmt } from '../testUtils'; describe('Rewrite scalar field to be a nested object with a single scalar field', () => { - it('rewrites a scalar field to be an objet field with 1 scalar subfield', () => { + it('rewrites a scalar field to be an object field with 1 scalar subfield', () => { const handler = new RewriteHandler([ new ScalarFieldToObjectFieldRewriter({ fieldName: 'title', @@ -226,4 +226,48 @@ describe('Rewrite scalar field to be a nested object with a single scalar field' } }); }); + + it('rewrites a scalar field array to be an array of object fields with 1 scalar subfield', () => { + const handler = new RewriteHandler([ + new ScalarFieldToObjectFieldRewriter({ + fieldName: 'titles', + objectFieldName: 'text' + }) + ]); + + const query = gqlFmt` + query getThing { + thing { + titles + } + } + `; + const expectedRewritenQuery = gqlFmt` + query getThing { + thing { + titles { + text + } + } + } + `; + expect(handler.rewriteRequest(query)).toEqual({ + query: expectedRewritenQuery + }); + expect( + handler.rewriteResponse({ + thing: { + titles: [ + { + text: 'THING' + } + ] + } + }) + ).toEqual({ + thing: { + titles: ['THING'] + } + }); + }); }); diff --git a/test/functional/rewriter.test.ts b/test/functional/rewriter.test.ts new file mode 100644 index 0000000..7da78d8 --- /dev/null +++ b/test/functional/rewriter.test.ts @@ -0,0 +1,86 @@ +import Rewriter, { RewriterOpts } from '../../src/rewriters/Rewriter'; + +describe('rewriter', () => { + class TestRewriter extends Rewriter { + constructor(options: RewriterOpts) { + super(options); + } + + public extractReponseElement(response: any, key: string, index?: number): any { + return super.extractReponseElement(response, key, index); + } + + public rewriteResponseElement( + response: any, + newElement: any, + key: string, + index?: number + ): any { + return super.rewriteResponseElement(response, newElement, key, index); + } + } + + describe('extractResponseElement', () => { + const rewriter = new TestRewriter({ fieldName: 'test' }); + + it('can extract element in object', () => { + const key = 'key'; + const element = { a: 1 }; + const response = { [key]: element }; + + expect(rewriter.extractReponseElement(response, key)).toEqual(element); + }); + + it('can extract element in array', () => { + const key = 'key'; + const element = { a: 1 }; + const response = { [key]: [element] }; + + expect(rewriter.extractReponseElement(response, key, 0)).toEqual(element); + }); + + it('does not fail on null, empty or malformed response', () => { + const key = 'key'; + + expect(rewriter.extractReponseElement(null, key)).toEqual(null); + expect(rewriter.extractReponseElement('string', key)).toEqual(null); + expect(rewriter.extractReponseElement({ a: 1 }, key)).toEqual(null); + }); + }); + + describe('rewriteResponseElement', () => { + const rewriter = new TestRewriter({ fieldName: 'test' }); + + it('can replace element in object', () => { + const key = 'key'; + const newElement = { a: 1 }; + const response = { [key]: 1 }; + + expect(rewriter.rewriteResponseElement(response, newElement, key)).toEqual({ + [key]: newElement + }); + }); + + it('can replace element in array', () => { + const key = 'key'; + const newElement = { a: 1 }; + const response = { [key]: [1] }; + + expect(rewriter.rewriteResponseElement(response, newElement, key, 0)).toEqual({ + [key]: [newElement] + }); + }); + + it('does not fail on null, empty or malformed response', () => { + const key = 'key'; + const newElement = { a: 1 }; + + expect(rewriter.rewriteResponseElement(null, newElement, key)).toEqual(null); + expect(rewriter.rewriteResponseElement('string', newElement, key)).toEqual('string'); + expect(rewriter.rewriteResponseElement({ a: 1 }, newElement, key)).toEqual({ + a: 1, + [key]: newElement + }); + }); + }); +});