Repository: lucasbento/create-graphql
Pull request #94 (2017-05-22 23:42:51)

Support mongoose array fields
Fixes https://github.com/lucasbento/create-graphql/issues/81


Patch:
diff --git a/packages/generator/fixtures/Comment.js b/packages/generator/fixtures/Comment.js
new file mode 100644
index 0000000..c7b3192
--- /dev/null
+++ b/packages/generator/fixtures/Comment.js
@@ -0,0 +1,31 @@
+// @flow
+import mongoose from 'mongoose';
+
+import User from './User';
+const { ObjectId } = mongoose.Schema.Types;
+
+const Schema = new mongoose.Schema({
+  author: {
+    type: ObjectId,
+    ref: 'User',
+    description: 'User that created this comment',
+    required: true,
+  },
+  score: {
+    type: Number,
+    description: 'Sum of all upvotes/downvotes this comment has',
+    required: false,
+  },
+  text: {
+    type: String,
+    required: true,
+  },
+}, {
+  timestamps: {
+    createdAt: 'createdAt',
+    updatedAt: 'updatedAt',
+  },
+  collection: 'comment',
+});
+
+export default mongoose.model('Comment', Schema);
diff --git a/packages/generator/fixtures/Post.js b/packages/generator/fixtures/Post.js
index bbce35c..ce77366 100644
--- a/packages/generator/fixtures/Post.js
+++ b/packages/generator/fixtures/Post.js
@@ -1,6 +1,7 @@
 import mongoose from 'mongoose';
 
 import User from './User';
+import Comment from './Comment';
 const { ObjectId } = mongoose.Schema.Types;
 
 const Schema = new mongoose.Schema({
@@ -20,6 +21,22 @@ const Schema = new mongoose.Schema({
     indexed: true,
     description: 'Used for SEO',
   },
+  tags: [String],
+  oldSlugs: {
+    type: [String],
+    description: 'Old slugs used by this post'
+  },
+  comments: [
+    {
+      type: ObjectId,
+      ref: 'Comment',
+    },
+  ],
+  externalComments: {
+    type: [ObjectId],
+    ref: 'Comment',
+    description: 'Comments from external source'
+  },
 }, {
   timestamps: {
     createdAt: 'createdAt',
diff --git a/packages/generator/src/utils.js b/packages/generator/src/utils.js
index 8775b79..e4f32a1 100644
--- a/packages/generator/src/utils.js
+++ b/packages/generator/src/utils.js
@@ -28,6 +28,10 @@ const parseFieldToGraphQL = (field, ref) => {
   const typeFileName = `${name}Type`;
   const loaderFileName = `${name}Loader`;
 
+  let parsedChildField;
+  let typeFileNameSingular;
+  let loaderFileNameSingular;
+
   switch (field.type) {
     case 'Number':
       return {
@@ -41,6 +45,28 @@ const parseFieldToGraphQL = (field, ref) => {
         type: 'GraphQLBoolean',
         flowType: 'boolean',
       };
+    case 'Array':
+      field.type = field.childType;
+
+      parsedChildField = parseFieldToGraphQL(field, ref);
+      parsedChildField.flowType = 'array';
+      parsedChildField.type = [parsedChildField.type];
+
+      if (field.childType === 'ObjectId' && ref) {
+        typeFileNameSingular = `${field.ref}Type`;
+        loaderFileNameSingular = `${field.ref}Loader`;
+
+        parsedChildField = {
+          ...parsedChildField,
+          type: [typeFileNameSingular],
+          resolve: `await ${loaderFileNameSingular}.load${name}ByIds(context, obj.${field.name})`,
+          resolveArgs: 'async (obj, args, context)',
+          graphqlType: typeFileNameSingular,
+          graphqlLoader: loaderFileNameSingular,
+        };
+      }
+
+      return parsedChildField;
     case 'ObjectId':
       if (ref) {
         return {
@@ -83,7 +109,30 @@ const parseGraphQLSchema = (mongooseFields, ref) => {
   const fields = Object.keys(mongooseFields).map((name) => {
     const field = parseFieldToGraphQL(mongooseFields[name], ref);
 
-    if (field.graphqlType) {
+    // we have a special case for array types, since we need to add as dependency
+    //  both the GraphQLList and the type itself.
+    if (Array.isArray(field.type)) {
+      // array of scalar types
+      if (!field.graphqlType) {
+        if (dependencies.indexOf(field.type[0]) === -1) {
+          dependencies.push(field.type[0]);
+        }
+      } else {
+        if (typeDependencies.indexOf(field.graphqlType) === -1) {
+          typeDependencies.push(field.graphqlType);
+        }
+
+        if (loaderDependencies.indexOf(field.graphqlLoader) === -1) {
+          loaderDependencies.push(field.graphqlLoader);
+        }
+      }
+
+      field.type = `new GraphQLList(${field.type[0]})`;
+
+      if (dependencies.indexOf('GraphQLList') === -1) {
+        dependencies.push('GraphQLList');
+      }
+    } else if (field.graphqlType) {
       if (typeDependencies.indexOf(field.graphqlType) === -1) {
         typeDependencies.push(field.graphqlType);
       }
@@ -131,28 +180,75 @@ const getSchemaTimestampsFromAst = (nodes) => {
   return timestampFields;
 };
 
+/**
+ * Check if there is an ObjectProperty with key named "type" and with value of type "ArrayExpression".
+ * @param properties {Array}
+ * @returns {Object} The ObjectProperty if found, undefined otherwise.
+ */
+const getArrayTypeElementFromPropertiesArray = properties => properties.find(
+  ({ key, value: item }) => key.name === 'type' && item.type === 'ArrayExpression',
+);
+
+// MemberExpression: { field1: Schema.Types.ObjectId }
+// Identifier: { field1: ObjectId }
+const validSingleValueTypes = ['MemberExpression', 'Identifier'];
+
+const getFieldDefinition = (field, parent = null) => {
+  const value = field.value || field;
+  let fieldDefinition = {};
+
+  const isArrayValue = value.type === 'ArrayExpression';
+  const isObjectValue = value.type === 'ObjectExpression';
+  const isArrayFieldDefinition = isArrayValue || (
+    isObjectValue && !!getArrayTypeElementFromPropertiesArray(value.properties)
+  );
+
+  // This handles the following array types:
+  // [Scalar]
+  //  or {type: [Scalar]}
+  //  or [{type:ObjectId,ref:'Object'}]
+  //  or {type:[ObjectId],ref:'Object'}
+  if (isArrayFieldDefinition) {
+    if (parent) {
+      throw new Error('Nested fields are not supported.');
+    }
+
+    let childValue;
+
+    if (isObjectValue) {
+      // we are going to change from { type: [Object] } to { type: Object }
+      const typeProperty = getArrayTypeElementFromPropertiesArray(value.properties);
+      typeProperty.value = typeProperty.value.elements[0];
+      childValue = value;
+    } else {
+      childValue = value.elements[0];
+    }
+
+    fieldDefinition = getFieldDefinition(childValue, value);
+
+    // override type, specify Array
+    fieldDefinition.childType = fieldDefinition.type;
+    fieldDefinition.type = 'Array';
+  } else if (validSingleValueTypes.indexOf(value.type) !== -1) {
+    fieldDefinition.type = value.property ? value.property.name : value.name;
+  } else {
+    value.properties.forEach(({ key, value: item }) => {
+      fieldDefinition[key.name] = item.name || item.value;
+    });
+  }
+
+  return fieldDefinition;
+};
+
 const getSchemaFieldsFromAst = (node, withTimestamps) => {
   const astSchemaFields = node.arguments[0].properties;
 
   const fields = [];
-  // MemberExpression: { field1: Schema.Types.ObjectId }
-  // Identifier: { field1: ObjectId }
-  const validSingleValueTypes = ['MemberExpression', 'Identifier'];
 
   astSchemaFields.forEach((field) => {
     const name = field.key.name;
 
-    const fieldDefinition = {};
-
-    if (field.value.type === 'ArrayExpression') {
-      return;
-    } else if (validSingleValueTypes.indexOf(field.value.type) !== -1) {
-      fieldDefinition.type = field.value.property ? field.value.property.name : field.value.name;
-    } else {
-      field.value.properties.forEach(({ key, value }) => {
-        fieldDefinition[key.name] = value.name || value.value;
-      });
-    }
+    const fieldDefinition = getFieldDefinition(field);
 
     fields[name] = {
       name,


Test patch:
diff --git a/packages/generator/src/loader/__tests__/__snapshots__/LoaderGenerator.spec.js.snap b/packages/generator/src/loader/__tests__/__snapshots__/LoaderGenerator.spec.js.snap
index 2d25fea..ea72e24 100644
--- a/packages/generator/src/loader/__tests__/__snapshots__/LoaderGenerator.spec.js.snap
+++ b/packages/generator/src/loader/__tests__/__snapshots__/LoaderGenerator.spec.js.snap
@@ -76,6 +76,10 @@ type PostType = {
   title: string,
   user: string,
   slug: string,
+  tags: array,
+  oldSlugs: array,
+  comments: array,
+  externalComments: array,
   createdAt: string,
   updatedAt: string,
 }
@@ -86,6 +90,10 @@ export default class Post {
   title: string;
   user: string;
   slug: string;
+  tags: array;
+  oldSlugs: array;
+  comments: array;
+  externalComments: array;
   createdAt: string;
   updatedAt: string;
 
@@ -97,6 +105,10 @@ export default class Post {
     this.title = data.title;
     this.user = data.user;
     this.slug = data.slug;
+    this.tags = data.tags;
+    this.oldSlugs = data.oldSlugs;
+    this.comments = data.comments;
+    this.externalComments = data.externalComments;
     this.createdAt = data.createdAt;
     this.updatedAt = data.updatedAt;
   }
diff --git a/packages/generator/src/mutation/__tests__/__snapshots__/MutationGenerator.spec.js.snap b/packages/generator/src/mutation/__tests__/__snapshots__/MutationGenerator.spec.js.snap
index 40d9ba5..d266479 100644
--- a/packages/generator/src/mutation/__tests__/__snapshots__/MutationGenerator.spec.js.snap
+++ b/packages/generator/src/mutation/__tests__/__snapshots__/MutationGenerator.spec.js.snap
@@ -261,6 +261,7 @@ Object {
 import {
   GraphQLString,
   GraphQLID,
+  GraphQLList,
   GraphQLNonNull,
 } from 'graphql';
 import {
@@ -287,6 +288,18 @@ export default mutationWithClientMutationId({
     slug: {
       type: GraphQLString,
     },
+    tags: {
+      type: new GraphQLList(GraphQLString),
+    },
+    oldSlugs: {
+      type: new GraphQLList(GraphQLString),
+    },
+    comments: {
+      type: new GraphQLList(GraphQLID),
+    },
+    externalComments: {
+      type: new GraphQLList(GraphQLID),
+    },
   },
   mutateAndGetPayload: async (args, { user }) => {
     // Verify if user is authorized
@@ -298,6 +311,10 @@ export default mutationWithClientMutationId({
       title,
       user,
       slug,
+      tags,
+      oldSlugs,
+      comments,
+      externalComments,
     } = args;
 
     // Create new record
@@ -305,6 +322,10 @@ export default mutationWithClientMutationId({
       title,
       user,
       slug,
+      tags,
+      oldSlugs,
+      comments,
+      externalComments,
     }).save();
 
     // TODO: mutation logic
@@ -361,12 +382,20 @@ it('should not allow anonymous user', async () => {
         title: \\"Example value\\"
         user: \\"Example value\\"
         slug: \\"Example value\\"
+        tags: \\"Example value\\"
+        oldSlugs: \\"Example value\\"
+        comments: \\"Example value\\"
+        externalComments: \\"Example value\\"
       }) {
         postEdge {
           node {
             title
             user
             slug
+            tags
+            oldSlugs
+            comments
+            externalComments
           }
         }
       }
@@ -396,12 +425,20 @@ it('should create a record on database', async () => {
         title: \\"Example value\\"
         user: \\"Example value\\"
         slug: \\"Example value\\"
+        tags: \\"Example value\\"
+        oldSlugs: \\"Example value\\"
+        comments: \\"Example value\\"
+        externalComments: \\"Example value\\"
       }) {
         postEdge {
           node {
             title
             user
             slug
+            tags
+            oldSlugs
+            comments
+            externalComments
           }
         }
       }
@@ -422,6 +459,7 @@ import {
   GraphQLString,
   GraphQLNonNull,
   GraphQLID,
+  GraphQLList,
 } from 'graphql';
 import {
   mutationWithClientMutationId,
@@ -448,6 +486,18 @@ export default mutationWithClientMutationId({
     slug: {
       type: GraphQLString,
     },
+    tags: {
+      type: new GraphQLList(GraphQLString),
+    },
+    oldSlugs: {
+      type: new GraphQLList(GraphQLString),
+    },
+    comments: {
+      type: new GraphQLList(GraphQLID),
+    },
+    externalComments: {
+      type: new GraphQLList(GraphQLID),
+    },
   },
   mutateAndGetPayload: async (args, context) => {
     const { user } = context;
@@ -462,6 +512,10 @@ export default mutationWithClientMutationId({
       title,
       user,
       slug,
+      tags,
+      oldSlugs,
+      comments,
+      externalComments,
     } = args;
 
     // Check if the provided ID is valid
@@ -479,6 +533,10 @@ export default mutationWithClientMutationId({
       title,
       user,
       slug,
+      tags,
+      oldSlugs,
+      comments,
+      externalComments,
     });
 
     // TODO: mutation logic
@@ -519,6 +577,10 @@ it('should not allow anonymous user', async () => {
     title: 'Example value',
     user: 'Example value',
     slug: 'Example value',
+    tags: 'Example value',
+    oldSlugs: 'Example value',
+    comments: 'Example value',
+    externalComments: 'Example value',
   });
 
   await post.save();
@@ -535,6 +597,10 @@ it('should not allow anonymous user', async () => {
           title
           user
           slug
+          tags
+          oldSlugs
+          comments
+          externalComments
         }
       }
     }
@@ -562,6 +628,10 @@ it('should edit a record on database', async () => {
     title: 'Example value',
     user: 'Example value',
     slug: 'Example value',
+    tags: 'Example value',
+    oldSlugs: 'Example value',
+    comments: 'Example value',
+    externalComments: 'Example value',
   });
 
   await post.save();
@@ -578,6 +648,10 @@ it('should edit a record on database', async () => {
           title
           user
           slug
+          tags
+          oldSlugs
+          comments
+          externalComments
         }
       }
     }
diff --git a/packages/generator/src/type/__tests__/__snapshots__/TypeGenerator.spec.js.snap b/packages/generator/src/type/__tests__/__snapshots__/TypeGenerator.spec.js.snap
index 3ca5066..672128c 100644
--- a/packages/generator/src/type/__tests__/__snapshots__/TypeGenerator.spec.js.snap
+++ b/packages/generator/src/type/__tests__/__snapshots__/TypeGenerator.spec.js.snap
@@ -68,12 +68,15 @@ Object {
 import {
   GraphQLObjectType,
   GraphQLString,
+  GraphQLList,
 } from 'graphql';
 import { globalIdField } from 'graphql-relay';
 
 import { NodeInterface } from '../interface/NodeInterface';
 import UserType from './UserType';
+import CommentType from './CommentType';
 import UserLoader from '../loader/UserLoader';
+import CommentLoader from '../loader/CommentLoader';
 
 export default new GraphQLObjectType({
   name: 'Post',
@@ -95,6 +98,26 @@ export default new GraphQLObjectType({
       description: 'Used for SEO',
       resolve: obj => obj.slug,
     },
+    tags: {
+      type: new GraphQLList(GraphQLString),
+      description: '',
+      resolve: obj => obj.tags,
+    },
+    oldSlugs: {
+      type: new GraphQLList(GraphQLString),
+      description: 'Old slugs used by this post',
+      resolve: obj => obj.oldSlugs,
+    },
+    comments: {
+      type: new GraphQLList(CommentType),
+      description: '',
+      resolve: async (obj, args, context) => await CommentLoader.loadCommentsByIds(context, obj.comments),
+    },
+    externalComments: {
+      type: new GraphQLList(CommentType),
+      description: 'Comments from external source',
+      resolve: async (obj, args, context) => await CommentLoader.loadExternalCommentsByIds(context, obj.externalComments),
+    },
     createdAt: {
       type: GraphQLString,
       description: '',