Skip to content

Commit 4003923

Browse files
committed
feat: add composeInputWithJson method
1 parent b73114b commit 4003923

File tree

6 files changed

+250
-448
lines changed

6 files changed

+250
-448
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ You have a sample response object `restApiResponse` which you can pass to `graph
2727
```js
2828
// person.js
2929

30-
import composeWithJson from 'graphql-compose-json';
30+
import { composeWithJson, composeInputWithJson } from 'graphql-compose-json';
3131

3232
const restApiResponse = {
3333
name: 'Anakin Skywalker',
@@ -49,15 +49,18 @@ const restApiResponse = {
4949
};
5050

5151
export const PersonTC = composeWithJson('Person', restApiResponse);
52-
export const PersonGraphQLType = PersonTC.getType();
52+
export const PersonGraphQLType = PersonTC.getType(); // GraphQLObjectType
53+
54+
export const PersonITC = composeInputWithJson('PersonInput', restApiResponse);
55+
export const PersonGraphQLInput = PersonITC.getType(); // GraphQLInputObjectType
5356
```
5457

5558
## Customization
5659

5760
You can write custom field configs directly to a field of your API response object via function (see `mass` and `starships_count` field):
5861

5962
```js
60-
import composeWithJson from 'graphql-compose-json';
63+
import { composeWithJson } from 'graphql-compose-json';
6164

6265
const restApiResponse = {
6366
name: 'Anakin Skywalker',

src/InputObjectParser.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
InputTypeComposer,
3+
upperFirst,
4+
InputTypeComposerFieldConfigDefinition,
5+
schemaComposer,
6+
SchemaComposer,
7+
} from 'graphql-compose';
8+
9+
type GetValueOpts = {
10+
typeName?: string;
11+
fieldName?: string;
12+
};
13+
14+
export default class InputObjectParser {
15+
static createITC(
16+
name: string,
17+
json: Record<string, any>,
18+
opts?: { schemaComposer: SchemaComposer<any> }
19+
): InputTypeComposer<any> {
20+
if (!json || typeof json !== 'object') {
21+
throw new Error('You provide empty object in second arg for `createITC` method.');
22+
}
23+
24+
const sc = opts?.schemaComposer || schemaComposer;
25+
26+
const tc = sc.createInputTC(name);
27+
Object.keys(json).forEach((fieldName) => {
28+
const fieldConfig = this.getFieldConfig(json[fieldName], { typeName: name, fieldName });
29+
tc.setField(fieldName, fieldConfig);
30+
});
31+
32+
return tc;
33+
}
34+
35+
static getFieldConfig(
36+
value: any,
37+
opts?: GetValueOpts | null
38+
): InputTypeComposerFieldConfigDefinition {
39+
const typeOf = typeof value;
40+
41+
if (typeOf === 'number') return 'Float';
42+
if (typeOf === 'string') return 'String';
43+
if (typeOf === 'boolean') return 'Boolean';
44+
45+
if (typeOf === 'object') {
46+
if (value === null) return 'JSON';
47+
48+
if (Array.isArray(value)) {
49+
if (Array.isArray(value[0])) return ['JSON'];
50+
51+
const val =
52+
typeof value[0] === 'object' && value[0] !== null
53+
? Object.assign({}, ...value)
54+
: value[0];
55+
56+
const args =
57+
opts && opts.typeName && opts.fieldName
58+
? {
59+
typeName: opts.typeName,
60+
fieldName: opts.fieldName,
61+
}
62+
: {};
63+
return [this.getFieldConfig(val, args)] as any;
64+
}
65+
66+
if (opts && opts.typeName && opts.fieldName) {
67+
return this.createITC(`${opts.typeName}_${upperFirst(opts.fieldName)}`, value);
68+
}
69+
}
70+
71+
if (typeOf === 'function') {
72+
return value();
73+
}
74+
return 'JSON';
75+
}
76+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { InputTypeComposer } from 'graphql-compose';
2+
import IOP from '../InputObjectParser';
3+
4+
describe('InputObjectParser', () => {
5+
describe('getFieldConfig()', () => {
6+
it('number', () => {
7+
expect(IOP.getFieldConfig(6)).toBe('Float');
8+
expect(IOP.getFieldConfig(77.7)).toBe('Float');
9+
});
10+
11+
it('string', () => {
12+
expect(IOP.getFieldConfig('test')).toBe('String');
13+
});
14+
15+
it('boolean', () => {
16+
expect(IOP.getFieldConfig(true)).toBe('Boolean');
17+
expect(IOP.getFieldConfig(false)).toBe('Boolean');
18+
});
19+
20+
it('null', () => {
21+
expect(IOP.getFieldConfig(null)).toBe('JSON');
22+
});
23+
24+
describe('array', () => {
25+
it('of number', () => {
26+
expect(IOP.getFieldConfig([1, 2, 3])).toEqual(['Float']);
27+
});
28+
29+
it('of string', () => {
30+
expect(IOP.getFieldConfig(['a', 'b', 'c'])).toEqual(['String']);
31+
});
32+
33+
it('of boolean', () => {
34+
expect(IOP.getFieldConfig([false, true])).toEqual(['Boolean']);
35+
});
36+
37+
it('of object', () => {
38+
const spy = jest.spyOn(IOP, 'createITC');
39+
const valueAsArrayOfObjects = [{ a: 123 }, { a: 456 }];
40+
IOP.getFieldConfig(valueAsArrayOfObjects, {
41+
typeName: 'ParentTypeName',
42+
fieldName: 'subDocument',
43+
});
44+
expect(spy).toHaveBeenCalledWith('ParentTypeName_SubDocument', { a: 456 });
45+
});
46+
47+
it('of any', () => {
48+
expect(IOP.getFieldConfig([null])).toEqual(['JSON']);
49+
});
50+
});
51+
52+
it('function', () => {
53+
const valueAsFn = () => 'abracadabra';
54+
const res = IOP.getFieldConfig(valueAsFn);
55+
expect(res).toBe('abracadabra');
56+
});
57+
58+
it('object', () => {
59+
const spy = jest.spyOn(IOP, 'createITC');
60+
const valueAsObj = { a: 123 };
61+
IOP.getFieldConfig(valueAsObj, {
62+
typeName: 'ParentTypeName',
63+
fieldName: 'subDocument',
64+
});
65+
expect(spy).toHaveBeenCalledWith('ParentTypeName_SubDocument', valueAsObj);
66+
});
67+
});
68+
69+
describe('createITC()', () => {
70+
it('return InputTypeComposer', () => {
71+
const tc = IOP.createITC('MyType', { a: 1 });
72+
expect(tc).toBeInstanceOf(InputTypeComposer);
73+
expect(tc.getTypeName()).toBe('MyType');
74+
});
75+
76+
it('creates fields', () => {
77+
const tc = IOP.createITC('MyType', { a: 1, b: true });
78+
expect(tc.getFieldNames()).toEqual(['a', 'b']);
79+
expect(tc.getFieldTypeName('a')).toBe('Float');
80+
expect(tc.getFieldTypeName('b')).toBe('Boolean');
81+
});
82+
83+
it('match snapshot', () => {
84+
const PeopleITC = IOP.createITC('PeopleInput', {
85+
name: 'Luke Skywalker',
86+
height: () => 'Int',
87+
mass: () => 'Int',
88+
hair_color: 'blond',
89+
skin_color: 'fair',
90+
eye_color: 'blue',
91+
birth_year: '19BBY',
92+
gender: 'male',
93+
homeworld: {
94+
name: 'Tatooine',
95+
rotation_period: '23',
96+
orbital_period: '304',
97+
terrain: 'desert',
98+
surface_water: '1',
99+
population: () => 'Int',
100+
},
101+
films: [
102+
'https://swapi.co/api/films/2/',
103+
'https://swapi.co/api/films/6/',
104+
'https://swapi.co/api/films/3/',
105+
],
106+
created: () => 'Date',
107+
edited: '2014-12-20T21:17:56.891000Z',
108+
});
109+
110+
expect(PeopleITC.toSDL({ deep: true, omitScalars: true })).toMatchInlineSnapshot(`
111+
"input PeopleInput {
112+
name: String
113+
height: Int
114+
mass: Int
115+
hair_color: String
116+
skin_color: String
117+
eye_color: String
118+
birth_year: String
119+
gender: String
120+
homeworld: PeopleInput_Homeworld
121+
films: [String]
122+
created: Date
123+
edited: String
124+
}
125+
126+
input PeopleInput_Homeworld {
127+
name: String
128+
rotation_period: String
129+
orbital_period: String
130+
terrain: String
131+
surface_water: String
132+
population: Int
133+
}"
134+
`);
135+
});
136+
});
137+
});

src/__tests__/ObjectParser-test.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { graphql, ObjectTypeComposer } from 'graphql-compose';
1+
import { ObjectTypeComposer } from 'graphql-compose';
22
import OP from '../ObjectParser';
33

4-
const { GraphQLFloat, GraphQLBoolean } = graphql;
5-
64
describe('ObjectParser', () => {
75
describe('getFieldConfig()', () => {
86
it('number', () => {
@@ -78,8 +76,8 @@ describe('ObjectParser', () => {
7876
it('creates fields', () => {
7977
const tc = OP.createTC('MyType', { a: 1, b: true });
8078
expect(tc.getFieldNames()).toEqual(['a', 'b']);
81-
expect(tc.getFieldType('a')).toBe(GraphQLFloat);
82-
expect(tc.getFieldType('b')).toBe(GraphQLBoolean);
79+
expect(tc.getFieldTypeName('a')).toBe('Float');
80+
expect(tc.getFieldTypeName('b')).toBe('Boolean');
8381
});
8482

8583
it('match snapshot', () => {
@@ -109,13 +107,31 @@ describe('ObjectParser', () => {
109107
edited: '2014-12-20T21:17:56.891000Z',
110108
});
111109

112-
const PeopleGraphQLType = PeopleTC.getType();
113-
expect(PeopleGraphQLType).toMatchSnapshot();
114-
expect(PeopleGraphQLType.getFields()).toMatchSnapshot();
115-
116-
const homeworldSubType = PeopleTC.getFieldType('homeworld') as any;
117-
expect(homeworldSubType).toMatchSnapshot();
118-
expect(homeworldSubType.getFields()).toMatchSnapshot();
110+
expect(PeopleTC.toSDL({ deep: true, omitScalars: true })).toMatchInlineSnapshot(`
111+
"type PeopleType {
112+
name: String
113+
height: Int
114+
mass: Int
115+
hair_color: String
116+
skin_color: String
117+
eye_color: String
118+
birth_year: String
119+
gender: String
120+
homeworld: PeopleType_Homeworld
121+
films: [String]
122+
created: Date
123+
edited: String
124+
}
125+
126+
type PeopleType_Homeworld {
127+
name: String
128+
rotation_period: String
129+
orbital_period: String
130+
terrain: String
131+
surface_water: String
132+
population: Int
133+
}"
134+
`);
119135
});
120136
});
121137
});

0 commit comments

Comments
 (0)