Skip to content

Commit 08e214f

Browse files
committed
Add one-to-many relationship support
1 parent 7537528 commit 08e214f

File tree

3 files changed

+120
-19
lines changed

3 files changed

+120
-19
lines changed

src/introspection/getSchemaFromData.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -157,25 +157,33 @@ export default data => {
157157
}, {}),
158158
});
159159

160-
let schema = new GraphQLSchema({
160+
const schema = new GraphQLSchema({
161161
query: queryType,
162162
mutation: mutationType,
163163
});
164164

165-
// extend schema to add relationship fields
166-
Object.values(typesByName).map(type => {
167-
Object.keys(type.getFields())
168-
.filter(isRelationshipField)
169-
.map(fieldName => {
170-
const relatedType = getRelatedType(fieldName);
171-
schema = extendSchema(
172-
schema,
173-
parse(
174-
`extend type ${type} { ${relatedType}: ${relatedType}} `,
175-
),
176-
);
177-
});
178-
});
165+
/**
166+
* extend schema to add relationship fields
167+
*
168+
* @example
169+
* If the `post` key contains a 'user_id' field, then
170+
* add one-to-many and many-to-one type extensions:
171+
* extend type Post { User: User }
172+
* extend type User { Posts: [Post] }
173+
*/
174+
const schemaExtension = Object.values(typesByName)
175+
.reduce((ext, type) => {
176+
Object.keys(type.getFields())
177+
.filter(isRelationshipField)
178+
.map(fieldName => {
179+
const relType = getRelatedType(fieldName);
180+
const rel = pluralize(type.toString());
181+
ext.push(`extend type ${type} { ${relType}: ${relType} }`);
182+
ext.push(`extend type ${relType} { ${rel}: [${type}] }`);
183+
});
184+
return ext;
185+
}, [])
186+
.join('\n');
179187

180-
return schema;
188+
return extendSchema(schema, parse(schemaExtension));
181189
};

src/nameConverter.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ import { camelize, pluralize, singularize } from 'inflection';
2121
* - related key: for a relationship field, the related key, e.g. 'user_id' => 'users'
2222
*/
2323

24+
/**
25+
*
26+
* @param {String} fieldName 'users'
27+
* @return {String} 'Users'
28+
*/
29+
export const getRelationshipFromKey = key => camelize(key);
30+
2431
/**
2532
*
2633
* @param {String} fieldName 'users'
@@ -36,6 +43,13 @@ export const getTypeFromKey = key => camelize(singularize(key));
3643
export const getRelatedKey = fieldName =>
3744
pluralize(fieldName.substr(0, fieldName.length - 3));
3845

46+
/**
47+
*
48+
* @param {String} key 'users'
49+
* @return {String} 'user_id'
50+
*/
51+
export const getReverseRelatedField = key => `${singularize(key)}_id`;
52+
3953
/**
4054
*
4155
* @param {String} fieldName 'user_id'

src/resolver/Entity/index.js

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,67 @@
11
import getFieldsFromEntities from '../../introspection/getFieldsFromEntities';
2-
import { getRelatedKey, getRelatedType } from '../../nameConverter';
2+
import {
3+
getRelatedKey,
4+
getRelatedType,
5+
getRelationshipFromKey,
6+
getReverseRelatedField,
7+
} from '../../nameConverter';
38
import { isRelationshipField } from '../../relationships';
49

5-
export default (key, data) =>
6-
Object.keys(getFieldsFromEntities(data[key]))
10+
/**
11+
* Add resolvers for relationship fields
12+
*
13+
* @example
14+
* Consider this data:
15+
*
16+
* {
17+
* posts: [
18+
* { id: 1, title: 'foo', user_id: 123 }
19+
* ],
20+
* users: [
21+
* { id: 123, name: 'John Doe' }
22+
* ]
23+
* }
24+
*
25+
* There is one relationship field here, posts.user_id.
26+
* The generated GraphQL schema is:
27+
*
28+
* type Post {
29+
* id: ID!
30+
* title: String
31+
* user_id: ID
32+
* User: User
33+
* }
34+
* type User {
35+
* id: ID!
36+
* name: String
37+
* Posts: [Post]
38+
* }
39+
*
40+
* This method generates resolvers for Post.User and User.Posts
41+
*
42+
* @param {String} key The entity key in the data map, e.g. "posts"
43+
* @param {Object} data The entire data map, e.g. { posts: [], users: [] }
44+
*
45+
* @return {Object} resolvers, e.g.
46+
*
47+
* {
48+
* Post: {
49+
* User: (post) => users.find(user => user.id == post.user_id)
50+
* },
51+
* }
52+
*
53+
* when called with the "posts" key, and
54+
*
55+
* {
56+
* User: {
57+
* Posts: (user) => posts.filter(post => post.user_id == user.id)
58+
* }
59+
* }
60+
*
61+
* when called with the "users" key
62+
*/
63+
export default (key, data) => {
64+
const manyToOneResolvers = Object.keys(getFieldsFromEntities(data[key]))
765
.filter(isRelationshipField)
866
.reduce(
967
(resolvers, fieldName) => ({
@@ -15,3 +73,24 @@ export default (key, data) =>
1573
}),
1674
{},
1775
);
76+
const relatedField = getReverseRelatedField(key); // 'posts' => 'post_id'
77+
const hasReverseRelationship = entityName =>
78+
getFieldsFromEntities(data[entityName]).hasOwnProperty(relatedField);
79+
const oneToManyResolvers = Object.keys(data)
80+
.filter(hasReverseRelationship)
81+
.reduce(
82+
(resolvers, entityName) => ({
83+
...resolvers,
84+
[getRelationshipFromKey(entityName)]: entity =>
85+
data[entityName].filter(
86+
record => record[relatedField] == entity.id,
87+
),
88+
}),
89+
{},
90+
);
91+
92+
return {
93+
...manyToOneResolvers,
94+
...oneToManyResolvers,
95+
};
96+
};

0 commit comments

Comments
 (0)