Skip to content

Commit eb14db1

Browse files
vgeyvandovchanind
authored andcommitted
In this commit: (#9)
- Adding JsonToTypedObjectRewriter class which extends Rewriter class and borrows some logic from ScalarFieldToObjectFieldRewriter - Adding JsonToTypedObjectRewriter class as an export - Adding JsonToTypedObjectRewriter tests The purpose of JsonToTypedObjectRewriter is to deal with a situation in which the initial GraphQL schema used a GraphQLJSON type (graphql-type-json) for a field, but there is subsequent desire to type the GraphQLJSON blob in order to strengthen the contract. The issue is that once the client expects a field to be GraphQLJSON, you can't type it out at the GraphQL layer without breaking said client. JsonToTypedObjectRewriter aims to resolve that issue by rewriting the query for the service but still returning the same response as initially expected by the client.
1 parent df6f4a6 commit eb14db1

File tree

3 files changed

+280
-0
lines changed

3 files changed

+280
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ASTNode, FieldNode, SelectionSetNode } from 'graphql';
2+
import { NodeAndVarDefs } from '../ast';
3+
import Rewriter, { RewriterOpts } from './Rewriter';
4+
5+
interface ObjectField {
6+
name: string;
7+
subFields?: ObjectField[];
8+
}
9+
10+
interface JsonToTypedObjectRewriterOpts extends RewriterOpts {
11+
objectFields: ObjectField[];
12+
}
13+
14+
export default class JsonToTypedObjectRewriter extends Rewriter {
15+
protected objectFields: ObjectField[];
16+
17+
constructor({ fieldName, objectFields }: JsonToTypedObjectRewriterOpts) {
18+
super({ fieldName });
19+
this.objectFields = objectFields;
20+
}
21+
22+
public matches(nodeAndVars: NodeAndVarDefs, parents: ASTNode[]): boolean {
23+
if (!super.matches(nodeAndVars, parents)) return false;
24+
const node = nodeAndVars.node as FieldNode;
25+
// make sure there's no subselections on this field
26+
if (node.selectionSet) return false;
27+
return true;
28+
}
29+
30+
public rewriteQuery(nodeAndVarDefs: NodeAndVarDefs): NodeAndVarDefs {
31+
const node = nodeAndVarDefs.node as FieldNode;
32+
const { variableDefinitions } = nodeAndVarDefs;
33+
// if there's a subselection already, just return
34+
if (node.selectionSet) return nodeAndVarDefs;
35+
36+
const selectionSet = this.generateSelectionSet(this.objectFields);
37+
38+
return {
39+
variableDefinitions,
40+
node: { ...node, selectionSet }
41+
} as NodeAndVarDefs;
42+
}
43+
44+
private generateSelectionSet(fields: ObjectField[]): SelectionSetNode {
45+
return {
46+
kind: 'SelectionSet',
47+
selections: fields.map(({ name, subFields }) => ({
48+
kind: 'Field',
49+
name: { kind: 'Name', value: name },
50+
...(subFields && {
51+
selectionSet: this.generateSelectionSet(subFields)
52+
})
53+
}))
54+
} as SelectionSetNode;
55+
}
56+
}

src/rewriters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { default as FieldArgsToInputTypeRewriter } from './FieldArgsToInputTypeR
44
export { default as FieldArgTypeRewriter } from './FieldArgTypeRewriter';
55
export { default as NestFieldOutputsRewriter } from './NestFieldOutputsRewriter';
66
export { default as ScalarFieldToObjectFieldRewriter } from './ScalarFieldToObjectFieldRewriter';
7+
export { default as JsonToTypedObjectRewriter } from './JsonToTypedObjectRewriter';
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import RewriteHandler from '../../src/RewriteHandler';
2+
import JsonToTypedObjectRewriter from '../../src/rewriters/JsonToTypedObjectRewriter';
3+
import { gqlFmt } from '../testUtils';
4+
5+
describe('Rewrite query for GraphQLJSON field to be a query for a nested object with multiple scalar and/or object types', () => {
6+
it('rewrites a GraphQLJSON field query to be an object field query with multiple scalar subfields', () => {
7+
const handler = new RewriteHandler([
8+
new JsonToTypedObjectRewriter({
9+
fieldName: 'thingJSON',
10+
objectFields: [{ name: 'title' }, { name: 'description' }]
11+
})
12+
]);
13+
14+
const query = gqlFmt`
15+
query getTheThing {
16+
theThing {
17+
thingField {
18+
id
19+
thingJSON
20+
color
21+
}
22+
}
23+
}
24+
`;
25+
const expectedRewrittenQuery = gqlFmt`
26+
query getTheThing {
27+
theThing {
28+
thingField {
29+
id
30+
thingJSON {
31+
title
32+
description
33+
}
34+
color
35+
}
36+
}
37+
}
38+
`;
39+
40+
expect(handler.rewriteRequest(query)).toEqual({
41+
query: expectedRewrittenQuery
42+
});
43+
});
44+
45+
it('rewrites a GraphQLJSON field query to be an object field query with multiple nested object types and subfields', () => {
46+
const handler = new RewriteHandler([
47+
new JsonToTypedObjectRewriter({
48+
fieldName: 'thingJSON',
49+
objectFields: [
50+
{
51+
name: 'user',
52+
subFields: [
53+
{ name: 'userId' },
54+
{ name: 'userHandle' },
55+
{
56+
name: 'item',
57+
subFields: [{ name: 'itemMeta' }]
58+
}
59+
]
60+
}
61+
]
62+
})
63+
]);
64+
65+
const query = gqlFmt`
66+
query getTheThing {
67+
theThing {
68+
thingField {
69+
id
70+
thingJSON
71+
color
72+
}
73+
}
74+
}
75+
`;
76+
const expectedRewrittenQuery = gqlFmt`
77+
query getTheThing {
78+
theThing {
79+
thingField {
80+
id
81+
thingJSON {
82+
user {
83+
userId
84+
userHandle
85+
item {
86+
itemMeta
87+
}
88+
}
89+
}
90+
color
91+
}
92+
}
93+
}
94+
`;
95+
96+
expect(handler.rewriteRequest(query)).toEqual({
97+
query: expectedRewrittenQuery
98+
});
99+
});
100+
101+
it('works with fragments', () => {
102+
const handler = new RewriteHandler([
103+
new JsonToTypedObjectRewriter({
104+
fieldName: 'thingJSON',
105+
objectFields: [{ name: 'title' }, { name: 'description' }]
106+
})
107+
]);
108+
109+
const query = gqlFmt`
110+
query getTheThing {
111+
theThing {
112+
...thingFragment
113+
}
114+
}
115+
116+
fragment thingFragment on Thing {
117+
id
118+
thingJSON
119+
}
120+
`;
121+
const expectedRewrittenQuery = gqlFmt`
122+
query getTheThing {
123+
theThing {
124+
...thingFragment
125+
}
126+
}
127+
128+
fragment thingFragment on Thing {
129+
id
130+
thingJSON {
131+
title
132+
description
133+
}
134+
}
135+
`;
136+
137+
expect(handler.rewriteRequest(query)).toEqual({
138+
query: expectedRewrittenQuery
139+
});
140+
});
141+
142+
it('works within repeated and nested fragments', () => {
143+
const handler = new RewriteHandler([
144+
new JsonToTypedObjectRewriter({
145+
fieldName: 'thingJSON',
146+
objectFields: [
147+
{
148+
name: 'user',
149+
subFields: [
150+
{ name: 'userId' },
151+
{ name: 'userHandle' },
152+
{
153+
name: 'item',
154+
subFields: [{ name: 'itemMeta' }]
155+
}
156+
]
157+
}
158+
]
159+
})
160+
]);
161+
162+
const query = gqlFmt`
163+
query getTheThing {
164+
theThing {
165+
...thingFragment
166+
}
167+
otherThing {
168+
...otherThingFragment
169+
}
170+
}
171+
172+
fragment thingFragment on Thing {
173+
id
174+
thingJSON
175+
}
176+
177+
fragment otherThingFragment on Thing {
178+
id
179+
edges {
180+
node {
181+
...thingFragment
182+
}
183+
}
184+
}
185+
`;
186+
const expectedRewritenQuery = gqlFmt`
187+
query getTheThing {
188+
theThing {
189+
...thingFragment
190+
}
191+
otherThing {
192+
...otherThingFragment
193+
}
194+
}
195+
196+
fragment thingFragment on Thing {
197+
id
198+
thingJSON {
199+
user {
200+
userId
201+
userHandle
202+
item {
203+
itemMeta
204+
}
205+
}
206+
}
207+
}
208+
209+
fragment otherThingFragment on Thing {
210+
id
211+
edges {
212+
node {
213+
...thingFragment
214+
}
215+
}
216+
}
217+
`;
218+
219+
expect(handler.rewriteRequest(query)).toEqual({
220+
query: expectedRewritenQuery
221+
});
222+
});
223+
});

0 commit comments

Comments
 (0)