Repository: lucasbento/create-graphql (js)

Issue:
handle mongoose ref
For instance

```jsx
const Schema = new mongoose.Schema({
   user: {
       type: ObjectId,
       ref: 'User',
       indexed: true,
       description: 'User that created this comment',
       required: true,
  }
});
```

user field should resolve to UserType

maybe ask to create UserType as well if not created already

Pull request description:
handle mongoose refs, fix #6

Patch:
diff --git a/packages/generator/src/graphqlrc.json b/packages/generator/src/graphqlrc.json
index a8acee1..2b51f5e 100644
--- a/packages/generator/src/graphqlrc.json
+++ b/packages/generator/src/graphqlrc.json
@@ -7,9 +7,10 @@
     "mutation": "mutation",
     "mutation_test": "mutation/__tests__",
     "type": "type",
-    "type_test": "type/__tests__"
+    "type_test": "type/__tests__",
+    "interface": "interface"
   },
   "files": {
     "schema": "schema"
   }
-}
\ No newline at end of file
+}
diff --git a/packages/generator/src/loader/index.js b/packages/generator/src/loader/index.js
index 5a4d6ee..bb0aebf 100644
--- a/packages/generator/src/loader/index.js
+++ b/packages/generator/src/loader/index.js
@@ -31,7 +31,7 @@ class LoaderGenerator extends Generator {
 
   generateLoader() {
     const schema = this.options.model ?
-      getMongooseModelSchema(this.options.model, true)
+      getMongooseModelSchema({ model: this.options.model, withTimestamps: true })
       : null;
 
     const name = uppercaseFirstLetter(this.options.name);
diff --git a/packages/generator/src/mutation/index.js b/packages/generator/src/mutation/index.js
index f326fa4..9641b9d 100644
--- a/packages/generator/src/mutation/index.js
+++ b/packages/generator/src/mutation/index.js
@@ -72,7 +72,7 @@ class MutationGenerator extends Generator {
   generateMutation() {
     let schema = null;
     if (this.options.model) {
-      const modelSchema = getMongooseModelSchema(this.options.model);
+      const modelSchema = getMongooseModelSchema({ model: this.options.model });
       schema = this._parseSchema(modelSchema);
     }
 
@@ -105,6 +105,8 @@ class MutationGenerator extends Generator {
       directories,
     };
 
+    // TODO: generate type and loader that do not exist yet
+
     Object.keys(mutations).forEach((mutationType) => {
       const { template, fileName } = mutations[mutationType];
 
diff --git a/packages/generator/src/type/index.js b/packages/generator/src/type/index.js
index 1a3de87..60056de 100644
--- a/packages/generator/src/type/index.js
+++ b/packages/generator/src/type/index.js
@@ -3,6 +3,7 @@ import {
   getMongooseModelSchema,
   getConfigDir,
   uppercaseFirstLetter,
+  getRelativeConfigDir,
 } from '../utils';
 
 class TypeGenerator extends Generator {
@@ -23,13 +24,15 @@ class TypeGenerator extends Generator {
   }
 
   generateType() {
-    let schema = this.options.model ?
-      getMongooseModelSchema(this.options.model, true)
+    const schema = this.options.model ?
+      getMongooseModelSchema({
+        model: this.options.model,
+        withTimestamps: true,
+        ref: true,
+      })
       : null;
 
-    if (schema) {
-      schema = this._parseSchemaResolvers(schema);
-    }
+    const directories = this._getConfigDirectories();
 
     const name = uppercaseFirstLetter(this.options.name);
     const typeFileName = `${name}Type`;
@@ -42,6 +45,7 @@ class TypeGenerator extends Generator {
     const templateVars = {
       name,
       schema,
+      directories,
     };
 
     this._generateTypeTest({
@@ -49,33 +53,13 @@ class TypeGenerator extends Generator {
       schema,
     });
 
+    // TODO: generate types and loaders that do not exist yet
+
     this.fs.copyTpl(templatePath, destinationPath, templateVars);
   }
 
-  /**
-   * Parse schema resolvers checking if the fields need different resolvers.
-   * @param schema {Array} The parsed Mongoose schema
-   * @returns {Array} The parsed schema with resolvers
-   */
-  _parseSchemaResolvers(schema) {
-    const fields = schema.fields.map((field) => {
-      if (field.originalType === 'Date') {
-        return {
-          ...field,
-          resolve: `obj.${field.name}.toISOString()`,
-        };
-      }
-
-      return {
-        ...field,
-        resolve: `obj.${field.name}`,
-      };
-    });
-
-    return {
-      ...schema,
-      fields,
-    };
+  _getConfigDirectories() {
+    return getRelativeConfigDir('type', ['model', 'type', 'loader', 'connection', 'interface']);
   }
 
   _generateTypeTest({ name, schema }) {
@@ -83,9 +67,12 @@ class TypeGenerator extends Generator {
 
     const destinationPath = this.destinationPath(`${this.destinationDir}/__tests__/${name}Type.spec.js`);
 
+    const directories = this._getConfigDirectories();
+
     const templateVars = {
       name,
       schema,
+      directories,
     };
 
     this.fs.copyTpl(templatePath, destinationPath, templateVars);
diff --git a/packages/generator/src/type/templates/Type.js.template b/packages/generator/src/type/templates/Type.js.template
index cad87bd..63057e5 100644
--- a/packages/generator/src/type/templates/Type.js.template
+++ b/packages/generator/src/type/templates/Type.js.template
@@ -4,6 +4,8 @@ import {
 } from 'graphql';
 import { globalIdField } from 'graphql-relay';
 
+import { NodeInterface } from '<%= directories.interface %>/NodeInterface';
+
 export default new GraphQLObjectType({
   name: '<%= name %>',
   description: 'Represents <%= name %>',
@@ -14,5 +16,6 @@ export default new GraphQLObjectType({
       description: 'My example field',
       resolve: obj => obj.example,
     },
+    interfaces: () => [NodeInterface],
   }),
 });
\ No newline at end of file
diff --git a/packages/generator/src/type/templates/TypeWithSchema.js.template b/packages/generator/src/type/templates/TypeWithSchema.js.template
index da3af24..86219a7 100644
--- a/packages/generator/src/type/templates/TypeWithSchema.js.template
+++ b/packages/generator/src/type/templates/TypeWithSchema.js.template
@@ -8,6 +8,14 @@ import {
 } from 'graphql';
 import { globalIdField } from 'graphql-relay';
 
+import { NodeInterface } from '<%= directories.interface %>/NodeInterface';
+<%_ for (i in schema.typeDependencies) { -%>
+import <%= schema.typeDependencies[i] %> from '<%= directories.type %>/<%=schema.typeDependencies[i]%>';
+<%_ } -%>
+<%_ for (i in schema.loaderDependencies) { -%>
+import <%= schema.loaderDependencies[i] %> from '<%= directories.loader %>/<%=schema.loaderDependencies[i]%>';
+<%_ } -%>
+
 export default new GraphQLObjectType({
   name: '<%= name %>',
   description: 'Represents <%= name %>',
@@ -17,8 +25,9 @@ export default new GraphQLObjectType({
     <%- field.name %>: {
       type: <%= field.type %>,
       description: '<%= field.description %>',
-      resolve: obj => <%= field.resolve %>,
+      resolve: <%if(field.resolveArgs){%><%=field.resolveArgs%><%}else{ %>obj<%} %> => <%= field.resolve %>,
     },
     <%_ } -%>
+    interfaces: () => [NodeInterface],
   }),
 });
diff --git a/packages/generator/src/utils.js b/packages/generator/src/utils.js
index 1721a37..56bacc4 100644
--- a/packages/generator/src/utils.js
+++ b/packages/generator/src/utils.js
@@ -7,50 +7,87 @@ const { visit } = recast.types;
 
 const rootPath = pkgDir.sync('.') || '.';
 
-const parseFieldToGraphQL = (field) => {
+const parseFieldToGraphQL = (field, ref) => {
   const graphQLField = {
     name: field.name,
     description: field.description,
     required: !!field.required,
     originalType: field.type,
+    resolve: `obj.${field.name}`,
   };
 
+  const name = uppercaseFirstLetter(field.name);
+  const typeFileName = `${name}Type`;
+  const loaderFileName = `${name}Loader`;
+
   switch (field.type) {
     case 'Number':
       return {
+        ...graphQLField,
         type: 'GraphQLInt',
         flowType: 'number',
-        ...graphQLField,
       };
     case 'Boolean':
       return {
+        ...graphQLField,
         type: 'GraphQLBoolean',
         flowType: 'boolean',
-        ...graphQLField,
       };
-    case 'ObjectId': {
+    case 'ObjectId':
+      if (ref) {
+        return {
+          ...graphQLField,
+          type: typeFileName,
+          flowType: 'string',
+          resolve: `await ${loaderFileName}.load(user, obj.${field.name})`,
+          resolveArgs: 'async (obj, args, { user })',
+          graphqlType: typeFileName,
+          graphqlLoader: loaderFileName,
+        };
+      } else {
+        return {
+          ...graphQLField,
+          type: 'GraphQLID',
+          flowType: 'string',
+        }
+      }
+    case 'Date':
       return {
-        type: 'GraphQLID',
-        flowType: 'string',
         ...graphQLField,
+        type: 'GraphQLString',
+        flowType: 'string',
+        resolve: `obj.${field.name}.toISOString()`,
       };
-    }
     default:
       return {
+        ...graphQLField,
         type: 'GraphQLString',
         flowType: 'string',
-        ...graphQLField,
       };
   }
 };
 
-const parseGraphQLSchema = (mongooseFields) => {
+const parseGraphQLSchema = (mongooseFields, ref) => {
   const dependencies = [];
+  const typeDependencies = [];
+  const loaderDependencies = [];
+
   const fields = Object.keys(mongooseFields).map((name) => {
-    const field = parseFieldToGraphQL(mongooseFields[name]);
+    const field = parseFieldToGraphQL(mongooseFields[name], ref);
 
-    if (dependencies.indexOf(field.type) === -1) {
-      dependencies.push(field.type);
+    if (field.graphqlType) {
+      if (typeDependencies.indexOf(field.graphqlType) === -1) {
+        typeDependencies.push(field.graphqlType);
+      }
+
+      if (loaderDependencies.indexOf(field.graphqlLoader) === -1) {
+        loaderDependencies.push(field.graphqlLoader);
+      }
+
+    } else {
+      if (dependencies.indexOf(field.type) === -1) {
+        dependencies.push(field.type);
+      }
     }
 
     return field;
@@ -59,6 +96,8 @@ const parseGraphQLSchema = (mongooseFields) => {
   return {
     fields,
     dependencies,
+    typeDependencies,
+    loaderDependencies,
   };
 };
 
@@ -120,7 +159,7 @@ const getSchemaTimestampsFromAst = (nodes) => {
   return timestampFields;
 };
 
-const getSchemaDefinition = (modelCode, withTimestamps) => {
+const getSchemaDefinition = (modelCode, withTimestamps, ref) => {
   const ast = recast.parse(modelCode, {
     parser: {
       parse: source => require('babylon').parse(source, {
@@ -158,7 +197,7 @@ const getSchemaDefinition = (modelCode, withTimestamps) => {
     },
   });
 
-  return parseGraphQLSchema(fields);
+  return parseGraphQLSchema(fields, ref);
 };
 
 /**
@@ -239,19 +278,23 @@ export const getRelativeConfigDir = (from, to) => {
 
     return {
       ...directories,
-      [dir]: relativePath,
+      [dir]: relativePath === '' ? '.' : relativePath,
     };
   }, {});
 };
 
-export const getMongooseModelSchema = (model, withTimestamps = false) => {
+export const getMongooseModelSchema = ({
+  model,
+  withTimestamps = false,
+  ref = false
+}) => {
   const modelDir = getCreateGraphQLConfig().directories.model;
 
   const modelPath = path.resolve(`${modelDir}/${model}.js`);
 
   const modelCode = getModelCode(modelPath);
 
-  return getSchemaDefinition(modelCode, withTimestamps);
+  return getSchemaDefinition(modelCode, withTimestamps, ref);
 };
 
 /**
@@ -281,4 +324,4 @@ export const camelCaseText = (text) => {
  * @param text {string}
  * @returns {string}
  */
-export const uppercaseFirstLetter = text => `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
\ No newline at end of file
+export const uppercaseFirstLetter = text => `${text.charAt(0).toUpperCase()}${text.slice(1)}`;


Test patch:
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 46d6f34..b0345d5 100644
--- a/packages/generator/src/type/__tests__/__snapshots__/TypeGenerator.spec.js.snap
+++ b/packages/generator/src/type/__tests__/__snapshots__/TypeGenerator.spec.js.snap
@@ -6,6 +6,8 @@ Object {
 } from \'graphql\';
 import { globalIdField } from \'graphql-relay\';
 
+import { NodeInterface } from \'../interface/NodeInterface\';
+
 export default new GraphQLObjectType({
   name: \'Example\',
   description: \'Represents Example\',
@@ -16,6 +18,7 @@ export default new GraphQLObjectType({
       description: \'My example field\',
       resolve: obj => obj.example,
     },
+    interfaces: () => [NodeInterface],
   }),
 });",
   "typeTest": "import { graphql } from \'graphql\';
@@ -62,10 +65,13 @@ Object {
 import {
   GraphQLObjectType,
   GraphQLString,
-  GraphQLID,
 } from \'graphql\';
 import { globalIdField } from \'graphql-relay\';
 
+import { NodeInterface } from \'../interface/NodeInterface\';
+import UserType from \'./UserType\';
+import UserLoader from \'../loader/UserLoader\';
+
 export default new GraphQLObjectType({
   name: \'Post\',
   description: \'Represents Post\',
@@ -77,9 +83,9 @@ export default new GraphQLObjectType({
       resolve: obj => obj.title,
     },
     user: {
-      type: GraphQLID,
+      type: UserType,
       description: \'User that created this post\',
-      resolve: obj => obj.user,
+      resolve: async (obj, args, { user }) => await UserLoader.load(user, obj.user),
     },
     slug: {
       type: GraphQLString,
@@ -96,6 +102,7 @@ export default new GraphQLObjectType({
       description: \'\',
       resolve: obj => obj.updatedAt.toISOString(),
     },
+    interfaces: () => [NodeInterface],
   }),
 });
 ",
@@ -147,6 +154,8 @@ import {
 } from \'graphql\';
 import { globalIdField } from \'graphql-relay\';
 
+import { NodeInterface } from \'../interface/NodeInterface\';
+
 export default new GraphQLObjectType({
   name: \'User\',
   description: \'Represents User\',
@@ -172,6 +181,7 @@ export default new GraphQLObjectType({
       description: \'\',
       resolve: obj => obj.active,
     },
+    interfaces: () => [NodeInterface],
   }),
 });
 ",