├── example ├── .eslint.yaml ├── package.json ├── user.js ├── app.js └── README.md ├── .npmignore ├── src ├── type │ ├── index.js │ ├── custom │ │ ├── generic.js │ │ ├── buffer.js │ │ ├── date.js │ │ ├── to-input-object.js │ │ ├── buffer.spec.js │ │ ├── generic.spec.js │ │ └── date.spec.js │ ├── type.spec.js │ └── type.js ├── model │ ├── index.js │ ├── viewer.js │ ├── README.md │ ├── model.spec.js │ └── model.js ├── query │ ├── index.js │ ├── projection │ │ ├── projection.spec.js │ │ └── index.js │ ├── query.spec.js │ └── query.js ├── schema │ ├── index.js │ ├── schema.spec.js │ └── schema.js ├── index.js ├── utils │ ├── index.js │ ├── Middleware.spec.js │ └── Middleware.js ├── setup-tests.spec.js └── e2e.spec.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── .babelrc ├── .gitignore ├── circle.yml ├── .eslintrc.yaml ├── LICENSE ├── fixture └── user.js ├── package.json ├── README.md └── CHANGELOG.md /example/.eslint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | no-console: 0 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .travis.yml 3 | example 4 | *.spec.js 5 | -------------------------------------------------------------------------------- /src/type/index.js: -------------------------------------------------------------------------------- 1 | export * from './type'; 2 | export { default } from './type'; 3 | -------------------------------------------------------------------------------- /src/model/index.js: -------------------------------------------------------------------------------- 1 | export * from './model'; 2 | export { default } from './model'; 3 | -------------------------------------------------------------------------------- /src/query/index.js: -------------------------------------------------------------------------------- 1 | export * from './query'; 2 | export { default } from './query'; 3 | -------------------------------------------------------------------------------- /src/schema/index.js: -------------------------------------------------------------------------------- 1 | export * from './schema'; 2 | export { default } from './schema'; 3 | -------------------------------------------------------------------------------- /src/model/viewer.js: -------------------------------------------------------------------------------- 1 | const viewer = { 2 | _type: 'Viewer', 3 | id: 'viewer' 4 | }; 5 | 6 | export default viewer; 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before sending a pull-request please open an issue to discuss new features or non-trivial changes. 2 | 3 | ### Thank you for your contribution! 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "passPerPreset": true, 3 | "presets": [{ 4 | "plugins": [ 5 | "transform-runtime", 6 | "array-includes" 7 | ] 8 | }, { 9 | "passPerPreset": false, 10 | "presets": [ 11 | "es2015", 12 | "stage-0" 13 | ] 14 | }] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | lib 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage directory used by tools like istanbul 12 | coverage 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.1.0 4 | environment: 5 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 6 | 7 | dependencies: 8 | override: 9 | - yarn 10 | cache_directories: 11 | - ~/.cache/yarn 12 | 13 | test: 14 | override: 15 | - yarn run lint 16 | - yarn test 17 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graffiti-mongoose-example", 3 | "description": "An example using graffiti-mongoose", 4 | "scripts": { 5 | "start": "babel-node app.js" 6 | }, 7 | "dependencies": { 8 | "@risingstack/graffiti": "3.1.2", 9 | "@risingstack/graffiti-mongoose": "../", 10 | "babel-cli": "6.14.0", 11 | "babel-runtime": "6.11.6", 12 | "koa": "1.2.4", 13 | "koa-bodyparser": "2.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser: babel-eslint 3 | extends: airbnb-base 4 | env: 5 | es6: true 6 | node: true 7 | mocha: true 8 | rules: 9 | generator-star-spacing: 0 10 | no-shadow: 0 11 | no-param-reassign: 0 12 | comma-dangle: 0 13 | no-underscore-dangle: 0 14 | import/no-extraneous-dependencies: 0 15 | no-prototype-builtins: 0 16 | max-len: 17 | - 2 18 | - 120 19 | - 2 20 | arrow-parens: 21 | - 2 22 | - always 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql'; 2 | import { getTypes } from './type'; 3 | import { getSchema } from './schema'; 4 | import { getModels } from './model'; 5 | 6 | function _getTypes(mongooseModels) { 7 | const graffitiModels = getModels(mongooseModels); 8 | return getTypes(graffitiModels); 9 | } 10 | 11 | export default { 12 | graphql, 13 | getSchema, 14 | getTypes: _getTypes 15 | }; 16 | 17 | export { 18 | graphql, 19 | getSchema, 20 | _getTypes as getTypes 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import Middleware from './Middleware'; 2 | 3 | function addHooks(resolver, { pre, post } = {}) { 4 | return async function resolve(...args) { 5 | const preMiddleware = new Middleware(pre); 6 | await preMiddleware.compose(...args); 7 | const postMiddleware = new Middleware(post); 8 | const result = await resolver(...args); 9 | return await postMiddleware.compose(result, ...args) || result; 10 | }; 11 | } 12 | 13 | export default { 14 | Middleware, 15 | addHooks 16 | }; 17 | 18 | export { 19 | Middleware, 20 | addHooks 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/Middleware.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Middleware } from './'; 3 | 4 | describe('Middleware', () => { 5 | it('should work properly', (done) => { 6 | const middleware = new Middleware(); 7 | 8 | const fn1 = (next, { data }) => { 9 | expect(data).to.be.eql(1); 10 | next({ data: 2 }); 11 | }; 12 | middleware.use(fn1); 13 | 14 | const fn2 = (next, { data }) => { 15 | expect(data).to.be.eql(2); 16 | next({ data: 3 }); 17 | }; 18 | middleware.use(fn2); 19 | 20 | middleware.compose({ data: 1 }).then((result) => { 21 | expect(result).to.be.eql({ data: 3 }); 22 | done(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/type/custom/generic.js: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, GraphQLError } from 'graphql'; 2 | import { Kind } from 'graphql/language'; 3 | 4 | function coerceDate(value) { 5 | const json = JSON.stringify(value); 6 | return json.replace(/\"/g, '\''); // eslint-disable-line 7 | } 8 | 9 | export default new GraphQLScalarType({ 10 | name: 'Generic', 11 | serialize: coerceDate, 12 | parseValue: coerceDate, 13 | parseLiteral(ast) { 14 | if (ast.kind !== Kind.STRING) { 15 | throw new GraphQLError(`Query error: Can only parse strings to buffers but got a: ${ast.kind}`, [ast]); 16 | } 17 | 18 | const json = ast.value.replace(/\'/g, '"'); // eslint-disable-line 19 | return JSON.parse(json); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/model/README.md: -------------------------------------------------------------------------------- 1 | ## Graffiti Model 2 | 3 | ```javascript 4 | { 5 | Name: { 6 | name: String!, 7 | description: String?, 8 | fields: { 9 | fieldName: { 10 | name: String? // default: key 11 | description: String?, 12 | nonNull: Boolean?, // required? 13 | hidden: Boolean? // included in the GraphQL schema 14 | hooks: { 15 | pre: (Function|Array)? 16 | post: (Function|Array)? 17 | }? 18 | type: String('String'|'Number'|'Date'|'Buffer'|'Boolean'|'ObjectID'|'Object'|'Array'), 19 | // if type == Array 20 | subtype: String('String'|'Number'|'Date'|'Buffer'|'Boolean'|'ObjectID'|'Object'|'Array'), 21 | // if type == Object 22 | fields: { 23 | // ... 24 | }, 25 | // if type == ObjectID 26 | reference: String! 27 | } 28 | } 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /src/type/custom/buffer.js: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | import { GraphQLError } from 'graphql/error'; 3 | import { Kind } from 'graphql/language'; 4 | 5 | function coerceBuffer(value) { 6 | if (!(value instanceof Buffer)) { 7 | throw new TypeError('Field error: value is not an instance of Buffer'); 8 | } 9 | 10 | return value.toString(); 11 | } 12 | 13 | export default new GraphQLScalarType({ 14 | name: 'Buffer', 15 | serialize: coerceBuffer, 16 | parseValue: coerceBuffer, 17 | parseLiteral(ast) { 18 | if (ast.kind !== Kind.STRING) { 19 | throw new GraphQLError(`Query error: Can only parse strings to buffers but got a: ${ast.kind}`, [ast]); 20 | } 21 | 22 | const result = new Buffer(ast.value); 23 | 24 | if (ast.value !== result.toString()) { 25 | throw new GraphQLError('Query error: Invalid buffer encoding', [ast]); 26 | } 27 | 28 | return result; 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/query/projection/projection.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import getFieldList from './'; 3 | 4 | describe('projection', () => { 5 | it('should return an object of fields (\'Field\' fragment)', () => { 6 | const info = { 7 | fieldNodes: { 8 | kind: 'Field', 9 | name: { value: 'foo' }, 10 | selectionSet: { 11 | selections: [{ 12 | kind: 'Field', 13 | name: { value: 'bar' }, 14 | selectionSet: { 15 | selections: [{ 16 | kind: 'Field', 17 | name: { value: 'baz' } 18 | }] 19 | } 20 | }] 21 | } 22 | } 23 | }; 24 | const fields = getFieldList(info); 25 | expect(fields).to.be.eql({ 26 | bar: true, 27 | baz: true 28 | }); 29 | }); 30 | 31 | it('should return an object of fields (\'InlineFragment\' fragment)'); 32 | it('should return an object of fields (\'FragmentSpread\' fragment)'); 33 | }); 34 | -------------------------------------------------------------------------------- /example/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | name: { 5 | type: String 6 | }, 7 | age: { 8 | type: Number, 9 | index: true 10 | }, 11 | createdAt: { 12 | type: Date, 13 | default: Date.now() 14 | }, 15 | friends: [ 16 | { 17 | type: mongoose.Schema.Types.ObjectId, 18 | ref: 'User' 19 | } 20 | ] 21 | }); 22 | 23 | const User = mongoose.model('User', UserSchema); 24 | 25 | // Generate sample data 26 | User.remove().then(() => { 27 | const users = []; 28 | for (let i = 0; i < 100; i++) { 29 | users.push(new User({ 30 | name: `User${i}`, 31 | age: i, 32 | createdAt: new Date() + (i * 100), 33 | friends: users.map((i) => i._id), 34 | nums: [0, i], 35 | bools: [true, false], 36 | strings: ['foo', 'bar'], 37 | removed: false, 38 | body: { 39 | eye: 'blue', 40 | hair: 'yellow' 41 | } 42 | })); 43 | } 44 | User.create(users); 45 | }); 46 | 47 | export default User; 48 | -------------------------------------------------------------------------------- /src/setup-tests.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import chai from 'chai'; 3 | import sinonChai from 'sinon-chai'; 4 | import chaiSubset from 'chai-subset'; 5 | 6 | import mongoose from 'mongoose'; 7 | 8 | before((done) => { 9 | chai.use(sinonChai); 10 | chai.use(chaiSubset); 11 | 12 | mongoose.Promise = global.Promise; 13 | 14 | mongoose.connect('mongodb://localhost/graffiti-mongoose-test', () => { 15 | mongoose.connection.db.dropDatabase(done); 16 | }); 17 | 18 | sinon.stub.returnsWithResolve = function returnsWithResolve(data) { 19 | return this.returns(Promise.resolve(data)); 20 | }; 21 | 22 | sinon.stub.returnsWithReject = function returnsWithReject(error) { 23 | return this.returns(Promise.reject(error)); 24 | }; 25 | }); 26 | 27 | after((done) => { 28 | mongoose.models = {}; 29 | mongoose.modelSchemas = {}; 30 | mongoose.connection.close(() => done()); 31 | }); 32 | 33 | beforeEach(function sandbox() { 34 | this.sandbox = sinon.sandbox.create(); 35 | }); 36 | 37 | afterEach(function sandbox() { 38 | this.sandbox.restore(); 39 | }); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 RisingStack, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | import koa from 'koa'; 2 | import parser from 'koa-bodyparser'; 3 | import mongoose from 'mongoose'; 4 | import graffiti from '@risingstack/graffiti'; 5 | import { getSchema } from '../src'; 6 | 7 | import User from './user'; 8 | 9 | mongoose.Promise = global.Promise; 10 | mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost/graphql'); 11 | const port = process.env.PORT || 8080; 12 | 13 | const hooks = { 14 | viewer: { 15 | pre: (next, root, args, request) => { 16 | console.log(request); 17 | next(); 18 | }, 19 | post: (next, value) => { 20 | console.log(value); 21 | next(); 22 | } 23 | } 24 | }; 25 | const schema = getSchema([User], { hooks }); 26 | 27 | // set up example server 28 | const app = koa(); 29 | 30 | // parse body 31 | app.use(parser()); 32 | 33 | // attach graffiti-mongoose middleware 34 | app.use(graffiti.koa({ 35 | schema 36 | })); 37 | 38 | // redirect all requests to /graphql 39 | app.use(function *redirect(next) { 40 | this.redirect('/graphql'); 41 | yield next; 42 | }); 43 | 44 | app.listen(port, (err) => { 45 | if (err) { 46 | throw err; 47 | } 48 | console.log(`Started on http://localhost:${port}/`); 49 | }); 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Overview of the Issue 2 | 3 | Give a short description of the issue. 4 | 5 | ```javascript 6 | If an error was thrown, paste here the stack trace. 7 | ``` 8 | 9 | ## Reproduce the Error 10 | 11 | Provide an unambiguous set of steps, input schema, Node version, package version, etc. 12 | 13 | Steps to reproduce: 14 | 1. step 15 | 2. step 16 | 3. step 17 | 18 | Input schema(s): 19 | ```javascript 20 | // for example 21 | const UserSchema = new mongoose.Schema({ 22 | name: { 23 | type: String, 24 | nonNull: true 25 | }, 26 | age: { 27 | type: Number, 28 | index: true 29 | }, 30 | createdAt: Date, 31 | friends: [ 32 | { 33 | type: mongoose.Schema.Types.ObjectId, 34 | ref: 'User' 35 | } 36 | ], 37 | nums: [Number], 38 | bools: [Boolean], 39 | strings: [String], 40 | removed: Boolean 41 | }); 42 | ``` 43 | 44 | Node version: `4.4.7` 45 | Graffiti-Mongoose version: `5.2.0` 46 | 47 | ## Related Issues 48 | 49 | Has a similar issue been reported before? 50 | 51 | ## Suggest a Fix 52 | 53 | If you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit) 54 | 55 | ### Thank you for your contribution! 56 | -------------------------------------------------------------------------------- /fixture/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | name: { 5 | type: String 6 | }, 7 | age: { 8 | type: Number, 9 | index: true 10 | }, 11 | mother: { 12 | type: mongoose.Schema.Types.ObjectId, 13 | ref: 'User' 14 | }, 15 | friends: [{ 16 | type: mongoose.Schema.Types.ObjectId, 17 | ref: 'User' 18 | }], 19 | objectIds: [{ 20 | type: mongoose.Schema.Types.ObjectId 21 | }], 22 | weight: Number, // to test "floatish" numbers 23 | createdAt: Date, 24 | removed: Boolean, 25 | nums: [Number], 26 | strings: [String], 27 | bools: { 28 | type: [Boolean], 29 | hooks: { 30 | post: [(next, result) => { 31 | next(result.map((el) => !el)); 32 | }] 33 | } 34 | }, 35 | dates: [Date], 36 | sub: { 37 | foo: String, 38 | nums: [Number], 39 | subsub: { 40 | bar: Number 41 | }, 42 | subref: { 43 | type: mongoose.Schema.Types.ObjectId, 44 | ref: 'User' 45 | }, 46 | }, 47 | subArray: [{ 48 | foo: String, 49 | nums: [Number], 50 | brother: { 51 | type: mongoose.Schema.Types.ObjectId, 52 | ref: 'User' 53 | } 54 | }] 55 | }); 56 | 57 | const User = mongoose.model('User', UserSchema); 58 | 59 | export default User; 60 | -------------------------------------------------------------------------------- /src/query/projection/index.js: -------------------------------------------------------------------------------- 1 | function getFieldList(info, fieldNodes) { 2 | if (!info) { 3 | return {}; 4 | } 5 | 6 | fieldNodes = fieldNodes || info.fieldNodes; 7 | 8 | // for recursion 9 | // Fragments doesn't have many sets 10 | let nodes = fieldNodes; 11 | if (!Array.isArray(nodes)) { 12 | nodes = nodes ? [nodes] : []; 13 | } 14 | 15 | // get all selectionSets 16 | const selections = nodes.reduce((selections, source) => { 17 | if (source.selectionSet) { 18 | return selections.concat(source.selectionSet.selections); 19 | } 20 | 21 | return selections; 22 | }, []); 23 | 24 | // return fields 25 | return selections.reduce((list, ast) => { 26 | const { name, kind } = ast; 27 | 28 | switch (kind) { 29 | case 'Field': 30 | return { 31 | ...list, 32 | ...getFieldList(info, ast), 33 | [name.value]: true 34 | }; 35 | case 'InlineFragment': 36 | return { 37 | ...list, 38 | ...getFieldList(info, ast) 39 | }; 40 | case 'FragmentSpread': 41 | return { 42 | ...list, 43 | ...getFieldList(info, info.fragments[name.value]) 44 | }; 45 | default: 46 | throw new Error('Unsuported query selection'); 47 | } 48 | }, {}); 49 | } 50 | 51 | export default getFieldList; 52 | -------------------------------------------------------------------------------- /src/type/custom/date.js: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | import { GraphQLError } from 'graphql/error'; 3 | import { Kind } from 'graphql/language'; 4 | 5 | export default new GraphQLScalarType({ 6 | name: 'Date', 7 | serialize(value) { 8 | if (!(value instanceof Date)) { 9 | throw new TypeError('Field error: value is not an instance of Date'); 10 | } 11 | 12 | if (isNaN(value.getTime())) { 13 | throw new TypeError('Field error: value is an invalid Date'); 14 | } 15 | 16 | return value.toJSON(); 17 | }, 18 | parseValue(value) { 19 | const date = new Date(value); 20 | 21 | if (isNaN(date.getTime())) { 22 | throw new TypeError('Field error: value is an invalid Date'); 23 | } 24 | 25 | return date; 26 | }, 27 | parseLiteral(ast) { 28 | if (ast.kind !== Kind.STRING) { 29 | throw new GraphQLError(`Query error: Can only parse strings to buffers but got a: ${ast.kind}`, [ast]); 30 | } 31 | 32 | const result = new Date(ast.value); 33 | if (isNaN(result.getTime())) { 34 | throw new GraphQLError('Query error: Invalid date', [ast]); 35 | } 36 | 37 | if (ast.value !== result.toJSON()) { 38 | throw new GraphQLError('Query error: Invalid date format, only accepts: YYYY-MM-DDTHH:MM:SS.SSSZ', [ast]); 39 | } 40 | 41 | return result; 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /src/utils/Middleware.js: -------------------------------------------------------------------------------- 1 | import { 2 | isArray, 3 | isFunction, 4 | reduceRight 5 | } from 'lodash'; 6 | 7 | export default class Middleware { 8 | middleware = []; 9 | 10 | constructor(middleware) { 11 | this.use(middleware); 12 | } 13 | 14 | /** 15 | * Add middleware 16 | * @param {Function} middleware 17 | */ 18 | use(middleware = []) { 19 | if (!isArray(middleware)) { 20 | middleware = [middleware]; 21 | } 22 | // eslint-disable-next-line no-restricted-syntax 23 | for (const fn of middleware) { 24 | if (!isFunction(fn)) { 25 | throw new TypeError('Middleware must be composed of functions!'); 26 | } 27 | } 28 | this.middleware = [...this.middleware, ...middleware]; 29 | } 30 | 31 | /** 32 | * Compose all middleware 33 | * @return {Function} 34 | */ 35 | compose(...args) { 36 | let lastResult; 37 | return reduceRight(this.middleware, (mw, fn) => { 38 | const next = async function next(...result) { 39 | if (!result.length) { 40 | result = args; 41 | } 42 | lastResult = result[0]; 43 | await mw.call(this, ...result); 44 | }; 45 | return async function composed(...result) { 46 | if (!result.length) { 47 | result = args; 48 | } 49 | await fn.call(this, next, ...result); 50 | return lastResult; 51 | }; 52 | }, () => null)(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Graffiti-Mongoose 2 | 3 | ## Issues: got a question or problem? 4 | 5 | Before you submit your issue search the archive, maybe your question was already answered. 6 | 7 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 8 | Help us to maximize the effort we can spend fixing issues and adding new 9 | features, by not reporting duplicate issues. Providing the following information will increase the 10 | chances of your issue being dealt with quickly: 11 | 12 | - **Overview of the Issue** 13 | - **Reproduce the Error** 14 | - **Related Issues** 15 | - **Suggest a Fix** 16 | 17 | ## Submitting a Pull Request 18 | 19 | 1. Fork the project 20 | 2. Make your changes in a new git branch: 21 | 22 | ```shell 23 | git checkout -b fix/my-branch master 24 | ``` 25 | 3. Commit your changes using a descriptive commit message that follows the [Angular commit message format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) 26 | 27 | ```shell 28 | git commit -a 29 | ``` 30 | 4. Push your branch to GitHub: 31 | 32 | ```shell 33 | git push origin fix/my-branch 34 | ``` 35 | 5. In GitHub, send a pull request to `master` 36 | 37 | General checklist: 38 | - [ ] tests must pass 39 | - [ ] follow existing coding style 40 | - [ ] if you add a feature, add documentation to `README` 41 | - [ ] if you fix a bug, add a test 42 | 43 | #### Thank you for your contribution! 44 | -------------------------------------------------------------------------------- /src/type/custom/to-input-object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Detailed explanation https://github.com/graphql/graphql-js/issues/312#issuecomment-196169994 3 | */ 4 | 5 | import { 6 | GraphQLScalarType, 7 | GraphQLInputObjectType, 8 | GraphQLEnumType, 9 | GraphQLID 10 | } from 'graphql'; 11 | import { nodeInterface } from '../../schema/schema'; 12 | 13 | function filterFields(obj, filter) { 14 | return Object.keys(obj) 15 | .filter(filter) 16 | .reduce((result, key) => ({ 17 | ...result, 18 | [key]: convertInputObjectField(obj[key]) // eslint-disable-line 19 | }), {}); 20 | } 21 | 22 | const cachedTypes = {}; 23 | function createInputObject(type) { 24 | const typeName = `${type.name}Input`; 25 | 26 | if (!cachedTypes.hasOwnProperty(typeName)) { 27 | cachedTypes[typeName] = new GraphQLInputObjectType({ 28 | name: typeName, 29 | fields: {} 30 | }); 31 | cachedTypes[typeName]._typeConfig.fields = 32 | () => filterFields(type.getFields(), (field) => (!field.noInputObject)); // eslint-disable-line 33 | } 34 | 35 | return cachedTypes[typeName]; 36 | } 37 | 38 | function convertInputObjectField(field) { 39 | let fieldType = field.type; 40 | const wrappers = []; 41 | 42 | while (fieldType.ofType) { 43 | wrappers.unshift(fieldType.constructor); 44 | fieldType = fieldType.ofType; 45 | } 46 | 47 | if (!(fieldType instanceof GraphQLInputObjectType || 48 | fieldType instanceof GraphQLScalarType || 49 | fieldType instanceof GraphQLEnumType)) { 50 | fieldType = fieldType.getInterfaces().includes(nodeInterface) ? GraphQLID : createInputObject(fieldType); 51 | } 52 | 53 | fieldType = wrappers.reduce((type, Wrapper) => new Wrapper(type), fieldType); 54 | 55 | return { type: fieldType }; 56 | } 57 | 58 | export default createInputObject; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@risingstack/graffiti-mongoose", 3 | "version": "6.0.0", 4 | "description": "Mongoose adapter for graffiti (Node.js GraphQL ORM)", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test mocha --compilers js:babel-register 'src/**/*.spec.js'", 8 | "lint": "eslint src", 9 | "prepublish": "npm run build", 10 | "build": "rm -rf lib/* && babel src --ignore *.spec.js --out-dir lib", 11 | "coverage": "NODE_ENV=test babel-istanbul cover _mocha 'src/**/*.spec.js'" 12 | }, 13 | "author": "RisingStack", 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:RisingStack/graffiti-mongoose.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/RisingStack/graffiti-mongoose/issues" 20 | }, 21 | "homepage": "https://github.com/RisingStack/graffiti-mongoose#readme", 22 | "license": "MIT", 23 | "keywords": [ 24 | "GraphQL", 25 | "graffiti", 26 | "mongoose", 27 | "ORM", 28 | "Relay" 29 | ], 30 | "dependencies": { 31 | "babel-runtime": "6.22.0", 32 | "graphql-relay": "0.5.1", 33 | "lodash": "4.17.4" 34 | }, 35 | "peerDependencies": { 36 | "graphql": "^0.9.0" 37 | }, 38 | "devDependencies": { 39 | "babel": "6.5.2", 40 | "babel-cli": "6.22.2", 41 | "babel-eslint": "7.1.1", 42 | "babel-istanbul": "0.12.1", 43 | "babel-plugin-array-includes": "2.0.3", 44 | "babel-plugin-transform-runtime": "6.22.0", 45 | "babel-preset-es2015": "6.22.0", 46 | "babel-preset-stage-0": "6.22.0", 47 | "babel-register": "6.22.0", 48 | "chai": "3.5.0", 49 | "chai-subset": "1.4.0", 50 | "eslint": "3.14.1", 51 | "eslint-config-airbnb-base": "11.0.1", 52 | "eslint-plugin-import": "2.2.0", 53 | "graphql": "0.9.1", 54 | "mocha": "3.2.0", 55 | "mongoose": "4.7.8", 56 | "objectid": "3.2.1", 57 | "pre-commit": "1.2.2", 58 | "sinon": "1.17.7", 59 | "sinon-chai": "2.8.0" 60 | }, 61 | "pre-commit": [ 62 | "lint", 63 | "test" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/type/custom/buffer.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | graphql, 4 | GraphQLSchema, 5 | GraphQLObjectType 6 | } from 'graphql'; 7 | 8 | import GraphQLBuffer from './buffer'; 9 | 10 | describe('GraphQL buffer type', () => { 11 | it('should coerse buffer object to string', () => { 12 | const buffer = new Buffer('foo'); 13 | expect(GraphQLBuffer.serialize(buffer)).to.equal('foo'); 14 | }); 15 | 16 | it('should stringifiy buffers', async () => { 17 | const buffer = new Buffer('bar'); 18 | 19 | const schema = new GraphQLSchema({ 20 | query: new GraphQLObjectType({ 21 | name: 'Query', 22 | fields: { 23 | foo: { 24 | type: GraphQLBuffer, 25 | resolve: () => buffer 26 | } 27 | } 28 | }) 29 | }); 30 | 31 | return expect( 32 | await graphql(schema, '{ foo }') 33 | ).to.deep.equal({ 34 | data: { 35 | foo: buffer.toString() 36 | } 37 | }); 38 | }); 39 | 40 | it('should handle null', async () => { 41 | const buffer = null; 42 | 43 | const schema = new GraphQLSchema({ 44 | query: new GraphQLObjectType({ 45 | name: 'Query', 46 | fields: { 47 | foo: { 48 | type: GraphQLBuffer, 49 | resolve: () => buffer 50 | } 51 | } 52 | }) 53 | }); 54 | 55 | return expect( 56 | await graphql(schema, '{ foo }') 57 | ).to.deep.equal({ 58 | data: { 59 | foo: null 60 | } 61 | }); 62 | }); 63 | 64 | it('should handle buffers as input', async () => { 65 | const schema = new GraphQLSchema({ 66 | query: new GraphQLObjectType({ 67 | name: 'Query', 68 | fields: { 69 | foo: { 70 | type: GraphQLBuffer, 71 | args: { 72 | bar: { 73 | type: GraphQLBuffer 74 | } 75 | }, 76 | resolve: (_, { bar }) => new Buffer(`${bar.toString()}-qux`) 77 | } 78 | } 79 | }) 80 | }); 81 | 82 | const buffer = 'baz'; 83 | const bufferBar = 'baz-qux'; 84 | 85 | return expect( 86 | await graphql(schema, `{ foo(bar: "${buffer}") }`) 87 | ).to.deep.equal({ 88 | data: { 89 | foo: bufferBar 90 | } 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/type/custom/generic.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | graphql, 4 | GraphQLSchema, 5 | GraphQLObjectType 6 | } from 'graphql'; 7 | 8 | import GraphQLGeneric from './generic'; 9 | 10 | class Unknown { 11 | constructor(field) { 12 | this.field = field; 13 | } 14 | } 15 | 16 | describe('GraphQL generic type', () => { 17 | it('should coerse generic object to string', () => { 18 | const unknown = new Unknown('foo'); 19 | expect(GraphQLGeneric.serialize(unknown)).to.equal('{\'field\':\'foo\'}'); 20 | }); 21 | 22 | it('should stringifiy generic types', async () => { 23 | const unknown = new Unknown('bar'); 24 | 25 | const schema = new GraphQLSchema({ 26 | query: new GraphQLObjectType({ 27 | name: 'Query', 28 | fields: { 29 | foo: { 30 | type: GraphQLGeneric, 31 | resolve: () => unknown 32 | } 33 | } 34 | }) 35 | }); 36 | 37 | return expect( 38 | await graphql(schema, '{ foo }') 39 | ).to.deep.equal({ 40 | data: { 41 | foo: GraphQLGeneric.serialize(unknown) 42 | } 43 | }); 44 | }); 45 | 46 | it('should handle null', async () => { 47 | const unknown = null; 48 | 49 | const schema = new GraphQLSchema({ 50 | query: new GraphQLObjectType({ 51 | name: 'Query', 52 | fields: { 53 | foo: { 54 | type: GraphQLGeneric, 55 | resolve: () => unknown 56 | } 57 | } 58 | }) 59 | }); 60 | 61 | return expect( 62 | await graphql(schema, '{ foo }') 63 | ).to.deep.equal({ 64 | data: { 65 | foo: null 66 | } 67 | }); 68 | }); 69 | 70 | it('should handle generic types as input', async () => { 71 | const schema = new GraphQLSchema({ 72 | query: new GraphQLObjectType({ 73 | name: 'Query', 74 | fields: { 75 | foo: { 76 | type: GraphQLGeneric, 77 | args: { 78 | bar: { 79 | type: GraphQLGeneric 80 | } 81 | }, 82 | resolve: (_, { bar }) => new Unknown(`${bar.field}-qux`) 83 | } 84 | } 85 | }) 86 | }); 87 | 88 | const unknown = GraphQLGeneric.serialize(new Unknown('baz')); 89 | 90 | return expect( 91 | await graphql(schema, `{ foo(bar: "${unknown}") }`) 92 | ).to.deep.equal({ 93 | data: { 94 | foo: unknown.replace('baz', 'baz-qux') 95 | } 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/model/model.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { extractPath, extractPaths } from './'; 3 | 4 | describe('model', () => { 5 | it('should extract tree chunk from path', () => { 6 | const treeChunk = extractPath({ 7 | path: 'foo.bar.so', 8 | instance: 'String' 9 | }, { 10 | name: 'User' 11 | }); 12 | 13 | expect(treeChunk).to.containSubset({ 14 | foo: { 15 | name: 'foo', 16 | nonNull: false, 17 | type: 'Object', 18 | fields: { 19 | bar: { 20 | name: 'bar', 21 | nonNull: false, 22 | type: 'Object', 23 | fields: { 24 | so: { 25 | name: 'so', 26 | type: 'String', 27 | nonNull: false 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }); 34 | }); 35 | 36 | it('should extract tree from paths', () => { 37 | const tree = extractPaths({ 38 | 'foo.bar.so': { 39 | path: 'foo.bar.so' 40 | }, 41 | 'foo.bar.very': { 42 | path: 'foo.bar.very' 43 | }, 44 | 'foo.grr': { 45 | path: 'foo.grr' 46 | }, 47 | simple: { 48 | path: 'simple' 49 | }, 50 | 'sub.sub': { 51 | path: 'sub.sub' 52 | } 53 | }, { 54 | name: 'User' 55 | }); 56 | 57 | expect(tree).to.containSubset({ 58 | foo: { 59 | name: 'foo', 60 | nonNull: false, 61 | type: 'Object', 62 | fields: { 63 | bar: { 64 | name: 'bar', 65 | nonNull: false, 66 | type: 'Object', 67 | fields: { 68 | so: { 69 | name: 'so', 70 | nonNull: false 71 | }, 72 | very: { 73 | name: 'very', 74 | nonNull: false 75 | } 76 | } 77 | }, 78 | grr: { 79 | name: 'grr', 80 | nonNull: false 81 | } 82 | } 83 | }, 84 | simple: { 85 | name: 'simple', 86 | nonNull: false 87 | }, 88 | sub: { 89 | name: 'sub', 90 | nonNull: false, 91 | type: 'Object', 92 | fields: { 93 | sub: { 94 | name: 'sub', 95 | nonNull: false 96 | } 97 | } 98 | } 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/type/custom/date.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | graphql, 4 | GraphQLSchema, 5 | GraphQLObjectType 6 | } from 'graphql'; 7 | 8 | import GraphQlDate from './date'; 9 | 10 | describe('GraphQL date type', () => { 11 | it('should coerse date object to string', () => { 12 | const aDateStr = '2015-07-24T10:56:42.744Z'; 13 | const aDateObj = new Date(aDateStr); 14 | 15 | expect( 16 | GraphQlDate.serialize(aDateObj) 17 | ).to.equal(aDateStr); 18 | }); 19 | 20 | it('should stringifiy dates', async () => { 21 | const now = new Date(); 22 | 23 | const schema = new GraphQLSchema({ 24 | query: new GraphQLObjectType({ 25 | name: 'Query', 26 | fields: { 27 | now: { 28 | type: GraphQlDate, 29 | resolve: () => now 30 | } 31 | } 32 | }) 33 | }); 34 | 35 | return expect( 36 | await graphql(schema, '{ now }') 37 | ).to.deep.equal({ 38 | data: { 39 | now: now.toJSON() 40 | } 41 | }); 42 | }); 43 | 44 | it('should handle null', async () => { 45 | const now = null; 46 | 47 | const schema = new GraphQLSchema({ 48 | query: new GraphQLObjectType({ 49 | name: 'Query', 50 | fields: { 51 | now: { 52 | type: GraphQlDate, 53 | resolve: () => now 54 | } 55 | } 56 | }) 57 | }); 58 | 59 | return expect( 60 | await graphql(schema, '{ now }') 61 | ).to.deep.equal({ 62 | data: { 63 | now: null 64 | } 65 | }); 66 | }); 67 | 68 | it('should fail when now is not a date', async () => { 69 | const now = 'invalid date'; 70 | 71 | const schema = new GraphQLSchema({ 72 | query: new GraphQLObjectType({ 73 | name: 'Query', 74 | fields: { 75 | now: { 76 | type: GraphQlDate, 77 | resolve: () => now 78 | } 79 | } 80 | }) 81 | }); 82 | 83 | return expect( 84 | await graphql(schema, '{ now }') 85 | ).to.containSubset({ 86 | errors: [ 87 | { 88 | message: 'Field error: value is not an instance of Date' 89 | } 90 | ] 91 | }); 92 | }); 93 | 94 | describe('input', () => { 95 | const schema = new GraphQLSchema({ 96 | query: new GraphQLObjectType({ 97 | name: 'Query', 98 | fields: { 99 | nextDay: { 100 | type: GraphQlDate, 101 | args: { 102 | date: { 103 | type: GraphQlDate 104 | } 105 | }, 106 | resolve: (_, { date }) => new Date(date.getTime() + (24 * 3600 * 1000)) 107 | } 108 | } 109 | }) 110 | }); 111 | 112 | it('should handle dates as input', async () => { 113 | const someday = '2015-07-24T10:56:42.744Z'; 114 | const nextDay = '2015-07-25T10:56:42.744Z'; 115 | 116 | return expect( 117 | await graphql(schema, `{ nextDay(date: "${someday}") }`) 118 | ).to.deep.equal({ 119 | data: { 120 | nextDay 121 | } 122 | }); 123 | }); 124 | 125 | it('should not accept alternative date formats', async () => { 126 | const someday = 'Fri Jul 24 2015 12:56:42 GMT+0200 (CEST)'; 127 | 128 | return expect( 129 | await graphql(schema, `{ nextDay(date: "${someday}") }`) 130 | ).to.containSubset({ 131 | errors: [ 132 | { 133 | locations: [], 134 | message: 'Query error: Invalid date format, only accepts: YYYY-MM-DDTHH:MM:SS.SSSZ' 135 | } 136 | ] 137 | }); 138 | }); 139 | 140 | it('should choke on invalid dates as input', async () => { 141 | const invalidDate = 'invalid data'; 142 | 143 | return expect( 144 | await graphql(schema, `{ nextDay(date: "${invalidDate}") }`) 145 | ).to.containSubset({ 146 | errors: [ 147 | { 148 | locations: [], 149 | message: 'Query error: Invalid date' 150 | } 151 | ] 152 | }); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/query/query.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { toGlobalId } from 'graphql-relay'; 3 | import objectid from 'objectid'; 4 | import { 5 | _idToCursor, 6 | getIdFetcher, 7 | getOneResolver, 8 | getListResolver, 9 | connectionFromModel 10 | } from './'; 11 | 12 | describe('query', () => { 13 | class MongooseObject { 14 | constructor(fields) { 15 | Object.assign(this, fields); 16 | } 17 | 18 | toObject() { 19 | return this; 20 | } 21 | } 22 | 23 | const fields = { name: 'foo' }; 24 | const type = 'type'; 25 | const objArray = []; 26 | const resultArray = []; 27 | for (let i = 0; i < 10; i += 1) { 28 | const objFields = { 29 | ...fields, 30 | _id: objectid().toString() 31 | }; 32 | objArray.push(new MongooseObject(objFields)); 33 | resultArray.push({ 34 | ...objFields, 35 | _type: type 36 | }); 37 | } 38 | 39 | const obj = objArray[0]; 40 | const resultObj = resultArray[0]; 41 | 42 | const graffitiModels = { 43 | type: { 44 | model: { 45 | modelName: type, 46 | findById(id) { 47 | const obj = objArray.find((obj) => obj._id === id); 48 | return Promise.resolve(obj); 49 | }, 50 | 51 | findOne() { 52 | return Promise.resolve(objArray[0]); 53 | }, 54 | 55 | find() { 56 | return Promise.resolve(objArray); 57 | }, 58 | 59 | count() { 60 | return Promise.resolve(objArray.length); 61 | } 62 | } 63 | } 64 | }; 65 | 66 | describe('getIdFetcher', () => { 67 | it('should return an idFetcher function', () => { 68 | expect(getIdFetcher({})).instanceOf(Function); 69 | }); 70 | 71 | it('should return an object based on a global id', async () => { 72 | const id = toGlobalId('type', obj._id); 73 | 74 | const idFetcher = getIdFetcher(graffitiModels); 75 | const result = await idFetcher({}, { id }); 76 | expect(result).to.eql(resultObj); 77 | }); 78 | 79 | it('should return the Viewer instance', async () => { 80 | const id = toGlobalId('Viewer', 'viewer'); 81 | 82 | const idFetcher = getIdFetcher(graffitiModels); 83 | const result = await idFetcher({}, { id }); 84 | expect(result).to.eql({ _type: 'Viewer', id: 'viewer' }); 85 | }); 86 | }); 87 | 88 | describe('getOneResolver', () => { 89 | let oneResolver; 90 | before(() => { 91 | oneResolver = getOneResolver(graffitiModels.type); 92 | }); 93 | 94 | it('should return a function', () => { 95 | expect(oneResolver).instanceOf(Function); 96 | }); 97 | 98 | it('should return an object', async () => { 99 | let result = await oneResolver({}, { id: obj._id }); 100 | expect(result).to.eql(resultObj); 101 | 102 | const id = toGlobalId('type', obj._id); 103 | result = await oneResolver({}, { id }); 104 | expect(result).to.eql(resultObj); 105 | }); 106 | }); 107 | 108 | describe('getListResolver', () => { 109 | let listResolver; 110 | before(() => { 111 | listResolver = getListResolver(graffitiModels.type); 112 | }); 113 | 114 | it('should return a function', () => { 115 | expect(listResolver).instanceOf(Function); 116 | }); 117 | 118 | it('should return an array of objects', async () => { 119 | const result = await listResolver(); 120 | expect(result).to.eql(resultArray); 121 | }); 122 | }); 123 | 124 | describe('connectionFromModel', () => { 125 | it('should return a connection', async () => { 126 | const result = await connectionFromModel(graffitiModels.type, {}); 127 | const edges = resultArray.map((obj) => ({ 128 | cursor: _idToCursor(obj._id), 129 | node: obj 130 | })); 131 | const startCursor = edges[0].cursor; 132 | const endCursor = edges[edges.length - 1].cursor; 133 | expect(result).to.containSubset({ 134 | count: resultArray.length, 135 | edges, 136 | pageInfo: { 137 | startCursor, 138 | endCursor, 139 | hasPreviousPage: false, 140 | hasNextPage: false 141 | } 142 | }); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /src/model/model.js: -------------------------------------------------------------------------------- 1 | import { reduce, reduceRight, merge } from 'lodash'; 2 | import mongoose from 'mongoose'; 3 | 4 | const embeddedModels = {}; 5 | 6 | /** 7 | * @method getField 8 | * @param schemaPaths 9 | * @return {Object} field 10 | */ 11 | function getField(schemaPath) { 12 | const { 13 | description, 14 | hidden, 15 | hooks, 16 | ref, 17 | index 18 | } = schemaPath.options || {}; 19 | const name = schemaPath.path.split('.').pop(); 20 | 21 | const field = { 22 | name, 23 | description, 24 | hidden, 25 | hooks, 26 | type: schemaPath.instance, 27 | nonNull: !!index 28 | }; 29 | 30 | if (schemaPath.enumValues && schemaPath.enumValues.length > 0) { 31 | field.enumValues = schemaPath.enumValues; 32 | } 33 | 34 | // ObjectID ref 35 | if (ref) { 36 | field.reference = ref; 37 | } 38 | 39 | // Caster 40 | if (schemaPath.caster) { 41 | const { 42 | instance, 43 | options 44 | } = schemaPath.caster; 45 | const { ref } = options || {}; 46 | 47 | field.subtype = instance; 48 | 49 | // ObjectID ref 50 | if (ref) { 51 | field.reference = ref; 52 | } 53 | } 54 | 55 | return field; 56 | } 57 | 58 | /** 59 | * Extracts tree chunk from path if it's a sub-document 60 | * @method extractPath 61 | * @param {Object} schemaPath 62 | * @param {Object} model 63 | * @return {Object} field 64 | */ 65 | function extractPath(schemaPath) { 66 | const subNames = schemaPath.path.split('.'); 67 | 68 | return reduceRight(subNames, (fields, name, key) => { 69 | const obj = {}; 70 | 71 | if (schemaPath instanceof mongoose.Schema.Types.DocumentArray) { 72 | const subSchemaPaths = schemaPath.schema.paths; 73 | const fields = extractPaths(subSchemaPaths, { name }); // eslint-disable-line no-use-before-define 74 | obj[name] = { 75 | name, 76 | fields, 77 | nonNull: false, 78 | type: 'Array', 79 | subtype: 'Object' 80 | }; 81 | } else if (schemaPath instanceof mongoose.Schema.Types.Embedded) { 82 | schemaPath.modelName = schemaPath.schema.options.graphqlTypeName || name; 83 | // embedded model must be unique Instance 84 | const embeddedModel = embeddedModels.hasOwnProperty(schemaPath.modelName) 85 | ? embeddedModels[schemaPath.modelName] 86 | : getModel(schemaPath); // eslint-disable-line no-use-before-define 87 | 88 | embeddedModels[schemaPath.modelName] = embeddedModel; 89 | obj[name] = { 90 | ...getField(schemaPath), 91 | embeddedModel 92 | }; 93 | } else if (key === subNames.length - 1) { 94 | obj[name] = getField(schemaPath); 95 | } else { 96 | obj[name] = { 97 | name, 98 | fields, 99 | nonNull: false, 100 | type: 'Object' 101 | }; 102 | } 103 | 104 | return obj; 105 | }, {}); 106 | } 107 | 108 | /** 109 | * Merge sub-document tree chunks 110 | * @method extractPaths 111 | * @param {Object} schemaPaths 112 | * @param {Object} model 113 | * @return {Object) extractedSchemaPaths 114 | */ 115 | function extractPaths(schemaPaths, model) { 116 | return reduce(schemaPaths, (fields, schemaPath) => ( 117 | merge(fields, extractPath(schemaPath, model)) 118 | ), {}); 119 | } 120 | 121 | /** 122 | * Turn mongoose model to graffiti model 123 | * @method getModel 124 | * @param {Object} model Mongoose model 125 | * @return {Object} graffiti model 126 | */ 127 | function getModel(model) { 128 | const schemaPaths = model.schema.paths; 129 | const name = model.modelName; 130 | 131 | const fields = extractPaths(schemaPaths, { name }); 132 | 133 | return { 134 | name, 135 | fields, 136 | model 137 | }; 138 | } 139 | 140 | /** 141 | * @method getModels 142 | * @param {Array} mongooseModels 143 | * @return {Object} - graffiti models 144 | */ 145 | function getModels(mongooseModels) { 146 | return mongooseModels 147 | .map(getModel) 148 | .reduce((models, model) => ({ 149 | ...models, 150 | [model.name]: model 151 | }), {}); 152 | } 153 | 154 | export default { 155 | getModels 156 | }; 157 | 158 | export { 159 | extractPath, 160 | extractPaths, 161 | getModel, 162 | getModels 163 | }; 164 | -------------------------------------------------------------------------------- /src/type/type.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | GraphQLString, 4 | GraphQLFloat, 5 | GraphQLBoolean, 6 | GraphQLID, 7 | GraphQLList, 8 | GraphQLNonNull 9 | } from 'graphql/type'; 10 | 11 | import { 12 | GraphQLDate, 13 | GraphQLGeneric, 14 | getType, 15 | getTypes 16 | } from './'; 17 | 18 | describe('type', () => { 19 | let user; 20 | before(() => { 21 | user = { 22 | name: 'User', 23 | fields: { 24 | name: { 25 | type: 'String' 26 | }, 27 | age: { 28 | type: 'Number' 29 | }, 30 | mother: { 31 | type: 'ObjectID', 32 | reference: 'User', 33 | description: 'The user\'s mother' 34 | }, 35 | friends: { 36 | type: 'Array', 37 | subtype: 'ObjectID', 38 | reference: 'User' 39 | }, 40 | weight: { 41 | type: 'Number' 42 | }, 43 | createdAt: { 44 | type: 'Date' 45 | }, 46 | removed: { 47 | type: 'Boolean' 48 | }, 49 | nums: { 50 | type: 'Array', 51 | subtype: 'Number' 52 | }, 53 | unknownType: { 54 | type: 'Unknown' 55 | }, 56 | hidden: { 57 | type: 'String', 58 | hidden: true 59 | }, 60 | sub: { 61 | type: 'Object', 62 | fields: { 63 | foo: { 64 | type: 'String' 65 | }, 66 | nums: { 67 | type: 'Array', 68 | subtype: 'Number' 69 | }, 70 | subsub: { 71 | type: 'Object', 72 | fields: { 73 | bar: { 74 | type: 'Number' 75 | }, 76 | sister: { 77 | type: 'ObjectID', 78 | reference: 'User', 79 | description: 'The user\'s sister' 80 | } 81 | } 82 | } 83 | } 84 | }, 85 | subArray: { 86 | type: 'Array', 87 | subtype: 'Object', 88 | fields: { 89 | foo: { 90 | type: 'String' 91 | }, 92 | nums: { 93 | type: 'Array', 94 | subtype: 'Number' 95 | }, 96 | brother: { 97 | type: 'ObjectID', 98 | reference: 'User', 99 | description: 'The user\'s brother' 100 | } 101 | } 102 | } 103 | } 104 | }; 105 | }); 106 | 107 | describe('getType', () => { 108 | it('should implement the Node interface', () => { 109 | const result = getType([], user); 110 | expect(result._typeConfig.interfaces).to.containSubset([{ 111 | name: 'Node' 112 | }]); 113 | expect(result._typeConfig.fields()).to.containSubset({ 114 | id: { 115 | type: new GraphQLNonNull(GraphQLID) 116 | } 117 | }); 118 | }); 119 | 120 | it('should specify the fields', () => { 121 | const result = getType([], user); 122 | const fields = result._typeConfig.fields(); 123 | expect(fields).to.containSubset({ 124 | name: { 125 | name: 'name', 126 | type: GraphQLString 127 | }, 128 | age: { 129 | name: 'age', 130 | type: GraphQLFloat 131 | }, 132 | mother: { 133 | name: 'mother', 134 | type: GraphQLID, 135 | description: 'The user\'s mother' 136 | }, 137 | friends: { 138 | name: 'friends', 139 | type: new GraphQLList(GraphQLID) 140 | }, 141 | weight: { 142 | name: 'weight', 143 | type: GraphQLFloat 144 | }, 145 | createdAt: { 146 | name: 'createdAt', 147 | type: GraphQLDate 148 | }, 149 | removed: { 150 | name: 'removed', 151 | type: GraphQLBoolean 152 | }, 153 | nums: { 154 | name: 'nums', 155 | type: new GraphQLList(GraphQLFloat) 156 | }, 157 | unknownType: { 158 | name: 'unknownType', 159 | type: GraphQLGeneric 160 | }, 161 | sub: { 162 | name: 'sub' 163 | } 164 | }); 165 | 166 | // sub 167 | const subFields = fields.sub.type._typeConfig.fields(); 168 | expect(subFields).to.containSubset({ 169 | foo: { 170 | name: 'foo', 171 | type: GraphQLString 172 | }, 173 | nums: { 174 | name: 'nums', 175 | type: new GraphQLList(GraphQLFloat) 176 | }, 177 | subsub: { 178 | name: 'subsub' 179 | } 180 | }); 181 | 182 | // subsub 183 | const subsubFields = subFields.subsub.type._typeConfig.fields(); 184 | expect(subsubFields).to.containSubset({ 185 | bar: { 186 | name: 'bar', 187 | type: GraphQLFloat 188 | }, 189 | sister: { 190 | name: 'sister', 191 | type: GraphQLID, 192 | description: 'The user\'s sister' 193 | } 194 | }); 195 | 196 | const subArrayFields = fields.subArray.type.ofType._typeConfig.fields(); 197 | expect(subArrayFields).to.containSubset({ 198 | foo: { 199 | name: 'foo', 200 | type: GraphQLString 201 | }, 202 | nums: { 203 | name: 'nums', 204 | type: new GraphQLList(GraphQLFloat) 205 | }, 206 | brother: { 207 | name: 'brother', 208 | type: GraphQLID, 209 | description: 'The user\'s brother' 210 | } 211 | }); 212 | }); 213 | 214 | it('should not include hidden fields', () => { 215 | const result = getType([], user); 216 | const fields = result._typeConfig.fields(); 217 | expect(fields.hidden).to.be.eql(undefined); 218 | }); 219 | }); 220 | 221 | describe('getTypes', () => { 222 | it('should resolve the references', () => { 223 | const result = getTypes([user]); 224 | const userType = result[user.name]; 225 | const fields = userType._typeConfig.fields(); 226 | 227 | expect(fields.mother.type).to.be.equal(userType); 228 | expect(fields.sub.type._typeConfig.fields().subsub.type._typeConfig.fields().sister.type).to.be.equal(userType); 229 | expect(fields.subArray.type.ofType._typeConfig.fields().brother.type).to.be.equal(userType); 230 | 231 | // connection type 232 | const nodeField = fields.friends.type._typeConfig.fields().edges.type.ofType._typeConfig.fields().node; 233 | expect(nodeField.type).to.be.equal(userType); 234 | }); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠ Notice: the development of the package is discontinued. Use it for educational purposes and hobby projects only. 2 | 3 | # ![graffiti](https://cloud.githubusercontent.com/assets/1764512/8900273/9ed758dc-343e-11e5-95ba-e82f876cf52d.png) Mongoose 4 | 5 | [![npm version](https://badge.fury.io/js/%40risingstack%2Fgraffiti-mongoose.svg)](https://badge.fury.io/js/%40risingstack%2Fgraffiti-mongoose) 6 | [![CircleCI](https://circleci.com/gh/RisingStack/graffiti-mongoose.svg?style=svg)](https://circleci.com/gh/RisingStack/graffiti-mongoose) 7 | [![bitHound Overall Score](https://www.bithound.io/github/RisingStack/graffiti-mongoose/badges/score.svg)](https://www.bithound.io/github/RisingStack/graffiti-mongoose) 8 | [![Known Vulnerabilities](https://snyk.io/test/npm/@risingstack/graffiti-mongoose/badge.svg)](https://snyk.io/test/npm/@risingstack/graffiti-mongoose) 9 | 10 | [Mongoose](http://mongoosejs.com) (MongoDB) adapter for [GraphQL](https://github.com/graphql/graphql-js). 11 | 12 | `graffiti-mongoose` generates `GraphQL` types and schemas from your existing `mongoose` models, that's how simple it is. The generated schema is compatible with [Relay](https://facebook.github.io/relay/). 13 | 14 | For quick jump check out the [Usage section](https://github.com/RisingStack/graffiti-mongoose#usage). 15 | 16 | ## Install 17 | 18 | ```shell 19 | npm install graphql @risingstack/graffiti-mongoose --save 20 | ``` 21 | 22 | ## Example 23 | 24 | Check out the [/example](https://github.com/RisingStack/graffiti-mongoose/tree/master/example) folder. 25 | 26 | ```shell 27 | cd graffiti-mongoose 28 | npm install # install dependencies in the main folder 29 | cd example 30 | npm install # install dependencies in the example folder 31 | npm start # run the example application and open your browser: http://localhost:8080 32 | ``` 33 | 34 | ## Usage 35 | 36 | This adapter is written in `ES6` and `ES7` with [Babel](https://babeljs.io) but it's published as transpiled `ES5` JavaScript code to `npm`, which means you don't need `ES7` support in your application to run it. 37 | 38 | __Example queries can be found in the [example folder](https://github.com/RisingStack/graffiti-mongoose/tree/master/example#example-queries).__ 39 | 40 | ##### usual mongoose model(s) 41 | ```javascript 42 | import mongoose from 'mongoose'; 43 | 44 | const UserSchema = new mongoose.Schema({ 45 | name: { 46 | type: String, 47 | // field description 48 | description: 'the full name of the user' 49 | }, 50 | hiddenField: { 51 | type: Date, 52 | default: Date.now, 53 | // the field is hidden, not available in GraphQL 54 | hidden: true 55 | }, 56 | age: { 57 | type: Number, 58 | indexed: true 59 | }, 60 | friends: [{ 61 | type: mongoose.Schema.Types.ObjectId, 62 | ref: 'User' 63 | }] 64 | }); 65 | 66 | const User = mongoose.model('User', UserSchema); 67 | export default User; 68 | ``` 69 | 70 | ##### graffiti-mongoose 71 | ```javascript 72 | import {getSchema} from '@risingstack/graffiti-mongoose'; 73 | import graphql from 'graphql'; 74 | import User from './User'; 75 | 76 | const options = { 77 | mutation: false, // mutation fields can be disabled 78 | allowMongoIDMutation: false // mutation of mongo _id can be enabled 79 | }; 80 | const schema = getSchema([User], options); 81 | 82 | const query = `{ 83 | users(age: 28) { 84 | name 85 | friends(first: 2) { 86 | edges { 87 | cursor 88 | node { 89 | name 90 | age 91 | } 92 | } 93 | pageInfo { 94 | startCursor 95 | endCursor 96 | hasPreviousPage 97 | hasNextPage 98 | } 99 | } 100 | } 101 | }`; 102 | 103 | graphql(schema, query) 104 | .then((result) => { 105 | console.log(result); 106 | }); 107 | ``` 108 | 109 | ## Supported mongoose types 110 | 111 | * Number 112 | * String 113 | * Boolean 114 | * Date 115 | * [Number] 116 | * [String] 117 | * [Boolean] 118 | * [Date] 119 | * ObjectId with ref (reference to other document, populate) 120 | 121 | ## Supported query types 122 | 123 | * query 124 | * singular: for example `user` 125 | * plural: for example `users` 126 | * [node](https://facebook.github.io/relay/docs/graphql-object-identification.html): takes a single argument, a unique `!ID`, and returns a `Node` 127 | * viewer: singular and plural queries as fields 128 | 129 | ## Supported query arguments 130 | 131 | * indexed fields 132 | * "id" on singular type 133 | * array of "id"s on plural type 134 | 135 | Which means, you are able to filter like below, if the age is indexed in your mongoose model: 136 | 137 | ``` 138 | users(age: 19) {} 139 | user(id: "mongoId1") {} 140 | user(id: "relayId") {} 141 | users(id: ["mongoId", "mongoId2"]) {} 142 | users(id: ["relayId1", "relayId2"]) {} 143 | ``` 144 | 145 | ## Supported mutation types 146 | 147 | * mutation 148 | * addX: for example `addUser` 149 | * updateX: for example `updateUser` 150 | * deleteX: for example `deleteUser` 151 | 152 | ## Supported mutation arguments 153 | 154 | * scalar types 155 | * arrays 156 | * references 157 | 158 | Examples: 159 | ``` 160 | mutation addX { 161 | addUser(input: {name: "X", age: 11, clientMutationId: "1"}) { 162 | changedUserEdge { 163 | node { 164 | id 165 | name 166 | } 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | ``` 173 | mutation updateX { 174 | updateUser(input: {id: "id=", age: 10, clientMutationId: "2"}) { 175 | changedUser { 176 | id 177 | name 178 | age 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | ``` 185 | mutation deleteX { 186 | deleteUser(input: {id: "id=", clientMutationId: "3"}) { 187 | ok 188 | } 189 | } 190 | ``` 191 | 192 | ## Resolve hooks 193 | 194 | You can specify pre- and post-resolve hooks on fields in order to manipulate arguments and data passed in to the database resolve function, and returned by the GraphQL resolve function. 195 | 196 | You can add hooks to type fields and query fields (singular & plural queries, mutations) too. 197 | By passing arguments to the `next` function, you can modify the parameters of the next hook or the return value of the `resolve` function. 198 | 199 | Examples: 200 | - Query, mutation hooks (`viewer`, `singular`, `plural`, `mutation`) 201 | ```javascript 202 | const hooks = { 203 | viewer: { 204 | pre: (next, root, args, request) => { 205 | // authorize the logged in user based on the request 206 | authorize(request); 207 | next(); 208 | }, 209 | post: (next, value) => { 210 | console.log(value); 211 | next(); 212 | } 213 | }, 214 | // singular: { 215 | // pre: (next, root, args, context) => next(), 216 | // post: (next, value, args, context) => next() 217 | // }, 218 | // plural: { 219 | // pre: (next, root, args, context) => next(), 220 | // post: (next, value, args, context) => next() 221 | // }, 222 | // mutation: { 223 | // pre: (next, args, context) => next(), 224 | // post: (next, value, args, context) => next() 225 | // } 226 | }; 227 | const schema = getSchema([User], {hooks}); 228 | ``` 229 | 230 | - Field hooks 231 | ```javascript 232 | const UserSchema = new mongoose.Schema({ 233 | name: { 234 | type: String, 235 | hooks: { 236 | pre: (next, root, args, request) => { 237 | // authorize the logged in user based on the request 238 | // throws error if the user has no right to request the user names 239 | authorize(request); 240 | next(); 241 | }, 242 | // manipulate response 243 | post: [ 244 | (next, name) => next(`${name} first hook`), 245 | (next, name) => next(`${name} & second hook`) 246 | ] 247 | } 248 | } 249 | }); 250 | ``` 251 | ``` 252 | query UsersQuery { 253 | viewer { 254 | users(first: 1) { 255 | edges { 256 | node { 257 | name 258 | } 259 | } 260 | } 261 | } 262 | } 263 | ``` 264 | ```json 265 | { 266 | "data": { 267 | "viewer": { 268 | "users": { 269 | "edges": [ 270 | { 271 | "node": { 272 | "name": "User0 first hook & second hook" 273 | } 274 | } 275 | ] 276 | } 277 | } 278 | } 279 | } 280 | ``` 281 | 282 | ## Test 283 | 284 | ```shell 285 | npm test 286 | ``` 287 | 288 | ## Contributing 289 | 290 | Please read the [CONTRIBUTING.md](https://github.com/RisingStack/graffiti-mongoose/tree/master/.github/CONTRIBUTING.md) file. 291 | 292 | ## License 293 | 294 | [MIT](https://github.com/RisingStack/graffiti-mongoose/tree/master/LICENSE) 295 | -------------------------------------------------------------------------------- /src/schema/schema.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import { expect } from 'chai'; 4 | import { 5 | GraphQLObjectType, 6 | GraphQLString, 7 | GraphQLList, 8 | GraphQLNonNull, 9 | GraphQLID, 10 | GraphQLSchema 11 | } from 'graphql'; 12 | import { 13 | getQueryField, 14 | getMutationField, 15 | getFields, 16 | getSchema 17 | } from './'; 18 | import query from '../query'; 19 | import model from '../model'; 20 | import type from '../type'; 21 | 22 | describe('schema', () => { 23 | const types = { 24 | Qux: new GraphQLObjectType({ 25 | name: 'Qux', 26 | fields: () => ({ 27 | bar: { 28 | name: 'bar', 29 | type: GraphQLString 30 | }, 31 | baz: { 32 | name: 'baz', 33 | type: GraphQLID 34 | } 35 | }) 36 | }), 37 | TestQuery: new GraphQLObjectType({ 38 | name: 'TestQuery', 39 | type: GraphQLString, 40 | fields: () => ({ 41 | fetchCount: { 42 | name: 'fetchCount', 43 | type: GraphQLString, 44 | resolve: () => '42' 45 | } 46 | }) 47 | }) 48 | }; 49 | 50 | describe('getQueryField', () => { 51 | it('should return a singular and a plural query field', function getQueryFieldTest() { 52 | this.sandbox.stub(query, 'getOneResolver').returns(() => null); 53 | this.sandbox.stub(query, 'getListResolver').returns(() => null); 54 | 55 | const graphQLType = types.Qux; 56 | const fields = getQueryField({ Qux: { model: {} } }, graphQLType); 57 | expect(fields).to.containSubset({ 58 | qux: { 59 | type: graphQLType, 60 | args: { 61 | id: { 62 | type: new GraphQLNonNull(GraphQLID) 63 | } 64 | } 65 | }, 66 | quxes: { 67 | type: new GraphQLList(graphQLType), 68 | args: { 69 | bar: { 70 | name: 'bar', 71 | type: GraphQLString 72 | } 73 | } 74 | } 75 | }); 76 | }); 77 | }); 78 | 79 | describe('getMutationField', () => { 80 | it('should return an addXyz and an updateXyz field', function getMutationFieldTest() { 81 | this.sandbox.stub(query, 'getAddOneMutateHandler').returns(() => null); 82 | this.sandbox.stub(query, 'getUpdateOneMutateHandler').returns(() => null); 83 | const graphQLType = types.Qux; 84 | const fields = getMutationField({ Qux: { model: {} } }, graphQLType); 85 | const args = { 86 | input: {} 87 | }; 88 | expect(fields).to.containSubset({ 89 | addQux: { 90 | args 91 | }, 92 | updateQux: { 93 | args 94 | } 95 | }); 96 | expect(fields.addQux.args.input.type.ofType._typeConfig.fields()).to.containSubset({ 97 | bar: { 98 | name: 'bar', 99 | type: GraphQLString 100 | }, 101 | baz: { 102 | name: 'baz', 103 | type: GraphQLID 104 | } 105 | }); 106 | }); 107 | }); 108 | 109 | describe('getFields', () => { 110 | it('should return query fields ; including node(id!)', function getFieldsTest() { 111 | this.sandbox.stub(type, 'getTypes').returns(types); 112 | 113 | const fields = getFields({}); 114 | expect(fields).to.containSubset({ 115 | query: { 116 | name: 'RootQuery', 117 | _typeConfig: { 118 | fields: { 119 | qux: {}, 120 | quxes: {}, 121 | viewer: {}, 122 | node: { 123 | name: 'node', 124 | args: { 125 | id: { 126 | type: new GraphQLNonNull(GraphQLID) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | }, 133 | mutation: { 134 | name: 'RootMutation', 135 | _typeConfig: { 136 | fields: { 137 | addQux: {}, 138 | updateQux: {} 139 | } 140 | } 141 | } 142 | }); 143 | expect(fields.query._typeConfig.fields.viewer.type._typeConfig.fields()).to.containSubset({ 144 | qux: {}, 145 | quxes: {} 146 | }); 147 | }); 148 | }); 149 | 150 | describe('getSchema', () => { 151 | beforeEach(function beforeEach() { 152 | this.sandbox.stub(model, 'getModels').returns({}); 153 | this.sandbox.stub(type, 'getTypes').returns(types); 154 | }); 155 | 156 | it('should return a GraphQL schema', () => { 157 | const schema = getSchema({}); 158 | expect(schema).instanceOf(GraphQLSchema); 159 | expect(schema._queryType.name).to.be.equal('RootQuery'); 160 | expect(schema._mutationType.name).to.be.equal('RootMutation'); 161 | }); 162 | 163 | it('should return a GraphQL schema without mutations', () => { 164 | const schema = getSchema({}, { mutation: false }); 165 | expect(schema).instanceOf(GraphQLSchema); 166 | expect(schema._queryType.name).to.be.equal('RootQuery'); 167 | expect(schema._mutationType).to.be.equal(undefined); 168 | }); 169 | 170 | it('should return a GraphQL schema with custom queries (object)', () => { 171 | const graphQLType = types.TestQuery; 172 | const customQueries = { 173 | testQuery: { 174 | type: graphQLType, 175 | args: { 176 | id: { 177 | type: new GraphQLNonNull(GraphQLID) 178 | } 179 | } 180 | } 181 | }; 182 | const schema = getSchema({}, { customQueries }); 183 | expect(schema).instanceOf(GraphQLSchema); 184 | expect(schema._queryType.name).to.be.equal('RootQuery'); 185 | expect(schema._mutationType.name).to.be.equal('RootMutation'); 186 | expect(schema._queryType._fields.testQuery.name).to.be.equal('testQuery'); 187 | expect(schema._queryType._fields.testQuery.type._fields.fetchCount.resolve()).to.be.equal('42'); 188 | }); 189 | 190 | it('should return a GraphQL schema with custom mutations (object)', () => { 191 | const graphQLType = types.TestQuery; 192 | const customMutations = { 193 | testQuery: { 194 | type: graphQLType, 195 | args: { 196 | id: { 197 | type: new GraphQLNonNull(GraphQLID) 198 | } 199 | } 200 | } 201 | }; 202 | const schema = getSchema({}, { customMutations }); 203 | expect(schema).instanceOf(GraphQLSchema); 204 | expect(schema._queryType.name).to.be.equal('RootQuery'); 205 | expect(schema._mutationType.name).to.be.equal('RootMutation'); 206 | expect(schema._mutationType._fields.testQuery.name).to.be.equal('testQuery'); 207 | expect(schema._mutationType._fields.testQuery.type._fields.fetchCount.resolve()).to.be.equal('42'); 208 | }); 209 | 210 | it('should return a GraphQL schema with custom queries (function)', () => { 211 | const graphQLType = types.TestQuery; 212 | const customQueries = (types) => ({ 213 | testQuery: { 214 | type: graphQLType, 215 | args: { 216 | qux: { 217 | type: types.Qux 218 | } 219 | } 220 | } 221 | }); 222 | const schema = getSchema({}, { customQueries }); 223 | expect(schema).instanceOf(GraphQLSchema); 224 | expect(schema._queryType.name).to.be.equal('RootQuery'); 225 | expect(schema._mutationType.name).to.be.equal('RootMutation'); 226 | expect(schema._queryType._fields.testQuery.args[0].name).to.be.equal('qux'); 227 | expect(schema._queryType._fields.testQuery.args[0].type._fields.bar).to.be.defined; 228 | expect(schema._queryType._fields.testQuery.args[0].type._fields.baz).to.be.defined; 229 | expect(schema._queryType._fields.testQuery.name).to.be.equal('testQuery'); 230 | expect(schema._queryType._fields.testQuery.type._fields.fetchCount.resolve()).to.be.equal('42'); 231 | }); 232 | 233 | it('should return a GraphQL schema with custom mutations (function)', () => { 234 | const graphQLType = types.TestQuery; 235 | const customMutations = (types) => ({ 236 | testQuery: { 237 | type: graphQLType, 238 | args: { 239 | qux: { 240 | type: types.Qux 241 | } 242 | } 243 | } 244 | }); 245 | const schema = getSchema({}, { customMutations }); 246 | expect(schema).instanceOf(GraphQLSchema); 247 | expect(schema._queryType.name).to.be.equal('RootQuery'); 248 | expect(schema._mutationType.name).to.be.equal('RootMutation'); 249 | expect(schema._mutationType._fields.testQuery.args[0].name).to.be.equal('qux'); 250 | expect(schema._mutationType._fields.testQuery.args[0].type._fields.bar).to.be.defined; 251 | expect(schema._mutationType._fields.testQuery.args[0].type._fields.baz).to.be.defined; 252 | expect(schema._mutationType._fields.testQuery.name).to.be.equal('testQuery'); 253 | expect(schema._mutationType._fields.testQuery.type._fields.fetchCount.resolve()).to.be.equal('42'); 254 | }); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /src/query/query.js: -------------------------------------------------------------------------------- 1 | import { forEach, isArray, isString } from 'lodash'; 2 | import { fromGlobalId, toGlobalId } from 'graphql-relay'; 3 | import getFieldList from './projection'; 4 | import viewer from '../model/viewer'; 5 | 6 | function processId({ id, _id = id }) { 7 | // global or mongo id 8 | if (isString(_id) && !/^[a-fA-F0-9]{24}$/.test(_id)) { 9 | const { type, id } = fromGlobalId(_id); 10 | if (type && /^[a-zA-Z]*$/.test(type)) { 11 | return id; 12 | } 13 | } 14 | 15 | return _id; 16 | } 17 | 18 | function getCount(Collection, selector) { 19 | if (selector && (isArray(selector.id) || isArray(selector._id))) { 20 | const { id, _id = id } = selector; 21 | delete selector.id; 22 | selector._id = { 23 | $in: _id.map((id) => processId({ id })) 24 | }; 25 | } 26 | 27 | return Collection.count(selector); 28 | } 29 | 30 | function getOne(Collection, args, context, info) { 31 | const id = processId(args); 32 | const projection = getFieldList(info); 33 | return Collection.findById(id, projection).then((result) => { 34 | if (result) { 35 | return { 36 | ...result.toObject(), 37 | _type: Collection.modelName 38 | }; 39 | } 40 | 41 | return null; 42 | }); 43 | } 44 | 45 | function addOne(Collection, args) { 46 | forEach(args, (arg, key) => { 47 | if (isArray(arg)) { 48 | args[key] = arg.map((id) => processId({ id })); 49 | } else { 50 | args[key] = processId({ id: arg }); 51 | } 52 | }); 53 | 54 | const instance = new Collection(args); 55 | return instance.save().then((result) => { 56 | if (result) { 57 | return { 58 | ...result.toObject(), 59 | _type: Collection.modelName 60 | }; 61 | } 62 | 63 | return null; 64 | }); 65 | } 66 | 67 | function updateOne(Collection, { id, _id, ...args }, context, info) { 68 | _id = processId({ id, _id }); 69 | 70 | forEach(args, (arg, key) => { 71 | if (isArray(arg)) { 72 | args[key] = arg.map((id) => processId({ id })); 73 | } else { 74 | args[key] = processId({ id: arg }); 75 | } 76 | 77 | if (key.endsWith('_add')) { 78 | const values = args[key]; 79 | args.$push = { 80 | [key.slice(0, -'_add'.length)]: { $each: values } 81 | }; 82 | delete args[key]; 83 | } 84 | }); 85 | 86 | return Collection.update({ _id }, args).then((res) => { 87 | if (res.ok) { 88 | return getOne(Collection, { _id }, context, info); 89 | } 90 | 91 | return null; 92 | }); 93 | } 94 | 95 | function deleteOne(Collection, args) { 96 | const _id = processId(args); 97 | 98 | return Collection.remove({ _id }).then(({ result }) => ({ 99 | id: toGlobalId(Collection.modelName, _id), 100 | ok: !!result.ok 101 | })); 102 | } 103 | 104 | function getList(Collection, selector, options = {}, context, info = null) { 105 | if (selector && (isArray(selector.id) || isArray(selector._id))) { 106 | const { id, _id = id } = selector; 107 | delete selector.id; 108 | selector._id = { 109 | $in: _id.map((id) => processId({ id })) 110 | }; 111 | } 112 | 113 | const projection = getFieldList(info); 114 | return Collection.find(selector, projection, options).then((result) => ( 115 | result.map((value) => ({ 116 | ...value.toObject(), 117 | _type: Collection.modelName 118 | })) 119 | )); 120 | } 121 | 122 | function getOneResolver(graffitiModel) { 123 | return (root, args, context, info) => { 124 | const Collection = graffitiModel.model; 125 | if (Collection) { 126 | return getOne(Collection, args, context, info); 127 | } 128 | 129 | return null; 130 | }; 131 | } 132 | 133 | function getAddOneMutateHandler(graffitiModel) { 134 | // eslint-disable-next-line no-unused-vars 135 | return ({ clientMutationId, ...args }) => { 136 | const Collection = graffitiModel.model; 137 | if (Collection) { 138 | return addOne(Collection, args); 139 | } 140 | 141 | return null; 142 | }; 143 | } 144 | 145 | function getUpdateOneMutateHandler(graffitiModel) { 146 | // eslint-disable-next-line no-unused-vars 147 | return ({ clientMutationId, ...args }) => { 148 | const Collection = graffitiModel.model; 149 | if (Collection) { 150 | return updateOne(Collection, args); 151 | } 152 | 153 | return null; 154 | }; 155 | } 156 | 157 | function getDeleteOneMutateHandler(graffitiModel) { 158 | // eslint-disable-next-line no-unused-vars 159 | return ({ clientMutationId, ...args }) => { 160 | const Collection = graffitiModel.model; 161 | if (Collection) { 162 | return deleteOne(Collection, args); 163 | } 164 | 165 | return null; 166 | }; 167 | } 168 | 169 | function getListResolver(graffitiModel) { 170 | return (root, { ids, ...args } = {}, context, info) => { 171 | if (ids) { 172 | args.id = ids; 173 | } 174 | 175 | const { orderBy: sort } = args; 176 | delete args.orderBy; 177 | 178 | const Collection = graffitiModel.model; 179 | if (Collection) { 180 | return getList(Collection, args, { sort }, context, info); 181 | } 182 | 183 | return null; 184 | }; 185 | } 186 | 187 | /** 188 | * Returns the first element in a Collection 189 | */ 190 | function getFirst(Collection) { 191 | return Collection.findOne({}, {}, { sort: { _id: 1 } }); 192 | } 193 | 194 | /** 195 | * Returns an idFetcher function, that can resolve 196 | * an object based on a global id 197 | */ 198 | function getIdFetcher(graffitiModels) { 199 | return function idFetcher(obj, { id: globalId }, context, info) { 200 | const { type, id } = fromGlobalId(globalId); 201 | 202 | if (type === 'Viewer') { 203 | return viewer; 204 | } else if (graffitiModels[type]) { 205 | const Collection = graffitiModels[type].model; 206 | return getOne(Collection, { id }, context, info); 207 | } 208 | 209 | return null; 210 | }; 211 | } 212 | 213 | /** 214 | * Helper to get an empty connection. 215 | */ 216 | function emptyConnection() { 217 | return { 218 | count: 0, 219 | edges: [], 220 | pageInfo: { 221 | startCursor: null, 222 | endCursor: null, 223 | hasPreviousPage: false, 224 | hasNextPage: false 225 | } 226 | }; 227 | } 228 | 229 | const PREFIX = 'connection.'; 230 | 231 | function base64(i) { 232 | return ((new Buffer(i, 'ascii')).toString('base64')); 233 | } 234 | 235 | function unbase64(i) { 236 | return ((new Buffer(i, 'base64')).toString('ascii')); 237 | } 238 | 239 | /** 240 | * Creates the cursor string from an offset. 241 | */ 242 | function idToCursor(id) { 243 | return base64(PREFIX + id); 244 | } 245 | 246 | /** 247 | * Rederives the offset from the cursor string. 248 | */ 249 | function cursorToId(cursor) { 250 | return unbase64(cursor).substring(PREFIX.length); 251 | } 252 | 253 | /** 254 | * Given an optional cursor and a default offset, returns the offset 255 | * to use; if the cursor contains a valid offset, that will be used, 256 | * otherwise it will be the default. 257 | */ 258 | function getId(cursor) { 259 | if (cursor === undefined || cursor === null) { 260 | return null; 261 | } 262 | 263 | return cursorToId(cursor); 264 | } 265 | 266 | /** 267 | * Returns a connection based on a graffitiModel 268 | */ 269 | async function connectionFromModel(graffitiModel, args, context, info) { 270 | const Collection = graffitiModel.model; 271 | if (!Collection) { 272 | return emptyConnection(); 273 | } 274 | 275 | const { before, after, first, last, id, orderBy = { _id: 1 }, ...selector } = args; 276 | 277 | const begin = getId(after); 278 | const end = getId(before); 279 | 280 | const offset = (first - last) || 0; 281 | const limit = last || first; 282 | 283 | if (id) { 284 | selector.id = id; 285 | } 286 | 287 | if (begin) { 288 | selector._id = selector._id || {}; 289 | selector._id.$gt = begin; 290 | } 291 | 292 | if (end) { 293 | selector._id = selector._id || {}; 294 | selector._id.$lt = end; 295 | } 296 | 297 | const result = await getList(Collection, selector, { 298 | limit, 299 | skip: offset, 300 | sort: orderBy 301 | }, context, info); 302 | const count = await getCount(Collection, selector); 303 | 304 | if (result.length === 0) { 305 | return emptyConnection(); 306 | } 307 | 308 | const edges = result.map((value) => ({ 309 | cursor: idToCursor(value._id), 310 | node: value 311 | })); 312 | 313 | const firstElement = await getFirst(Collection); 314 | return { 315 | count, 316 | edges, 317 | pageInfo: { 318 | startCursor: edges[0].cursor, 319 | endCursor: edges[edges.length - 1].cursor, 320 | hasPreviousPage: cursorToId(edges[0].cursor) !== firstElement._id.toString(), 321 | hasNextPage: result.length === limit 322 | } 323 | }; 324 | } 325 | 326 | export default { 327 | getOneResolver, 328 | getListResolver, 329 | getAddOneMutateHandler, 330 | getUpdateOneMutateHandler, 331 | getDeleteOneMutateHandler 332 | }; 333 | 334 | export { 335 | idToCursor as _idToCursor, 336 | idToCursor, 337 | getIdFetcher, 338 | getOneResolver, 339 | getAddOneMutateHandler, 340 | getUpdateOneMutateHandler, 341 | getDeleteOneMutateHandler, 342 | getListResolver, 343 | connectionFromModel 344 | }; 345 | -------------------------------------------------------------------------------- /src/schema/schema.js: -------------------------------------------------------------------------------- 1 | import { reduce, isArray, isFunction, mapValues } from 'lodash'; 2 | import { toCollectionName } from 'mongoose/lib/utils'; 3 | import { 4 | GraphQLList, 5 | GraphQLNonNull, 6 | GraphQLID, 7 | GraphQLObjectType, 8 | GraphQLSchema, 9 | GraphQLBoolean, 10 | GraphQLFloat 11 | } from 'graphql'; 12 | import { 13 | mutationWithClientMutationId, 14 | connectionArgs, 15 | connectionDefinitions, 16 | globalIdField 17 | } from 'graphql-relay'; 18 | import model from './../model'; 19 | import type, { 20 | GraphQLViewer, 21 | nodeInterface, 22 | getTypeFields, 23 | getArguments, 24 | setTypeFields 25 | } from './../type'; 26 | import query, { 27 | idToCursor, 28 | getIdFetcher, 29 | connectionFromModel 30 | } from './../query'; 31 | import { addHooks } from '../utils'; 32 | import viewerInstance from '../model/viewer'; 33 | import createInputObject from '../type/custom/to-input-object'; 34 | 35 | const idField = { 36 | name: 'id', 37 | type: new GraphQLNonNull(GraphQLID) 38 | }; 39 | 40 | function getSingularQueryField(graffitiModel, type, hooks = {}) { 41 | const { name } = type; 42 | const { singular } = hooks; 43 | const singularName = name.toLowerCase(); 44 | 45 | return { 46 | [singularName]: { 47 | type, 48 | args: { 49 | id: idField 50 | }, 51 | resolve: addHooks(query.getOneResolver(graffitiModel), singular) 52 | } 53 | }; 54 | } 55 | 56 | function getPluralQueryField(graffitiModel, type, hooks = {}) { 57 | const { name } = type; 58 | const { plural } = hooks; 59 | const pluralName = toCollectionName(name); 60 | 61 | return { 62 | [pluralName]: { 63 | type: new GraphQLList(type), 64 | args: getArguments(type, { 65 | id: { 66 | type: new GraphQLList(GraphQLID), 67 | description: `The ID of a ${name}` 68 | }, 69 | ids: { 70 | type: new GraphQLList(GraphQLID), 71 | description: `The ID of a ${name}` 72 | } 73 | }), 74 | resolve: addHooks(query.getListResolver(graffitiModel), plural) 75 | } 76 | }; 77 | } 78 | 79 | function getQueryField(graffitiModel, type, hooks) { 80 | return { 81 | ...getSingularQueryField(graffitiModel, type, hooks), 82 | ...getPluralQueryField(graffitiModel, type, hooks) 83 | }; 84 | } 85 | 86 | function getConnectionField(graffitiModel, type, hooks = {}) { 87 | const { name } = type; 88 | const { plural } = hooks; 89 | const pluralName = toCollectionName(name.toLowerCase()); 90 | const { connectionType } = connectionDefinitions({ 91 | name, 92 | nodeType: type, 93 | connectionFields: { 94 | count: { 95 | name: 'count', 96 | type: GraphQLFloat 97 | } 98 | } 99 | }); 100 | 101 | return { 102 | [pluralName]: { 103 | args: getArguments(type, connectionArgs), 104 | type: connectionType, 105 | resolve: addHooks((rootValue, args, info) => connectionFromModel(graffitiModel, args, info), plural) 106 | } 107 | }; 108 | } 109 | 110 | function getMutationField(graffitiModel, type, viewer, hooks = {}, allowMongoIDMutation) { 111 | const { name } = type; 112 | const { mutation } = hooks; 113 | 114 | const fields = getTypeFields(type); 115 | const inputFields = reduce(fields, (inputFields, field) => { 116 | if (field.type instanceof GraphQLObjectType) { 117 | if (field.type.name.endsWith('Connection')) { 118 | inputFields[field.name] = { 119 | name: field.name, 120 | type: new GraphQLList(GraphQLID) 121 | }; 122 | } else if (field.type.mongooseEmbedded) { 123 | inputFields[field.name] = { 124 | name: field.name, 125 | type: createInputObject(field.type) 126 | }; 127 | } else { 128 | inputFields[field.name] = { 129 | name: field.name, 130 | type: GraphQLID 131 | }; 132 | } 133 | } 134 | 135 | if (field.type instanceof GraphQLList && field.type.ofType instanceof GraphQLObjectType) { 136 | inputFields[field.name] = { 137 | name: field.name, 138 | type: new GraphQLList(createInputObject(field.type.ofType)) 139 | }; 140 | } else if (!(field.type instanceof GraphQLObjectType) 141 | && field.name !== 'id' && field.name !== '__v' 142 | && (allowMongoIDMutation || field.name !== '_id')) { 143 | inputFields[field.name] = { 144 | name: field.name, 145 | type: field.type 146 | }; 147 | } 148 | 149 | return inputFields; 150 | }, {}); 151 | 152 | const updateInputFields = reduce(fields, (inputFields, field) => { 153 | if (field.type instanceof GraphQLObjectType && field.type.name.endsWith('Connection')) { 154 | inputFields[`${field.name}_add`] = { 155 | name: field.name, 156 | type: new GraphQLList(GraphQLID) 157 | }; 158 | } 159 | 160 | return inputFields; 161 | }, {}); 162 | 163 | const changedName = `changed${name}`; 164 | const edgeName = `${changedName}Edge`; 165 | const nodeName = `${changedName}Node`; 166 | 167 | const addName = `add${name}`; 168 | const updateName = `update${name}`; 169 | const deleteName = `delete${name}`; 170 | 171 | return { 172 | [addName]: mutationWithClientMutationId({ 173 | name: addName, 174 | inputFields, 175 | outputFields: { 176 | viewer, 177 | [edgeName]: { 178 | type: connectionDefinitions({ 179 | name: changedName, 180 | nodeType: new GraphQLObjectType({ 181 | name: nodeName, 182 | fields 183 | }) 184 | }).edgeType, 185 | resolve: (node) => ({ 186 | node, 187 | cursor: idToCursor(node.id) 188 | }) 189 | } 190 | }, 191 | mutateAndGetPayload: addHooks(query.getAddOneMutateHandler(graffitiModel), mutation) 192 | }), 193 | [updateName]: mutationWithClientMutationId({ 194 | name: updateName, 195 | inputFields: { 196 | ...inputFields, 197 | ...updateInputFields, 198 | id: idField 199 | }, 200 | outputFields: { 201 | [changedName]: { 202 | type, 203 | resolve: (node) => node 204 | } 205 | }, 206 | mutateAndGetPayload: addHooks(query.getUpdateOneMutateHandler(graffitiModel), mutation) 207 | }), 208 | [deleteName]: mutationWithClientMutationId({ 209 | name: deleteName, 210 | inputFields: { 211 | id: idField 212 | }, 213 | outputFields: { 214 | viewer, 215 | ok: { 216 | type: GraphQLBoolean 217 | }, 218 | id: idField 219 | }, 220 | mutateAndGetPayload: addHooks(query.getDeleteOneMutateHandler(graffitiModel), mutation) 221 | }) 222 | }; 223 | } 224 | 225 | /** 226 | * Returns query and mutation root fields 227 | * @param {Array} graffitiModels 228 | * @param {{Object, Boolean}} {hooks, mutation, allowMongoIDMutation} 229 | * @return {Object} 230 | */ 231 | function getFields(graffitiModels, { 232 | hooks = {}, mutation = true, allowMongoIDMutation = false, 233 | customQueries = {}, customMutations = {} 234 | } = {}) { 235 | const types = type.getTypes(graffitiModels); 236 | const { viewer, singular } = hooks; 237 | 238 | const viewerFields = reduce(types, (fields, type, key) => { 239 | type.name = type.name || key; 240 | const graffitiModel = graffitiModels[type.name]; 241 | return { 242 | ...fields, 243 | ...getConnectionField(graffitiModel, type, hooks), 244 | ...getSingularQueryField(graffitiModel, type, hooks) 245 | }; 246 | }, { 247 | id: globalIdField('Viewer') 248 | }); 249 | setTypeFields(GraphQLViewer, viewerFields); 250 | 251 | const viewerField = { 252 | name: 'Viewer', 253 | type: GraphQLViewer, 254 | resolve: addHooks(() => viewerInstance, viewer) 255 | }; 256 | 257 | const { queries, mutations } = reduce(types, ({ queries, mutations }, type, key) => { 258 | type.name = type.name || key; 259 | const graffitiModel = graffitiModels[type.name]; 260 | return { 261 | queries: { 262 | ...queries, 263 | ...getQueryField(graffitiModel, type, hooks) 264 | }, 265 | mutations: { 266 | ...mutations, 267 | ...getMutationField(graffitiModel, type, viewerField, hooks, allowMongoIDMutation) 268 | } 269 | }; 270 | }, { 271 | queries: isFunction(customQueries) 272 | ? customQueries(mapValues(types, (type) => createInputObject(type)), types) 273 | : customQueries, 274 | mutations: isFunction(customMutations) 275 | ? customMutations(mapValues(types, (type) => createInputObject(type)), types) 276 | : customMutations 277 | }); 278 | 279 | const RootQuery = new GraphQLObjectType({ 280 | name: 'RootQuery', 281 | fields: { 282 | ...queries, 283 | viewer: viewerField, 284 | node: { 285 | name: 'node', 286 | description: 'Fetches an object given its ID', 287 | type: nodeInterface, 288 | args: { 289 | id: { 290 | type: new GraphQLNonNull(GraphQLID), 291 | description: 'The ID of an object' 292 | } 293 | }, 294 | resolve: addHooks(getIdFetcher(graffitiModels), singular) 295 | } 296 | } 297 | }); 298 | 299 | const RootMutation = new GraphQLObjectType({ 300 | name: 'RootMutation', 301 | fields: mutations 302 | }); 303 | 304 | const fields = { 305 | query: RootQuery 306 | }; 307 | 308 | if (mutation) { 309 | fields.mutation = RootMutation; 310 | } 311 | 312 | return fields; 313 | } 314 | 315 | /** 316 | * Returns a GraphQL schema including query and mutation fields 317 | * @param {Array} mongooseModels 318 | * @param {Object} options 319 | * @return {GraphQLSchema} 320 | */ 321 | function getSchema(mongooseModels, options) { 322 | if (!isArray(mongooseModels)) { 323 | mongooseModels = [mongooseModels]; 324 | } 325 | const graffitiModels = model.getModels(mongooseModels); 326 | const fields = getFields(graffitiModels, options); 327 | return new GraphQLSchema(fields); 328 | } 329 | 330 | export { 331 | getQueryField, 332 | getMutationField, 333 | getFields, 334 | getSchema 335 | }; 336 | -------------------------------------------------------------------------------- /src/type/type.js: -------------------------------------------------------------------------------- 1 | import { 2 | reduce, 3 | forEach, 4 | isFunction 5 | } from 'lodash'; 6 | import { 7 | globalIdField, 8 | connectionArgs, 9 | connectionDefinitions, 10 | nodeDefinitions 11 | } from 'graphql-relay'; 12 | import { 13 | GraphQLString, 14 | GraphQLFloat, 15 | GraphQLBoolean, 16 | GraphQLID, 17 | GraphQLList, 18 | GraphQLObjectType, 19 | GraphQLNonNull, 20 | GraphQLScalarType, 21 | GraphQLEnumType 22 | } from 'graphql/type'; 23 | import { addHooks } from '../utils'; 24 | import GraphQLDate from './custom/date'; 25 | import GraphQLBuffer from './custom/buffer'; 26 | import GraphQLGeneric from './custom/generic'; 27 | import { connectionFromModel, getOneResolver } from '../query'; 28 | 29 | // Registered types will be saved, we can access them later to resolve types 30 | const types = []; 31 | 32 | /** 33 | * Add new type 34 | * @param {String} name 35 | * @param {GraphQLType} type 36 | */ 37 | function addType(name, type) { 38 | types[name] = type; 39 | } 40 | 41 | // Node interface 42 | const { nodeInterface } = nodeDefinitions(null, (obj) => ( 43 | // Type resolver 44 | obj._type ? types[obj._type] : null 45 | )); 46 | 47 | // GraphQL Viewer type 48 | const GraphQLViewer = new GraphQLObjectType({ 49 | name: 'Viewer', 50 | interfaces: [nodeInterface] 51 | }); 52 | 53 | // Register Viewer type 54 | addType('Viewer', GraphQLViewer); 55 | 56 | /** 57 | * Returns a GraphQL type based on a String representation 58 | * @param {String} type 59 | * @return {GraphQLType} 60 | */ 61 | function stringToGraphQLType(type) { 62 | switch (type) { 63 | case 'String': 64 | return GraphQLString; 65 | case 'Number': 66 | return GraphQLFloat; 67 | case 'Date': 68 | return GraphQLDate; 69 | case 'Buffer': 70 | return GraphQLBuffer; 71 | case 'Boolean': 72 | return GraphQLBoolean; 73 | case 'ObjectID': 74 | return GraphQLID; 75 | default: 76 | return GraphQLGeneric; 77 | } 78 | } 79 | 80 | /** 81 | * Returns a GraphQL Enum type based on a List of Strings 82 | * @param {Array} list 83 | * @param {String} name 84 | * @return {Object} 85 | */ 86 | function listToGraphQLEnumType(list, name) { 87 | const values = reduce(list, (values, val) => { 88 | values[val] = { value: val }; 89 | return values; 90 | }, {}); 91 | return new GraphQLEnumType({ name, values }); 92 | } 93 | 94 | /** 95 | * Extracts the fields of a GraphQL type 96 | * @param {GraphQLType} type 97 | * @return {Object} 98 | */ 99 | function getTypeFields(type) { 100 | const fields = type._typeConfig.fields; 101 | return isFunction(fields) ? fields() : fields; 102 | } 103 | 104 | /** 105 | * Assign fields to a GraphQL type 106 | * @param {GraphQLType} type 107 | * @param {Object} fields 108 | */ 109 | function setTypeFields(type, fields) { 110 | type._typeConfig.fields = () => fields; 111 | } 112 | 113 | const orderByTypes = {}; 114 | /** 115 | * Returns order by GraphQLEnumType for fields 116 | * @param {{String}} {name} 117 | * @param {Object} fields 118 | * @return {GraphQLEnumType} 119 | */ 120 | function getOrderByType({ name }, fields) { 121 | if (!orderByTypes[name]) { 122 | // save new enum 123 | orderByTypes[name] = new GraphQLEnumType({ 124 | name: `orderBy${name}`, 125 | values: reduce(fields, (values, field) => { 126 | if (field.type instanceof GraphQLScalarType) { 127 | const upperCaseName = field.name.toUpperCase(); 128 | values[`${upperCaseName}_ASC`] = { 129 | name: `${upperCaseName}_ASC`, 130 | value: { 131 | [field.name]: 1 132 | } 133 | }; 134 | values[`${upperCaseName}_DESC`] = { 135 | name: `${upperCaseName}_DESC`, 136 | value: { 137 | [field.name]: -1 138 | } 139 | }; 140 | } 141 | 142 | return values; 143 | }, {}) 144 | }); 145 | } 146 | return orderByTypes[name]; 147 | } 148 | 149 | /** 150 | * Returns query arguments for a GraphQL type 151 | * @param {GraphQLType} type 152 | * @param {Object} args 153 | * @return {Object} 154 | */ 155 | function getArguments(type, args = {}) { 156 | const fields = getTypeFields(type); 157 | 158 | return reduce(fields, (args, field) => { 159 | // Extract non null fields, those are not required in the arguments 160 | if (field.type instanceof GraphQLNonNull && field.name !== 'id') { 161 | field.type = field.type.ofType; 162 | } 163 | 164 | if (field.type instanceof GraphQLScalarType) { 165 | args[field.name] = field; 166 | } 167 | 168 | return args; 169 | }, { 170 | ...args, 171 | orderBy: { 172 | name: 'orderBy', 173 | type: getOrderByType(type, fields) 174 | } 175 | }); 176 | } 177 | 178 | /** 179 | * Returns a concatenation of type and field name, used for nestedObjects 180 | * @param {String} typeName 181 | * @param {String} fieldName 182 | * @returns {String} 183 | */ 184 | function getTypeFieldName(typeName, fieldName) { 185 | const fieldNameCapitalized = fieldName.charAt(0).toUpperCase() + fieldName.slice(1); 186 | return `${typeName}${fieldNameCapitalized}`; 187 | } 188 | 189 | // Holds references to fields that later have to be resolved 190 | const resolveReference = {}; 191 | 192 | /** 193 | * Returns GraphQLType for a graffiti model 194 | * @param {Object} graffitiModels 195 | * @param {{String, String, Object}} {name, description, fields} 196 | * @param {Boolean} root 197 | * @return {GraphQLObjectType} 198 | */ 199 | function getType(graffitiModels, { name, description, fields }, path = [], rootType = null) { 200 | const root = path.length === 0; 201 | const graphQLType = { name, description }; 202 | rootType = rootType || graphQLType; 203 | 204 | // These references have to be resolved when all type definitions are avaiable 205 | resolveReference[graphQLType.name] = resolveReference[graphQLType.name] || {}; 206 | const graphQLTypeFields = reduce(fields, (graphQLFields, 207 | { name, description, type, subtype, reference, nonNull, hidden, hooks, 208 | fields: subfields, embeddedModel, enumValues }, key) => { 209 | name = name || key; 210 | const newPath = [...path, name]; 211 | 212 | // Don't add hidden fields to the GraphQLObjectType 213 | if (hidden || name.startsWith('__')) { 214 | return graphQLFields; 215 | } 216 | 217 | const graphQLField = { name, description }; 218 | 219 | if (type === 'Array') { 220 | if (subtype === 'Object') { 221 | const fields = subfields; 222 | const nestedObjectName = getTypeFieldName(graphQLType.name, name); 223 | graphQLField.type = new GraphQLList( 224 | getType(graffitiModels, { name: nestedObjectName, description, fields }, newPath, rootType)); 225 | } else { 226 | graphQLField.type = new GraphQLList(stringToGraphQLType(subtype)); 227 | if (reference) { 228 | resolveReference[rootType.name][name] = { 229 | name, 230 | type: reference, 231 | args: connectionArgs, 232 | resolve: addHooks((rootValue, args, context, info) => { 233 | args.id = rootValue[name].map((i) => i.toString()); 234 | return connectionFromModel(graffitiModels[reference], args, context, info); 235 | }, hooks) 236 | }; 237 | } 238 | } 239 | } else if (type === 'Object') { 240 | const fields = subfields; 241 | const nestedObjectName = getTypeFieldName(graphQLType.name, name); 242 | graphQLField.type = getType(graffitiModels, { name: nestedObjectName, description, fields }, newPath, rootType); 243 | } else if (type === 'Embedded') { 244 | const type = types.hasOwnProperty(name) 245 | ? types[name] 246 | : getType(graffitiModels, embeddedModel, ['embedded']); 247 | type.mongooseEmbedded = true; 248 | graphQLField.type = type; 249 | } else if (enumValues && type === 'String') { 250 | graphQLField.type = listToGraphQLEnumType(enumValues, getTypeFieldName(graphQLType.name, `${name}Enum`)); 251 | } else { 252 | graphQLField.type = stringToGraphQLType(type); 253 | } 254 | 255 | if (reference && (graphQLField.type === GraphQLID || graphQLField.type === new GraphQLNonNull(GraphQLID))) { 256 | resolveReference[rootType.name][newPath.join('.')] = { 257 | name, 258 | type: reference, 259 | resolve: addHooks((rootValue, args, context, info) => { 260 | const resolver = getOneResolver(graffitiModels[reference]); 261 | return resolver(rootValue, { id: rootValue[name] ? rootValue[name].toString() : null }, context, info); 262 | }, hooks) 263 | }; 264 | } 265 | 266 | if (nonNull && graphQLField.type) { 267 | graphQLField.type = new GraphQLNonNull(graphQLField.type); 268 | } 269 | 270 | if (!graphQLField.resolve) { 271 | graphQLField.resolve = addHooks((source) => source[name], hooks); 272 | } 273 | 274 | graphQLFields[name] = graphQLField; 275 | return graphQLFields; 276 | }, {}); 277 | 278 | if (root) { 279 | // Implement the Node interface 280 | graphQLType.interfaces = [nodeInterface]; 281 | graphQLTypeFields.id = globalIdField(name, (obj) => obj._id); 282 | } 283 | 284 | // Add fields to the GraphQL type 285 | graphQLType.fields = () => graphQLTypeFields; 286 | 287 | // Define type 288 | const GraphQLObjectTypeDefinition = new GraphQLObjectType(graphQLType); 289 | 290 | // Register type 291 | if (root) { 292 | addType(name, GraphQLObjectTypeDefinition); 293 | } 294 | 295 | return GraphQLObjectTypeDefinition; 296 | } 297 | 298 | function getTypes(graffitiModels) { 299 | const types = reduce(graffitiModels, (types, model) => { 300 | types[model.name] = getType(graffitiModels, model); 301 | return types; 302 | }, {}); 303 | 304 | // Resolve references, all types are defined / avaiable 305 | forEach(resolveReference, (fields, typeName) => { 306 | const type = types[typeName]; 307 | if (type) { 308 | const typeFields = reduce(fields, (typeFields, field, fieldName) => { 309 | if (field.args === connectionArgs) { 310 | // It's a connection 311 | const connectionName = getTypeFieldName(typeName, fieldName); 312 | const { connectionType } = connectionDefinitions({ 313 | name: connectionName, 314 | nodeType: types[field.type], 315 | connectionFields: { 316 | count: { 317 | name: 'count', 318 | type: GraphQLFloat 319 | } 320 | } 321 | }); 322 | field.type = connectionType; 323 | } else { 324 | // It's an object reference 325 | field.type = types[field.type]; 326 | } 327 | 328 | // deeply find the path of the field we want to resolve the reference of 329 | const path = fieldName.split('.'); 330 | 331 | path.reduce((parent, segment, idx) => { 332 | if (parent[segment]) { 333 | if (parent[segment].type instanceof GraphQLObjectType) { 334 | parent = getTypeFields(parent[segment].type); 335 | } else if (parent[segment].type instanceof GraphQLList && 336 | parent[segment].type.ofType instanceof GraphQLObjectType) { 337 | parent = getTypeFields(parent[segment].type.ofType); 338 | } 339 | } 340 | 341 | if (idx === path.length - 1) { 342 | parent[segment] = field; 343 | } 344 | 345 | return parent; 346 | }, typeFields); 347 | 348 | return typeFields; 349 | }, getTypeFields(type)); 350 | 351 | // Add new fields 352 | setTypeFields(type, typeFields); 353 | } 354 | }); 355 | 356 | return types; 357 | } 358 | 359 | export default { 360 | getTypes 361 | }; 362 | 363 | export { 364 | GraphQLViewer, 365 | GraphQLDate, 366 | GraphQLGeneric, 367 | getType, 368 | getTypes, 369 | addType, 370 | nodeInterface, 371 | getTypeFields, 372 | setTypeFields, 373 | getArguments 374 | }; 375 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ### Running the example 2 | 3 | ```shell 4 | cd graffiti-mongoose 5 | npm install # install dependencies in the main folder 6 | cd example 7 | npm install # install dependencies in the example folder 8 | npm start # run the example application and open your browser: http://localhost:8080 9 | ``` 10 | 11 | ### Mongoose schema 12 | 13 | ```javascript 14 | const UserSchema = new mongoose.Schema({ 15 | name: { 16 | type: String 17 | }, 18 | age: { 19 | type: Number, 20 | index: true 21 | }, 22 | createdAt: Date, 23 | friends: [ 24 | { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: 'User' 27 | } 28 | ], 29 | nums: [Number], 30 | bools: [Boolean], 31 | strings: [String], 32 | removed: Boolean, 33 | 34 | body: { 35 | eye: String, 36 | hair: Number 37 | } 38 | }); 39 | 40 | const User = mongoose.model('User', UserSchema); 41 | ``` 42 | 43 | ### Example queries 44 | 45 | ##### _Singular query on a type_ 46 | 47 | * arguments: `id: ID!` 48 | 49 | ``` 50 | query UserQuery { 51 | user(id: "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZmI=") { 52 | id 53 | name 54 | friends(first: 5) { 55 | count 56 | edges { 57 | cursor 58 | node { 59 | id 60 | name 61 | friends { 62 | count 63 | } 64 | } 65 | } 66 | pageInfo { 67 | startCursor 68 | endCursor 69 | hasPreviousPage 70 | hasNextPage 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | ```json 77 | { 78 | "data": { 79 | "user": { 80 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZmI=", 81 | "name": "User38", 82 | "friends": { 83 | "count": 38, 84 | "edges": [ 85 | { 86 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDU=", 87 | "node": { 88 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDU=", 89 | "name": "User0", 90 | "friends": { 91 | "count": 0 92 | } 93 | } 94 | }, 95 | { 96 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDY=", 97 | "node": { 98 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDY=", 99 | "name": "User1", 100 | "friends": { 101 | "count": 1 102 | } 103 | } 104 | }, 105 | { 106 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDc=", 107 | "node": { 108 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDc=", 109 | "name": "User2", 110 | "friends": { 111 | "count": 2 112 | } 113 | } 114 | }, 115 | { 116 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDg=", 117 | "node": { 118 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDg=", 119 | "name": "User3", 120 | "friends": { 121 | "count": 3 122 | } 123 | } 124 | }, 125 | { 126 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDk=", 127 | "node": { 128 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDk=", 129 | "name": "User4", 130 | "friends": { 131 | "count": 4 132 | } 133 | } 134 | } 135 | ], 136 | "pageInfo": { 137 | "startCursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDU=", 138 | "endCursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDk=", 139 | "hasPreviousPage": true, 140 | "hasNextPage": true 141 | } 142 | } 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | ##### _Singular query on node_ 149 | 150 | * arguments: `id: ID!` 151 | 152 | ``` 153 | query NodeQuery { 154 | user(id: "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZmI=") { 155 | id 156 | ... on User { 157 | name 158 | friends(first: 5) { 159 | count 160 | edges { 161 | cursor 162 | node { 163 | id 164 | name 165 | friends { 166 | count 167 | } 168 | } 169 | } 170 | pageInfo { 171 | startCursor 172 | endCursor 173 | hasPreviousPage 174 | hasNextPage 175 | } 176 | } 177 | } 178 | } 179 | } 180 | ``` 181 | ```json 182 | { 183 | "data": { 184 | "node": { 185 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZmI=", 186 | "name": "User38", 187 | "friends": { 188 | "count": 38, 189 | "edges": [ 190 | { 191 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDU=", 192 | "node": { 193 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDU=", 194 | "name": "User0", 195 | "friends": { 196 | "count": 0 197 | } 198 | } 199 | }, 200 | { 201 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDY=", 202 | "node": { 203 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDY=", 204 | "name": "User1", 205 | "friends": { 206 | "count": 1 207 | } 208 | } 209 | }, 210 | { 211 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDc=", 212 | "node": { 213 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDc=", 214 | "name": "User2", 215 | "friends": { 216 | "count": 2 217 | } 218 | } 219 | }, 220 | { 221 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDg=", 222 | "node": { 223 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDg=", 224 | "name": "User3", 225 | "friends": { 226 | "count": 3 227 | } 228 | } 229 | }, 230 | { 231 | "cursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDk=", 232 | "node": { 233 | "id": "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDk=", 234 | "name": "User4", 235 | "friends": { 236 | "count": 4 237 | } 238 | } 239 | } 240 | ], 241 | "pageInfo": { 242 | "startCursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDU=", 243 | "endCursor": "Y29ubmVjdGlvbi41NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZDk=", 244 | "hasPreviousPage": true, 245 | "hasNextPage": true 246 | } 247 | } 248 | } 249 | } 250 | } 251 | ``` 252 | 253 | ___ 254 | 255 | ##### _Plural query on a type_ 256 | 257 | * arguments: `id: [ID], ids: [ID], name: String, age: Float, createdAt: Date, removed: Boolean, _id: ID` 258 | 259 | ``` 260 | query UsersQuery { 261 | users { 262 | id 263 | name 264 | age 265 | body { 266 | eye 267 | } 268 | createdAt 269 | } 270 | } 271 | ``` 272 | ```json 273 | { 274 | "data": { 275 | "users": [ 276 | { 277 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDM=", 278 | "name": "User0", 279 | "age": 0, 280 | "body": { 281 | "eye": "blue" 282 | }, 283 | "createdAt": "2015-10-19T14:04:44.000Z" 284 | }, 285 | { 286 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDQ=", 287 | "name": "User1", 288 | "age": 1, 289 | "body": { 290 | "eye": "blue" 291 | }, 292 | "createdAt": "2015-10-19T14:04:44.000Z" 293 | }, 294 | { 295 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDU=", 296 | "name": "User2", 297 | "age": 2, 298 | "body": { 299 | "eye": "blue" 300 | }, 301 | "createdAt": "2015-10-19T14:04:44.000Z" 302 | }, 303 | { 304 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDY=", 305 | "name": "User3", 306 | "age": 3, 307 | "body": { 308 | "eye": "blue" 309 | }, 310 | "createdAt": "2015-10-19T14:04:44.000Z" 311 | }, 312 | { 313 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDc=", 314 | "name": "User4", 315 | "age": 4, 316 | "body": { 317 | "eye": "blue" 318 | }, 319 | "createdAt": "2015-10-19T14:04:44.000Z" 320 | } 321 | ] 322 | } 323 | } 324 | ``` 325 | 326 | ##### _Singular query on viewer_ 327 | 328 | ``` 329 | query ViewerUserQuery { 330 | viewer { 331 | user(id: "VXNlcjo1NjI2MzgwZTU2NWQ2Y2E3NGUzNzc3ZGM=") { 332 | name 333 | age 334 | friends { 335 | count 336 | } 337 | } 338 | } 339 | } 340 | ``` 341 | ```json 342 | { 343 | "data": { 344 | "viewer": { 345 | "user": { 346 | "name": "User7", 347 | "age": 7, 348 | "friends": { 349 | "count": 7 350 | } 351 | } 352 | } 353 | } 354 | } 355 | ``` 356 | 357 | ##### _Plural query on viewer_ 358 | 359 | ``` 360 | query ViewerUsersQuery { 361 | viewer { 362 | users(first: 5, after: "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDU=") { 363 | edges { 364 | cursor 365 | node { 366 | id 367 | name 368 | friends { 369 | count 370 | } 371 | } 372 | } 373 | pageInfo { 374 | startCursor 375 | endCursor 376 | hasPreviousPage 377 | hasNextPage 378 | } 379 | } 380 | } 381 | } 382 | ``` 383 | ```json 384 | { 385 | "data": { 386 | "viewer": { 387 | "users": { 388 | "edges": [ 389 | { 390 | "cursor": "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDY=", 391 | "node": { 392 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDY=", 393 | "name": "User3", 394 | "friends": { 395 | "count": 3 396 | } 397 | } 398 | }, 399 | { 400 | "cursor": "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDc=", 401 | "node": { 402 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDc=", 403 | "name": "User4", 404 | "friends": { 405 | "count": 4 406 | } 407 | } 408 | }, 409 | { 410 | "cursor": "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDg=", 411 | "node": { 412 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDg=", 413 | "name": "User5", 414 | "friends": { 415 | "count": 5 416 | } 417 | } 418 | }, 419 | { 420 | "cursor": "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDk=", 421 | "node": { 422 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDk=", 423 | "name": "User6", 424 | "friends": { 425 | "count": 6 426 | } 427 | } 428 | }, 429 | { 430 | "cursor": "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NGE=", 431 | "node": { 432 | "id": "VXNlcjo1NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NGE=", 433 | "name": "User7", 434 | "friends": { 435 | "count": 7 436 | } 437 | } 438 | } 439 | ], 440 | "pageInfo": { 441 | "startCursor": "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NDY=", 442 | "endCursor": "Y29ubmVjdGlvbi41NjI0Zjg3Y2ZmZTZmZDMwMzM1NjQ1NGE=", 443 | "hasPreviousPage": true, 444 | "hasNextPage": true 445 | } 446 | } 447 | } 448 | } 449 | } 450 | ``` 451 | 452 | ##### _Add mutation_ 453 | 454 | * arguments: `input: addUserInput!{ 455 | name: String, 456 | age: Float, 457 | createdAt: Date, 458 | friends: [ID], 459 | nums: [Float], 460 | bools: [Boolean], 461 | strings: [String], 462 | removed: Boolean, 463 | clientMutationId: String! 464 | }` 465 | 466 | ``` 467 | mutation AddUser { 468 | addUser(input: {clientMutationId: "1", name: "Test", bools: [true, false], friends: ["56264133565d6ca74e377841"]}) { 469 | changedUserEdge { 470 | cursor 471 | node { 472 | _id 473 | id 474 | name 475 | bools 476 | friends { 477 | count 478 | } 479 | } 480 | } 481 | } 482 | } 483 | ``` 484 | ```json 485 | { 486 | "data": { 487 | "addUser": { 488 | "changedUserEdge": { 489 | "cursor": "Y29ubmVjdGlvbi51bmRlZmluZWQ=", 490 | "node": { 491 | "_id": "56264152565d6ca74e377843", 492 | "id": "VXNlcjo1NjI2NDE1MjU2NWQ2Y2E3NGUzNzc4NDM=", 493 | "name": "Test", 494 | "bools": [ 495 | true, 496 | false 497 | ], 498 | "friends": { 499 | "count": 1 500 | } 501 | } 502 | } 503 | } 504 | } 505 | } 506 | ``` 507 | 508 | ##### _Update mutation_ 509 | 510 | * arguments: `input: updateUserInput!{ 511 | name: String, 512 | age: Float, 513 | createdAt: Date, 514 | friends: [ID], 515 | friends_add: [ID], 516 | nums: [Float], 517 | nums_add: [Float], 518 | bools: [Boolean], 519 | bools_add: [Boolean], 520 | strings: [String], 521 | strings_add: [String], 522 | removed: Boolean, 523 | id: ID!, 524 | clientMutationId: String! 525 | }` 526 | 527 | ``` 528 | mutation UpdateUser { 529 | updateUser(input: {clientMutationId: "1", id: "VXNlcjo1NjI2NDE1MjU2NWQ2Y2E3NGUzNzc4NDM=", name: "New Name"}) { 530 | changedUser { 531 | id 532 | name 533 | createdAt 534 | } 535 | } 536 | } 537 | ``` 538 | ```json 539 | { 540 | "data": { 541 | "updateUser": { 542 | "changedUser": { 543 | "id": "VXNlcjo1NjI2NDE1MjU2NWQ2Y2E3NGUzNzc4NDM=", 544 | "name": "New Name", 545 | "createdAt": null 546 | } 547 | } 548 | } 549 | } 550 | ``` 551 | 552 | Append to array (`fieldName_add`): 553 | 554 | ``` 555 | mutation UpdateUser { 556 | updateUser(input: {clientMutationId: "1", id: "VXNlcjo1NjI2NDE1MjU2NWQ2Y2E3NGUzNzc4NDM=", friends_add: ["VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYjQ=", "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYjk="]}) { 557 | changedUser { 558 | id 559 | friends { 560 | count 561 | edges { 562 | node { 563 | name 564 | id 565 | } 566 | } 567 | } 568 | } 569 | } 570 | } 571 | ``` 572 | ```json 573 | { 574 | "data": { 575 | "updateUser": { 576 | "clientMutationId": "1", 577 | "changedUser": { 578 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYTM=", 579 | "friends": { 580 | "count": 10, 581 | "edges": [ 582 | { 583 | "node": { 584 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkOWI=", 585 | "name": "User0" 586 | } 587 | }, 588 | { 589 | "node": { 590 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkOWM=", 591 | "name": "User1" 592 | } 593 | }, 594 | { 595 | "node": { 596 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkOWQ=", 597 | "name": "User2" 598 | } 599 | }, 600 | { 601 | "node": { 602 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkOWU=", 603 | "name": "User3" 604 | } 605 | }, 606 | { 607 | "node": { 608 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkOWY=", 609 | "name": "User4" 610 | } 611 | }, 612 | { 613 | "node": { 614 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYTA=", 615 | "name": "User5" 616 | } 617 | }, 618 | { 619 | "node": { 620 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYTE=", 621 | "name": "User6" 622 | } 623 | }, 624 | { 625 | "node": { 626 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYTI=", 627 | "name": "User7" 628 | } 629 | }, 630 | { 631 | "node": { 632 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYjQ=", 633 | "name": "User25" 634 | } 635 | }, 636 | { 637 | "node": { 638 | "id": "VXNlcjo1NjZlYmI1MWRmZGRmYjYyYmFjNTRkYjk=", 639 | "name": "User30" 640 | } 641 | } 642 | ] 643 | } 644 | } 645 | } 646 | } 647 | } 648 | ``` 649 | 650 | ##### _Delete mutation_ 651 | 652 | * arguments: `input: deleteUserInput!{ 653 | id: ID!, 654 | clientMutationId: String! 655 | }` 656 | 657 | ``` 658 | mutation DeleteUser { 659 | deleteUser(input: {clientMutationId: "3", id: "VXNlcjo1NjI2NDE1MjU2NWQ2Y2E3NGUzNzc4NDM="}) { 660 | id 661 | viewer { 662 | users { 663 | count 664 | } 665 | } 666 | } 667 | } 668 | ``` 669 | ```json 670 | { 671 | "data": { 672 | "deleteUser": { 673 | "id": "VXNlcjo1NjI2NDE1MjU2NWQ2Y2E3NGUzNzc4NDM=", 674 | "viewer": { 675 | "users": { 676 | "count": 206 677 | } 678 | } 679 | } 680 | } 681 | } 682 | ``` 683 | -------------------------------------------------------------------------------- /src/e2e.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import { expect } from 'chai'; 4 | import { spy } from 'sinon'; 5 | 6 | import { getSchema, graphql } from './'; 7 | import User from '../fixture/user'; 8 | 9 | describe('e2e', () => { 10 | describe('get schema', () => { 11 | let motherUser; 12 | let user1; 13 | let user2; 14 | let schema; 15 | let hooks; 16 | before(() => { 17 | hooks = { 18 | viewer: { 19 | pre: spy(), 20 | post: spy() 21 | }, 22 | singular: { 23 | pre: spy(), 24 | post: spy() 25 | }, 26 | plural: { 27 | pre: spy(), 28 | post: spy() 29 | }, 30 | mutation: { 31 | pre: spy(), 32 | post: spy() 33 | } 34 | }; 35 | schema = getSchema([User], { hooks }); 36 | }); 37 | 38 | beforeEach(async () => { 39 | motherUser = new User({ 40 | name: 'Mother', 41 | age: 54, 42 | bools: [true, true] 43 | }); 44 | 45 | await motherUser.save(); 46 | 47 | user1 = new User({ 48 | name: 'Foo', 49 | age: 28 50 | }); 51 | 52 | await user1.save(); 53 | 54 | user2 = new User({ 55 | name: 'Bar', 56 | age: 28, 57 | mother: motherUser._id, 58 | friends: [user1._id], 59 | objectIds: [user1._id], 60 | sub: { 61 | subref: motherUser._id 62 | } 63 | }); 64 | 65 | await user2.save(); 66 | }); 67 | 68 | afterEach(async () => { 69 | await [motherUser.remove(), user1.remove(), user2.remove()]; 70 | }); 71 | 72 | describe('singular query', () => { 73 | it('should get data from database by id', async () => { 74 | const result = await graphql(schema, `{ 75 | user(id: "${user2._id}") { 76 | _id 77 | name 78 | age 79 | mother { 80 | _id 81 | name 82 | bools 83 | } 84 | friends { 85 | edges { 86 | node { 87 | _id 88 | name 89 | } 90 | } 91 | } 92 | objectIds 93 | sub { 94 | subref { 95 | name 96 | } 97 | } 98 | } 99 | }`); 100 | 101 | expect(result).to.be.eql({ 102 | data: { 103 | user: { 104 | _id: user2._id.toString(), 105 | name: 'Bar', 106 | age: 28, 107 | mother: { 108 | _id: motherUser._id.toString(), 109 | name: 'Mother', 110 | bools: [false, false] 111 | }, 112 | friends: { 113 | edges: [{ 114 | node: { 115 | _id: user1._id.toString(), 116 | name: 'Foo' 117 | } 118 | }] 119 | }, 120 | objectIds: [user1._id.toString()], 121 | sub: { 122 | subref: { 123 | name: 'Mother' 124 | } 125 | } 126 | } 127 | } 128 | }); 129 | }); 130 | 131 | describe('with fragments', () => { 132 | it('should support fragments', async () => { 133 | const result = await graphql(schema, ` 134 | query GetUser { 135 | user(id: "${user2._id}") { 136 | ...UserFragment 137 | friends { 138 | count 139 | edges { 140 | node { 141 | ...UserFragment 142 | } 143 | } 144 | } 145 | } 146 | } 147 | fragment UserFragment on User { 148 | _id 149 | name 150 | age 151 | } 152 | `); 153 | 154 | expect(result).to.be.eql({ 155 | data: { 156 | user: { 157 | _id: user2._id.toString(), 158 | name: 'Bar', 159 | age: 28, 160 | friends: { 161 | count: 1, 162 | edges: [{ 163 | node: { 164 | _id: user1._id.toString(), 165 | name: 'Foo', 166 | age: 28 167 | } 168 | }] 169 | } 170 | } 171 | } 172 | }); 173 | }); 174 | 175 | it('should support inline fragments', async () => { 176 | const result = await graphql(schema, `{ 177 | user(id: "${user2._id}") { 178 | _id 179 | name, 180 | ... on User { 181 | age 182 | } 183 | } 184 | }`); 185 | 186 | expect(result).to.be.eql({ 187 | data: { 188 | user: { 189 | _id: user2._id.toString(), 190 | name: 'Bar', 191 | age: 28 192 | } 193 | } 194 | }); 195 | }); 196 | }); 197 | }); 198 | 199 | describe('plural query', () => { 200 | it('should get data from database and filter by number', async () => { 201 | let result = await graphql(schema, `{ 202 | users(age: 28) { 203 | _id 204 | name 205 | age 206 | } 207 | }`); 208 | 209 | const expected = [ 210 | { 211 | _id: user1._id.toString(), 212 | name: 'Foo', 213 | age: 28 214 | }, { 215 | _id: user2._id.toString(), 216 | name: 'Bar', 217 | age: 28 218 | } 219 | ]; 220 | 221 | expect(result.data.users).to.deep.include.members(expected); 222 | 223 | result = await graphql(schema, `{ 224 | users(id: ["${user1._id.toString()}", "${user2._id.toString()}"]) { 225 | _id 226 | name 227 | age 228 | } 229 | }`); 230 | 231 | expect(result.data.users).to.deep.include.members(expected); 232 | 233 | result = await graphql(schema, `{ 234 | users(ids: ["${user1._id.toString()}", "${user2._id.toString()}"]) { 235 | _id 236 | name 237 | age 238 | } 239 | }`); 240 | 241 | expect(result.data.users).to.deep.include.members(expected); 242 | }); 243 | 244 | it('should get data from database and filter by array of _id(s)', async () => { 245 | const result = await graphql(schema, `{ 246 | users(id: ["${user1._id}", "${user2._id}"]) { 247 | _id 248 | } 249 | }`); 250 | 251 | expect(result).to.be.eql({ 252 | data: { 253 | users: [{ 254 | _id: user1._id.toString() 255 | }, { 256 | _id: user2._id.toString() 257 | }] 258 | } 259 | }); 260 | }); 261 | 262 | it('should support viewer field', async () => { 263 | const result = await graphql(schema, `{ 264 | viewer { 265 | id 266 | users { 267 | count 268 | edges { 269 | cursor 270 | node { 271 | name 272 | } 273 | } 274 | } 275 | } 276 | }`); 277 | 278 | const { id, users } = result.data.viewer; 279 | expect(id).to.be.ok; 280 | expect(users.count).to.be.equal(3); 281 | 282 | expect(users.edges).to.containSubset([ 283 | { node: { name: 'Mother' } }, 284 | { node: { name: 'Foo' } }, 285 | { node: { name: 'Bar' } } 286 | ]); 287 | }); 288 | 289 | it('should filter connections by arguments', async () => { 290 | const result = await graphql(schema, `{ 291 | viewer { 292 | users(name: "Foo") { 293 | count 294 | edges { 295 | node { 296 | name 297 | } 298 | } 299 | } 300 | } 301 | }`); 302 | 303 | const { users } = result.data.viewer; 304 | expect(users.count).to.be.eql(1); 305 | 306 | expect(users.edges).to.containSubset([ 307 | { node: { name: 'Foo' } } 308 | ]); 309 | }); 310 | 311 | it('should return results in ascending order', async () => { 312 | let result = await graphql(schema, `{ 313 | viewer { 314 | users(orderBy: NAME_ASC) { 315 | edges { 316 | node { 317 | name 318 | } 319 | } 320 | } 321 | } 322 | }`); 323 | 324 | expect(result.data.viewer.users.edges).to.be.eql([ 325 | { node: { name: 'Bar' } }, 326 | { node: { name: 'Foo' } }, 327 | { node: { name: 'Mother' } } 328 | ]); 329 | 330 | result = await graphql(schema, `{ 331 | users(orderBy: NAME_ASC) { 332 | name 333 | } 334 | }`); 335 | 336 | expect(result.data.users).to.be.eql([ 337 | { name: 'Bar' }, 338 | { name: 'Foo' }, 339 | { name: 'Mother' } 340 | ]); 341 | }); 342 | 343 | it('should return results in descending order', async () => { 344 | let result = await graphql(schema, `{ 345 | viewer { 346 | users(orderBy: NAME_DESC) { 347 | edges { 348 | node { 349 | name 350 | } 351 | } 352 | } 353 | } 354 | }`); 355 | 356 | expect(result.data.viewer.users.edges).to.be.eql([ 357 | { node: { name: 'Mother' } }, 358 | { node: { name: 'Foo' } }, 359 | { node: { name: 'Bar' } } 360 | ]); 361 | 362 | result = await graphql(schema, `{ 363 | users(orderBy: NAME_DESC) { 364 | name 365 | } 366 | }`); 367 | 368 | expect(result.data.users).to.be.eql([ 369 | { name: 'Mother' }, 370 | { name: 'Foo' }, 371 | { name: 'Bar' } 372 | ]); 373 | }); 374 | 375 | it('should be able to limit the ordered results', async () => { 376 | const result = await graphql(schema, `{ 377 | viewer { 378 | users(orderBy: NAME_DESC, first: 2) { 379 | edges { 380 | node { 381 | name 382 | } 383 | } 384 | } 385 | } 386 | }`); 387 | 388 | expect(result.data.viewer.users.edges).to.be.eql([ 389 | { node: { name: 'Mother' } }, 390 | { node: { name: 'Foo' } } 391 | ]); 392 | }); 393 | 394 | it('should be able to paginate the ordered results', async () => { 395 | let result = await graphql(schema, `{ 396 | viewer { 397 | users(orderBy: NAME_DESC) { 398 | edges { 399 | cursor 400 | node { 401 | name 402 | } 403 | } 404 | } 405 | } 406 | }`); 407 | 408 | const edges1 = result.data.viewer.users.edges; 409 | const cursor = edges1[0].cursor; 410 | 411 | result = await graphql(schema, `{ 412 | viewer { 413 | users(orderBy: NAME_DESC, after: "${cursor}", first: 1) { 414 | edges { 415 | cursor 416 | node { 417 | name 418 | } 419 | } 420 | } 421 | } 422 | }`); 423 | 424 | const edges2 = result.data.viewer.users.edges; 425 | expect(edges2.length).to.eql(1); 426 | expect(edges2).to.eql(edges1.slice(1, 2)); 427 | }); 428 | }); 429 | 430 | describe('mutations', () => { 431 | it('should add data to the database', async () => { 432 | let result = await graphql(schema, ` 433 | mutation addUserMutation { 434 | addUser(input: {name: "Test User", clientMutationId: "1"}) { 435 | changedUserEdge { 436 | node { 437 | id 438 | _id 439 | name 440 | } 441 | } 442 | } 443 | } 444 | `); 445 | 446 | const node = result.data.addUser.changedUserEdge.node; 447 | const { id } = node; 448 | expect(typeof node._id).to.be.equal('string'); 449 | expect(node.name).to.be.equal('Test User'); 450 | 451 | result = await graphql(schema, ` 452 | mutation addUserMutation { 453 | addUser(input: {name: "Test User", friends: ["${id}"], mother: "${id}", clientMutationId: "2"}) { 454 | changedUserEdge { 455 | node { 456 | id 457 | } 458 | } 459 | } 460 | } 461 | `); 462 | 463 | expect(result.errors).not.to.be.ok; 464 | }); 465 | 466 | it('should update data', async () => { 467 | let result = await graphql(schema, ` 468 | mutation addUserMutation { 469 | addUser(input: {name: "Test User", clientMutationId: "1"}) { 470 | changedUserEdge { 471 | node { 472 | _id 473 | } 474 | } 475 | } 476 | } 477 | `); 478 | const id = result.data.addUser.changedUserEdge.node._id; 479 | 480 | result = await graphql(schema, ` 481 | mutation updateUserMutation { 482 | updateUser(input: {id: "${id}", name: "Updated Test User", clientMutationId: "2"}) { 483 | clientMutationId 484 | changedUser { 485 | name 486 | friends { 487 | count 488 | } 489 | } 490 | } 491 | } 492 | `); 493 | expect(result).to.containSubset({ 494 | data: { 495 | updateUser: { 496 | clientMutationId: '2', 497 | changedUser: { 498 | name: 'Updated Test User', 499 | friends: { 500 | count: 0 501 | } 502 | } 503 | } 504 | } 505 | }); 506 | 507 | result = await graphql(schema, ` 508 | mutation updateUserMutation { 509 | updateUser(input: {id: "${id}", friends_add: ["${id}"], clientMutationId: "3"}) { 510 | clientMutationId 511 | changedUser { 512 | name 513 | friends { 514 | count 515 | } 516 | } 517 | } 518 | } 519 | `); 520 | expect(result).to.containSubset({ 521 | data: { 522 | updateUser: { 523 | clientMutationId: '3', 524 | changedUser: { 525 | friends: { 526 | count: 1 527 | } 528 | } 529 | } 530 | } 531 | }); 532 | }); 533 | 534 | it('should delete data', async () => { 535 | let result = await graphql(schema, ` 536 | mutation addUserMutation { 537 | addUser(input: {name: "Test User", clientMutationId: "1"}) { 538 | changedUserEdge { 539 | node { 540 | id 541 | } 542 | } 543 | } 544 | } 545 | `); 546 | const { id } = result.data.addUser.changedUserEdge.node; 547 | 548 | result = await graphql(schema, ` 549 | mutation deleteUserMutation { 550 | deleteUser(input: {id: "${id}", clientMutationId: "2"}) { 551 | id 552 | ok 553 | clientMutationId 554 | } 555 | } 556 | `); 557 | expect(result).to.containSubset({ 558 | data: { 559 | deleteUser: { 560 | id, 561 | ok: true, 562 | clientMutationId: '2' 563 | } 564 | } 565 | }); 566 | }); 567 | }); 568 | 569 | describe('hooks', () => { 570 | it('should call viewer hooks on a viewer query', async () => { 571 | const { pre, post } = hooks.viewer; 572 | pre.reset(); 573 | post.reset(); 574 | 575 | expect(pre.called).to.be.false; 576 | expect(post.called).to.be.false; 577 | await graphql(schema, `{ 578 | viewer { 579 | users { 580 | count 581 | } 582 | } 583 | }`); 584 | expect(pre.called).to.be.true; 585 | expect(post.called).to.be.true; 586 | }); 587 | 588 | it('should call singular hooks on a singular query', async () => { 589 | const { pre, post } = hooks.singular; 590 | pre.reset(); 591 | post.reset(); 592 | 593 | expect(pre.called).to.be.false; 594 | expect(post.called).to.be.false; 595 | await graphql(schema, `{ 596 | user(id: "${user2._id}") { 597 | _id 598 | } 599 | }`); 600 | expect(pre.called).to.be.true; 601 | expect(post.called).to.be.true; 602 | }); 603 | 604 | it('should call plural hooks on a plural query', async () => { 605 | const { pre, post } = hooks.plural; 606 | pre.reset(); 607 | post.reset(); 608 | 609 | expect(pre.called).to.be.false; 610 | expect(post.called).to.be.false; 611 | await graphql(schema, `{ 612 | users(age: 28) { 613 | _id 614 | } 615 | }`); 616 | expect(pre.called).to.be.true; 617 | expect(post.called).to.be.true; 618 | }); 619 | 620 | it('should call mutation hooks on a mutation', async () => { 621 | const { pre, post } = hooks.mutation; 622 | pre.reset(); 623 | post.reset(); 624 | 625 | expect(pre.called).to.be.false; 626 | expect(post.called).to.be.false; 627 | await graphql(schema, ` 628 | mutation addUserMutation { 629 | addUser(input: {name: "Test User", clientMutationId: "1"}) { 630 | changedUserEdge { 631 | node { 632 | id 633 | } 634 | } 635 | } 636 | } 637 | `); 638 | expect(pre.called).to.be.true; 639 | expect(post.called).to.be.true; 640 | }); 641 | }); 642 | }); 643 | }); 644 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 6.0.0 (2017-01-27) 3 | 4 | 5 | ### chore 6 | 7 | * chore(circle): add badge, use yarn ([764eaf5](https://github.com/RisingStack/graffiti-mongoose/commit/764eaf5)) 8 | * chore(circle): use CircleCI ([690b8aa](https://github.com/RisingStack/graffiti-mongoose/commit/690b8aa)) 9 | * chore(package): update dependencies ([75d9085](https://github.com/RisingStack/graffiti-mongoose/commit/75d9085)) 10 | * chore(package): update example dependencies ([9391856](https://github.com/RisingStack/graffiti-mongoose/commit/9391856)) 11 | * chore(package): update lodash to version 4.16.0 (#153) ([0bfcfc0](https://github.com/RisingStack/graffiti-mongoose/commit/0bfcfc0)) 12 | * chore(package): update sinon to version 1.17.6 (#152) ([1d1f6d1](https://github.com/RisingStack/graffiti-mongoose/commit/1d1f6d1)) 13 | 14 | ### fix 15 | 16 | * fix(projection): fieldASTs is called fieldNodes ([fced319](https://github.com/RisingStack/graffiti-mongoose/commit/fced319)) 17 | * fix(projection): fix "can't get selectionSet of undefined" error (#204) ([f8edeb9](https://github.com/RisingStack/graffiti-mongoose/commit/f8edeb9)), closes [(#204](https://github.com/(/issues/204) 18 | 19 | 20 | 21 | 22 | # 5.3.0 (2016-09-12) 23 | 24 | 25 | ### chore 26 | 27 | * chore(package): bump version to 5.3.0 ([a6b98cc](https://github.com/RisingStack/graffiti-mongoose/commit/a6b98cc)) 28 | * chore(package): update babel-cli to version 6.14.0 (#136) ([703be9e](https://github.com/RisingStack/graffiti-mongoose/commit/703be9e)) 29 | * chore(package): update babel-preset-es2015 to version 6.14.0 (#137) ([dc0c28f](https://github.com/RisingStack/graffiti-mongoose/commit/dc0c28f)) 30 | * chore(package): update dependencies ([6df0fe0](https://github.com/RisingStack/graffiti-mongoose/commit/6df0fe0)) 31 | * chore(package): update dependencies ([46e4b48](https://github.com/RisingStack/graffiti-mongoose/commit/46e4b48)) 32 | * chore(package): update dependencies (#134) ([b19ab86](https://github.com/RisingStack/graffiti-mongoose/commit/b19ab86)) 33 | * chore(package): update eslint to version 3.4.0 (#140) ([bc47722](https://github.com/RisingStack/graffiti-mongoose/commit/bc47722)) 34 | * chore(package): update mongoose to version 4.5.10 (#135) ([32ac18e](https://github.com/RisingStack/graffiti-mongoose/commit/32ac18e)) 35 | 36 | ### feat 37 | 38 | * feat(schema): pass in types for customQueries and customMutations (#149) ([e6e9d8c](https://github.com/RisingStack/graffiti-mongoose/commit/e6e9d8c)) 39 | 40 | 41 | 42 | 43 | ## 5.2.1 (2016-08-16) 44 | 45 | 46 | ### chore 47 | 48 | * chore(package): bump version to 5.2.1 ([d0cf049](https://github.com/RisingStack/graffiti-mongoose/commit/d0cf049)) 49 | 50 | ### docs 51 | 52 | * docs(package): update documents ([5b226c8](https://github.com/RisingStack/graffiti-mongoose/commit/5b226c8)) 53 | 54 | ### feat 55 | 56 | * feat(example): update example ([33c66e2](https://github.com/RisingStack/graffiti-mongoose/commit/33c66e2)) 57 | 58 | ### fix 59 | 60 | * fix(type): fix embedded object reference Type creation (#132) ([436e56e](https://github.com/RisingStack/graffiti-mongoose/commit/436e56e)), closes [(#132](https://github.com/(/issues/132) [#131](https://github.com/RisingStack/graffiti-mongoose/issues/131) 61 | 62 | ### refactor 63 | 64 | * refactor(package): refresh code ([a5b1298](https://github.com/RisingStack/graffiti-mongoose/commit/a5b1298)) 65 | 66 | 67 | 68 | 69 | # 5.2.0 (2016-07-26) 70 | 71 | 72 | ### chore 73 | 74 | * chore(package): bump version to 5.2.0 ([2e4578c](https://github.com/RisingStack/graffiti-mongoose/commit/2e4578c)) 75 | * chore(package): update dependencies ([f726498](https://github.com/RisingStack/graffiti-mongoose/commit/f726498)) 76 | 77 | ### docs 78 | 79 | * docs(readme): fix anchor link ([d58b449](https://github.com/RisingStack/graffiti-mongoose/commit/d58b449)) 80 | 81 | ### feat 82 | 83 | * feat(schema): allow to use functions for custom mutations and queries to be able to use defined type ([0cb244f](https://github.com/RisingStack/graffiti-mongoose/commit/0cb244f)) 84 | 85 | 86 | 87 | 88 | # 5.1.0 (2016-04-30) 89 | 90 | 91 | ### chore 92 | 93 | * chore(package): bump version ([e64f06a](https://github.com/RisingStack/graffiti-mongoose/commit/e64f06a)) 94 | * chore(package): update dependencies ([8a54912](https://github.com/RisingStack/graffiti-mongoose/commit/8a54912)) 95 | 96 | ### feat 97 | 98 | * feat(model): add support for enums ([a0d151a](https://github.com/RisingStack/graffiti-mongoose/commit/a0d151a)) 99 | * feat(model): add support for mongoose embedded schemas ([21e1e31](https://github.com/RisingStack/graffiti-mongoose/commit/21e1e31)) 100 | * feat(schema): add allowMongoIDMutation to options ([5df7147](https://github.com/RisingStack/graffiti-mongoose/commit/5df7147)) 101 | * feat(schema): add customQueries and customMutations to options ([b2b841e](https://github.com/RisingStack/graffiti-mongoose/commit/b2b841e)) 102 | * feat(schema): add support for objects nested in lists ([564cb7c](https://github.com/RisingStack/graffiti-mongoose/commit/564cb7c)) 103 | 104 | ### fix 105 | 106 | * fix(type): add to input-object ability for self-nesting ([c28e59e](https://github.com/RisingStack/graffiti-mongoose/commit/c28e59e)) 107 | 108 | ### style 109 | 110 | * style(type): using reduce for enum values ([44356a3](https://github.com/RisingStack/graffiti-mongoose/commit/44356a3)) 111 | 112 | 113 | 114 | 115 | ## 5.0.10 (2016-03-18) 116 | 117 | 118 | ### chore 119 | 120 | * chore(package): bump version to 5.0.10 ([a47f729](https://github.com/RisingStack/graffiti-mongoose/commit/a47f729)) 121 | 122 | ### fix 123 | 124 | * fix(type): resolve null id on optionnal fields ([fc9c50f](https://github.com/RisingStack/graffiti-mongoose/commit/fc9c50f)) 125 | 126 | 127 | 128 | 129 | ## 5.0.9 (2016-03-13) 130 | 131 | 132 | ### chore 133 | 134 | * chore(package): bump version to 5.0.9 ([f3f1afc](https://github.com/RisingStack/graffiti-mongoose/commit/f3f1afc)) 135 | 136 | ### feat 137 | 138 | * feat(schema): use mongoose compatible plurals for model names ([9fd9680](https://github.com/RisingStack/graffiti-mongoose/commit/9fd9680)) 139 | 140 | ### test 141 | 142 | * test(e2e): add test for pagination with ordering ([c4e2d49](https://github.com/RisingStack/graffiti-mongoose/commit/c4e2d49)) 143 | 144 | 145 | 146 | 147 | ## 5.0.8 (2016-02-15) 148 | 149 | 150 | ### chore 151 | 152 | * chore(package): bump version to 5.0.8 ([ae061a3](https://github.com/RisingStack/graffiti-mongoose/commit/ae061a3)) 153 | 154 | ### fix 155 | 156 | * fix(query): fix orderBy argument on lists ([ef9a603](https://github.com/RisingStack/graffiti-mongoose/commit/ef9a603)) 157 | 158 | 159 | 160 | 161 | ## 5.0.7 (2016-02-11) 162 | 163 | 164 | ### chore 165 | 166 | * chore(package): bump version to 5.0.7 ([9054e96](https://github.com/RisingStack/graffiti-mongoose/commit/9054e96)) 167 | * chore(package): update dependencies ([7600d6b](https://github.com/RisingStack/graffiti-mongoose/commit/7600d6b)) 168 | 169 | ### refactor 170 | 171 | * refactor(example): change port to 8080 ([d7bf170](https://github.com/RisingStack/graffiti-mongoose/commit/d7bf170)) 172 | 173 | 174 | 175 | 176 | ## 5.0.6 (2016-02-02) 177 | 178 | 179 | ### chore 180 | 181 | * chore(package): bump version to 3.0.1 ([12b7436](https://github.com/RisingStack/graffiti-mongoose/commit/12b7436)) 182 | * chore(package): update dependencies ([7e4b785](https://github.com/RisingStack/graffiti-mongoose/commit/7e4b785)) 183 | 184 | ### fix 185 | 186 | * fix(test): fix coverage test run ([3345a59](https://github.com/RisingStack/graffiti-mongoose/commit/3345a59)) 187 | 188 | 189 | 190 | 191 | ## 5.0.5 (2016-01-27) 192 | 193 | 194 | ### chore 195 | 196 | * chore(package): bump version to 5.0.5 ([04da755](https://github.com/RisingStack/graffiti-mongoose/commit/04da755)) 197 | * chore(package): update dependencies ([22cebcb](https://github.com/RisingStack/graffiti-mongoose/commit/22cebcb)) 198 | 199 | ### docs 200 | 201 | * docs(readme): add bitHound badge ([f9b9251](https://github.com/RisingStack/graffiti-mongoose/commit/f9b9251)) 202 | 203 | ### fix 204 | 205 | * fix(type): fix conflit for nested object names ([5c5dd0e](https://github.com/RisingStack/graffiti-mongoose/commit/5c5dd0e)), closes [RisingStack/graffiti-mongoose#84](https://github.com/RisingStack/graffiti-mongoose/issues/84) 206 | 207 | 208 | 209 | 210 | ## 5.0.4 (2016-01-18) 211 | 212 | 213 | ### chore 214 | 215 | * chore(example): update dependencies ([de00242](https://github.com/RisingStack/graffiti-mongoose/commit/de00242)) 216 | * chore(package): bump version to 5.0.4 ([cdbdbf3](https://github.com/RisingStack/graffiti-mongoose/commit/cdbdbf3)) 217 | * chore(package): update dependencies ([db90b07](https://github.com/RisingStack/graffiti-mongoose/commit/db90b07)) 218 | 219 | ### docs 220 | 221 | * docs(readme): fix example mutations ([d182b8f](https://github.com/RisingStack/graffiti-mongoose/commit/d182b8f)) 222 | 223 | 224 | 225 | 226 | ## 5.0.3 (2016-01-02) 227 | 228 | 229 | ### chore 230 | 231 | * chore(package): bump version to 5.0.3 ([eafcd37](https://github.com/RisingStack/graffiti-mongoose/commit/eafcd37)) 232 | 233 | ### fix 234 | 235 | * fix(type): fix orderBy arguments ([0975ae2](https://github.com/RisingStack/graffiti-mongoose/commit/0975ae2)) 236 | 237 | 238 | 239 | 240 | ## 5.0.2 (2015-12-31) 241 | 242 | 243 | ### chore 244 | 245 | * chore(package): bump version to 5.0.2 ([84294f8](https://github.com/RisingStack/graffiti-mongoose/commit/84294f8)) 246 | * chore(package): update dependencies ([33c18dc](https://github.com/RisingStack/graffiti-mongoose/commit/33c18dc)) 247 | 248 | ### fix 249 | 250 | * fix(Date): parseValue takes a String as input ([4a0d254](https://github.com/RisingStack/graffiti-mongoose/commit/4a0d254)) 251 | 252 | 253 | 254 | 255 | ## 5.0.1 (2015-12-18) 256 | 257 | 258 | ### chore 259 | 260 | * chore(package): bump version to 5.0.1 ([b71b072](https://github.com/RisingStack/graffiti-mongoose/commit/b71b072)) 261 | 262 | ### fix 263 | 264 | * fix(query): fix id translation ([7ec8fc1](https://github.com/RisingStack/graffiti-mongoose/commit/7ec8fc1)) 265 | 266 | 267 | 268 | 269 | # 5.0.0 (2015-12-18) 270 | 271 | 272 | ### chore 273 | 274 | * chore(package): add babel-polyfill to peer dependencies ([215cfb9](https://github.com/RisingStack/graffiti-mongoose/commit/215cfb9)) 275 | * chore(package): bump version to 5.0.0 ([0fb74d0](https://github.com/RisingStack/graffiti-mongoose/commit/0fb74d0)) 276 | * chore(package): remove .jscsrc ([b8662a3](https://github.com/RisingStack/graffiti-mongoose/commit/b8662a3)) 277 | * chore(package): run eslint before commits ([9a5b8f2](https://github.com/RisingStack/graffiti-mongoose/commit/9a5b8f2)) 278 | * chore(package): update dependencies ([8b56a10](https://github.com/RisingStack/graffiti-mongoose/commit/8b56a10)) 279 | 280 | ### feat 281 | 282 | * feat(mutation): support append to arrays in update mutation (with fieldName_add input field) ([6aa5820](https://github.com/RisingStack/graffiti-mongoose/commit/6aa5820)) 283 | * feat(schema): add read support types for objects within a list ([c473304](https://github.com/RisingStack/graffiti-mongoose/commit/c473304)) 284 | 285 | ### fix 286 | 287 | * fix(type): fix connections for different models with the same field names ([4da2042](https://github.com/RisingStack/graffiti-mongoose/commit/4da2042)) 288 | 289 | 290 | 291 | 292 | ## 4.3.3 (2015-12-11) 293 | 294 | 295 | ### chore 296 | 297 | * chore(package): update dependencies ([d6e080c](https://github.com/RisingStack/graffiti-mongoose/commit/d6e080c)) 298 | * chore(package): update version to 4.3.3 ([02eaf3e](https://github.com/RisingStack/graffiti-mongoose/commit/02eaf3e)) 299 | 300 | ### fix 301 | 302 | * fix(update): use original type on update mutation ([bdc7fed](https://github.com/RisingStack/graffiti-mongoose/commit/bdc7fed)) 303 | 304 | 305 | 306 | 307 | ## 4.3.2 (2015-12-08) 308 | 309 | 310 | ### chore 311 | 312 | * chore(package): bump version to 4.3.2 ([1504f51](https://github.com/RisingStack/graffiti-mongoose/commit/1504f51)) 313 | 314 | ### fix 315 | 316 | * fix(mutation): fix query and add reference support for mutations ([a7c97a4](https://github.com/RisingStack/graffiti-mongoose/commit/a7c97a4)) 317 | 318 | 319 | 320 | 321 | ## 4.3.1 (2015-12-03) 322 | 323 | 324 | ### chore 325 | 326 | * chore(package): bump version to 4.3.1 ([3d43c04](https://github.com/RisingStack/graffiti-mongoose/commit/3d43c04)) 327 | * chore(package): update dependencies ([63d8263](https://github.com/RisingStack/graffiti-mongoose/commit/63d8263)) 328 | 329 | ### fix 330 | 331 | * fix(hooks): add arguments to post hooks too ([d083b76](https://github.com/RisingStack/graffiti-mongoose/commit/d083b76)) 332 | 333 | ### refactor 334 | 335 | * refactor(package): cleanup code ([db24cf4](https://github.com/RisingStack/graffiti-mongoose/commit/db24cf4)) 336 | 337 | 338 | 339 | 340 | # 4.3.0 (2015-11-20) 341 | 342 | 343 | ### chore 344 | 345 | * chore(package): bump version to 4.3.0 ([33e2c80](https://github.com/RisingStack/graffiti-mongoose/commit/33e2c80)) 346 | 347 | ### feat 348 | 349 | * feat(sort): support orderBy argument ([480ca7c](https://github.com/RisingStack/graffiti-mongoose/commit/480ca7c)) 350 | 351 | 352 | 353 | 354 | # 4.2.0 (2015-11-16) 355 | 356 | 357 | ### chore 358 | 359 | * chore(package): bump version to 4.2.0 ([6de5888](https://github.com/RisingStack/graffiti-mongoose/commit/6de5888)) 360 | 361 | ### docs 362 | 363 | * docs(hooks): pass values to the next function ([c12fa8e](https://github.com/RisingStack/graffiti-mongoose/commit/c12fa8e)) 364 | 365 | ### feat 366 | 367 | * feat(hooks): support query, mutation hooks ([a822459](https://github.com/RisingStack/graffiti-mongoose/commit/a822459)) 368 | 369 | 370 | 371 | 372 | # 4.1.0 (2015-11-10) 373 | 374 | 375 | ### chore 376 | 377 | * chore(package): bump version to 4.1.0 ([c424eca](https://github.com/RisingStack/graffiti-mongoose/commit/c424eca)) 378 | 379 | ### feat 380 | 381 | * feat(resolve): support for custom resolve write and read middleware ([53b0a67](https://github.com/RisingStack/graffiti-mongoose/commit/53b0a67)) 382 | 383 | 384 | 385 | 386 | ## 4.0.3 (2015-11-09) 387 | 388 | 389 | ### chore 390 | 391 | * chore(package): bump version to 4.0.3 ([b36e1c3](https://github.com/RisingStack/graffiti-mongoose/commit/b36e1c3)) 392 | 393 | ### fix 394 | 395 | * fix(schema): add arguments to connections based on the fields of the type ([b6f6684](https://github.com/RisingStack/graffiti-mongoose/commit/b6f6684)) 396 | 397 | 398 | 399 | 400 | ## 4.0.2 (2015-11-05) 401 | 402 | 403 | ### chore 404 | 405 | * chore(package): bumping version to 4.0.2 ([f1e1ace](https://github.com/RisingStack/graffiti-mongoose/commit/f1e1ace)) 406 | 407 | ### fix 408 | 409 | * fix(schema): add nodeInterface to viewer field, resolve viewer from node field ([03b3c05](https://github.com/RisingStack/graffiti-mongoose/commit/03b3c05)) 410 | * fix(schema): fix edge and node type names ([736fed4](https://github.com/RisingStack/graffiti-mongoose/commit/736fed4)) 411 | 412 | ### refactor 413 | 414 | * refactor(example): use latest graffiti ([6917d92](https://github.com/RisingStack/graffiti-mongoose/commit/6917d92)) 415 | 416 | 417 | 418 | 419 | ## 4.0.1 (2015-10-30) 420 | 421 | 422 | ### chore 423 | 424 | * chore(ci): use codeship instead of travis ([47e01fe](https://github.com/RisingStack/graffiti-mongoose/commit/47e01fe)) 425 | * chore(package): bumping version to 4.0.1 ([bd09316](https://github.com/RisingStack/graffiti-mongoose/commit/bd09316)) 426 | * chore(package): update dependencies ([3d5715b](https://github.com/RisingStack/graffiti-mongoose/commit/3d5715b)) 427 | 428 | ### docs 429 | 430 | * docs(example): add example queries ([b276a0b](https://github.com/RisingStack/graffiti-mongoose/commit/b276a0b)) 431 | * docs(readme): update example queries link ([9d5d284](https://github.com/RisingStack/graffiti-mongoose/commit/9d5d284)) 432 | 433 | ### refactor 434 | 435 | * refactor(example): rename queries.md to README.md ([0611696](https://github.com/RisingStack/graffiti-mongoose/commit/0611696)) 436 | 437 | 438 | 439 | 440 | # 4.0.0 (2015-10-20) 441 | 442 | 443 | ### chore 444 | 445 | * chore(package): add coverage reporter ([e426611](https://github.com/RisingStack/graffiti-mongoose/commit/e426611)) 446 | * chore(package): bumping version to 4.0.0 ([88baa18](https://github.com/RisingStack/graffiti-mongoose/commit/88baa18)) 447 | * chore(package): update eslint ([8cab848](https://github.com/RisingStack/graffiti-mongoose/commit/8cab848)) 448 | * chore(package): update example dependencies ([132ac9e](https://github.com/RisingStack/graffiti-mongoose/commit/132ac9e)) 449 | 450 | ### docs 451 | 452 | * docs(example): fix example start ([21a476d](https://github.com/RisingStack/graffiti-mongoose/commit/21a476d)) 453 | 454 | ### feat 455 | 456 | * feat(connection): add count field to connections ([7c6c82a](https://github.com/RisingStack/graffiti-mongoose/commit/7c6c82a)) 457 | * feat(mutation): add viewer and edge fields to payloads ([e0031ef](https://github.com/RisingStack/graffiti-mongoose/commit/e0031ef)) 458 | * feat(mutations): add id field to the delete mutation payload ([3255ab9](https://github.com/RisingStack/graffiti-mongoose/commit/3255ab9)) 459 | * feat(update): change update payload format ([691cc71](https://github.com/RisingStack/graffiti-mongoose/commit/691cc71)) 460 | 461 | ### refactor 462 | 463 | * refactor(example): optimise seed data persist ([4b71175](https://github.com/RisingStack/graffiti-mongoose/commit/4b71175)) 464 | 465 | 466 | 467 | 468 | # 3.2.0 (2015-10-16) 469 | 470 | 471 | ### chore 472 | 473 | * chore(package): bumping version to 3.2.0 ([f22cc8f](https://github.com/RisingStack/graffiti-mongoose/commit/f22cc8f)) 474 | 475 | ### feat 476 | 477 | * feat(schema): use connection type on plural fields on viewer query ([4fda024](https://github.com/RisingStack/graffiti-mongoose/commit/4fda024)) 478 | 479 | 480 | 481 | 482 | ## 3.1.1 (2015-10-16) 483 | 484 | 485 | ### chore 486 | 487 | * chore(package): bumping version to 3.1.1 ([367305c](https://github.com/RisingStack/graffiti-mongoose/commit/367305c)) 488 | 489 | ### docs 490 | 491 | * docs(readme): add npm version badge ([fe2a0ab](https://github.com/RisingStack/graffiti-mongoose/commit/fe2a0ab)) 492 | 493 | ### fix 494 | 495 | * fix(viewer): fix viewer query field ([a9627bb](https://github.com/RisingStack/graffiti-mongoose/commit/a9627bb)) 496 | 497 | ### refactor 498 | 499 | * refactor(index): export statement in one line ([8ff21d7](https://github.com/RisingStack/graffiti-mongoose/commit/8ff21d7)) 500 | 501 | 502 | 503 | 504 | # 3.1.0 (2015-10-16) 505 | 506 | 507 | ### chore 508 | 509 | * chore(changelog): update CHANGELOG.md ([cb9009f](https://github.com/RisingStack/graffiti-mongoose/commit/cb9009f)) 510 | * chore(package): bumping version to 3.1.0 ([9259f4d](https://github.com/RisingStack/graffiti-mongoose/commit/9259f4d)) 511 | * chore(package): update dependencies ([a0054ae](https://github.com/RisingStack/graffiti-mongoose/commit/a0054ae)) 512 | 513 | ### docs 514 | 515 | * docs(graffiti-model): add documentation for graffiti model ([73ec052](https://github.com/RisingStack/graffiti-mongoose/commit/73ec052)) 516 | * docs(readme): add mutation ([6ec9418](https://github.com/RisingStack/graffiti-mongoose/commit/6ec9418)) 517 | 518 | ### feat 519 | 520 | * feat(model): description and visibility can be specified in the mongoose schema ([aa81f33](https://github.com/RisingStack/graffiti-mongoose/commit/aa81f33)) 521 | * feat(mutations): support `add` and `update` mutations ([477105b](https://github.com/RisingStack/graffiti-mongoose/commit/477105b)) 522 | * feat(mutations): support `delete` mutations ([63e7d99](https://github.com/RisingStack/graffiti-mongoose/commit/63e7d99)) 523 | * feat(schema): mutation fields can be disabled ([b4cd5a4](https://github.com/RisingStack/graffiti-mongoose/commit/b4cd5a4)) 524 | * feat(viewer): add viewer field to RootQuery ([f25c304](https://github.com/RisingStack/graffiti-mongoose/commit/f25c304)) 525 | 526 | ### refactor 527 | 528 | * refactor(field): naming coherence ([c8801db](https://github.com/RisingStack/graffiti-mongoose/commit/c8801db)) 529 | * refactor(field): rename field to schema ([48cf1b7](https://github.com/RisingStack/graffiti-mongoose/commit/48cf1b7)) 530 | * refactor(mutations): use mutationWithClientMutationId from the graphql-relay module ([7776a27](https://github.com/RisingStack/graffiti-mongoose/commit/7776a27)) 531 | 532 | 533 | 534 | 535 | ## 3.0.1 (2015-10-09) 536 | 537 | 538 | ### chore 539 | 540 | * chore(package): bump version to 3.0.1 ([de701ed](https://github.com/RisingStack/graffiti-mongoose/commit/de701ed)) 541 | 542 | ### docs 543 | 544 | * docs(readme): fix usage, install graphql as peer dep ([d8c19c1](https://github.com/RisingStack/graffiti-mongoose/commit/d8c19c1)) 545 | 546 | ### feat 547 | 548 | * feat(fields): add support for `ids` argument on plural fields ([2a41753](https://github.com/RisingStack/graffiti-mongoose/commit/2a41753)) 549 | 550 | ### fix 551 | 552 | * fix(projection): fix `Field` fragment ([bafbb2f](https://github.com/RisingStack/graffiti-mongoose/commit/bafbb2f)) 553 | 554 | ### refactor 555 | 556 | * refactor(index): move logic out of index.js files ([26ddd10](https://github.com/RisingStack/graffiti-mongoose/commit/26ddd10)) 557 | * refactor(model): change graffiti-model format ([e5b005e](https://github.com/RisingStack/graffiti-mongoose/commit/e5b005e)) 558 | 559 | 560 | 561 | 562 | # 3.0.0 (2015-10-08) 563 | 564 | 565 | ### chore 566 | 567 | * chore(changelog): update changelog for v3.0.0 ([ac5702c](https://github.com/RisingStack/graffiti-mongoose/commit/ac5702c)) 568 | * chore(package): bump version to 3.0.0 ([d573fc6](https://github.com/RisingStack/graffiti-mongoose/commit/d573fc6)) 569 | * chore(relay): major refactor and basic Relay support: node(id), pagination ([89dc30a](https://github.com/RisingStack/graffiti-mongoose/commit/89dc30a)) 570 | 571 | ### docs 572 | 573 | * docs(readme): update docs for relay compatibility ([c406009](https://github.com/RisingStack/graffiti-mongoose/commit/c406009)) 574 | 575 | 576 | 577 | 578 | # 2.0.0 (2015-09-29) 579 | 580 | 581 | ### chore 582 | 583 | * chore(package): bump version to 2.0.0 ([01a8480](https://github.com/RisingStack/graffiti-mongoose/commit/01a8480)) 584 | 585 | ### fix 586 | 587 | * fix(package): require graphql to be installed alongside graffiti ([71e2c85](https://github.com/RisingStack/graffiti-mongoose/commit/71e2c85)) 588 | 589 | 590 | ### BREAKING CHANGE 591 | 592 | * move graphql to peerDependencies 593 | 594 | 595 | 596 | ## 1.6.3 (2015-09-23) 597 | 598 | 599 | ### chore 600 | 601 | * chore(package): bump version to 1.6.3 ([e86bb9a](https://github.com/RisingStack/graffiti-mongoose/commit/e86bb9a)) 602 | * chore(package): update graphql-js to latest ([0cef077](https://github.com/RisingStack/graffiti-mongoose/commit/0cef077)) 603 | 604 | 605 | 606 | 607 | ## 1.6.2 (2015-09-22) 608 | 609 | 610 | ### chore 611 | 612 | * chore(package): bump version to 1.6.2 ([b5b670a](https://github.com/RisingStack/graffiti-mongoose/commit/b5b670a)) 613 | 614 | ### docs 615 | 616 | * docs(readme): fix minor typo in example ([5a2813d](https://github.com/RisingStack/graffiti-mongoose/commit/5a2813d)) 617 | 618 | ### refactor 619 | 620 | * refactor(objectid): remove bson-objectid from project ([c4e7ad1](https://github.com/RisingStack/graffiti-mongoose/commit/c4e7ad1)) 621 | 622 | 623 | 624 | 625 | ## 1.6.1 (2015-08-03) 626 | 627 | 628 | ### chore 629 | 630 | * chore(changelog): update changelog ([aeed95c](https://github.com/RisingStack/graffiti-mongoose/commit/aeed95c)) 631 | * chore(package): bump version to 1.6.1 ([b37e64a](https://github.com/RisingStack/graffiti-mongoose/commit/b37e64a)) 632 | 633 | ### fix 634 | 635 | * fix(type): fix array of ObjectId without ref ([e5575f3](https://github.com/RisingStack/graffiti-mongoose/commit/e5575f3)) 636 | 637 | ### refactor 638 | 639 | * refactor(model): improve internal model for sub-documents ([95cf81f](https://github.com/RisingStack/graffiti-mongoose/commit/95cf81f)) 640 | 641 | 642 | 643 | 644 | # 1.6.0 (2015-08-02) 645 | 646 | 647 | ### chore 648 | 649 | * chore(changelog): fix git commit links ([044f311](https://github.com/RisingStack/graffiti-mongoose/commit/044f311)) 650 | * chore(changelog): update changelog ([ff20a60](https://github.com/RisingStack/graffiti-mongoose/commit/ff20a60)) 651 | * chore(package): add git links ([2602415](https://github.com/RisingStack/graffiti-mongoose/commit/2602415)) 652 | * chore(package): bump version to 1.6.0 ([08677e6](https://github.com/RisingStack/graffiti-mongoose/commit/08677e6)) 653 | 654 | ### feat 655 | 656 | * feat(model): extract sub-documents to tree ([ddba1d3](https://github.com/RisingStack/graffiti-mongoose/commit/ddba1d3)) 657 | * feat(type): ObjectID with reference ([653e1d1](https://github.com/RisingStack/graffiti-mongoose/commit/653e1d1)) 658 | 659 | ### refactor 660 | 661 | * refactor(field): separate date resolve fn ([5731f50](https://github.com/RisingStack/graffiti-mongoose/commit/5731f50)) 662 | 663 | 664 | 665 | 666 | # 1.5.0 (2015-08-01) 667 | 668 | 669 | ### chore 670 | 671 | * chore(changelog): update CHANGELOG.md ([fa3bf6c](https://github.com/RisingStack/graffiti-mongoose/commit/fa3bf6c)) 672 | * chore(package): bump version to 1.5.0 ([4cc15c7](https://github.com/RisingStack/graffiti-mongoose/commit/4cc15c7)) 673 | 674 | ### docs 675 | 676 | * docs(readme): add ES6 explanation to readme ([ceb9aa8](https://github.com/RisingStack/graffiti-mongoose/commit/ceb9aa8)) 677 | * docs(readme): fix example typo ([4b56823](https://github.com/RisingStack/graffiti-mongoose/commit/4b56823)) 678 | * docs(readme): fix typo in ES6 explanation ([a287183](https://github.com/RisingStack/graffiti-mongoose/commit/a287183)) 679 | * docs(readme): fix usage link ([ac31470](https://github.com/RisingStack/graffiti-mongoose/commit/ac31470)) 680 | * docs(readme): improve README ([2ee943c](https://github.com/RisingStack/graffiti-mongoose/commit/2ee943c)) 681 | * docs(readme): improve README ([24569aa](https://github.com/RisingStack/graffiti-mongoose/commit/24569aa)) 682 | * docs(readme): improve usage example ([afdf24e](https://github.com/RisingStack/graffiti-mongoose/commit/afdf24e)) 683 | * docs(readme): improve usage section ([0486ce7](https://github.com/RisingStack/graffiti-mongoose/commit/0486ce7)) 684 | * docs(readme): separate description better ([5feca41](https://github.com/RisingStack/graffiti-mongoose/commit/5feca41)) 685 | 686 | ### feat 687 | 688 | * feat(fragment): add fragment support with broken projection ([25fc49c](https://github.com/RisingStack/graffiti-mongoose/commit/25fc49c)) 689 | 690 | 691 | 692 | 693 | ## 1.4.1 (2015-07-31) 694 | 695 | 696 | ### chore 697 | 698 | * chore(changelog): update changelog ([60d3a28](https://github.com/RisingStack/graffiti-mongoose/commit/60d3a28)) 699 | * chore(changelog): update changelog ([89a2a89](https://github.com/RisingStack/graffiti-mongoose/commit/89a2a89)) 700 | * chore(package): bump version to 1.4.1 ([a106290](https://github.com/RisingStack/graffiti-mongoose/commit/a106290)) 701 | * chore(package): update graphql version ([e99623e](https://github.com/RisingStack/graffiti-mongoose/commit/e99623e)) 702 | 703 | 704 | 705 | 706 | # 1.4.0 (2015-07-31) 707 | 708 | 709 | ### chore 710 | 711 | * chore(package): bump version to 1.4.0 ([efe3a4b](https://github.com/RisingStack/graffiti-mongoose/commit/efe3a4b)) 712 | 713 | ### fix 714 | 715 | * fix(model): handle mongoose models without caster option ([5a52ef2](https://github.com/RisingStack/graffiti-mongoose/commit/5a52ef2)) 716 | 717 | ### refactor 718 | 719 | * refactor(export): refactor export to support ES6 impots better ([f3912d4](https://github.com/RisingStack/graffiti-mongoose/commit/f3912d4)) 720 | * refactor(interface): use ES6 exports ([7b5ea46](https://github.com/RisingStack/graffiti-mongoose/commit/7b5ea46)) 721 | * refactor(type): graffiti level models ([d7bb657](https://github.com/RisingStack/graffiti-mongoose/commit/d7bb657)) 722 | 723 | ### style 724 | 725 | * style(imports): refactor imports to ES6 style ([ebdabd2](https://github.com/RisingStack/graffiti-mongoose/commit/ebdabd2)) 726 | 727 | ### test 728 | 729 | * test(e2e): improve test coverage ([b751814](https://github.com/RisingStack/graffiti-mongoose/commit/b751814)) 730 | * test(query): cover with unit tests ([d4635eb](https://github.com/RisingStack/graffiti-mongoose/commit/d4635eb)) 731 | * test(schema): add new prefix to ObjectId creation ([50d7df4](https://github.com/RisingStack/graffiti-mongoose/commit/50d7df4)) 732 | * test(schema): write test skeletons ([2cfa589](https://github.com/RisingStack/graffiti-mongoose/commit/2cfa589)) 733 | * test(type): cover type resolves with tests ([c37d5bf](https://github.com/RisingStack/graffiti-mongoose/commit/c37d5bf)) 734 | * test(type): cover type with tests ([c64a248](https://github.com/RisingStack/graffiti-mongoose/commit/c64a248)) 735 | 736 | 737 | 738 | 739 | # 1.3.0 (2015-07-30) 740 | 741 | 742 | ### chore 743 | 744 | * chore(changelog): update changelog ([d97e2ad](https://github.com/RisingStack/graffiti-mongoose/commit/d97e2ad)) 745 | * chore(package): bump version to 1.3.0 ([ef362a2](https://github.com/RisingStack/graffiti-mongoose/commit/ef362a2)) 746 | 747 | ### feat 748 | 749 | * feat(types): expose types ([b0ef6ff](https://github.com/RisingStack/graffiti-mongoose/commit/b0ef6ff)) 750 | 751 | 752 | 753 | 754 | # 1.2.0 (2015-07-29) 755 | 756 | 757 | ### chore 758 | 759 | * chore(changelog): update changelog ([c198dbc](https://github.com/RisingStack/graffiti-mongoose/commit/c198dbc)) 760 | * chore(package): bump version to 1.2.0 ([ac26a30](https://github.com/RisingStack/graffiti-mongoose/commit/ac26a30)) 761 | 762 | ### docs 763 | 764 | * docs(readme): fix example in README ([2015908](https://github.com/RisingStack/graffiti-mongoose/commit/2015908)) 765 | * docs(readme): fix typo ([02e6fe1](https://github.com/RisingStack/graffiti-mongoose/commit/02e6fe1)) 766 | * docs(readme): use promise instead of yield ([0ec8290](https://github.com/RisingStack/graffiti-mongoose/commit/0ec8290)) 767 | 768 | ### feat 769 | 770 | * feat(schema): support inline fragments ([184ffed](https://github.com/RisingStack/graffiti-mongoose/commit/184ffed)) 771 | 772 | 773 | 774 | 775 | ## 1.1.1 (2015-07-28) 776 | 777 | 778 | ### chore 779 | 780 | * chore(changelog): update changelog ([329d6ff](https://github.com/RisingStack/graffiti-mongoose/commit/329d6ff)) 781 | * chore(changelog): update changelog ([dfc28de](https://github.com/RisingStack/graffiti-mongoose/commit/dfc28de)) 782 | * chore(npmignore): add src to npmignore ([7c01e31](https://github.com/RisingStack/graffiti-mongoose/commit/7c01e31)) 783 | * chore(package): bump version to 1.1.0 ([8f6e97f](https://github.com/RisingStack/graffiti-mongoose/commit/8f6e97f)) 784 | * chore(package): bump version to 1.1.1 ([827be8c](https://github.com/RisingStack/graffiti-mongoose/commit/827be8c)) 785 | 786 | ### feat 787 | 788 | * feat(schema): expose graphql ([65d1893](https://github.com/RisingStack/graffiti-mongoose/commit/65d1893)) 789 | 790 | ### test 791 | 792 | * test(schema): use exposed graphql in tests ([6690b6c](https://github.com/RisingStack/graffiti-mongoose/commit/6690b6c)) 793 | 794 | 795 | 796 | 797 | ## 1.0.5 (2015-07-28) 798 | 799 | 800 | ### chore 801 | 802 | * chore(changelog): update ([bae7e01](https://github.com/RisingStack/graffiti-mongoose/commit/bae7e01)) 803 | * chore(package): bump version to 1.0.5 ([cbbd650](https://github.com/RisingStack/graffiti-mongoose/commit/cbbd650)) 804 | * chore(package): udpdate dependencies ([b3987fb](https://github.com/RisingStack/graffiti-mongoose/commit/b3987fb)) 805 | 806 | 807 | 808 | 809 | ## 1.0.4 (2015-07-27) 810 | 811 | 812 | ### chore 813 | 814 | * chore(changelog): update CHANGELOG.md ([ba1e0d9](https://github.com/RisingStack/graffiti-mongoose/commit/ba1e0d9)) 815 | * chore(package): bump version to 1.0.4 ([09ddbbc](https://github.com/RisingStack/graffiti-mongoose/commit/09ddbbc)) 816 | * chore(package): remove unused dependencies ([42fa4ac](https://github.com/RisingStack/graffiti-mongoose/commit/42fa4ac)) 817 | 818 | 819 | 820 | 821 | ## 1.0.3 (2015-07-27) 822 | 823 | 824 | ### chore 825 | 826 | * chore(changelog): update CHANGELOG.md ([5c2cd49](https://github.com/RisingStack/graffiti-mongoose/commit/5c2cd49)) 827 | * chore(package): bump version to 1.0.3 ([5971779](https://github.com/RisingStack/graffiti-mongoose/commit/5971779)) 828 | 829 | ### refactor 830 | 831 | * refactor(schema): remove mongoose from schema to avoid conflicts ([8a37235](https://github.com/RisingStack/graffiti-mongoose/commit/8a37235)) 832 | 833 | 834 | 835 | 836 | ## 1.0.2 (2015-07-27) 837 | 838 | 839 | ### chore 840 | 841 | * chore(build): publish built version to npm ([43f89d8](https://github.com/RisingStack/graffiti-mongoose/commit/43f89d8)) 842 | * chore(changelog): update ([edc788f](https://github.com/RisingStack/graffiti-mongoose/commit/edc788f)) 843 | * chore(package): bumping version to 1.0.2 ([7c21116](https://github.com/RisingStack/graffiti-mongoose/commit/7c21116)) 844 | * chore(package): update description ([0f929df](https://github.com/RisingStack/graffiti-mongoose/commit/0f929df)) 845 | 846 | 847 | 848 | 849 | ## 1.0.1 (2015-07-27) 850 | 851 | 852 | ### chore 853 | 854 | * chore(changelog): update CHANGELOG.md ([f49637c](https://github.com/RisingStack/graffiti-mongoose/commit/f49637c)) 855 | * chore(npmignore): add .npmignore file to project ([fd76552](https://github.com/RisingStack/graffiti-mongoose/commit/fd76552)) 856 | * chore(package): add keywords and fix license ([71713ad](https://github.com/RisingStack/graffiti-mongoose/commit/71713ad)) 857 | * chore(package): bump version to 1.0.1 ([ee6448c](https://github.com/RisingStack/graffiti-mongoose/commit/ee6448c)) 858 | 859 | 860 | 861 | 862 | # 1.0.0 (2015-07-27) 863 | 864 | 865 | ### chore 866 | 867 | * chore(changelog): add CHANGELOG file ([2550e0b](https://github.com/RisingStack/graffiti-mongoose/commit/2550e0b)) 868 | * chore(changelog): update changelog ([c002cad](https://github.com/RisingStack/graffiti-mongoose/commit/c002cad)) 869 | * chore(changelog): update CHANGELOG ([3cc3e48](https://github.com/RisingStack/graffiti-mongoose/commit/3cc3e48)) 870 | * chore(changelog): update CHANGELOG.md ([b106ae1](https://github.com/RisingStack/graffiti-mongoose/commit/b106ae1)) 871 | * chore(ci): integrate TravisCI via config file ([833833d](https://github.com/RisingStack/graffiti-mongoose/commit/833833d)) 872 | * chore(gitignore): add .gitignore file to the project ([2a487bc](https://github.com/RisingStack/graffiti-mongoose/commit/2a487bc)) 873 | * chore(guidelines): add CONTRIBUTING guideline to the project ([88837cf](https://github.com/RisingStack/graffiti-mongoose/commit/88837cf)) 874 | * chore(license): add LICENSE file to the project ([d97609e](https://github.com/RisingStack/graffiti-mongoose/commit/d97609e)) 875 | * chore(listing): add ESLint to the project ([6068476](https://github.com/RisingStack/graffiti-mongoose/commit/6068476)) 876 | * chore(pre-commit): add tests to pre-commit hooks ([e498710](https://github.com/RisingStack/graffiti-mongoose/commit/e498710)) 877 | * chore(project): rename project to "graffiti-mongo" ([7c9141a](https://github.com/RisingStack/graffiti-mongoose/commit/7c9141a)) 878 | * chore(project): rename project to "graffiti-mongoose" ([b5e1e3e](https://github.com/RisingStack/graffiti-mongoose/commit/b5e1e3e)) 879 | 880 | ### docs 881 | 882 | * docs(read): add JavaScript style to README example ([880fdcd](https://github.com/RisingStack/graffiti-mongoose/commit/880fdcd)) 883 | * docs(read): improve model style in README ([c57d83e](https://github.com/RisingStack/graffiti-mongoose/commit/c57d83e)) 884 | * docs(readme): add example to the README ([bb49bda](https://github.com/RisingStack/graffiti-mongoose/commit/bb49bda)) 885 | * docs(readme): add mongoose link to the README ([61dcfb3](https://github.com/RisingStack/graffiti-mongoose/commit/61dcfb3)) 886 | * docs(readme): add ref example to the README ([ff12c36](https://github.com/RisingStack/graffiti-mongoose/commit/ff12c36)) 887 | * docs(readme): Add supported types and queries to the README ([c1ff505](https://github.com/RisingStack/graffiti-mongoose/commit/c1ff505)) 888 | * docs(readme): add test section to the README ([6d5b04d](https://github.com/RisingStack/graffiti-mongoose/commit/6d5b04d)) 889 | 890 | ### feat 891 | 892 | * feat(schema): add schema file to project ([ab94cba](https://github.com/RisingStack/graffiti-mongoose/commit/ab94cba)) 893 | * feat(schema): add support for filtering plural resource by indexed fields ([1debcdd](https://github.com/RisingStack/graffiti-mongoose/commit/1debcdd)) 894 | * feat(schema): add support for filtering singular resource by indexed fields ([805753a](https://github.com/RisingStack/graffiti-mongoose/commit/805753a)) 895 | * feat(schema): add support for Number, Boolean and Date types ([a07767c](https://github.com/RisingStack/graffiti-mongoose/commit/a07767c)) 896 | * feat(schema): change getSchema's interface to handle array of mongoose models ([2b62c48](https://github.com/RisingStack/graffiti-mongoose/commit/2b62c48)) 897 | * feat(schema): filter plural resource by array of _id -s ([26e642f](https://github.com/RisingStack/graffiti-mongoose/commit/26e642f)) 898 | * feat(schema): support array of dates type ([787b640](https://github.com/RisingStack/graffiti-mongoose/commit/787b640)) 899 | * feat(schema): support array of primitives ([9e6a439](https://github.com/RisingStack/graffiti-mongoose/commit/9e6a439)) 900 | * feat(schema): support plural queries ([edca7e0](https://github.com/RisingStack/graffiti-mongoose/commit/edca7e0)) 901 | 902 | ### fix 903 | 904 | * fix(schema): change query interface for singular resource ([3de8b59](https://github.com/RisingStack/graffiti-mongoose/commit/3de8b59)) 905 | * fix(schema): handle float like numbers ([5fcc91a](https://github.com/RisingStack/graffiti-mongoose/commit/5fcc91a)) 906 | * fix(schema): rename query argument "id" to "_id" ([9f83ff7](https://github.com/RisingStack/graffiti-mongoose/commit/9f83ff7)) 907 | 908 | ### style 909 | 910 | * style(schema): add TODO comments ([e71a187](https://github.com/RisingStack/graffiti-mongoose/commit/e71a187)) 911 | 912 | ### test 913 | 914 | * test(schema): add test skeleton ([3b962d6](https://github.com/RisingStack/graffiti-mongoose/commit/3b962d6)) 915 | * test(schema): add unit tests for simple queries ([28d7ee6](https://github.com/RisingStack/graffiti-mongoose/commit/28d7ee6)) 916 | * test(schema): cover Date, String, Number and Boolean types with tests ([fa67a21](https://github.com/RisingStack/graffiti-mongoose/commit/fa67a21)) 917 | * test(schema): cover projection with unit tests ([12a0c4c](https://github.com/RisingStack/graffiti-mongoose/commit/12a0c4c)) 918 | 919 | 920 | ### BREAKING CHANGE 921 | 922 | * array of models instead of objects 923 | * project name changed to "graffiti-mongo" 924 | * project name changed to "graffiti-mongoose" 925 | * query argument "id" rename to "_id" 926 | * user resource name instead of "findOneResource" 927 | 928 | 929 | --------------------------------------------------------------------------------