├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── lib ├── getType.js ├── getTypeObjects.js ├── getTypeTree.js └── index.js ├── package.json ├── rollup.config.js └── test ├── getType-test.js ├── getTypeObjects-test.js ├── getTypeTree-test.js ├── index-test.js └── models.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongoose-graphql [![CircleCI](https://circleci.com/gh/zipdrug/mongoose-graphql.svg?style=svg)](https://circleci.com/gh/zipdrug/mongoose-graphql) 2 | 3 | [mongoose-graphql](https://github.com/zipdrug/mongoose-graphql) converts a mongoose model to graphql types. 4 | 5 | ## Installation 6 | 7 | Using [npm](https://www.npmjs.org/): 8 | 9 | $ npm install --save mongoose-graphql 10 | 11 | ```js 12 | // using ES6 modules 13 | import { modelToType } from 'mongoose-graphql'; 14 | 15 | // using CommonJS modules 16 | var mongooseGraphQL = require('mongoose-graphql'); 17 | var modelToType = mongooseGraphQL.modelToType; 18 | ``` 19 | ## API 20 | 21 | ### modelToType 22 | 23 | > `modelToType(model, options)` 24 | 25 | Convert a mongoose model to graphql types. 26 | 27 | You can use this type definition in [graphql-tools](https://github.com/apollostack/graphql-tools) to build an executable schema. 28 | 29 | ```js 30 | const CategorySchema = new Schema({ 31 | type: String, 32 | }); 33 | 34 | const BookModel = mongoose.model('Book', new Schema({ 35 | category: CategorySchema, 36 | name: String, 37 | })); 38 | 39 | const typeDef = modelToType(BookModel, { 40 | extend: { 41 | Book: { 42 | publishers: '[Publisher]', 43 | }, 44 | BookCategory: { 45 | genre: 'Genre', 46 | }, 47 | }, 48 | }); 49 | 50 | console.log(typeDef); 51 | ``` 52 | 53 | Outputs: 54 | ``` 55 | type BookCategory { 56 | _id: String 57 | genre: Genre 58 | type: String 59 | } 60 | type Book { 61 | _id: String 62 | category: BookCategory 63 | name: String 64 | publishers: [Publisher] 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /lib/getType.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | const getType = (typeObject) => { 4 | let typeString = `type ${typeObject.type} {\n`; 5 | 6 | Object.keys(typeObject.properties) 7 | .sort() 8 | .forEach((key) => { 9 | typeString += ` ${key}: ${typeObject.properties[key]}\n`; 10 | }); 11 | 12 | typeString += '}'; 13 | 14 | return typeString; 15 | }; 16 | 17 | export default getType; 18 | -------------------------------------------------------------------------------- /lib/getTypeObjects.js: -------------------------------------------------------------------------------- 1 | import { forOwn, upperFirst } from 'lodash'; 2 | import pluralize from 'pluralize'; 3 | 4 | const getTypeObjects = (name, typeTree) => { 5 | let typeObjects = []; 6 | 7 | const currentType = { 8 | type: name, 9 | properties: {}, 10 | }; 11 | 12 | forOwn(typeTree, (value, key) => { 13 | const isArray = Array.isArray(value); 14 | const typeValue = isArray ? value[0] : value; 15 | 16 | let type; 17 | if (typeof typeValue === 'object') { 18 | const childTypeName = pluralize(`${name}${upperFirst(key)}`, 1); 19 | 20 | // Add the child type objects to the front 21 | const childTypeObjects = getTypeObjects(childTypeName, typeValue); 22 | typeObjects = childTypeObjects.concat(typeObjects); 23 | 24 | type = childTypeName; 25 | } else { 26 | type = typeValue; 27 | } 28 | 29 | if (isArray) { 30 | type = `[${type}]`; 31 | } 32 | 33 | currentType.properties[key] = type; 34 | }); 35 | 36 | 37 | if (Object.keys(currentType.properties).length > 0) { 38 | typeObjects.push(currentType); 39 | } 40 | 41 | return typeObjects; 42 | }; 43 | 44 | export default getTypeObjects; 45 | -------------------------------------------------------------------------------- /lib/getTypeTree.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | import { forOwn } from 'lodash'; 3 | 4 | const setDescendant = (tree, key, value) => { 5 | let parentTree = tree; 6 | 7 | // Make sure there is an object for each of the ancestors 8 | // Ex. 'location.address.street1'' -> { location: { address: {} } } 9 | const splitPath = key.split('.'); 10 | for (let i = 0; i < splitPath.length - 1; i += 1) { 11 | const ancestor = splitPath[i]; 12 | parentTree[ancestor] = parentTree[ancestor] || {}; 13 | parentTree = parentTree[ancestor]; 14 | } 15 | 16 | const property = splitPath[splitPath.length - 1]; 17 | parentTree[property] = value; 18 | }; 19 | 20 | const instanceToType = (instance) => { 21 | switch (instance) { 22 | case 'Boolean': 23 | return 'Boolean'; 24 | case 'ObjectID': 25 | case 'String': 26 | return 'String'; 27 | case 'Date': 28 | case 'Number': 29 | return 'Float'; 30 | default: 31 | throw new Error(`${instance} not implemented yet in instanceToType`); 32 | } 33 | }; 34 | 35 | const arrayToTree = (path) => { 36 | if (path.caster && path.caster.instance) { 37 | return [instanceToType(path.caster.instance)]; 38 | } else if (path.casterConstructor) { 39 | return [getTypeTree(path.casterConstructor.schema.paths)]; 40 | } 41 | 42 | throw new Error(`${path} is not a supported path`); 43 | }; 44 | 45 | const getTypeTree = (schemaPaths) => { 46 | const typeTree = {}; 47 | 48 | forOwn(schemaPaths, (path, key) => { 49 | if (key === '__v') { 50 | return; 51 | } 52 | 53 | let value; 54 | 55 | if (path.instance === 'Array') { 56 | value = arrayToTree(path); 57 | } else if (path.instance === 'Embedded') { 58 | value = getTypeTree(path.caster.schema.paths); 59 | } else { 60 | value = instanceToType(path.instance); 61 | } 62 | 63 | setDescendant(typeTree, key, value); 64 | }); 65 | 66 | return typeTree; 67 | }; 68 | 69 | export default getTypeTree; 70 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { find, forOwn } from 'lodash'; 3 | 4 | import getType from './getType'; 5 | import getTypeObjects from './getTypeObjects'; 6 | import getTypeTree from './getTypeTree'; 7 | 8 | export const modelToType = (model, options = {}) => { 9 | const schema = model.schema; 10 | const typeTree = getTypeTree(schema.paths); 11 | 12 | const typeObjects = getTypeObjects(options.name || model.modelName, typeTree); 13 | if (options.extend) { 14 | forOwn(options.extend, (extension, type) => { 15 | const typeObject = find(typeObjects, t => t.type === type); 16 | Object.assign(typeObject.properties, extension); 17 | }); 18 | } 19 | 20 | const typeStrings = typeObjects.map(getType); 21 | return typeStrings.join('\n'); 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-graphql", 3 | "version": "0.0.8", 4 | "description": "Convert a mongoose model to a GraphQL type string", 5 | "repository": "zipdrug/mongoose-graphql", 6 | "main": "dist/mongoose-graphql.js", 7 | "module": "dist/mongoose-graphql.mjs", 8 | "jsnext:main": "dist/mongoose-graphql.mjs", 9 | "scripts": { 10 | "build": "rollup -c", 11 | "lint": "eslint lib test", 12 | "prepublish": "npm run lint && npm run test && npm run build", 13 | "test": "ava test/**-test.js", 14 | "test:watch": "npm test -- --watch" 15 | }, 16 | "author": "Zipdrug", 17 | "license": "MIT", 18 | "files": [ 19 | "lib", 20 | "dist" 21 | ], 22 | "ava": { 23 | "require": [ 24 | "babel-register" 25 | ] 26 | }, 27 | "dependencies": { 28 | "lodash": "^4.16.4", 29 | "pluralize": "^3.0.0" 30 | }, 31 | "devDependencies": { 32 | "ava": "^0.16.0", 33 | "babel-plugin-external-helpers": "^6.8.0", 34 | "babel-preset-es2015": "^6.16.0", 35 | "babel-register": "^6.16.3", 36 | "babelrc-rollup": "^3.0.0", 37 | "eslint": "^3.8.0", 38 | "eslint-config-airbnb-base": "^9.0.0", 39 | "eslint-plugin-import": "^2.0.1", 40 | "mongoose": "^4.6.4", 41 | "rollup": "^0.36.3", 42 | "rollup-plugin-babel": "^2.6.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import babelrc from 'babelrc-rollup'; 3 | 4 | let pkg = require('./package.json'); 5 | let external = Object.keys(pkg.dependencies); 6 | 7 | export default { 8 | entry: 'lib/index.js', 9 | plugins: [ 10 | babel(babelrc()), 11 | ], 12 | external, 13 | targets: [ 14 | { 15 | dest: pkg['main'], 16 | format: 'umd', 17 | moduleName: 'mongooseGraphql', 18 | sourceMap: true, 19 | }, 20 | { 21 | dest: pkg['jsnext:main'], 22 | format: 'es', 23 | sourceMap: true, 24 | } 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /test/getType-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import test from 'ava'; 3 | import getType from '../lib/getType'; 4 | 5 | test('converts a type object to a type string', (t) => { 6 | const typeObject = { 7 | type: 'OrderNote', 8 | properties: { 9 | author: 'String', 10 | message: 'String', 11 | }, 12 | }; 13 | 14 | const typeString = `type OrderNote { 15 | author: String 16 | message: String 17 | }`; 18 | 19 | t.is(getType(typeObject), typeString); 20 | }); 21 | -------------------------------------------------------------------------------- /test/getTypeObjects-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import test from 'ava'; 3 | import getTypeObjects from '../lib/getTypeObjects'; 4 | 5 | test('getTypeObjects supports primitive paths', (t) => { 6 | const primitiveTree = { 7 | name: 'String', 8 | no: 'Float', 9 | }; 10 | 11 | const primitiveTypeObjects = [ 12 | { 13 | type: 'Order', 14 | properties: { 15 | name: 'String', 16 | no: 'Float', 17 | }, 18 | }, 19 | ]; 20 | 21 | t.deepEqual(getTypeObjects('Order', primitiveTree), primitiveTypeObjects); 22 | }); 23 | 24 | test('getTypeObjects supports complex paths', (t) => { 25 | const complexTree = { 26 | name: 'String', 27 | location: { 28 | instructions: 'String', 29 | placeId: 'String', 30 | }, 31 | }; 32 | 33 | const complexTypeObjects = [ 34 | { 35 | type: 'OrderLocation', 36 | properties: { 37 | instructions: 'String', 38 | placeId: 'String', 39 | }, 40 | }, 41 | { 42 | type: 'Order', 43 | properties: { 44 | location: 'OrderLocation', 45 | name: 'String', 46 | }, 47 | }, 48 | ]; 49 | 50 | t.deepEqual(getTypeObjects('Order', complexTree), complexTypeObjects); 51 | }); 52 | 53 | test('getTypeObjects supports primitive arrays', (t) => { 54 | const primitiveArrayTree = { 55 | statuses: ['String'], 56 | }; 57 | 58 | const primitiveArrayObjects = [ 59 | { 60 | type: 'Order', 61 | properties: { 62 | statuses: '[String]', 63 | }, 64 | }, 65 | ]; 66 | 67 | t.deepEqual(getTypeObjects('Order', primitiveArrayTree), primitiveArrayObjects); 68 | }); 69 | 70 | test('getTypeObjects supports complex arrays', (t) => { 71 | const complexArrayTree = { 72 | notes: [{ 73 | author: 'String', 74 | message: 'String', 75 | }], 76 | }; 77 | 78 | const complexArrayObjects = [ 79 | { 80 | type: 'OrderNote', 81 | properties: { 82 | author: 'String', 83 | message: 'String', 84 | }, 85 | }, 86 | { 87 | type: 'Order', 88 | properties: { 89 | notes: '[OrderNote]', 90 | }, 91 | }, 92 | ]; 93 | 94 | t.deepEqual(getTypeObjects('Order', complexArrayTree), complexArrayObjects); 95 | }); 96 | -------------------------------------------------------------------------------- /test/getTypeTree-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import test from 'ava'; 3 | import getTypeTree from '../lib/getTypeTree'; 4 | 5 | test('getTypeTree supports primitive paths', (t) => { 6 | const primitivePaths = { 7 | isRush: { 8 | instance: 'Boolean', 9 | }, 10 | name: { 11 | instance: 'String', 12 | }, 13 | no: { 14 | instance: 'Number', 15 | }, 16 | }; 17 | 18 | const primitiveTree = { 19 | isRush: 'Boolean', 20 | name: 'String', 21 | no: 'Float', 22 | }; 23 | 24 | t.deepEqual(getTypeTree(primitivePaths), primitiveTree); 25 | }); 26 | 27 | test('getTypeTree supports complex paths', (t) => { 28 | const complexPaths = { 29 | 'location.instructions': { 30 | instance: 'String', 31 | }, 32 | 'location.placeId': { 33 | instance: 'ObjectID', 34 | }, 35 | name: { 36 | instance: 'String', 37 | }, 38 | }; 39 | 40 | const complexTree = { 41 | name: 'String', 42 | location: { 43 | instructions: 'String', 44 | placeId: 'String', 45 | }, 46 | }; 47 | 48 | t.deepEqual(getTypeTree(complexPaths), complexTree); 49 | }); 50 | 51 | test('getTypeTree supports embedded paths', (t) => { 52 | const embeddedPath = { 53 | category: { 54 | instance: 'Embedded', 55 | caster: { 56 | schema: { 57 | paths: { 58 | author: { 59 | instance: 'String', 60 | }, 61 | message: { 62 | instance: 'String', 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | }; 69 | 70 | const embeddedTree = { 71 | category: { 72 | author: 'String', 73 | message: 'String', 74 | }, 75 | }; 76 | 77 | t.deepEqual(getTypeTree(embeddedPath), embeddedTree); 78 | }); 79 | 80 | test('getTypeTree supports primitive arrays', (t) => { 81 | const primitiveArray = { 82 | statuses: { 83 | instance: 'Array', 84 | caster: { 85 | instance: 'String', 86 | }, 87 | }, 88 | }; 89 | 90 | const primitiveArrayTree = { 91 | statuses: ['String'], 92 | }; 93 | 94 | t.deepEqual(getTypeTree(primitiveArray), primitiveArrayTree); 95 | }); 96 | 97 | test('getTypeTree supports complex arrays', (t) => { 98 | const complexArray = { 99 | notes: { 100 | instance: 'Array', 101 | casterConstructor: { 102 | schema: { 103 | paths: { 104 | author: { 105 | instance: 'String', 106 | }, 107 | message: { 108 | instance: 'String', 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | }; 115 | 116 | const complexArrayTree = { 117 | notes: [{ 118 | author: 'String', 119 | message: 'String', 120 | }], 121 | }; 122 | 123 | t.deepEqual(getTypeTree(complexArray), complexArrayTree); 124 | }); 125 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import test from 'ava'; 3 | import { modelToType } from '../lib/index'; 4 | import { BookModel, BookTypes, BookTypesExtended, NotebookTypes, OrderModel, OrderTypes, StoreModel, StoreType } from './models'; 5 | 6 | test('flat StoreModel converts to a type string', (t) => { 7 | const schema = modelToType(StoreModel); 8 | t.is(schema, StoreType); 9 | }); 10 | 11 | test('nested OrderModel converts to a type string', (t) => { 12 | const schema = modelToType(OrderModel); 13 | t.is(schema, OrderTypes, `Expected\n${schema}\nto equal\n${OrderTypes}`); 14 | }); 15 | 16 | test('embedded BookModel converts to a type string', (t) => { 17 | const schema = modelToType(BookModel); 18 | t.is(schema, BookTypes, `Expected\n${schema}\nto equal\n${BookTypes}`); 19 | }); 20 | 21 | test('can extend generated types', (t) => { 22 | const schema = modelToType(BookModel, { 23 | extend: { 24 | Book: { 25 | publishers: '[Publisher]', 26 | }, 27 | BookCategory: { 28 | genre: 'Genre', 29 | }, 30 | }, 31 | }); 32 | 33 | t.is(schema, BookTypesExtended, `Expected\n${schema}\nto equal\n${BookTypesExtended}`); 34 | }); 35 | 36 | test('can overwrite generated type name', (t) => { 37 | const schema = modelToType(BookModel, { 38 | name: 'Notebook', 39 | }); 40 | 41 | t.is(schema, NotebookTypes, `Expected\n${schema}\nto equal\n${NotebookTypes}`); 42 | }); 43 | -------------------------------------------------------------------------------- /test/models.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import mongoose, { Schema } from 'mongoose'; 3 | 4 | export const StoreModel = mongoose.model('Store', new Schema({ 5 | joinDate: Date, 6 | name: String, 7 | no: Number, 8 | placeId: String, 9 | })); 10 | 11 | export const StoreType = `type Store { 12 | _id: String 13 | joinDate: Float 14 | name: String 15 | no: Float 16 | placeId: String 17 | }`; 18 | 19 | export const OrderModel = mongoose.model('Order', new Schema({ 20 | name: String, 21 | location: { 22 | deliveryInstructions: String, 23 | placeId: String, 24 | }, 25 | statusHistory: [{ 26 | note: String, 27 | status: String, 28 | }], 29 | tags: [String], 30 | })); 31 | 32 | export const OrderTypes = `type OrderStatusHistory { 33 | _id: String 34 | note: String 35 | status: String 36 | } 37 | type OrderLocation { 38 | deliveryInstructions: String 39 | placeId: String 40 | } 41 | type Order { 42 | _id: String 43 | location: OrderLocation 44 | name: String 45 | statusHistory: [OrderStatusHistory] 46 | tags: [String] 47 | }`; 48 | 49 | const CategorySchema = new Schema({ 50 | type: String, 51 | }); 52 | 53 | export const BookModel = mongoose.model('Book', new Schema({ 54 | category: CategorySchema, 55 | name: String, 56 | })); 57 | 58 | export const BookTypes = `type BookCategory { 59 | _id: String 60 | type: String 61 | } 62 | type Book { 63 | _id: String 64 | category: BookCategory 65 | name: String 66 | }`; 67 | 68 | export const BookTypesExtended = `type BookCategory { 69 | _id: String 70 | genre: Genre 71 | type: String 72 | } 73 | type Book { 74 | _id: String 75 | category: BookCategory 76 | name: String 77 | publishers: [Publisher] 78 | }`; 79 | 80 | export const NotebookTypes = `type NotebookCategory { 81 | _id: String 82 | type: String 83 | } 84 | type Notebook { 85 | _id: String 86 | category: NotebookCategory 87 | name: String 88 | }`; 89 | --------------------------------------------------------------------------------