Skip to content

Commit 9bb1893

Browse files
committed
feat: create ObjectParser
1 parent 9ab8570 commit 9bb1893

File tree

11 files changed

+868
-16
lines changed

11 files changed

+868
-16
lines changed

.eslintrc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"arrow-body-style": 0,
1010
"no-unused-expressions": 0,
1111
"no-plusplus": 0,
12-
"import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*-test.js", "**/__mocks__/**"]}],
12+
"import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*-test.js", "**/__mocks__/**", "**/__fixtures__/**"]}],
1313
"no-prototype-builtins": 0,
1414
"no-restricted-syntax": 0,
1515
"no-mixed-operators": 0,
@@ -18,16 +18,17 @@
1818
"objects": "always-multiline",
1919
"imports": "always-multiline",
2020
"exports": "always-multiline",
21-
"functions": "ignore",
21+
"functions": "ignore"
2222
}],
2323
"prettier/prettier": ["error", {
2424
"printWidth": 100,
2525
"singleQuote": true,
26-
"trailingComma": "es5",
26+
"trailingComma": "es5"
2727
}],
2828
"import/prefer-default-export": 0,
2929
"arrow-parens": 0,
30-
"prefer-destructuring": 0
30+
"prefer-destructuring": 0,
31+
"no-use-before-define": 0
3132
},
3233
"env": {
3334
"jasmine": true,
@@ -42,5 +43,6 @@
4243
"Iterator": true,
4344
"$Shape": true,
4445
"$Keys": true,
46+
"$FlowFixMe": true
4547
}
4648
}

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@
4242
"eslint-plugin-flowtype": "^2.39.1",
4343
"eslint-plugin-import": "^2.8.0",
4444
"eslint-plugin-prettier": "^2.3.1",
45+
"express": "^4.16.2",
46+
"express-graphql": "^0.6.11",
4547
"flow-bin": "^0.57.3",
4648
"graphql": "^0.11.7",
4749
"graphql-compose": "^2.9.2",
4850
"jest": "^21.2.1",
51+
"node-fetch": "^1.7.3",
4952
"prettier": "^1.7.4",
5053
"rimraf": "^2.6.2",
5154
"semantic-release": "^8.2.1"
@@ -69,6 +72,7 @@
6972
"lint": "eslint --ext .js ./src",
7073
"flow": "./node_modules/.bin/flow",
7174
"test": "npm run coverage && npm run lint && npm run flow",
72-
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
75+
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
76+
"fixture-demo": "./node_modules/.bin/babel-node ./src/__fixtures__/app.js"
7377
}
7478
}

src/ObjectParser.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* @flow */
2+
3+
import { graphql, TypeComposer, upperFirst } from 'graphql-compose';
4+
5+
const { isOutputType } = graphql;
6+
7+
type GetValueOpts = {
8+
typeName: string,
9+
fieldName: string,
10+
};
11+
12+
export default class ObjectParser {
13+
static createTC(name: string, json: Object): TypeComposer {
14+
if (!json || typeof json !== 'object') {
15+
throw new Error('You provide empty object in second arg for `createTC` method.');
16+
}
17+
const tc = TypeComposer.create(name);
18+
19+
const fields = {};
20+
Object.keys(json).forEach(k => {
21+
fields[k] = this.getValueType(json[k], { typeName: name, fieldName: k });
22+
});
23+
24+
tc.setFields(fields);
25+
26+
return tc;
27+
}
28+
29+
static getValueType(
30+
value: any,
31+
opts: ?GetValueOpts
32+
): string | [string] | graphql.GraphQLOutputType | TypeComposer {
33+
const typeOf = typeof value;
34+
35+
if (typeOf === 'number') return 'Float';
36+
if (typeOf === 'string') return 'String';
37+
if (typeOf === 'boolean') return 'Boolean';
38+
39+
if (typeOf === 'object') {
40+
if (value === null) return 'JSON';
41+
42+
if (Array.isArray(value)) {
43+
const val = value[0];
44+
if (Array.isArray(val)) return ['JSON'];
45+
return [(this.getValueType(val): any)];
46+
}
47+
48+
if (opts && opts.typeName && opts.fieldName) {
49+
return this.createTC(`${opts.typeName}_${upperFirst(opts.fieldName)}`, value);
50+
}
51+
}
52+
53+
if (typeOf === 'function') {
54+
return this.getValueTypeFromFunction(value);
55+
}
56+
57+
return 'JSON';
58+
}
59+
60+
static getValueTypeFromFunction(
61+
value: () => any
62+
): string | graphql.GraphQLOutputType | TypeComposer {
63+
const type = value();
64+
65+
if (typeof type === 'string') return type;
66+
if (isOutputType(type)) return type;
67+
if (type instanceof TypeComposer) return type;
68+
69+
throw new Error(
70+
'Your type function should return: `string`, `GraphQLOutputType`, `TypeComposer`.'
71+
);
72+
}
73+
}

src/__fixtures__/Film.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* @flow */
2+
3+
import fetch from 'node-fetch';
4+
import composeWithRest from '../index';
5+
import { PeopleTC } from './People';
6+
7+
const responseFromRestApi = {
8+
title: 'The Empire Strikes Back',
9+
episode_id: 5,
10+
opening_crawl: 'It is a dark time for ...',
11+
director: 'Irvin Kershner',
12+
producer: 'Gary Kurtz, Rick McCallum',
13+
release_date: '1980-05-17',
14+
characters: [
15+
'https://swapi.co/api/people/1/',
16+
'https://swapi.co/api/people/2/',
17+
'https://swapi.co/api/people/3/',
18+
],
19+
};
20+
21+
export const FilmTC = composeWithRest('Film', responseFromRestApi);
22+
23+
// //////////////
24+
// RESOLVERS aka FieldConfig in GraphQL
25+
// //////////////
26+
27+
FilmTC.addResolver({
28+
name: 'findById',
29+
type: FilmTC,
30+
args: {
31+
id: 'Int!',
32+
},
33+
resolve: rp => {
34+
return fetch(`https://swapi.co/api/films/${rp.args.id}/`).then(r => r.json());
35+
},
36+
});
37+
38+
FilmTC.addResolver({
39+
name: 'findByUrl',
40+
type: FilmTC,
41+
args: {
42+
url: 'String!',
43+
},
44+
resolve: rp => fetch(rp.args.url).then(r => r.json()),
45+
});
46+
47+
FilmTC.addResolver({
48+
name: 'findByUrlList',
49+
type: [FilmTC],
50+
args: {
51+
urls: '[String]!',
52+
},
53+
resolve: rp => {
54+
return Promise.all(rp.args.urls.map(url => fetch(url).then(r => r.json())));
55+
},
56+
});
57+
58+
// //////////////
59+
// RELATIONS
60+
// //////////////
61+
62+
FilmTC.addRelation('characters', {
63+
resolver: () => PeopleTC.getResolver('findByUrlList'),
64+
prepareArgs: {
65+
urls: source => source.characters,
66+
},
67+
});
68+
69+
FilmTC.addFields({
70+
currentTime: { type: 'String', resolve: () => Date.now() },
71+
});

src/__fixtures__/People.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* @flow */
2+
3+
import fetch from 'node-fetch';
4+
import { FilmTC } from './Film';
5+
import composeWithRest from '../index';
6+
7+
const responseFromRestApi = {
8+
name: 'Luke Skywalker',
9+
height: '172',
10+
mass: '77',
11+
hair_color: 'blond',
12+
skin_color: 'fair',
13+
eye_color: 'blue',
14+
birth_year: '19BBY',
15+
gender: 'male',
16+
films: [
17+
'https://swapi.co/api/films/2/',
18+
'https://swapi.co/api/films/6/',
19+
'https://swapi.co/api/films/3/',
20+
],
21+
};
22+
23+
export const PeopleTC = composeWithRest('People', responseFromRestApi);
24+
25+
// //////////////
26+
// RESOLVERS aka FieldConfig in GraphQL
27+
// //////////////
28+
29+
PeopleTC.addResolver({
30+
name: 'findById',
31+
type: PeopleTC,
32+
args: {
33+
id: 'Int!',
34+
},
35+
resolve: rp => {
36+
return fetch(`https://swapi.co/api/people/${rp.args.id}/`).then(r => r.json());
37+
},
38+
});
39+
40+
PeopleTC.addResolver({
41+
name: 'findByUrl',
42+
type: PeopleTC,
43+
args: {
44+
url: 'String!',
45+
},
46+
resolve: rp => fetch(rp.args.url).then(r => r.json()),
47+
});
48+
49+
PeopleTC.addResolver({
50+
name: 'findByUrlList',
51+
type: [PeopleTC],
52+
args: {
53+
urls: '[String]!',
54+
},
55+
resolve: rp => {
56+
return Promise.all(rp.args.urls.map(url => fetch(url).then(r => r.json())));
57+
},
58+
});
59+
60+
// //////////////
61+
// RELATIONS
62+
// //////////////
63+
64+
PeopleTC.addRelation('filmObjs', {
65+
resolver: () => FilmTC.getResolver('findByUrlList'),
66+
prepareArgs: {
67+
urls: source => source.films,
68+
},
69+
});

src/__fixtures__/Schema.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* @flow */
2+
3+
import { GQC } from 'graphql-compose';
4+
import { FilmTC } from './Film';
5+
import { PeopleTC } from './People';
6+
7+
GQC.rootQuery().addFields({
8+
film: FilmTC.getResolver('findById'),
9+
people: PeopleTC.getResolver('findById'),
10+
peopleByUrl: PeopleTC.getResolver('findByUrl'),
11+
peopleByUrls: PeopleTC.getResolver('findByUrlList'),
12+
});
13+
14+
const schema = GQC.buildSchema();
15+
16+
export default schema;

src/__fixtures__/app.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* @flow */
2+
3+
import express from 'express';
4+
import graphqlHTTP from 'express-graphql';
5+
import schema from './Schema';
6+
7+
const PORT = 4000;
8+
const app = express();
9+
10+
app.use(
11+
'/graphql',
12+
graphqlHTTP(req => ({
13+
schema,
14+
graphiql: true,
15+
context: req,
16+
}))
17+
);
18+
19+
app.listen(PORT, () => {
20+
console.log(`App running on port ${PORT}`);
21+
console.log(`Open http://localhost:${PORT}/graphql`);
22+
});

0 commit comments

Comments
 (0)