Skip to content

Commit

Permalink
Ensure that arrays are properly supported (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sytten authored Nov 12, 2020
1 parent 809517d commit adae5d5
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 44 deletions.
6 changes: 4 additions & 2 deletions src/RewriteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
});
});
Expand Down
12 changes: 5 additions & 7 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ interface ResultObj {
export const rewriteResultsAtPath = (
results: ResultObj,
path: ReadonlyArray<string>,
callback: (parentResult: any, key: string | number) => any
callback: (parentResult: any, key: string, position?: number) => any
): ResultObj => {
if (path.length === 0) return results;

Expand All @@ -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);
Expand Down
24 changes: 9 additions & 15 deletions src/rewriters/NestFieldOutputsRewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
52 changes: 51 additions & 1 deletion src/rewriters/Rewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
20 changes: 8 additions & 12 deletions src/rewriters/ScalarFieldToObjectFieldRewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/rewriters/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
8 changes: 4 additions & 4 deletions test/ast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down
56 changes: 55 additions & 1 deletion test/functional/rewriteNestFieldOutputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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'
}
]
});
});
});
46 changes: 45 additions & 1 deletion test/functional/rewriteScalarFieldToObjectField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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']
}
});
});
});
86 changes: 86 additions & 0 deletions test/functional/rewriter.test.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
});
});

0 comments on commit adae5d5

Please sign in to comment.