Skip to content

Commit adae5d5

Browse files
authored
Ensure that arrays are properly supported (#29)
1 parent 809517d commit adae5d5

10 files changed

+268
-44
lines changed

src/RewriteHandler.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ export default class RewriteHandler {
7272
let rewrittenResponse = response;
7373
this.matches.reverse().forEach(({ rewriter, paths }) => {
7474
paths.forEach(path => {
75-
rewrittenResponse = rewriteResultsAtPath(rewrittenResponse, path, (parentResponse, key) =>
76-
rewriter.rewriteResponse(parentResponse, key)
75+
rewrittenResponse = rewriteResultsAtPath(
76+
rewrittenResponse,
77+
path,
78+
(parentResponse, key, index) => rewriter.rewriteResponse(parentResponse, key, index)
7779
);
7880
});
7981
});

src/ast.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ interface ResultObj {
261261
export const rewriteResultsAtPath = (
262262
results: ResultObj,
263263
path: ReadonlyArray<string>,
264-
callback: (parentResult: any, key: string | number) => any
264+
callback: (parentResult: any, key: string, position?: number) => any
265265
): ResultObj => {
266266
if (path.length === 0) return results;
267267

@@ -271,12 +271,10 @@ export const rewriteResultsAtPath = (
271271

272272
if (path.length === 1) {
273273
if (Array.isArray(curResults)) {
274-
newResults[curPathElm] = curResults.map((_, index) => {
275-
const newValue = callback(curResults, index);
276-
return newValue;
277-
});
278-
279-
return newResults;
274+
return curResults.reduce(
275+
(reducedResults, _, index) => callback(reducedResults, curPathElm, index),
276+
results
277+
);
280278
}
281279

282280
return callback(results, curPathElm);

src/rewriters/NestFieldOutputsRewriter.ts

+9-15
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,17 @@ class NestFieldOutputsRewriter extends Rewriter {
6565
} as NodeAndVarDefs;
6666
}
6767

68-
public rewriteResponse(response: any, key: string | number) {
69-
const pathResponse = response[key];
68+
public rewriteResponse(response: any, key: string, index?: number) {
69+
// Extract the element we are working on
70+
const element = super.extractReponseElement(response, key, index);
71+
if (element === null || typeof element !== 'object') return response;
7072

71-
if (typeof pathResponse === 'object') {
72-
// undo the nesting in the response so it matches the original query
73-
if (
74-
pathResponse[this.newOutputName] &&
75-
typeof pathResponse[this.newOutputName] === 'object'
76-
) {
77-
const rewrittenResponse = { ...pathResponse, ...pathResponse[this.newOutputName] };
78-
delete rewrittenResponse[this.newOutputName];
73+
// Undo the nesting in the response so it matches the original query
74+
if (element[this.newOutputName] && typeof element[this.newOutputName] === 'object') {
75+
const newElement = { ...element, ...element[this.newOutputName] };
76+
delete newElement[this.newOutputName];
7977

80-
return {
81-
...response,
82-
[key]: rewrittenResponse
83-
};
84-
}
78+
return super.rewriteResponseElement(response, newElement, key, index);
8579
}
8680

8781
return response;

src/rewriters/Rewriter.ts

+51-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,57 @@ abstract class Rewriter {
5656
return variables;
5757
}
5858

59-
public rewriteResponse(response: any, key: string | number): any {
59+
/*
60+
* Receives the parent object of the matched field with the key of the matched field.
61+
* For arrays, the index of the element is also present.
62+
*/
63+
public rewriteResponse(response: any, key: string, index?: number): any {
64+
return response;
65+
}
66+
67+
/*
68+
* Helper that extracts the element from the response if possible otherwise returns null.
69+
*/
70+
protected extractReponseElement(response: any, key: string, index?: number): any {
71+
// Verify the response format
72+
let element = null;
73+
if (response === null || typeof response !== 'object') return element;
74+
75+
// Extract the key
76+
element = response[key] || null;
77+
78+
// Extract the position
79+
if (Array.isArray(element)) {
80+
element = element[index!] || null;
81+
}
82+
83+
return element;
84+
}
85+
86+
/*
87+
* Helper that rewrite the element from the response if possible and returns the response.
88+
*/
89+
protected rewriteResponseElement(
90+
response: any,
91+
newElement: any,
92+
key: string,
93+
index?: number
94+
): any {
95+
// Verify the response format
96+
if (response === null || typeof response !== 'object') return response;
97+
98+
// Extract the key
99+
let element = response[key];
100+
101+
// Extract the position
102+
// NOTE: We might eventually want to create an array if one is not present at the key
103+
// and we receive an index in input
104+
if (Array.isArray(element)) {
105+
element[index!] = newElement;
106+
} else {
107+
response[key] = newElement;
108+
}
109+
60110
return response;
61111
}
62112
}

src/rewriters/ScalarFieldToObjectFieldRewriter.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface ScalarFieldToObjectFieldRewriterOpts extends RewriterOpts {
77
}
88

99
/**
10-
* Rewriter which nests output fields inside of a new output object
10+
* Rewriter which nests a scalar field inside of a new output object
1111
* ex: change from `field { subField }` to `field { subField { objectfield } }`
1212
*/
1313
class ScalarFieldToObjectFieldRewriter extends Rewriter {
@@ -48,18 +48,14 @@ class ScalarFieldToObjectFieldRewriter extends Rewriter {
4848
} as NodeAndVarDefs;
4949
}
5050

51-
public rewriteResponse(response: any, key: string | number) {
52-
if (typeof response === 'object') {
53-
const pathResponse = response[key];
51+
public rewriteResponse(response: any, key: string, index?: number) {
52+
// Extract the element we are working on
53+
const element = super.extractReponseElement(response, key, index);
54+
if (element === null) return response;
5455

55-
// undo the nesting in the response so it matches the original query
56-
return {
57-
...response,
58-
[key]: pathResponse[this.objectFieldName]
59-
};
60-
}
61-
62-
return response;
56+
// Undo the nesting in the response so it matches the original query
57+
const newElement = element[this.objectFieldName];
58+
return super.rewriteResponseElement(response, newElement, key, index);
6359
}
6460
}
6561

src/rewriters/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { default as Rewriter } from './Rewriter';
1+
export { default as Rewriter, RewriterOpts } from './Rewriter';
22
export { default as FieldArgNameRewriter } from './FieldArgNameRewriter';
33
export { default as FieldArgsToInputTypeRewriter } from './FieldArgsToInputTypeRewriter';
44
export { default as FieldArgTypeRewriter } from './FieldArgTypeRewriter';

test/ast.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ describe('ast utils', () => {
6868
]
6969
});
7070
expect(
71-
rewriteResultsAtPath(obj, ['things', 'moreThings'], (elm, path) => ({
72-
...elm[path],
73-
meh: '7'
74-
}))
71+
rewriteResultsAtPath(obj, ['things', 'moreThings'], (elm, path, index) => {
72+
elm[path][index!] = { ...elm[path][index!], meh: '7' };
73+
return elm;
74+
})
7575
).toEqual({
7676
things: [
7777
{

test/functional/rewriteNestFieldOutputs.test.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import RewriteHandler from '../../src/RewriteHandler';
22
import NestFieldOutputsRewriter from '../../src/rewriters/NestFieldOutputsRewriter';
33
import { gqlFmt } from '../testUtils';
44

5-
describe('Rewrite field args to input type', () => {
5+
describe('Rewrite output fields inside of a new output object', () => {
66
it('allows nesting the args provided into an input type', () => {
77
const handler = new RewriteHandler([
88
new NestFieldOutputsRewriter({
@@ -102,4 +102,58 @@ describe('Rewrite field args to input type', () => {
102102
}
103103
});
104104
});
105+
106+
it('allows nesting the args provided in an array', () => {
107+
const handler = new RewriteHandler([
108+
new NestFieldOutputsRewriter({
109+
fieldName: 'createCats',
110+
newOutputName: 'cat',
111+
outputsToNest: ['name', 'color', 'id']
112+
})
113+
]);
114+
const query = gqlFmt`
115+
mutation createManyCats {
116+
createCats {
117+
id
118+
name
119+
color
120+
}
121+
}
122+
`;
123+
const expectedRewritenQuery = gqlFmt`
124+
mutation createManyCats {
125+
createCats {
126+
cat {
127+
id
128+
name
129+
color
130+
}
131+
}
132+
}
133+
`;
134+
expect(handler.rewriteRequest(query)).toEqual({
135+
query: expectedRewritenQuery
136+
});
137+
expect(
138+
handler.rewriteResponse({
139+
createCats: [
140+
{
141+
cat: {
142+
id: 1,
143+
name: 'jack',
144+
color: 'blue'
145+
}
146+
}
147+
]
148+
})
149+
).toEqual({
150+
createCats: [
151+
{
152+
id: 1,
153+
name: 'jack',
154+
color: 'blue'
155+
}
156+
]
157+
});
158+
});
105159
});

test/functional/rewriteScalarFieldToObjectField.test.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import ScalarFieldToObjectFieldRewriter from '../../src/rewriters/ScalarFieldToO
33
import { gqlFmt } from '../testUtils';
44

55
describe('Rewrite scalar field to be a nested object with a single scalar field', () => {
6-
it('rewrites a scalar field to be an objet field with 1 scalar subfield', () => {
6+
it('rewrites a scalar field to be an object field with 1 scalar subfield', () => {
77
const handler = new RewriteHandler([
88
new ScalarFieldToObjectFieldRewriter({
99
fieldName: 'title',
@@ -226,4 +226,48 @@ describe('Rewrite scalar field to be a nested object with a single scalar field'
226226
}
227227
});
228228
});
229+
230+
it('rewrites a scalar field array to be an array of object fields with 1 scalar subfield', () => {
231+
const handler = new RewriteHandler([
232+
new ScalarFieldToObjectFieldRewriter({
233+
fieldName: 'titles',
234+
objectFieldName: 'text'
235+
})
236+
]);
237+
238+
const query = gqlFmt`
239+
query getThing {
240+
thing {
241+
titles
242+
}
243+
}
244+
`;
245+
const expectedRewritenQuery = gqlFmt`
246+
query getThing {
247+
thing {
248+
titles {
249+
text
250+
}
251+
}
252+
}
253+
`;
254+
expect(handler.rewriteRequest(query)).toEqual({
255+
query: expectedRewritenQuery
256+
});
257+
expect(
258+
handler.rewriteResponse({
259+
thing: {
260+
titles: [
261+
{
262+
text: 'THING'
263+
}
264+
]
265+
}
266+
})
267+
).toEqual({
268+
thing: {
269+
titles: ['THING']
270+
}
271+
});
272+
});
229273
});

test/functional/rewriter.test.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Rewriter, { RewriterOpts } from '../../src/rewriters/Rewriter';
2+
3+
describe('rewriter', () => {
4+
class TestRewriter extends Rewriter {
5+
constructor(options: RewriterOpts) {
6+
super(options);
7+
}
8+
9+
public extractReponseElement(response: any, key: string, index?: number): any {
10+
return super.extractReponseElement(response, key, index);
11+
}
12+
13+
public rewriteResponseElement(
14+
response: any,
15+
newElement: any,
16+
key: string,
17+
index?: number
18+
): any {
19+
return super.rewriteResponseElement(response, newElement, key, index);
20+
}
21+
}
22+
23+
describe('extractResponseElement', () => {
24+
const rewriter = new TestRewriter({ fieldName: 'test' });
25+
26+
it('can extract element in object', () => {
27+
const key = 'key';
28+
const element = { a: 1 };
29+
const response = { [key]: element };
30+
31+
expect(rewriter.extractReponseElement(response, key)).toEqual(element);
32+
});
33+
34+
it('can extract element in array', () => {
35+
const key = 'key';
36+
const element = { a: 1 };
37+
const response = { [key]: [element] };
38+
39+
expect(rewriter.extractReponseElement(response, key, 0)).toEqual(element);
40+
});
41+
42+
it('does not fail on null, empty or malformed response', () => {
43+
const key = 'key';
44+
45+
expect(rewriter.extractReponseElement(null, key)).toEqual(null);
46+
expect(rewriter.extractReponseElement('string', key)).toEqual(null);
47+
expect(rewriter.extractReponseElement({ a: 1 }, key)).toEqual(null);
48+
});
49+
});
50+
51+
describe('rewriteResponseElement', () => {
52+
const rewriter = new TestRewriter({ fieldName: 'test' });
53+
54+
it('can replace element in object', () => {
55+
const key = 'key';
56+
const newElement = { a: 1 };
57+
const response = { [key]: 1 };
58+
59+
expect(rewriter.rewriteResponseElement(response, newElement, key)).toEqual({
60+
[key]: newElement
61+
});
62+
});
63+
64+
it('can replace element in array', () => {
65+
const key = 'key';
66+
const newElement = { a: 1 };
67+
const response = { [key]: [1] };
68+
69+
expect(rewriter.rewriteResponseElement(response, newElement, key, 0)).toEqual({
70+
[key]: [newElement]
71+
});
72+
});
73+
74+
it('does not fail on null, empty or malformed response', () => {
75+
const key = 'key';
76+
const newElement = { a: 1 };
77+
78+
expect(rewriter.rewriteResponseElement(null, newElement, key)).toEqual(null);
79+
expect(rewriter.rewriteResponseElement('string', newElement, key)).toEqual('string');
80+
expect(rewriter.rewriteResponseElement({ a: 1 }, newElement, key)).toEqual({
81+
a: 1,
82+
[key]: newElement
83+
});
84+
});
85+
});
86+
});

0 commit comments

Comments
 (0)