Skip to content

Commit 0237d43

Browse files
authored
Merge pull request #12 from marmelab/relationships
[RFR] Relationships
2 parents 381f409 + 8fea2d5 commit 0237d43

File tree

9 files changed

+259
-73
lines changed

9 files changed

+259
-73
lines changed

README.md

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,16 @@ Your data file should be an object where the keys are the entity types. The valu
2020
```json
2121
{
2222
"posts": [
23-
{
24-
"id": 1,
25-
"title": "Lorem Ipsum",
26-
"views": 254,
27-
"user_id": 123,
28-
"tag_id": "foo"
29-
},
30-
{
31-
"id": 2,
32-
"title": "Sic Dolor amet",
33-
"views": 65,
34-
"user_id": 456,
35-
"tag_id": "bar"
36-
},
23+
{ "id": 1, "title": "Lorem Ipsum", "views": 254, "user_id": 123 },
24+
{ "id": 2, "title": "Sic Dolor amet", "views": 65, "user_id": 456 },
3725
],
3826
"users": [
39-
{
40-
"id": 123,
41-
"name": "John Doe"
42-
},
43-
{
44-
"id": 456,
45-
"name": "Jane Doe"
46-
}
27+
{ "id": 123, "name": "John Doe" },
28+
{ "id": 456, "name": "Jane Doe" }
4729
],
48-
"tags": [
49-
{
50-
"id": "foo",
51-
"name": "Foo"
52-
},
53-
{
54-
"id": "bar",
55-
"name": "Bar"
56-
}
30+
"comments": [
31+
{ "id": 987, "post_id": 1, "body": "Consectetur adipiscing elit" },
32+
{ "id": 995, "post_id": 1, "body": "Nam molestie pellentesque dui" }
5733
]
5834
}
5935
```
@@ -67,16 +43,22 @@ json-graphql-server db.json
6743
Now you can query your data in graphql. For instance, to issue the following query:
6844

6945
```graphql
70-
query {
71-
Customer(id: 1) {
46+
{
47+
Post(id: 1) {
7248
id
73-
first_name
74-
last_name
49+
title
50+
views
51+
User {
52+
name
53+
}
54+
Comments {
55+
body
56+
}
7557
}
7658
}
7759
```
7860

79-
Go to http://localhost:3000/?query=query%20%7B%20Post(id%3A%201)%20%7Bid%20title%20views%20%7D%7D. You'll get the following result:
61+
Go to http://localhost:3000/?query=%7B%20Post%28id%3A%201%29%20%7B%20id%20title%20views%20User%20%7B%20name%20%7D%20Comments%20%7B%20body%20%7D%20%7D%20%7D. You'll get the following result:
8062

8163
```json
8264
{
@@ -85,12 +67,19 @@ Go to http://localhost:3000/?query=query%20%7B%20Post(id%3A%201)%20%7Bid%20title
8567
"id": "1",
8668
"title": "Lorem Ipsum",
8769
"views": 254,
70+
"User": {
71+
"name": "John Doe"
72+
},
73+
"Comments": [
74+
{ "body": "Consectetur adipiscing elit" },
75+
{ "body": "Nam molestie pellentesque dui" },
76+
]
8877
}
8978
}
9079
}
9180
```
9281

93-
The json-graphql-server accepts queries in GET and POST. Under the hood, it uses [Apollo's `graphql-server` module](http://dev.apollodata.com/tools/graphql-server/requests.html). Please refer to their documentations for details about passing variables, etc.
82+
The json-graphql-server accepts queries in GET and POST. Under the hood, it uses [the `express-graphql` module](https://github.com/graphql/express-graphql). Please refer to their documentations for details about passing variables, etc.
9483

9584
Note that the server is [GraphiQL](https://github.com/skevy/graphiql-app/releases) enabled, so you can query your server using a full-featured graphical user interface, providing autosuggest, history, etc.
9685

@@ -112,7 +101,8 @@ type Post {
112101
title: String!
113102
views: Int
114103
user_id: ID
115-
tag_id: ID
104+
User: User
105+
Comments: [Comment]
116106
}
117107
type Query {
118108
Post(id: ID!): Post
@@ -173,7 +163,6 @@ Here is how you can use the queries and mutations generated for your data, using
173163
title
174164
views
175165
user_id
176-
tag_id
177166
}
178167
}
179168
</pre>
@@ -186,8 +175,7 @@ Here is how you can use the queries and mutations generated for your data, using
186175
"id": 1,
187176
"title": "Lorem Ipsum",
188177
"views": 254,
189-
"user_id": 123,
190-
"tag_id": "foo"
178+
"user_id": 123
191179
}
192180
}
193181
}
@@ -246,7 +234,7 @@ Deploy with Heroku or Next.js.
246234

247235
## Roadmap
248236

249-
* Handle relationships
237+
* Filtering in the `all*` queries
250238
* Client-side mocking (à la [FakeRest](https://github.com/marmelab/FakeRest))
251239
* CLI options (port, https, watch, delay, custom schema)
252240

@@ -265,6 +253,3 @@ make format
265253
## License
266254

267255
Admin-on-rest is licensed under the [MIT Licence](https://github.com/marmelab/json-graphql-server/blob/master/LICENSE.md), sponsored and supported by [marmelab](http://marmelab.com).
268-
269-
270-

src/introspection/getFieldsFromEntities.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import getValuesFromEntities from './getValuesFromEntities';
1919
* "user_id": 456,
2020
* },
2121
* ];
22-
* const types = getFieldsFromData(entities);
22+
* const types = getFieldsFromEntities(entities);
2323
* // {
2424
* // id: { type: graphql.GraphQLString },
2525
* // title: { type: graphql.GraphQLString },

src/introspection/getSchemaFromData.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import {
77
GraphQLObjectType,
88
GraphQLSchema,
99
GraphQLString,
10+
parse,
11+
extendSchema,
1012
} from 'graphql';
1113
import { pluralize, camelize } from 'inflection';
1214

1315
import getTypesFromData from './getTypesFromData';
16+
import { isRelationshipField } from '../relationships';
17+
import { getRelatedType } from '../nameConverter';
1418

1519
/**
1620
* Get a GraphQL schema from data
@@ -109,8 +113,6 @@ export default data => {
109113
args: {
110114
page: { type: GraphQLInt },
111115
perPage: { type: GraphQLInt },
112-
sortField: { type: GraphQLString },
113-
sortOrder: { type: GraphQLString },
114116
filter: { type: GraphQLString },
115117
},
116118
};
@@ -153,5 +155,34 @@ export default data => {
153155
}, {}),
154156
});
155157

156-
return new GraphQLSchema({ query: queryType, mutation: mutationType });
158+
const schema = new GraphQLSchema({
159+
query: queryType,
160+
mutation: mutationType,
161+
});
162+
163+
/**
164+
* extend schema to add relationship fields
165+
*
166+
* @example
167+
* If the `post` key contains a 'user_id' field, then
168+
* add one-to-many and many-to-one type extensions:
169+
* extend type Post { User: User }
170+
* extend type User { Posts: [Post] }
171+
*/
172+
const schemaExtension = Object.values(typesByName).reduce((ext, type) => {
173+
Object.keys(type.getFields())
174+
.filter(isRelationshipField)
175+
.map(fieldName => {
176+
const relType = getRelatedType(fieldName);
177+
const rel = pluralize(type.toString());
178+
ext += `
179+
extend type ${type} { ${relType}: ${relType} }
180+
extend type ${relType} { ${rel}: [${type}] }`;
181+
});
182+
return ext;
183+
}, '');
184+
185+
return schemaExtension
186+
? extendSchema(schema, parse(schemaExtension))
187+
: schema;
157188
};

src/introspection/getSchemaFromData.spec.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,33 @@ const data = {
3838

3939
const PostType = new GraphQLObjectType({
4040
name: 'Post',
41-
fields: {
41+
fields: () => ({
4242
id: { type: new GraphQLNonNull(GraphQLID) },
4343
title: { type: new GraphQLNonNull(GraphQLString) },
4444
views: { type: new GraphQLNonNull(GraphQLInt) },
4545
user_id: { type: new GraphQLNonNull(GraphQLID) },
46-
},
46+
User: { type: UserType },
47+
}),
4748
});
49+
4850
const UserType = new GraphQLObjectType({
4951
name: 'User',
50-
fields: {
52+
fields: () => ({
5153
id: { type: new GraphQLNonNull(GraphQLID) },
5254
name: { type: new GraphQLNonNull(GraphQLString) },
53-
},
55+
Posts: { type: new GraphQLList(PostType) },
56+
}),
5457
});
5558

59+
/*
5660
const ListMetadataType = new GraphQLObjectType({
5761
name: 'ListMetadata',
5862
fields: {
5963
count: { type: GraphQLInt },
6064
},
6165
});
6266
63-
const QueryType = new GraphQLObjectType({ // eslint-disable-line
67+
const QueryType = new GraphQLObjectType({
6468
name: 'Query',
6569
fields: {
6670
getPost: {
@@ -97,13 +101,28 @@ const QueryType = new GraphQLObjectType({ // eslint-disable-line
97101
},
98102
},
99103
});
104+
*/
100105

101106
test('creates one type per data type', () => {
102107
const typeMap = getSchemaFromData(data).getTypeMap();
103108
expect(typeMap['Post'].name).toEqual(PostType.name);
104-
expect(typeMap['Post'].fields).toEqual(PostType.fields);
109+
expect(Object.keys(typeMap['Post'].getFields())).toEqual(
110+
Object.keys(PostType.getFields()),
111+
);
105112
expect(typeMap['User'].name).toEqual(UserType.name);
106-
expect(typeMap['User'].fields).toEqual(UserType.fields);
113+
expect(Object.keys(typeMap['User'].getFields())).toEqual(
114+
Object.keys(UserType.getFields()),
115+
);
116+
});
117+
118+
test('creates one field per relationship', () => {
119+
const typeMap = getSchemaFromData(data).getTypeMap();
120+
expect(Object.keys(typeMap['Post'].getFields())).toContain('User');
121+
});
122+
123+
test('creates one field per reverse relationship', () => {
124+
const typeMap = getSchemaFromData(data).getTypeMap();
125+
expect(Object.keys(typeMap['User'].getFields())).toContain('Posts');
107126
});
108127

109128
test('creates three query fields per data type', () => {
@@ -117,7 +136,7 @@ test('creates three query fields per data type', () => {
117136
type: new GraphQLNonNull(GraphQLID),
118137
},
119138
]);
120-
expect(queries['allPosts'].type).toMatchObject(new GraphQLList(PostType));
139+
expect(queries['allPosts'].type.toString()).toEqual('[Post]');
121140
expect(queries['allPosts'].args).toEqual([
122141
{
123142
defaultValue: undefined,
@@ -150,7 +169,7 @@ test('creates three query fields per data type', () => {
150169
type: GraphQLString,
151170
},
152171
]);
153-
expect(queries['_allPostsMeta'].type).toMatchObject(ListMetadataType);
172+
expect(queries['_allPostsMeta'].type.toString()).toEqual('ListMetadata');
154173

155174
expect(queries['User'].type.name).toEqual(UserType.name);
156175
expect(queries['User'].args).toEqual([
@@ -161,7 +180,7 @@ test('creates three query fields per data type', () => {
161180
type: new GraphQLNonNull(GraphQLID),
162181
},
163182
]);
164-
expect(queries['allUsers'].type).toMatchObject(new GraphQLList(UserType));
183+
expect(queries['allUsers'].type.toString()).toEqual('[User]');
165184
expect(queries['allUsers'].args).toEqual([
166185
{
167186
defaultValue: undefined,
@@ -194,7 +213,7 @@ test('creates three query fields per data type', () => {
194213
type: GraphQLString,
195214
},
196215
]);
197-
expect(queries['_allPostsMeta'].type).toMatchObject(ListMetadataType);
216+
expect(queries['_allPostsMeta'].type.toString()).toEqual('ListMetadata');
198217
});
199218

200219
test('creates three mutation fields per data type', () => {
@@ -305,7 +324,7 @@ test('creates three mutation fields per data type', () => {
305324

306325
test('pluralizes and capitalizes correctly', () => {
307326
const data = {
308-
foot: [{ id: 1, size: 42 }, { id: 2, size: 39 }],
327+
feet: [{ id: 1, size: 42 }, { id: 2, size: 39 }],
309328
categories: [{ id: 1, name: 'foo' }],
310329
};
311330
const queries = getSchemaFromData(data).getQueryType().getFields();

src/introspection/getTypesFromData.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { GraphQLObjectType } from 'graphql';
22
import { singularize, camelize } from 'inflection';
33

44
import getFieldsFromEntities from './getFieldsFromEntities';
5+
import { getTypeFromKey } from '../nameConverter';
56

67
/**
78
* Get a list of GraphQLObjectType from data
@@ -62,4 +63,4 @@ export default data =>
6263
.map(typeObject => new GraphQLObjectType(typeObject));
6364

6465
export const getTypeNamesFromData = data =>
65-
Object.keys(data).map(typeName => camelize(singularize(typeName)));
66+
Object.keys(data).map(getTypeFromKey);

src/nameConverter.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { camelize, pluralize, singularize } from 'inflection';
2+
3+
/**
4+
* A bit of vocabulary
5+
*
6+
* Consider this data:
7+
* {
8+
* posts: [
9+
* { id: 1, title: 'foo', user_id: 123 }
10+
* ],
11+
* users: [
12+
* { id: 123, name: 'John Doe' }
13+
* ]
14+
* }
15+
*
16+
* We'll use the following names:
17+
* - key: the keys in the data map, e.g. 'posts', 'users'
18+
* - type: for a key, the related type in the graphQL schema, e.g. 'posts' => 'Post', 'users' => 'User'
19+
* - field: the keys in a record, e.g. 'id', 'foo', user_id'
20+
* - relationship field: a key ending in '_id', e.g. 'user_id'
21+
* - related key: for a relationship field, the related key, e.g. 'user_id' => 'users'
22+
*/
23+
24+
/**
25+
*
26+
* @param {String} fieldName 'users'
27+
* @return {String} 'Users'
28+
*/
29+
export const getRelationshipFromKey = key => camelize(key);
30+
31+
/**
32+
*
33+
* @param {String} fieldName 'users'
34+
* @return {String} 'User'
35+
*/
36+
export const getTypeFromKey = key => camelize(singularize(key));
37+
38+
/**
39+
*
40+
* @param {String} fieldName 'user_id'
41+
* @return {String} 'users'
42+
*/
43+
export const getRelatedKey = fieldName =>
44+
pluralize(fieldName.substr(0, fieldName.length - 3));
45+
46+
/**
47+
*
48+
* @param {String} key 'users'
49+
* @return {String} 'user_id'
50+
*/
51+
export const getReverseRelatedField = key => `${singularize(key)}_id`;
52+
53+
/**
54+
*
55+
* @param {String} fieldName 'user_id'
56+
* @return {String} 'User'
57+
*/
58+
export const getRelatedType = fieldName =>
59+
getTypeFromKey(fieldName.substr(0, fieldName.length - 3));

0 commit comments

Comments
 (0)