From c473304dbbf14792e6ba65fdffdcec3f929187f6 Mon Sep 17 00:00:00 2001 From: Nate Murray Date: Sun, 6 Dec 2015 21:17:48 -0800 Subject: [PATCH] feat(schema): add read support types for objects within a list For example take this schema: ```javascript const PostSchema = new mongoose.Schema({ title: String, authors: [{ username: String, email: String }] }) ``` Before this commit `authors` would be mapped to `GraphQLGeneric`. This commit generates the appropriate nested types. Incidentally, this commit also fixes resolving refs within nested objects and lists. For instance: ```javascript const UserSchema = new mongoose.Schema({ name: { type: String }, sub: { subsub: { sister: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } } } }); ``` Previously `user.sub.subsub.sister` would have resolved only to an `ID`. This commit causes this case to resolve properly to a `User`. This change only applies to queries. Mutations of objects nested in a list are a TODO. --- fixture/user.js | 10 ++++++- src/model/model.js | 13 ++++++++- src/schema/schema.js | 4 ++- src/type/type.js | 65 ++++++++++++++++++++++++++++++------------- src/type/type.spec.js | 57 +++++++++++++++++++++++++++++++++---- 5 files changed, 122 insertions(+), 27 deletions(-) diff --git a/fixture/user.js b/fixture/user.js index a79114a..b8df891 100644 --- a/fixture/user.js +++ b/fixture/user.js @@ -39,7 +39,15 @@ const UserSchema = new mongoose.Schema({ subsub: { bar: Number } - } + }, + subArray: [{ + foo: String, + nums: [Number], + brother: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + } + }] }); const User = mongoose.model('User', UserSchema); diff --git a/src/model/model.js b/src/model/model.js index a1fda2d..df009f5 100644 --- a/src/model/model.js +++ b/src/model/model.js @@ -1,4 +1,5 @@ import {reduce, reduceRight, merge} from 'lodash'; +import mongoose from 'mongoose'; /** * @method getField @@ -62,7 +63,17 @@ function extractPath(schemaPath) { return reduceRight(subs, (field, sub, key) => { const obj = {}; - if (key === (subs.length - 1)) { + if (schemaPath instanceof mongoose.Schema.Types.DocumentArray) { + const subSchemaPaths = schemaPath.schema.paths; + const fields = extractPaths(subSchemaPaths, {name: sub}); // eslint-disable-line no-use-before-define + obj[sub] = { + name: sub, + nonNull: false, + type: 'Array', + subtype: 'Object', + fields + }; + } else if (key === (subs.length - 1)) { obj[sub] = getField(schemaPath); } else { obj[sub] = { diff --git a/src/schema/schema.js b/src/schema/schema.js index b5a16b0..10d64ba 100644 --- a/src/schema/schema.js +++ b/src/schema/schema.js @@ -123,7 +123,9 @@ function getMutationField(graffitiModel, type, viewer, hooks = {}) { } } - if (!(field.type instanceof GraphQLObjectType) && field.name !== 'id' && !field.name.startsWith('_')) { + if (field.type instanceof GraphQLList && field.type.ofType instanceof GraphQLObjectType) { + // TODO support objects nested in lists + } else if (!(field.type instanceof GraphQLObjectType) && field.name !== 'id' && !field.name.startsWith('_')) { inputFields[field.name] = field; } diff --git a/src/type/type.js b/src/type/type.js index beffdc7..44c0d4f 100644 --- a/src/type/type.js +++ b/src/type/type.js @@ -165,13 +165,16 @@ const resolveReference = {}; * @param {Boolean} root * @return {GraphQLObjectType} */ -export default function getType(graffitiModels, {name, description, fields}, root = true) { +export default function getType(graffitiModels, {name, description, fields}, path = [], rootType = null) { + const root = path.length === 0; const graphQLType = {name, description}; + rootType = rootType || graphQLType; - // These references has to be resolved when all type definitions are avaiable + // These references have to be resolved when all type definitions are avaiable resolveReference[graphQLType.name] = resolveReference[graphQLType.name] || {}; const graphQLTypeFields = reduce(fields, (graphQLFields, {name, description, type, subtype, reference, nonNull, hidden, hooks, fields: subfields}, key) => { name = name || key; + const newPath = [...path, name]; // Don't add hidden fields to the GraphQLObjectType if (hidden || name.startsWith('__')) { @@ -181,27 +184,32 @@ export default function getType(graffitiModels, {name, description, fields}, roo const graphQLField = {name, description}; if (type === 'Array') { - graphQLField.type = new GraphQLList(stringToGraphQLType(subtype)); - if (reference) { - resolveReference[graphQLType.name][name] = { - name, - type: reference, - args: connectionArgs, - resolve: addHooks((rootValue, args, info) => { - args.id = rootValue[name].map((i) => i.toString()); - return connectionFromModel(graffitiModels[reference], args, info); - }, hooks) - }; + if (subtype === 'Object') { + const fields = subfields; + graphQLField.type = new GraphQLList(getType(graffitiModels, {name, description, fields}, newPath, rootType)); + } else { + graphQLField.type = new GraphQLList(stringToGraphQLType(subtype)); + if (reference) { + resolveReference[rootType.name][name] = { + name, + type: reference, + args: connectionArgs, + resolve: addHooks((rootValue, args, info) => { + args.id = rootValue[name].map((i) => i.toString()); + return connectionFromModel(graffitiModels[reference], args, info); + }, hooks) + }; + } } } else if (type === 'Object') { const fields = subfields; - graphQLField.type = getType(graffitiModels, {name, description, fields}, false); + graphQLField.type = getType(graffitiModels, {name, description, fields}, newPath, rootType); } else { graphQLField.type = stringToGraphQLType(type); } if (reference && (graphQLField.type === GraphQLID || graphQLField.type === new GraphQLNonNull(GraphQLID))) { - resolveReference[graphQLType.name][name] = { + resolveReference[rootType.name][newPath.join('.')] = { name, type: reference, resolve: addHooks((rootValue, args, info) => { @@ -270,10 +278,29 @@ function getTypes(graffitiModels) { field.type = types[field.type]; } - return { - ...typeFields, - [fieldName]: field - }; + // deeply find the path of the field we want to resolve the reference of + const path = fieldName.split('.'); + const newTypeFields = {...typeFields}; + let parent = newTypeFields; + let segment; + + while (path.length > 0) { + segment = path.shift(); + + if (parent[segment]) { + if (parent[segment].type instanceof GraphQLObjectType) { + parent = parent[segment].type.getFields(); + } else if (parent[segment].type instanceof GraphQLList && + parent[segment].type.ofType instanceof GraphQLObjectType) { + parent = getTypeFields(parent[segment].type.ofType); + } + } + } + + if (path.length === 0) { + parent[segment] = field; + } + return newTypeFields; }, getTypeFields(type)); // Add new fields diff --git a/src/type/type.spec.js b/src/type/type.spec.js index a19f609..db7ce4b 100644 --- a/src/type/type.spec.js +++ b/src/type/type.spec.js @@ -72,10 +72,33 @@ describe('type', () => { fields: { bar: { type: 'Number' + }, + sister: { + type: 'ObjectID', + reference: 'User', + description: 'The user\'s sister' } } } } + }, + subArray: { + type: 'Array', + subtype: 'Object', + fields: { + foo: { + type: 'String' + }, + nums: { + type: 'Array', + subtype: 'Number' + }, + brother: { + type: 'ObjectID', + reference: 'User', + description: 'The user\'s brother' + } + } } } }; @@ -96,7 +119,7 @@ describe('type', () => { it('should specify the fields', () => { const result = getType([], user); - let fields = result._typeConfig.fields(); + const fields = result._typeConfig.fields(); expect(fields).to.containSubset({ name: { name: 'name', @@ -141,8 +164,8 @@ describe('type', () => { }); // sub - fields = fields.sub.type._typeConfig.fields(); - expect(fields).to.containSubset({ + const subFields = fields.sub.type._typeConfig.fields(); + expect(subFields).to.containSubset({ foo: { name: 'foo', type: GraphQLString @@ -157,11 +180,33 @@ describe('type', () => { }); // subsub - fields = fields.subsub.type._typeConfig.fields(); - expect(fields).to.containSubset({ + const subsubFields = subFields.subsub.type._typeConfig.fields(); + expect(subsubFields).to.containSubset({ bar: { name: 'bar', type: GraphQLFloat + }, + sister: { + name: 'sister', + type: GraphQLID, + description: 'The user\'s sister' + } + }); + + const subArrayFields = fields.subArray.type.ofType._typeConfig.fields(); + expect(subArrayFields).to.containSubset({ + foo: { + name: 'foo', + type: GraphQLString + }, + nums: { + name: 'nums', + type: new GraphQLList(GraphQLFloat) + }, + brother: { + name: 'brother', + type: GraphQLID, + description: 'The user\'s brother' } }); }); @@ -180,6 +225,8 @@ describe('type', () => { const fields = userType._typeConfig.fields(); expect(fields.mother.type).to.be.equal(userType); + expect(fields.sub.type._fields.subsub.type._fields.sister.type).to.be.equal(userType); + expect(fields.subArray.type.ofType._typeConfig.fields().brother.type).to.be.equal(userType); // connection type const nodeField = fields.friends.type._typeConfig.fields().edges.type.ofType._typeConfig.fields().node;