├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── .babelrc ├── README.md ├── app │ ├── graphql │ │ ├── author │ │ │ ├── authorQuery.js │ │ │ └── authorType.js │ │ ├── comment │ │ │ ├── commentQuery.js │ │ │ └── commentType.js │ │ ├── graphql-thinky.js │ │ ├── index.js │ │ ├── post │ │ │ ├── postQuery.js │ │ │ └── postType.js │ │ ├── query.js │ │ ├── todo │ │ │ ├── todoQuery.js │ │ │ └── todoType.js │ │ └── user │ │ │ ├── userQuery.js │ │ │ └── userType.js │ ├── models │ │ ├── author.js │ │ ├── comment.js │ │ ├── index.js │ │ ├── post.js │ │ ├── todo.js │ │ └── user.js │ └── thinky.js ├── author_post_comment.json ├── data.json ├── package.json ├── server.babel.js ├── server.js └── yarn.lock ├── package.json ├── src ├── base64.js ├── commonArgs.js ├── dataloader │ ├── loaderFilter.js │ └── modelLoader.js ├── index.js ├── modelToGqlObjectType.js ├── node.js ├── queryBuilder.js ├── relay │ ├── index.js │ ├── nodeDefinition.js │ ├── nodeMapper.js │ └── resolver.js ├── resolver.js ├── simplifyAst.js └── typeMapper.js ├── test ├── helpers │ ├── db │ │ ├── index.js │ │ ├── models.js │ │ └── thinky.js │ ├── graphql │ │ ├── index.js │ │ └── loaders.js │ └── index.js ├── integration │ ├── graphql-thinky.test.js │ ├── graphus.test.js │ ├── relay.test.js │ └── resolver.test.js └── unit │ ├── modelToGQLObjectType.js │ ├── simplyAST.test.js │ └── typeMapper.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "env": { 7 | "development": { 8 | "sourceMaps": "inline" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | rethinkdb_data/ 3 | .idea 4 | coverage 5 | .nyc_output 6 | lib/ 7 | example/node_modules/ 8 | example/rethinkdb_data/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | rethinkdb_data 3 | src 4 | example 5 | .idea 6 | coverage 7 | .nyc_output -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6.9.0" 5 | - "4.0" 6 | 7 | addons: 8 | rethinkdb: "2.3.2" 9 | 10 | before_install: 11 | - source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list 12 | - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - 13 | - sudo apt-get update -q 14 | - sudo apt-get -y --force-yes install rethinkdb 15 | 16 | before_script: 17 | - rethinkdb --daemon 18 | 19 | script: 20 | - "npm run test" 21 | 22 | notifications: 23 | email: false 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Fabrizio 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-thinky 2 | 3 | ![graphql-thinky-logo](http://www.cagacaga.com/fab_full_big_dark.png) 4 | 5 | [![Build Status](https://travis-ci.org/fenos/graphql-thinky.svg?branch=master)](https://travis-ci.org/fenos/graphql-thinky) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/fenos/graphql-thinky/master/LICENSE) [![npm](https://img.shields.io/badge/npm-0.4.0--rc--3-blue.svg)](https://www.npmjs.com/package/graphql-thinky) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 6 | 7 | 8 | Graphql-thinky helps you to construct your GraphQL schema and the communication layer to a RethinkDB backend. It will perform **batched** and **optimised** queries, during graphql requests. the library is powered by the fantastic thinky ORM and Graphql / RelayJS which rely on. 9 | 10 | Inspired by the great [graphql-sequelize](https://github.com/mickhansen/graphql-sequelize). If you have a SQLs application i suggest to look at it. 11 | 12 | ###Documentation 13 | You can find it here: [https://graphql-thinky.readme.io](https://graphql-thinky.readme.io) 14 | 15 | Example: [Here](https://github.com/fenos/graphql-thinky/tree/master/example) 16 | 17 | ### Milestone 18 | 19 | This library has been just backed with :heartpulse: and with in it's early version can already provide pleasure to build your own GraphQL schema; although it needs the **community support** to grow and evolve to achieve the **v1.0**. 20 | 21 | Here is the features not yet implemented, that i'm willing to add over the time. 22 | 23 | - **Subscription** helpers for graphql, so that we can fully use RethinkDB amazing **change feed** 24 | - **Mutation** helpers, to allow create simple mutation in few lines 25 | - Custom query overwrites - to allow the developer to extend the default query behaviour of **graphql-thinky** on every Node. 26 | - Increase test case coverage 27 | - and much more when new feature are requested from the folks... 28 | 29 | ### Contribution 30 | 31 | To contribute to the repo, you can do it in few ways: 32 | 33 | - **Bugs**: Open a issue into github, add the test case to reproduce the bug (if possible) 34 | - **New features**: Open an issue into github, explain the needs of the feature once the feature is agreed i will happy to receive Pull request with related tests cases, if you can't do that I'll try to help for the implementation. 35 | 36 | **Note**: As the repository grow we will add more strict guidelines for contribution 37 | 38 | 39 | ### Credits 40 | 41 | ##### I want to thanks the technologies that allowed **graphql-thinky** to be built. Here the links 42 | 43 | - [Thinky](http://thinky.io) 44 | - [GraphQL](http://graphql.org) 45 | - [RelayJs](https://facebook.github.io/relay) 46 | - [RethinkDB](http://www.rethinkdb.com/) 47 | - [graphql-sequelize](https://github.com/mickhansen/graphql-sequelize) 48 | - [ava](https://github.com/avajs/ava) 49 | - [babel](https://babeljs.io) 50 | - And all the entire MIT projects communities 51 | 52 | ##### Folks & contributors of the repo 53 | 54 | **Developers** 55 | - Me: [fenos](https://github.com/fenos/graphql-thinky) 56 | 57 | 58 | **Doc Design and Logo**: 59 | - [Tan](https://github.com/slow-motion) 60 | - [Fatos](https://www.behance.net/roza) 61 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "env": { 7 | "development": { 8 | "sourceMaps": "inline" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ##Installation 2 | 3 | ~~~ 4 | git clone https://github.com/fenos/graphql-thinky 5 | cd graphql-thinky/example 6 | npm install 7 | rethinkdb --daemon 8 | npm start 9 | ~~~ 10 | -------------------------------------------------------------------------------- /example/app/graphql/author/authorQuery.js: -------------------------------------------------------------------------------- 1 | import GraphqlThinky from './../graphql-thinky'; 2 | import AuthorType from './authorType'; 3 | import {GraphQLList} from 'graphql'; 4 | 5 | const { resolve, connect, commonArgs } = GraphqlThinky; 6 | export default { 7 | 8 | authors: { 9 | type: new GraphQLList(AuthorType), 10 | args: { 11 | ...commonArgs, 12 | }, 13 | resolve: resolve('author') 14 | }, 15 | 16 | authorsConnection: { 17 | ...connect('author', null, { 18 | connection: { 19 | name: 'AuthorConnection', 20 | type: AuthorType 21 | } 22 | }) 23 | } 24 | } -------------------------------------------------------------------------------- /example/app/graphql/author/authorType.js: -------------------------------------------------------------------------------- 1 | import GraphQLThinky from '../graphql-thinky'; 2 | import { 3 | GraphQLInt, 4 | GraphQLList 5 | } from 'graphql'; 6 | import PostType from '../post/postType'; 7 | const { resolve, commonArgs } = GraphQLThinky; 8 | 9 | export default GraphQLThinky.createModelType('author', { 10 | globalId: true, 11 | fields: () => ({ 12 | fullCount: { 13 | type: GraphQLInt 14 | }, 15 | posts: { 16 | type: new GraphQLList(PostType), 17 | args: { 18 | ...commonArgs, 19 | }, 20 | resolve: resolve('author','posts') 21 | } 22 | }) 23 | }); -------------------------------------------------------------------------------- /example/app/graphql/comment/commentQuery.js: -------------------------------------------------------------------------------- 1 | import GraphqlThinky from './../graphql-thinky'; 2 | import CommentType from './commentType'; 3 | import {GraphQLList} from 'graphql'; 4 | 5 | const { resolve, connect, commonArgs } = GraphqlThinky; 6 | 7 | export default { 8 | 9 | comments: { 10 | type: new GraphQLList(CommentType), 11 | args: { 12 | ...commonArgs, 13 | }, 14 | resolve: resolve('comment') 15 | }, 16 | 17 | commentsConnection: { 18 | ...connect('comment', null, { 19 | connection: { 20 | name: 'CommentConnection', 21 | type: CommentType 22 | } 23 | }) 24 | } 25 | } -------------------------------------------------------------------------------- /example/app/graphql/comment/commentType.js: -------------------------------------------------------------------------------- 1 | import GraphQLThinky from '../graphql-thinky'; 2 | import { 3 | GraphQLInt 4 | } from 'graphql'; 5 | import PostType from '../post/postType'; 6 | const { resolve } = GraphQLThinky; 7 | 8 | export default GraphQLThinky.createModelType('comment', { 9 | globalId: true, 10 | fields: () => ({ 11 | fullCount: { 12 | type: GraphQLInt 13 | }, 14 | post: { 15 | type: PostType, 16 | resolve: resolve('comment','post') 17 | } 18 | }) 19 | }); -------------------------------------------------------------------------------- /example/app/graphql/graphql-thinky.js: -------------------------------------------------------------------------------- 1 | import GraphQLThinky from 'graphql-thinky'; 2 | import './../models'; 3 | import thinky from '../thinky'; 4 | 5 | export default new GraphQLThinky(thinky); -------------------------------------------------------------------------------- /example/app/graphql/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | } from 'graphql'; 4 | 5 | import Query from './query'; 6 | 7 | const Schema = new GraphQLSchema({ 8 | query: Query, 9 | }); 10 | 11 | export default Schema; -------------------------------------------------------------------------------- /example/app/graphql/post/postQuery.js: -------------------------------------------------------------------------------- 1 | import GraphqlThinky from './../graphql-thinky'; 2 | import PostType from './postType'; 3 | import {GraphQLList} from 'graphql'; 4 | 5 | const { resolve, connect, commonArgs } = GraphqlThinky; 6 | 7 | export default { 8 | 9 | posts: { 10 | type: new GraphQLList(PostType), 11 | args: { 12 | ...commonArgs, 13 | }, 14 | resolve: (parent, args, context, info) => { 15 | return resolve('post')(parent, args, context, info); 16 | }, 17 | }, 18 | 19 | postsConnection: { 20 | ...connect('post', null, { 21 | connection: { 22 | name: 'PostConnection', 23 | type: PostType 24 | } 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /example/app/graphql/post/postType.js: -------------------------------------------------------------------------------- 1 | import GraphQLThinky from '../graphql-thinky'; 2 | import { 3 | GraphQLInt, 4 | GraphQLList, 5 | } from 'graphql'; 6 | import CommentType from '../comment/commentType'; 7 | const { resolve, commonArgs } = GraphQLThinky; 8 | 9 | export default GraphQLThinky.createModelType('post', { 10 | globalId: true, 11 | fields: () => ({ 12 | fullCount: { 13 | type: GraphQLInt 14 | }, 15 | comments: { 16 | type: new GraphQLList(CommentType), 17 | args: { 18 | ...commonArgs, 19 | }, 20 | resolve: resolve('post','comments') 21 | } 22 | }) 23 | }); -------------------------------------------------------------------------------- /example/app/graphql/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | } from 'graphql'; 4 | 5 | import userQuery from './user/userQuery'; 6 | import todoQuery from './todo/todoQuery'; 7 | import authorQuery from './author/authorQuery'; 8 | import postQuery from './post/postQuery'; 9 | import GraphqlThinky from './graphql-thinky'; 10 | 11 | export default new GraphQLObjectType({ 12 | name: 'Query', 13 | fields: () => ({ 14 | ...userQuery, 15 | ...todoQuery, 16 | ...authorQuery, 17 | ...postQuery, 18 | node: GraphqlThinky.nodeField 19 | }) 20 | }); -------------------------------------------------------------------------------- /example/app/graphql/todo/todoQuery.js: -------------------------------------------------------------------------------- 1 | import GraphqlThinky from './../graphql-thinky'; 2 | import TodoType from './todoType'; 3 | import {GraphQLList} from 'graphql'; 4 | 5 | const { resolve, connect } = GraphqlThinky; 6 | 7 | export default { 8 | 9 | todos: { 10 | type: new GraphQLList(TodoType), 11 | resolve: resolve('todo') 12 | }, 13 | 14 | todosConnection: { 15 | ...connect('todo', null, { 16 | connection: { 17 | name: 'TodoConnection', 18 | type: TodoType 19 | } 20 | }) 21 | } 22 | } -------------------------------------------------------------------------------- /example/app/graphql/todo/todoType.js: -------------------------------------------------------------------------------- 1 | import GraphQLThinky from '../graphql-thinky'; 2 | import UserType from './../user/userType'; 3 | import { 4 | GraphQLInt 5 | } from 'graphql'; 6 | 7 | const { resolve } = GraphQLThinky; 8 | 9 | export default GraphQLThinky.createModelType('todo', { 10 | globalId: true, 11 | fields: () => ({ 12 | fullCount: { 13 | type: GraphQLInt 14 | }, 15 | user: { 16 | type: UserType, 17 | resolve: resolve('todo','user') 18 | } 19 | }) 20 | }); -------------------------------------------------------------------------------- /example/app/graphql/user/userQuery.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLList, 3 | GraphQLString, 4 | GraphQLNonNull, 5 | GraphQLInt, 6 | } from 'graphql'; 7 | 8 | import UserType from './userType'; 9 | import GraphQLThinky from '../graphql-thinky'; 10 | 11 | const { resolve, connect } = GraphQLThinky; 12 | 13 | export default { 14 | /** 15 | * User query 16 | */ 17 | users: { 18 | args: { 19 | name: { 20 | type: GraphQLString 21 | }, 22 | offset: { 23 | type: GraphQLInt 24 | } 25 | }, 26 | type: new GraphQLNonNull(new GraphQLList(UserType)), 27 | resolve: resolve('user'), 28 | }, 29 | user: { 30 | type: UserType, 31 | args: { 32 | name: { 33 | type: new GraphQLNonNull(GraphQLString) 34 | } 35 | }, 36 | resolve: resolve('user') 37 | }, 38 | 39 | /** 40 | * User Connection query, 41 | * it use relay spec 42 | */ 43 | usersConnection: { 44 | ...connect('user', null, { 45 | args: { 46 | name: { 47 | type: GraphQLString 48 | } 49 | }, 50 | connection: { 51 | name: 'UserConnection', 52 | type: UserType 53 | } 54 | }) 55 | } 56 | } -------------------------------------------------------------------------------- /example/app/graphql/user/userType.js: -------------------------------------------------------------------------------- 1 | import GraphQLThinky from '../graphql-thinky'; 2 | import { 3 | GraphQLInt, 4 | GraphQLList, 5 | GraphQLBoolean, 6 | } from 'graphql'; 7 | 8 | const { resolve, connect } = GraphQLThinky; 9 | 10 | import TodoType from '../todo/todoType'; 11 | 12 | export default GraphQLThinky.createModelType('user', { 13 | globalId: true, 14 | fields: () => ({ 15 | fullCount: { 16 | type: GraphQLInt, 17 | }, 18 | todos: { 19 | args: { 20 | offset: { 21 | type: GraphQLInt, 22 | }, 23 | skip: { 24 | type: GraphQLInt, 25 | }, 26 | completed: { 27 | type: GraphQLBoolean, 28 | }, 29 | }, 30 | type: new GraphQLList(TodoType), 31 | resolve: resolve('user','todos', { 32 | filterQuery: true, 33 | }) 34 | }, 35 | todosConnection: { 36 | ...connect('user','todos', { 37 | args: { 38 | completed: { 39 | type: GraphQLBoolean, 40 | } 41 | }, 42 | filterQuery: false, 43 | connection: { 44 | name: 'UserTodo', 45 | type: TodoType, 46 | }, 47 | }) 48 | } 49 | }) 50 | }) 51 | ; -------------------------------------------------------------------------------- /example/app/models/author.js: -------------------------------------------------------------------------------- 1 | import thinky from '../thinky'; 2 | const type = thinky.type; 3 | 4 | const Author = thinky.createModel('author', { 5 | name: type.object().schema({ 6 | first: type.string(), 7 | last: type.string() 8 | }), 9 | email: type.string(), 10 | website: type.string() 11 | }); 12 | 13 | Author.relations = () => { 14 | Author.hasMany(thinky.models.post, "posts", "id", "authorId"); 15 | } 16 | 17 | 18 | Author.ensureIndex("name"); 19 | 20 | export default Author; -------------------------------------------------------------------------------- /example/app/models/comment.js: -------------------------------------------------------------------------------- 1 | import thinky from '../thinky'; 2 | const type = thinky.type; 3 | const r = thinky.r; 4 | 5 | const Comment = thinky.createModel('comment', { 6 | name: type.string(), 7 | comment: type.string(), 8 | postId: type.string(), 9 | date: type.date().default(r.now()) 10 | }); 11 | 12 | Comment.relations = () => { 13 | Comment.belongsTo(thinky.models.post, "post", "postId", "id"); 14 | } 15 | 16 | Comment.ensureIndex("completed"); 17 | 18 | export default Comment; -------------------------------------------------------------------------------- /example/app/models/index.js: -------------------------------------------------------------------------------- 1 | const normalizedPath = require("path").join(__dirname); 2 | const Models = {}; 3 | 4 | require("fs").readdirSync(normalizedPath).forEach(function(file) { 5 | if (file === 'index.js') return; 6 | 7 | const model = require("./" + file).default; 8 | const tableName = model.getTableName(); 9 | Models[tableName] = model; 10 | }); 11 | 12 | Object.keys(Models).forEach((modelname) => { 13 | if (typeof Models[modelname].relations === 'function') { 14 | 15 | Models[modelname].relations(); 16 | } 17 | }) 18 | 19 | export default Models; 20 | -------------------------------------------------------------------------------- /example/app/models/post.js: -------------------------------------------------------------------------------- 1 | import thinky from '../thinky'; 2 | const type = thinky.type; 3 | 4 | const Post = thinky.createModel('post', { 5 | text: type.string(), 6 | picture: type.string(), 7 | tag: type.string(), 8 | authorId: type.string().allowNull(false), 9 | }); 10 | 11 | Post.relations = () => { 12 | Post.belongsTo(thinky.models.author, "author", "authorId", "id"); 13 | Post.hasMany(thinky.models.comment, "comments", "id", "postId"); 14 | }; 15 | 16 | Post.ensureIndex("date"); 17 | 18 | export default Post; -------------------------------------------------------------------------------- /example/app/models/todo.js: -------------------------------------------------------------------------------- 1 | import thinky from '../thinky'; 2 | const type = thinky.type; 3 | 4 | const Todo = thinky.createModel('todo', { 5 | text: type.string(), 6 | completed: type.boolean(), 7 | user_id: type.string().allowNull(false) 8 | }); 9 | 10 | Todo.relations = () => { 11 | Todo.belongsTo(thinky.models.user,'user','user_id','id'); 12 | } 13 | 14 | Todo.ensureIndex("completed"); 15 | 16 | export default Todo; -------------------------------------------------------------------------------- /example/app/models/user.js: -------------------------------------------------------------------------------- 1 | import thinky from '../thinky'; 2 | const type = thinky.type; 3 | 4 | 5 | const User = thinky.createModel('user', { 6 | name: type.string(), 7 | }); 8 | 9 | User.relations = () => { 10 | User.hasMany(thinky.models.todo,'todos','id','user_id'); 11 | } 12 | 13 | User.ensureIndex("name"); 14 | 15 | export default User; -------------------------------------------------------------------------------- /example/app/thinky.js: -------------------------------------------------------------------------------- 1 | import thinky from 'thinky'; 2 | import {wrap, repeat} from 'lodash'; 3 | 4 | const Thinky = thinky(); 5 | 6 | /** 7 | * Console log queries executed 8 | * in development mode 9 | */ 10 | export function bindLogger() { 11 | 12 | if (process.env.NODE_ENV !== 'production') { 13 | 14 | Thinky.r._Term.prototype.run = wrap(Thinky.r._Term.prototype.run, function (func) { 15 | 16 | const queryToString = this.toString(); 17 | const lines = 100; 18 | 19 | console.info(`START${repeat('-', lines)}`); 20 | console.info(queryToString); 21 | console.info(`END${repeat('-', lines + 2)}`); 22 | console.log(''); 23 | 24 | var trailingArguments = [].slice.call(arguments, 1); 25 | return func.apply(this, trailingArguments); 26 | }); 27 | } 28 | } 29 | 30 | export default Thinky; -------------------------------------------------------------------------------- /example/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Eunice Powell", 4 | "todos": [ 5 | { 6 | "text": "Dolor ullamco aliquip irure mollit nostrud enim dolore consequat excepteur est velit aute minim dolor. Do reprehenderit laborum sint consectetur do elit. Esse quis voluptate voluptate Lorem amet non velit id deserunt nisi. Deserunt ipsum sit laboris quis ea nulla duis. Irure cillum anim veniam enim adipisicing veniam pariatur aute qui irure reprehenderit ea excepteur. Quis voluptate aliquip Lorem eiusmod minim est magna ullamco. In occaecat enim id adipisicing adipisicing amet esse culpa fugiat sint nostrud officia.\r\n", 7 | "completed": false 8 | }, 9 | { 10 | "text": "Qui amet eu minim sint. Enim mollit laborum reprehenderit enim nostrud ex. Proident Lorem et voluptate cillum ullamco esse deserunt aliqua incididunt exercitation deserunt. In laboris voluptate sit est nulla pariatur mollit est ipsum do. Aliquip proident duis veniam nisi do commodo elit.\r\n", 11 | "completed": true 12 | }, 13 | { 14 | "text": "Exercitation consequat amet non ullamco. Velit ex anim ex ex ea ullamco voluptate laboris nisi ex. Lorem in pariatur anim commodo Lorem fugiat. Do nulla minim exercitation anim consectetur Lorem dolore minim eu. Eu voluptate incididunt excepteur quis dolore aliquip duis est sint pariatur do fugiat est.\r\n", 15 | "completed": false 16 | }, 17 | { 18 | "text": "Et elit adipisicing nostrud cillum voluptate. Voluptate cillum ullamco proident aliquip enim qui aute. In elit aute commodo enim.\r\n", 19 | "completed": false 20 | }, 21 | { 22 | "text": "Nostrud ipsum sint laboris laborum aute irure laborum tempor est est minim reprehenderit. Do do labore anim id exercitation pariatur incididunt esse qui. Voluptate sint non adipisicing in enim labore quis consectetur occaecat adipisicing consectetur officia laborum eiusmod.\r\n", 23 | "completed": true 24 | }, 25 | { 26 | "text": "Aute sunt Lorem id sunt mollit nostrud incididunt. Commodo pariatur ex minim irure consequat est laboris non quis commodo laboris. Elit officia sit Lorem veniam in culpa Lorem cupidatat officia proident sint officia eu. Consectetur veniam consectetur aliqua incididunt sint reprehenderit adipisicing tempor culpa eiusmod aliquip reprehenderit. Lorem veniam nulla ad pariatur.\r\n", 27 | "completed": false 28 | } 29 | ] 30 | }, 31 | { 32 | "index": 1, 33 | "name": "Julie Serrano", 34 | "todos": [ 35 | { 36 | "text": "Excepteur aute ex aliqua excepteur occaecat et id pariatur deserunt proident excepteur. Veniam aliqua nisi in commodo elit proident qui magna elit Lorem nisi quis consectetur ipsum. Minim adipisicing pariatur dolor est sit laborum ullamco sint labore eiusmod commodo eiusmod. Id id quis est cupidatat elit eu cillum ea eiusmod est anim mollit Lorem. Amet velit sint fugiat Lorem qui non. Ex laborum qui veniam incididunt.\r\n", 37 | "completed": true 38 | }, 39 | { 40 | "text": "In laborum consequat deserunt adipisicing elit adipisicing do qui commodo nisi. Lorem cillum proident esse nostrud sunt sunt nostrud proident culpa adipisicing. Ex excepteur velit sint laboris incididunt et officia irure adipisicing id. Proident labore et nostrud eu amet. Irure culpa irure reprehenderit adipisicing fugiat ad et velit aute. Esse nisi Lorem adipisicing anim sunt sunt incididunt non sunt et.\r\n", 41 | "completed": true 42 | }, 43 | { 44 | "text": "Aliqua velit commodo voluptate pariatur duis id exercitation veniam est esse adipisicing. Veniam consectetur adipisicing ullamco excepteur incididunt elit excepteur consectetur quis ipsum anim ullamco est. Pariatur id nulla cillum eiusmod eu veniam elit magna fugiat. Lorem velit irure consequat tempor deserunt duis non ipsum. Nulla aliquip ad elit fugiat est sint. Tempor ut aute commodo commodo officia sunt non minim non anim sint ut. Quis aliquip do cillum laborum ex aliquip ad nostrud aute.\r\n", 45 | "completed": false 46 | }, 47 | { 48 | "text": "Anim consectetur voluptate irure incididunt. Anim dolor laboris elit amet laborum consequat reprehenderit in. Do deserunt minim occaecat sint exercitation nulla velit elit incididunt deserunt irure quis voluptate. Eu consequat nulla amet officia consectetur sint et eu consequat cupidatat culpa. Aute incididunt tempor mollit aliquip irure nostrud id aliqua anim quis esse.\r\n", 49 | "completed": false 50 | }, 51 | { 52 | "text": "Excepteur ullamco laboris dolor velit nostrud non. Aliquip velit ad cillum do. Nulla ex veniam magna id aliquip non velit occaecat. Sint commodo est laborum in labore. Pariatur eu incididunt do ullamco. Mollit cillum velit laboris tempor.\r\n", 53 | "completed": false 54 | }, 55 | { 56 | "text": "Est officia dolor pariatur amet laborum mollit id dolor ea cillum anim pariatur mollit. Culpa eu minim tempor tempor est eu proident ad. Aliqua elit adipisicing enim cupidatat anim sint et ea nostrud in non magna.\r\n", 57 | "completed": true 58 | } 59 | ] 60 | }, 61 | { 62 | "name": "Jaime Bonner", 63 | "todos": [ 64 | { 65 | "text": "Consectetur pariatur officia commodo anim eu incididunt nisi cillum labore. Occaecat ex dolor laborum duis. Ad consectetur nostrud duis qui qui laborum Lorem laboris nulla nulla.\r\n", 66 | "completed": true 67 | }, 68 | { 69 | "text": "Ex officia occaecat minim mollit exercitation amet do anim exercitation esse ex. In in magna deserunt Lorem consequat occaecat esse et irure magna dolore ut. Velit incididunt deserunt cillum nostrud.\r\n", 70 | "completed": true 71 | }, 72 | { 73 | "text": "Consectetur ea qui duis consequat amet ea id. Aliquip cillum minim sit cupidatat adipisicing proident nisi cillum aliquip culpa anim officia mollit. Excepteur exercitation est non magna consectetur Lorem cupidatat et consectetur tempor officia magna non. Aute est pariatur fugiat cupidatat.\r\n", 74 | "completed": true 75 | }, 76 | { 77 | "text": "In enim fugiat esse consectetur mollit reprehenderit qui minim ex consectetur. Et amet consequat excepteur deserunt qui cupidatat reprehenderit dolor aute non. Aute qui consequat magna irure minim laboris nisi nostrud dolor aliquip. Aute incididunt ipsum ut aliquip cillum qui proident sunt enim proident minim laborum velit.\r\n", 78 | "completed": true 79 | }, 80 | { 81 | "text": "Duis occaecat esse in labore sint esse aute Lorem quis. Dolor nulla cupidatat veniam fugiat amet non. Dolore eu ut nostrud labore do qui minim occaecat et. Nisi ad reprehenderit proident deserunt est mollit minim aliquip exercitation in eiusmod. Adipisicing ipsum fugiat consectetur laborum pariatur. Exercitation ea laborum cupidatat quis anim quis nulla est id adipisicing reprehenderit ex.\r\n", 82 | "completed": true 83 | }, 84 | { 85 | "text": "Sunt elit quis eu voluptate sunt. Esse in esse exercitation et ullamco laboris qui commodo quis non sint non. Velit ut non velit proident fugiat. Magna cupidatat incididunt sit adipisicing proident consequat labore anim voluptate irure duis reprehenderit. Quis sint ut dolor excepteur anim sint reprehenderit aute. Ad adipisicing nostrud veniam fugiat officia culpa aute ea do eu proident ea ea. In sit do laborum eiusmod amet ullamco dolor ex dolor culpa.\r\n", 86 | "completed": false 87 | }, 88 | { 89 | "text": "Reprehenderit consequat aliqua anim non. Mollit ex cupidatat eiusmod sit. Fugiat adipisicing sit tempor ad tempor sunt eu nostrud incididunt aliqua qui ipsum esse reprehenderit. Commodo aliquip veniam mollit consectetur occaecat labore minim quis ea. Anim laborum labore est mollit esse id non do et eu anim esse. Ea adipisicing minim sint veniam sunt.\r\n", 90 | "completed": false 91 | } 92 | ] 93 | }, 94 | { 95 | "name": "Merrill Chandler", 96 | "todos": [ 97 | { 98 | "text": "Sunt labore cillum dolor sint id proident. Labore consectetur nulla sit ad anim ullamco ex et minim ex elit ipsum in cupidatat. Fugiat velit elit commodo aute mollit. Amet officia commodo irure excepteur magna amet. Ullamco ad ad in nostrud qui est mollit amet officia magna cillum incididunt occaecat. Anim laboris quis pariatur ad aute nulla proident ut.\r\n", 99 | "completed": false 100 | }, 101 | { 102 | "text": "Quis aute ad enim dolor qui eu nostrud. Dolor ut eu nostrud ad. Qui qui sit sint est do minim commodo fugiat pariatur ex dolor irure. Sit exercitation ut cillum pariatur nisi aliquip est sit. Consequat fugiat proident culpa ad consequat mollit deserunt occaecat.\r\n", 103 | "completed": false 104 | }, 105 | { 106 | "text": "Officia commodo nulla officia ea et Lorem. Do pariatur culpa adipisicing officia eu cillum cillum dolore dolor est. Sit sint reprehenderit ullamco et exercitation cillum mollit officia mollit esse. Laborum ullamco incididunt tempor deserunt in labore pariatur enim. Occaecat tempor id duis sunt. Nisi veniam ut aliqua minim enim nostrud laboris veniam occaecat.\r\n", 107 | "completed": false 108 | }, 109 | { 110 | "text": "Adipisicing id sit fugiat magna aliqua tempor ex. Mollit Lorem et ut quis commodo ex. Aute nostrud veniam incididunt ullamco excepteur mollit dolor nostrud aliquip ea nisi irure nisi.\r\n", 111 | "completed": false 112 | }, 113 | { 114 | "text": "Adipisicing deserunt ex mollit in deserunt laboris aute deserunt id ut nostrud. Proident sit aute commodo est ipsum ut ut ut qui ullamco eu ut qui quis. Deserunt et officia non laboris. Ex veniam magna voluptate aliqua culpa. Laborum mollit tempor irure consequat officia.\r\n", 115 | "completed": false 116 | }, 117 | { 118 | "text": "Nostrud exercitation eiusmod exercitation dolore non. Esse minim fugiat ex culpa adipisicing enim exercitation proident. Aliqua adipisicing fugiat elit et magna labore est mollit Lorem tempor cillum.\r\n", 119 | "completed": true 120 | }, 121 | { 122 | "text": "Eiusmod dolor labore fugiat aute eiusmod amet sunt. Mollit consequat ullamco ex cupidatat esse aute voluptate elit. Consequat labore mollit sint elit anim. Lorem duis enim adipisicing ad est elit occaecat ipsum anim deserunt. Aliqua pariatur et dolore id irure sit consectetur. Ea Lorem ipsum veniam irure non officia elit nisi non dolore sint occaecat id ipsum. Proident do anim aliqua ex ad nisi exercitation in ipsum duis Lorem nulla magna.\r\n", 123 | "completed": false 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "Sargent Nunez", 129 | "todos": [ 130 | { 131 | "text": "In nulla irure laboris ut quis ullamco reprehenderit aliqua veniam do anim deserunt consequat incididunt. Occaecat labore magna nisi reprehenderit eu do aliqua magna enim veniam. Ipsum consequat consequat et consectetur dolore laborum aute adipisicing sint. Id sunt qui sunt Lorem adipisicing officia qui tempor exercitation aliqua magna.\r\n", 132 | "completed": true 133 | }, 134 | { 135 | "text": "Laboris aliqua labore irure nostrud labore voluptate minim in anim ex nulla voluptate nisi. Laborum ex est laborum ex cupidatat mollit sint. Incididunt officia commodo ipsum dolore eu labore ut. Irure sunt officia nulla dolor adipisicing ex dolore laboris nostrud deserunt in duis. Dolore quis adipisicing aute ad nostrud ad incididunt deserunt magna dolor Lorem dolor exercitation qui.\r\n", 136 | "completed": false 137 | }, 138 | { 139 | "text": "Nulla ut aute minim occaecat excepteur Lorem aliqua nulla veniam non laborum labore pariatur. Consequat est incididunt esse ad irure anim aliquip voluptate commodo sint elit. Officia reprehenderit consectetur duis dolore magna id irure ut nostrud id reprehenderit. Fugiat labore mollit est magna. Reprehenderit laborum veniam anim sunt quis. Non laborum non deserunt laborum sunt non pariatur aliquip aliqua dolore veniam. Ipsum anim aute eu laboris sint aliqua et ea ullamco laboris anim in fugiat veniam.\r\n", 140 | "completed": false 141 | }, 142 | { 143 | "text": "Ad exercitation occaecat dolor esse incididunt sint do in pariatur amet nulla. Aliquip tempor magna nostrud Lorem deserunt commodo culpa aliqua id cupidatat eu qui. Ex incididunt sint duis sint ea et elit commodo minim non irure dolore consectetur. Enim sunt do eu nostrud consequat culpa mollit qui esse exercitation. Nostrud voluptate minim proident culpa ea aliqua anim non aute et voluptate fugiat aliquip.\r\n", 144 | "completed": true 145 | }, 146 | { 147 | "text": "Voluptate exercitation anim nulla consequat deserunt ullamco dolore dolor laboris magna esse mollit non. Exercitation ea laboris nulla consequat mollit ipsum adipisicing. Ullamco officia adipisicing nisi tempor. Ad cillum exercitation ad nostrud ipsum commodo ipsum consectetur eu esse consequat do officia. Eiusmod anim ea laboris fugiat ad labore qui irure ullamco esse. Exercitation nostrud cillum sunt ipsum occaecat magna ullamco voluptate consectetur laborum nostrud fugiat sit.\r\n", 148 | "completed": false 149 | }, 150 | { 151 | "text": "Nostrud nisi laborum eu nisi veniam. Eu esse amet sunt nulla. Enim aliquip aute quis esse proident.\r\n", 152 | "completed": true 153 | } 154 | ] 155 | }, 156 | { 157 | "name": "Diaz Bean", 158 | "todos": [ 159 | { 160 | "text": "Voluptate culpa nisi irure ipsum dolore commodo Lorem aliqua laboris ullamco ullamco eiusmod ex fugiat. Aliquip aliquip irure duis officia consequat non sint ea deserunt id ipsum. Deserunt nisi exercitation nulla veniam reprehenderit dolor officia minim irure. Mollit mollit ad laboris incididunt fugiat ea mollit non officia dolor enim anim. Pariatur excepteur tempor eiusmod id ea qui est enim.\r\n", 161 | "completed": false 162 | }, 163 | { 164 | "text": "Nostrud eu ad cupidatat officia fugiat ea occaecat culpa cupidatat ex et voluptate dolore nostrud. Sit consequat non excepteur qui mollit quis ipsum aute pariatur est duis. In et sit deserunt laboris irure occaecat qui sit quis. Eiusmod tempor ipsum voluptate pariatur labore est laborum non deserunt tempor tempor nulla. Labore quis magna amet ipsum consequat aute mollit aliquip veniam officia qui esse. Commodo Lorem dolor qui magna cillum consectetur aliqua. Consequat amet officia proident laboris dolore cupidatat laboris nostrud nulla.\r\n", 165 | "completed": true 166 | }, 167 | { 168 | "text": "Irure aliquip nisi sunt aute enim pariatur minim sint fugiat. Ut aliqua sunt voluptate est. Reprehenderit ea veniam cupidatat minim cupidatat sunt. Ad cillum elit ullamco magna aliqua sit in proident elit ex cillum et. Fugiat ea magna ut consequat dolore cillum excepteur ut commodo labore aute.\r\n", 169 | "completed": true 170 | }, 171 | { 172 | "text": "Aliqua aute consectetur eu cupidatat sit cillum amet fugiat sint. Officia velit cillum in et sint qui quis enim magna et enim. Ut anim qui incididunt laboris eu incididunt.\r\n", 173 | "completed": true 174 | }, 175 | { 176 | "text": "Qui ad nisi quis sint adipisicing do ad labore exercitation nulla aute est. Ea eiusmod ipsum excepteur occaecat laboris deserunt magna cillum voluptate. Fugiat magna fugiat elit mollit dolore veniam nisi culpa. Proident veniam veniam anim do culpa do tempor pariatur sint consequat dolore quis.\r\n", 177 | "completed": false 178 | }, 179 | { 180 | "text": "Fugiat eiusmod amet duis eu esse irure ut aute sit deserunt. Pariatur deserunt exercitation et qui esse quis tempor velit occaecat consequat cupidatat. Cillum eu reprehenderit nulla nisi ea ullamco fugiat proident nulla. Consectetur dolore et et ut laboris nulla consectetur elit cupidatat nulla quis laborum ullamco. Occaecat nulla laboris fugiat eiusmod officia aliquip proident ad consequat adipisicing nostrud velit anim. Ex eiusmod laboris nulla ad.\r\n", 181 | "completed": false 182 | }, 183 | { 184 | "text": "Voluptate voluptate eu commodo velit do. Nisi sunt sunt fugiat ullamco labore nisi cupidatat sunt amet laborum id eu. Sunt voluptate amet sint veniam minim ex veniam cupidatat ea nisi in magna dolor. Eiusmod laboris consequat commodo et deserunt sit sunt consectetur veniam enim ut. Do dolore magna irure est est ipsum ea aliquip.\r\n", 185 | "completed": true 186 | } 187 | ] 188 | } 189 | ] -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "graphql-thinky-example", 4 | "version": "0.1.0", 5 | "description": "Graphql thinky example", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node server.babel.js" 9 | }, 10 | "author": "Fabrizio", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.15.1", 14 | "dataloader": "^1.2.0", 15 | "express": "^4.13.4", 16 | "express-graphql": "^0.5.1", 17 | "graphql": "^0.8.2", 18 | "graphql-relay": "^0.4.1", 19 | "graphql-thinky": "^0.4.0-rc-3", 20 | "thinky": "^2.3.2" 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^6.9.0", 24 | "babel-polyfill": "^6.9.0", 25 | "babel-preset-es2015": "^6.6.0", 26 | "babel-preset-stage-0": "^6.5.0", 27 | "babel-register": "^6.9.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/server.babel.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | require('./server'); -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import expressGraphql from 'express-graphql'; 4 | import schema from './app/graphql'; 5 | import models from './app/models'; 6 | import {bindLogger} from './app/thinky'; 7 | import GT from './app/graphql/graphql-thinky'; 8 | 9 | import data from './data.json'; 10 | import authorPostCommentData from './author_post_comment.json'; 11 | 12 | const app = express(); 13 | 14 | app.use(bodyParser.json()); 15 | app.use('/graphql',(req,res) => { 16 | return expressGraphql({ 17 | schema, 18 | graphiql: true, 19 | pretty: true, 20 | context: { 21 | loaders: GT.getModelLoaders(), 22 | }, 23 | formatError: error => { 24 | console.error(error); 25 | 26 | return error; 27 | } 28 | })(req,res); 29 | }); 30 | 31 | app.listen(7000, async function() { 32 | 33 | await models.user.delete(); 34 | await models.todo.delete(); 35 | 36 | const saved = []; 37 | 38 | data.forEach((user) => { 39 | const userModel = new models.user(user); 40 | saved.push(userModel.saveAll({todos: true})); 41 | }); 42 | 43 | authorPostCommentData.forEach(author => { 44 | const authorModel = new models.author(author); 45 | saved.push(authorModel.saveAll({posts: {comments: true}})); 46 | }); 47 | 48 | try { 49 | 50 | await Promise.all(saved); 51 | } catch (e) { 52 | console.log(e); 53 | } 54 | 55 | console.log("Graphql-thinky started on port: 7000"); 56 | 57 | bindLogger(); 58 | }); -------------------------------------------------------------------------------- /example/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@^1.3.0, accepts@~1.3.3: 6 | version "1.3.3" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 8 | dependencies: 9 | mime-types "~2.1.11" 10 | negotiator "0.6.1" 11 | 12 | ansi-regex@^2.0.0: 13 | version "2.0.0" 14 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" 15 | 16 | ansi-styles@^2.2.1: 17 | version "2.2.1" 18 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 19 | 20 | array-flatten@1.1.1: 21 | version "1.1.1" 22 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 23 | 24 | babel-code-frame@^6.16.0: 25 | version "6.16.0" 26 | resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.16.0.tgz#f90e60da0862909d3ce098733b5d3987c97cb8de" 27 | dependencies: 28 | chalk "^1.1.0" 29 | esutils "^2.0.2" 30 | js-tokens "^2.0.0" 31 | 32 | babel-core@^6.18.0, babel-core@^6.9.0: 33 | version "6.18.2" 34 | resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.18.2.tgz#d8bb14dd6986fa4f3566a26ceda3964fa0e04e5b" 35 | dependencies: 36 | babel-code-frame "^6.16.0" 37 | babel-generator "^6.18.0" 38 | babel-helpers "^6.16.0" 39 | babel-messages "^6.8.0" 40 | babel-register "^6.18.0" 41 | babel-runtime "^6.9.1" 42 | babel-template "^6.16.0" 43 | babel-traverse "^6.18.0" 44 | babel-types "^6.18.0" 45 | babylon "^6.11.0" 46 | convert-source-map "^1.1.0" 47 | debug "^2.1.1" 48 | json5 "^0.5.0" 49 | lodash "^4.2.0" 50 | minimatch "^3.0.2" 51 | path-is-absolute "^1.0.0" 52 | private "^0.1.6" 53 | slash "^1.0.0" 54 | source-map "^0.5.0" 55 | 56 | babel-generator@^6.18.0: 57 | version "6.19.0" 58 | resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.19.0.tgz#9b2f244204777a3d6810ec127c673c87b349fac5" 59 | dependencies: 60 | babel-messages "^6.8.0" 61 | babel-runtime "^6.9.0" 62 | babel-types "^6.19.0" 63 | detect-indent "^4.0.0" 64 | jsesc "^1.3.0" 65 | lodash "^4.2.0" 66 | source-map "^0.5.0" 67 | 68 | babel-helper-bindify-decorators@^6.18.0: 69 | version "6.18.0" 70 | resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.18.0.tgz#fc00c573676a6e702fffa00019580892ec8780a5" 71 | dependencies: 72 | babel-runtime "^6.0.0" 73 | babel-traverse "^6.18.0" 74 | babel-types "^6.18.0" 75 | 76 | babel-helper-builder-binary-assignment-operator-visitor@^6.8.0: 77 | version "6.18.0" 78 | resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.18.0.tgz#8ae814989f7a53682152e3401a04fabd0bb333a6" 79 | dependencies: 80 | babel-helper-explode-assignable-expression "^6.18.0" 81 | babel-runtime "^6.0.0" 82 | babel-types "^6.18.0" 83 | 84 | babel-helper-call-delegate@^6.18.0: 85 | version "6.18.0" 86 | resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.18.0.tgz#05b14aafa430884b034097ef29e9f067ea4133bd" 87 | dependencies: 88 | babel-helper-hoist-variables "^6.18.0" 89 | babel-runtime "^6.0.0" 90 | babel-traverse "^6.18.0" 91 | babel-types "^6.18.0" 92 | 93 | babel-helper-define-map@^6.18.0, babel-helper-define-map@^6.8.0: 94 | version "6.18.0" 95 | resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.18.0.tgz#8d6c85dc7fbb4c19be3de40474d18e97c3676ec2" 96 | dependencies: 97 | babel-helper-function-name "^6.18.0" 98 | babel-runtime "^6.9.0" 99 | babel-types "^6.18.0" 100 | lodash "^4.2.0" 101 | 102 | babel-helper-explode-assignable-expression@^6.18.0: 103 | version "6.18.0" 104 | resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.18.0.tgz#14b8e8c2d03ad735d4b20f1840b24cd1f65239fe" 105 | dependencies: 106 | babel-runtime "^6.0.0" 107 | babel-traverse "^6.18.0" 108 | babel-types "^6.18.0" 109 | 110 | babel-helper-explode-class@^6.8.0: 111 | version "6.18.0" 112 | resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.18.0.tgz#c44f76f4fa23b9c5d607cbac5d4115e7a76f62cb" 113 | dependencies: 114 | babel-helper-bindify-decorators "^6.18.0" 115 | babel-runtime "^6.0.0" 116 | babel-traverse "^6.18.0" 117 | babel-types "^6.18.0" 118 | 119 | babel-helper-function-name@^6.18.0, babel-helper-function-name@^6.8.0: 120 | version "6.18.0" 121 | resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.18.0.tgz#68ec71aeba1f3e28b2a6f0730190b754a9bf30e6" 122 | dependencies: 123 | babel-helper-get-function-arity "^6.18.0" 124 | babel-runtime "^6.0.0" 125 | babel-template "^6.8.0" 126 | babel-traverse "^6.18.0" 127 | babel-types "^6.18.0" 128 | 129 | babel-helper-get-function-arity@^6.18.0: 130 | version "6.18.0" 131 | resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.18.0.tgz#a5b19695fd3f9cdfc328398b47dafcd7094f9f24" 132 | dependencies: 133 | babel-runtime "^6.0.0" 134 | babel-types "^6.18.0" 135 | 136 | babel-helper-hoist-variables@^6.18.0: 137 | version "6.18.0" 138 | resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.18.0.tgz#a835b5ab8b46d6de9babefae4d98ea41e866b82a" 139 | dependencies: 140 | babel-runtime "^6.0.0" 141 | babel-types "^6.18.0" 142 | 143 | babel-helper-optimise-call-expression@^6.18.0: 144 | version "6.18.0" 145 | resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.18.0.tgz#9261d0299ee1a4f08a6dd28b7b7c777348fd8f0f" 146 | dependencies: 147 | babel-runtime "^6.0.0" 148 | babel-types "^6.18.0" 149 | 150 | babel-helper-regex@^6.8.0: 151 | version "6.18.0" 152 | resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.18.0.tgz#ae0ebfd77de86cb2f1af258e2cc20b5fe893ecc6" 153 | dependencies: 154 | babel-runtime "^6.9.0" 155 | babel-types "^6.18.0" 156 | lodash "^4.2.0" 157 | 158 | babel-helper-remap-async-to-generator@^6.16.0, babel-helper-remap-async-to-generator@^6.16.2: 159 | version "6.18.0" 160 | resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.18.0.tgz#336cdf3cab650bb191b02fc16a3708e7be7f9ce5" 161 | dependencies: 162 | babel-helper-function-name "^6.18.0" 163 | babel-runtime "^6.0.0" 164 | babel-template "^6.16.0" 165 | babel-traverse "^6.18.0" 166 | babel-types "^6.18.0" 167 | 168 | babel-helper-replace-supers@^6.18.0, babel-helper-replace-supers@^6.8.0: 169 | version "6.18.0" 170 | resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.18.0.tgz#28ec69877be4144dbd64f4cc3a337e89f29a924e" 171 | dependencies: 172 | babel-helper-optimise-call-expression "^6.18.0" 173 | babel-messages "^6.8.0" 174 | babel-runtime "^6.0.0" 175 | babel-template "^6.16.0" 176 | babel-traverse "^6.18.0" 177 | babel-types "^6.18.0" 178 | 179 | babel-helpers@^6.16.0: 180 | version "6.16.0" 181 | resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.16.0.tgz#1095ec10d99279460553e67eb3eee9973d3867e3" 182 | dependencies: 183 | babel-runtime "^6.0.0" 184 | babel-template "^6.16.0" 185 | 186 | babel-messages@^6.8.0: 187 | version "6.8.0" 188 | resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.8.0.tgz#bf504736ca967e6d65ef0adb5a2a5f947c8e0eb9" 189 | dependencies: 190 | babel-runtime "^6.0.0" 191 | 192 | babel-plugin-check-es2015-constants@^6.3.13: 193 | version "6.8.0" 194 | resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.8.0.tgz#dbf024c32ed37bfda8dee1e76da02386a8d26fe7" 195 | dependencies: 196 | babel-runtime "^6.0.0" 197 | 198 | babel-plugin-syntax-async-functions@^6.8.0: 199 | version "6.13.0" 200 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" 201 | 202 | babel-plugin-syntax-async-generators@^6.5.0: 203 | version "6.13.0" 204 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" 205 | 206 | babel-plugin-syntax-class-constructor-call@^6.18.0: 207 | version "6.18.0" 208 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" 209 | 210 | babel-plugin-syntax-class-properties@^6.8.0: 211 | version "6.13.0" 212 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" 213 | 214 | babel-plugin-syntax-decorators@^6.13.0: 215 | version "6.13.0" 216 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" 217 | 218 | babel-plugin-syntax-do-expressions@^6.8.0: 219 | version "6.13.0" 220 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz#5747756139aa26d390d09410b03744ba07e4796d" 221 | 222 | babel-plugin-syntax-dynamic-import@^6.18.0: 223 | version "6.18.0" 224 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" 225 | 226 | babel-plugin-syntax-exponentiation-operator@^6.8.0: 227 | version "6.13.0" 228 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" 229 | 230 | babel-plugin-syntax-export-extensions@^6.8.0: 231 | version "6.13.0" 232 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" 233 | 234 | babel-plugin-syntax-function-bind@^6.8.0: 235 | version "6.13.0" 236 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46" 237 | 238 | babel-plugin-syntax-object-rest-spread@^6.8.0: 239 | version "6.13.0" 240 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" 241 | 242 | babel-plugin-syntax-trailing-function-commas@^6.3.13: 243 | version "6.13.0" 244 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.13.0.tgz#2b84b7d53dd744f94ff1fad7669406274b23f541" 245 | 246 | babel-plugin-transform-async-generator-functions@^6.17.0: 247 | version "6.17.0" 248 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.17.0.tgz#d0b5a2b2f0940f2b245fa20a00519ed7bc6cae54" 249 | dependencies: 250 | babel-helper-remap-async-to-generator "^6.16.2" 251 | babel-plugin-syntax-async-generators "^6.5.0" 252 | babel-runtime "^6.0.0" 253 | 254 | babel-plugin-transform-async-to-generator@^6.16.0: 255 | version "6.16.0" 256 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.16.0.tgz#19ec36cb1486b59f9f468adfa42ce13908ca2999" 257 | dependencies: 258 | babel-helper-remap-async-to-generator "^6.16.0" 259 | babel-plugin-syntax-async-functions "^6.8.0" 260 | babel-runtime "^6.0.0" 261 | 262 | babel-plugin-transform-class-constructor-call@^6.3.13: 263 | version "6.18.0" 264 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.18.0.tgz#80855e38a1ab47b8c6c647f8ea1bcd2c00ca3aae" 265 | dependencies: 266 | babel-plugin-syntax-class-constructor-call "^6.18.0" 267 | babel-runtime "^6.0.0" 268 | babel-template "^6.8.0" 269 | 270 | babel-plugin-transform-class-properties@^6.18.0: 271 | version "6.19.0" 272 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.19.0.tgz#1274b349abaadc835164e2004f4a2444a2788d5f" 273 | dependencies: 274 | babel-helper-function-name "^6.18.0" 275 | babel-plugin-syntax-class-properties "^6.8.0" 276 | babel-runtime "^6.9.1" 277 | babel-template "^6.15.0" 278 | 279 | babel-plugin-transform-decorators@^6.13.0: 280 | version "6.13.0" 281 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.13.0.tgz#82d65c1470ae83e2d13eebecb0a1c2476d62da9d" 282 | dependencies: 283 | babel-helper-define-map "^6.8.0" 284 | babel-helper-explode-class "^6.8.0" 285 | babel-plugin-syntax-decorators "^6.13.0" 286 | babel-runtime "^6.0.0" 287 | babel-template "^6.8.0" 288 | babel-types "^6.13.0" 289 | 290 | babel-plugin-transform-do-expressions@^6.3.13: 291 | version "6.8.0" 292 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.8.0.tgz#fda692af339835cc255bb7544efb8f7c1306c273" 293 | dependencies: 294 | babel-plugin-syntax-do-expressions "^6.8.0" 295 | babel-runtime "^6.0.0" 296 | 297 | babel-plugin-transform-es2015-arrow-functions@^6.3.13: 298 | version "6.8.0" 299 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.8.0.tgz#5b63afc3181bdc9a8c4d481b5a4f3f7d7fef3d9d" 300 | dependencies: 301 | babel-runtime "^6.0.0" 302 | 303 | babel-plugin-transform-es2015-block-scoped-functions@^6.3.13: 304 | version "6.8.0" 305 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.8.0.tgz#ed95d629c4b5a71ae29682b998f70d9833eb366d" 306 | dependencies: 307 | babel-runtime "^6.0.0" 308 | 309 | babel-plugin-transform-es2015-block-scoping@^6.18.0: 310 | version "6.18.0" 311 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.18.0.tgz#3bfdcfec318d46df22525cdea88f1978813653af" 312 | dependencies: 313 | babel-runtime "^6.9.0" 314 | babel-template "^6.15.0" 315 | babel-traverse "^6.18.0" 316 | babel-types "^6.18.0" 317 | lodash "^4.2.0" 318 | 319 | babel-plugin-transform-es2015-classes@^6.18.0: 320 | version "6.18.0" 321 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.18.0.tgz#ffe7a17321bf83e494dcda0ae3fc72df48ffd1d9" 322 | dependencies: 323 | babel-helper-define-map "^6.18.0" 324 | babel-helper-function-name "^6.18.0" 325 | babel-helper-optimise-call-expression "^6.18.0" 326 | babel-helper-replace-supers "^6.18.0" 327 | babel-messages "^6.8.0" 328 | babel-runtime "^6.9.0" 329 | babel-template "^6.14.0" 330 | babel-traverse "^6.18.0" 331 | babel-types "^6.18.0" 332 | 333 | babel-plugin-transform-es2015-computed-properties@^6.3.13: 334 | version "6.8.0" 335 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.8.0.tgz#f51010fd61b3bd7b6b60a5fdfd307bb7a5279870" 336 | dependencies: 337 | babel-helper-define-map "^6.8.0" 338 | babel-runtime "^6.0.0" 339 | babel-template "^6.8.0" 340 | 341 | babel-plugin-transform-es2015-destructuring@^6.18.0: 342 | version "6.19.0" 343 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.19.0.tgz#ff1d911c4b3f4cab621bd66702a869acd1900533" 344 | dependencies: 345 | babel-runtime "^6.9.0" 346 | 347 | babel-plugin-transform-es2015-duplicate-keys@^6.6.0: 348 | version "6.8.0" 349 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.8.0.tgz#fd8f7f7171fc108cc1c70c3164b9f15a81c25f7d" 350 | dependencies: 351 | babel-runtime "^6.0.0" 352 | babel-types "^6.8.0" 353 | 354 | babel-plugin-transform-es2015-for-of@^6.18.0: 355 | version "6.18.0" 356 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.18.0.tgz#4c517504db64bf8cfc119a6b8f177211f2028a70" 357 | dependencies: 358 | babel-runtime "^6.0.0" 359 | 360 | babel-plugin-transform-es2015-function-name@^6.9.0: 361 | version "6.9.0" 362 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.9.0.tgz#8c135b17dbd064e5bba56ec511baaee2fca82719" 363 | dependencies: 364 | babel-helper-function-name "^6.8.0" 365 | babel-runtime "^6.9.0" 366 | babel-types "^6.9.0" 367 | 368 | babel-plugin-transform-es2015-literals@^6.3.13: 369 | version "6.8.0" 370 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.8.0.tgz#50aa2e5c7958fc2ab25d74ec117e0cc98f046468" 371 | dependencies: 372 | babel-runtime "^6.0.0" 373 | 374 | babel-plugin-transform-es2015-modules-amd@^6.18.0: 375 | version "6.18.0" 376 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.18.0.tgz#49a054cbb762bdf9ae2d8a807076cfade6141e40" 377 | dependencies: 378 | babel-plugin-transform-es2015-modules-commonjs "^6.18.0" 379 | babel-runtime "^6.0.0" 380 | babel-template "^6.8.0" 381 | 382 | babel-plugin-transform-es2015-modules-commonjs@^6.18.0: 383 | version "6.18.0" 384 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.18.0.tgz#c15ae5bb11b32a0abdcc98a5837baa4ee8d67bcc" 385 | dependencies: 386 | babel-plugin-transform-strict-mode "^6.18.0" 387 | babel-runtime "^6.0.0" 388 | babel-template "^6.16.0" 389 | babel-types "^6.18.0" 390 | 391 | babel-plugin-transform-es2015-modules-systemjs@^6.18.0: 392 | version "6.19.0" 393 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.19.0.tgz#50438136eba74527efa00a5b0fefaf1dc4071da6" 394 | dependencies: 395 | babel-helper-hoist-variables "^6.18.0" 396 | babel-runtime "^6.11.6" 397 | babel-template "^6.14.0" 398 | 399 | babel-plugin-transform-es2015-modules-umd@^6.18.0: 400 | version "6.18.0" 401 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.18.0.tgz#23351770ece5c1f8e83ed67cb1d7992884491e50" 402 | dependencies: 403 | babel-plugin-transform-es2015-modules-amd "^6.18.0" 404 | babel-runtime "^6.0.0" 405 | babel-template "^6.8.0" 406 | 407 | babel-plugin-transform-es2015-object-super@^6.3.13: 408 | version "6.8.0" 409 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.8.0.tgz#1b858740a5a4400887c23dcff6f4d56eea4a24c5" 410 | dependencies: 411 | babel-helper-replace-supers "^6.8.0" 412 | babel-runtime "^6.0.0" 413 | 414 | babel-plugin-transform-es2015-parameters@^6.18.0: 415 | version "6.18.0" 416 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.18.0.tgz#9b2cfe238c549f1635ba27fc1daa858be70608b1" 417 | dependencies: 418 | babel-helper-call-delegate "^6.18.0" 419 | babel-helper-get-function-arity "^6.18.0" 420 | babel-runtime "^6.9.0" 421 | babel-template "^6.16.0" 422 | babel-traverse "^6.18.0" 423 | babel-types "^6.18.0" 424 | 425 | babel-plugin-transform-es2015-shorthand-properties@^6.18.0: 426 | version "6.18.0" 427 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.18.0.tgz#e2ede3b7df47bf980151926534d1dd0cbea58f43" 428 | dependencies: 429 | babel-runtime "^6.0.0" 430 | babel-types "^6.18.0" 431 | 432 | babel-plugin-transform-es2015-spread@^6.3.13: 433 | version "6.8.0" 434 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.8.0.tgz#0217f737e3b821fa5a669f187c6ed59205f05e9c" 435 | dependencies: 436 | babel-runtime "^6.0.0" 437 | 438 | babel-plugin-transform-es2015-sticky-regex@^6.3.13: 439 | version "6.8.0" 440 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.8.0.tgz#e73d300a440a35d5c64f5c2a344dc236e3df47be" 441 | dependencies: 442 | babel-helper-regex "^6.8.0" 443 | babel-runtime "^6.0.0" 444 | babel-types "^6.8.0" 445 | 446 | babel-plugin-transform-es2015-template-literals@^6.6.0: 447 | version "6.8.0" 448 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.8.0.tgz#86eb876d0a2c635da4ec048b4f7de9dfc897e66b" 449 | dependencies: 450 | babel-runtime "^6.0.0" 451 | 452 | babel-plugin-transform-es2015-typeof-symbol@^6.18.0: 453 | version "6.18.0" 454 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.18.0.tgz#0b14c48629c90ff47a0650077f6aa699bee35798" 455 | dependencies: 456 | babel-runtime "^6.0.0" 457 | 458 | babel-plugin-transform-es2015-unicode-regex@^6.3.13: 459 | version "6.11.0" 460 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.11.0.tgz#6298ceabaad88d50a3f4f392d8de997260f6ef2c" 461 | dependencies: 462 | babel-helper-regex "^6.8.0" 463 | babel-runtime "^6.0.0" 464 | regexpu-core "^2.0.0" 465 | 466 | babel-plugin-transform-exponentiation-operator@^6.3.13: 467 | version "6.8.0" 468 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.8.0.tgz#db25742e9339eade676ca9acec46f955599a68a4" 469 | dependencies: 470 | babel-helper-builder-binary-assignment-operator-visitor "^6.8.0" 471 | babel-plugin-syntax-exponentiation-operator "^6.8.0" 472 | babel-runtime "^6.0.0" 473 | 474 | babel-plugin-transform-export-extensions@^6.3.13: 475 | version "6.8.0" 476 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.8.0.tgz#fa80ff655b636549431bfd38f6b817bd82e47f5b" 477 | dependencies: 478 | babel-plugin-syntax-export-extensions "^6.8.0" 479 | babel-runtime "^6.0.0" 480 | 481 | babel-plugin-transform-function-bind@^6.3.13: 482 | version "6.8.0" 483 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.8.0.tgz#e7f334ce69f50d28fe850a822eaaab9fa4f4d821" 484 | dependencies: 485 | babel-plugin-syntax-function-bind "^6.8.0" 486 | babel-runtime "^6.0.0" 487 | 488 | babel-plugin-transform-object-rest-spread@^6.16.0: 489 | version "6.19.0" 490 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.19.0.tgz#f6ac428ee3cb4c6aa00943ed1422ce813603b34c" 491 | dependencies: 492 | babel-plugin-syntax-object-rest-spread "^6.8.0" 493 | babel-runtime "^6.0.0" 494 | 495 | babel-plugin-transform-regenerator@^6.16.0: 496 | version "6.16.1" 497 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.16.1.tgz#a75de6b048a14154aae14b0122756c5bed392f59" 498 | dependencies: 499 | babel-runtime "^6.9.0" 500 | babel-types "^6.16.0" 501 | private "~0.1.5" 502 | 503 | babel-plugin-transform-strict-mode@^6.18.0: 504 | version "6.18.0" 505 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.18.0.tgz#df7cf2991fe046f44163dcd110d5ca43bc652b9d" 506 | dependencies: 507 | babel-runtime "^6.0.0" 508 | babel-types "^6.18.0" 509 | 510 | babel-polyfill@^6.9.0: 511 | version "6.16.0" 512 | resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.16.0.tgz#2d45021df87e26a374b6d4d1a9c65964d17f2422" 513 | dependencies: 514 | babel-runtime "^6.9.1" 515 | core-js "^2.4.0" 516 | regenerator-runtime "^0.9.5" 517 | 518 | babel-preset-es2015@^6.6.0: 519 | version "6.18.0" 520 | resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.18.0.tgz#b8c70df84ec948c43dcf2bf770e988eb7da88312" 521 | dependencies: 522 | babel-plugin-check-es2015-constants "^6.3.13" 523 | babel-plugin-transform-es2015-arrow-functions "^6.3.13" 524 | babel-plugin-transform-es2015-block-scoped-functions "^6.3.13" 525 | babel-plugin-transform-es2015-block-scoping "^6.18.0" 526 | babel-plugin-transform-es2015-classes "^6.18.0" 527 | babel-plugin-transform-es2015-computed-properties "^6.3.13" 528 | babel-plugin-transform-es2015-destructuring "^6.18.0" 529 | babel-plugin-transform-es2015-duplicate-keys "^6.6.0" 530 | babel-plugin-transform-es2015-for-of "^6.18.0" 531 | babel-plugin-transform-es2015-function-name "^6.9.0" 532 | babel-plugin-transform-es2015-literals "^6.3.13" 533 | babel-plugin-transform-es2015-modules-amd "^6.18.0" 534 | babel-plugin-transform-es2015-modules-commonjs "^6.18.0" 535 | babel-plugin-transform-es2015-modules-systemjs "^6.18.0" 536 | babel-plugin-transform-es2015-modules-umd "^6.18.0" 537 | babel-plugin-transform-es2015-object-super "^6.3.13" 538 | babel-plugin-transform-es2015-parameters "^6.18.0" 539 | babel-plugin-transform-es2015-shorthand-properties "^6.18.0" 540 | babel-plugin-transform-es2015-spread "^6.3.13" 541 | babel-plugin-transform-es2015-sticky-regex "^6.3.13" 542 | babel-plugin-transform-es2015-template-literals "^6.6.0" 543 | babel-plugin-transform-es2015-typeof-symbol "^6.18.0" 544 | babel-plugin-transform-es2015-unicode-regex "^6.3.13" 545 | babel-plugin-transform-regenerator "^6.16.0" 546 | 547 | babel-preset-stage-0@^6.5.0: 548 | version "6.16.0" 549 | resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.16.0.tgz#f5a263c420532fd57491f1a7315b3036e428f823" 550 | dependencies: 551 | babel-plugin-transform-do-expressions "^6.3.13" 552 | babel-plugin-transform-function-bind "^6.3.13" 553 | babel-preset-stage-1 "^6.16.0" 554 | 555 | babel-preset-stage-1@^6.16.0: 556 | version "6.16.0" 557 | resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.16.0.tgz#9d31fbbdae7b17c549fd3ac93e3cf6902695e479" 558 | dependencies: 559 | babel-plugin-transform-class-constructor-call "^6.3.13" 560 | babel-plugin-transform-export-extensions "^6.3.13" 561 | babel-preset-stage-2 "^6.16.0" 562 | 563 | babel-preset-stage-2@^6.16.0: 564 | version "6.18.0" 565 | resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.18.0.tgz#9eb7bf9a8e91c68260d5ba7500493caaada4b5b5" 566 | dependencies: 567 | babel-plugin-syntax-dynamic-import "^6.18.0" 568 | babel-plugin-transform-class-properties "^6.18.0" 569 | babel-plugin-transform-decorators "^6.13.0" 570 | babel-preset-stage-3 "^6.17.0" 571 | 572 | babel-preset-stage-3@^6.17.0: 573 | version "6.17.0" 574 | resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.17.0.tgz#b6638e46db6e91e3f889013d8ce143917c685e39" 575 | dependencies: 576 | babel-plugin-syntax-trailing-function-commas "^6.3.13" 577 | babel-plugin-transform-async-generator-functions "^6.17.0" 578 | babel-plugin-transform-async-to-generator "^6.16.0" 579 | babel-plugin-transform-exponentiation-operator "^6.3.13" 580 | babel-plugin-transform-object-rest-spread "^6.16.0" 581 | 582 | babel-register@^6.18.0, babel-register@^6.9.0: 583 | version "6.18.0" 584 | resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.18.0.tgz#892e2e03865078dd90ad2c715111ec4449b32a68" 585 | dependencies: 586 | babel-core "^6.18.0" 587 | babel-runtime "^6.11.6" 588 | core-js "^2.4.0" 589 | home-or-tmp "^2.0.0" 590 | lodash "^4.2.0" 591 | mkdirp "^0.5.1" 592 | source-map-support "^0.4.2" 593 | 594 | babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.9.0, babel-runtime@^6.9.1: 595 | version "6.18.0" 596 | resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.18.0.tgz#0f4177ffd98492ef13b9f823e9994a02584c9078" 597 | dependencies: 598 | core-js "^2.4.0" 599 | regenerator-runtime "^0.9.5" 600 | 601 | babel-template@^6.14.0, babel-template@^6.15.0, babel-template@^6.16.0, babel-template@^6.8.0: 602 | version "6.16.0" 603 | resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.16.0.tgz#e149dd1a9f03a35f817ddbc4d0481988e7ebc8ca" 604 | dependencies: 605 | babel-runtime "^6.9.0" 606 | babel-traverse "^6.16.0" 607 | babel-types "^6.16.0" 608 | babylon "^6.11.0" 609 | lodash "^4.2.0" 610 | 611 | babel-traverse@^6.16.0, babel-traverse@^6.18.0: 612 | version "6.19.0" 613 | resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.19.0.tgz#68363fb821e26247d52a519a84b2ceab8df4f55a" 614 | dependencies: 615 | babel-code-frame "^6.16.0" 616 | babel-messages "^6.8.0" 617 | babel-runtime "^6.9.0" 618 | babel-types "^6.19.0" 619 | babylon "^6.11.0" 620 | debug "^2.2.0" 621 | globals "^9.0.0" 622 | invariant "^2.2.0" 623 | lodash "^4.2.0" 624 | 625 | babel-types@^6.13.0, babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.8.0, babel-types@^6.9.0: 626 | version "6.19.0" 627 | resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.19.0.tgz#8db2972dbed01f1192a8b602ba1e1e4c516240b9" 628 | dependencies: 629 | babel-runtime "^6.9.1" 630 | esutils "^2.0.2" 631 | lodash "^4.2.0" 632 | to-fast-properties "^1.0.1" 633 | 634 | babylon@^6.11.0: 635 | version "6.14.1" 636 | resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.14.1.tgz#956275fab72753ad9b3435d7afe58f8bf0a29815" 637 | 638 | balanced-match@^0.4.1: 639 | version "0.4.2" 640 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 641 | 642 | "bluebird@>= 3.0.1": 643 | version "3.4.6" 644 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" 645 | 646 | bluebird@~2.10.2: 647 | version "2.10.2" 648 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" 649 | 650 | body-parser@^1.15.1: 651 | version "1.15.2" 652 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.2.tgz#d7578cf4f1d11d5f6ea804cef35dc7a7ff6dae67" 653 | dependencies: 654 | bytes "2.4.0" 655 | content-type "~1.0.2" 656 | debug "~2.2.0" 657 | depd "~1.1.0" 658 | http-errors "~1.5.0" 659 | iconv-lite "0.4.13" 660 | on-finished "~2.3.0" 661 | qs "6.2.0" 662 | raw-body "~2.1.7" 663 | type-is "~1.6.13" 664 | 665 | brace-expansion@^1.0.0: 666 | version "1.1.6" 667 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" 668 | dependencies: 669 | balanced-match "^0.4.1" 670 | concat-map "0.0.1" 671 | 672 | bytes@2.4.0: 673 | version "2.4.0" 674 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" 675 | 676 | chalk@^1.1.0: 677 | version "1.1.3" 678 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 679 | dependencies: 680 | ansi-styles "^2.2.1" 681 | escape-string-regexp "^1.0.2" 682 | has-ansi "^2.0.0" 683 | strip-ansi "^3.0.0" 684 | supports-color "^2.0.0" 685 | 686 | concat-map@0.0.1: 687 | version "0.0.1" 688 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 689 | 690 | content-disposition@0.5.1: 691 | version "0.5.1" 692 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" 693 | 694 | content-type@^1.0.0, content-type@~1.0.2: 695 | version "1.0.2" 696 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 697 | 698 | convert-source-map@^1.1.0: 699 | version "1.3.0" 700 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" 701 | 702 | cookie-signature@1.0.6: 703 | version "1.0.6" 704 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 705 | 706 | cookie@0.3.1: 707 | version "0.3.1" 708 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 709 | 710 | core-js@^2.4.0: 711 | version "2.4.1" 712 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" 713 | 714 | dataloader@^1.2.0: 715 | version "1.2.0" 716 | resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.2.0.tgz#3f73ea657c492c860c1633348adc55ca9bf2107e" 717 | 718 | debug@^2.1.1, debug@^2.2.0, debug@~2.2.0: 719 | version "2.2.0" 720 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" 721 | dependencies: 722 | ms "0.7.1" 723 | 724 | depd@~1.1.0: 725 | version "1.1.0" 726 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 727 | 728 | destroy@~1.0.4: 729 | version "1.0.4" 730 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 731 | 732 | detect-indent@^4.0.0: 733 | version "4.0.0" 734 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" 735 | dependencies: 736 | repeating "^2.0.0" 737 | 738 | ee-first@1.1.1: 739 | version "1.1.1" 740 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 741 | 742 | encodeurl@~1.0.1: 743 | version "1.0.1" 744 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 745 | 746 | escape-html@~1.0.3: 747 | version "1.0.3" 748 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 749 | 750 | escape-string-regexp@^1.0.2: 751 | version "1.0.5" 752 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 753 | 754 | esutils@^2.0.2: 755 | version "2.0.2" 756 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 757 | 758 | etag@~1.7.0: 759 | version "1.7.0" 760 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" 761 | 762 | express-graphql@^0.5.1: 763 | version "0.5.4" 764 | resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.5.4.tgz#413477e3efda6d7437e788f7ef6c994914e79f88" 765 | dependencies: 766 | accepts "^1.3.0" 767 | content-type "^1.0.0" 768 | http-errors "^1.3.0" 769 | raw-body "^2.1.0" 770 | 771 | express@^4.13.4: 772 | version "4.14.0" 773 | resolved "https://registry.yarnpkg.com/express/-/express-4.14.0.tgz#c1ee3f42cdc891fb3dc650a8922d51ec847d0d66" 774 | dependencies: 775 | accepts "~1.3.3" 776 | array-flatten "1.1.1" 777 | content-disposition "0.5.1" 778 | content-type "~1.0.2" 779 | cookie "0.3.1" 780 | cookie-signature "1.0.6" 781 | debug "~2.2.0" 782 | depd "~1.1.0" 783 | encodeurl "~1.0.1" 784 | escape-html "~1.0.3" 785 | etag "~1.7.0" 786 | finalhandler "0.5.0" 787 | fresh "0.3.0" 788 | merge-descriptors "1.0.1" 789 | methods "~1.1.2" 790 | on-finished "~2.3.0" 791 | parseurl "~1.3.1" 792 | path-to-regexp "0.1.7" 793 | proxy-addr "~1.1.2" 794 | qs "6.2.0" 795 | range-parser "~1.2.0" 796 | send "0.14.1" 797 | serve-static "~1.11.1" 798 | type-is "~1.6.13" 799 | utils-merge "1.0.0" 800 | vary "~1.1.0" 801 | 802 | finalhandler@0.5.0: 803 | version "0.5.0" 804 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" 805 | dependencies: 806 | debug "~2.2.0" 807 | escape-html "~1.0.3" 808 | on-finished "~2.3.0" 809 | statuses "~1.3.0" 810 | unpipe "~1.0.0" 811 | 812 | forwarded@~0.1.0: 813 | version "0.1.0" 814 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 815 | 816 | fresh@0.3.0: 817 | version "0.3.0" 818 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" 819 | 820 | globals@^9.0.0: 821 | version "9.14.0" 822 | resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" 823 | 824 | graphql-relay@^0.4.1: 825 | version "0.4.4" 826 | resolved "https://registry.yarnpkg.com/graphql-relay/-/graphql-relay-0.4.4.tgz#876a654445b6af4539f81cb9befd5cd7ead129dd" 827 | 828 | graphql-thinky@^0.4.0-rc-2: 829 | version "0.4.0-rc-2" 830 | resolved "https://registry.yarnpkg.com/graphql-thinky/-/graphql-thinky-0.4.0-rc-2.tgz#31caad82920ca1a00a579883709b836861cc2f31" 831 | dependencies: 832 | dataloader "^1.2.0" 833 | graphql-relay "^0.4.1" 834 | lodash "^4.0.0" 835 | thinky-export-schema "^2.0.0" 836 | 837 | graphql@^0.8.2: 838 | version "0.8.2" 839 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.8.2.tgz#eb1bb524b38104bbf2c9157f9abc67db2feba7d2" 840 | dependencies: 841 | iterall "1.0.2" 842 | 843 | has-ansi@^2.0.0: 844 | version "2.0.0" 845 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 846 | dependencies: 847 | ansi-regex "^2.0.0" 848 | 849 | home-or-tmp@^2.0.0: 850 | version "2.0.0" 851 | resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" 852 | dependencies: 853 | os-homedir "^1.0.0" 854 | os-tmpdir "^1.0.1" 855 | 856 | http-errors@^1.3.0, http-errors@~1.5.0: 857 | version "1.5.1" 858 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" 859 | dependencies: 860 | inherits "2.0.3" 861 | setprototypeof "1.0.2" 862 | statuses ">= 1.3.1 < 2" 863 | 864 | iconv-lite@0.4.13: 865 | version "0.4.13" 866 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" 867 | 868 | inherits@2.0.3: 869 | version "2.0.3" 870 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 871 | 872 | invariant@^2.2.0: 873 | version "2.2.2" 874 | resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" 875 | dependencies: 876 | loose-envify "^1.0.0" 877 | 878 | ipaddr.js@1.1.1: 879 | version "1.1.1" 880 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.1.1.tgz#c791d95f52b29c1247d5df80ada39b8a73647230" 881 | 882 | is-finite@^1.0.0: 883 | version "1.0.2" 884 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" 885 | dependencies: 886 | number-is-nan "^1.0.0" 887 | 888 | iterall@1.0.2: 889 | version "1.0.2" 890 | resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.2.tgz#41a2e96ce9eda5e61c767ee5dc312373bb046e91" 891 | 892 | js-tokens@^2.0.0: 893 | version "2.0.0" 894 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" 895 | 896 | jsesc@^1.3.0: 897 | version "1.3.0" 898 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" 899 | 900 | jsesc@~0.5.0: 901 | version "0.5.0" 902 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" 903 | 904 | json5@^0.5.0: 905 | version "0.5.1" 906 | resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" 907 | 908 | lodash.mapvalues@^4.0.1: 909 | version "4.6.0" 910 | resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" 911 | 912 | lodash@^4.0.0, lodash@^4.2.0: 913 | version "4.17.2" 914 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42" 915 | 916 | loose-envify@^1.0.0: 917 | version "1.3.0" 918 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.0.tgz#6b26248c42f6d4fa4b0d8542f78edfcde35642a8" 919 | dependencies: 920 | js-tokens "^2.0.0" 921 | 922 | media-typer@0.3.0: 923 | version "0.3.0" 924 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 925 | 926 | merge-descriptors@1.0.1: 927 | version "1.0.1" 928 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 929 | 930 | methods@~1.1.2: 931 | version "1.1.2" 932 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 933 | 934 | mime-db@~1.25.0: 935 | version "1.25.0" 936 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" 937 | 938 | mime-types@~2.1.11, mime-types@~2.1.13: 939 | version "2.1.13" 940 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" 941 | dependencies: 942 | mime-db "~1.25.0" 943 | 944 | mime@1.3.4: 945 | version "1.3.4" 946 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 947 | 948 | minimatch@^3.0.2: 949 | version "3.0.3" 950 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" 951 | dependencies: 952 | brace-expansion "^1.0.0" 953 | 954 | minimist@0.0.8: 955 | version "0.0.8" 956 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 957 | 958 | mkdirp@^0.5.1: 959 | version "0.5.1" 960 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 961 | dependencies: 962 | minimist "0.0.8" 963 | 964 | ms@0.7.1: 965 | version "0.7.1" 966 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" 967 | 968 | negotiator@0.6.1: 969 | version "0.6.1" 970 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 971 | 972 | number-is-nan@^1.0.0: 973 | version "1.0.1" 974 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 975 | 976 | on-finished@~2.3.0: 977 | version "2.3.0" 978 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 979 | dependencies: 980 | ee-first "1.1.1" 981 | 982 | os-homedir@^1.0.0: 983 | version "1.0.2" 984 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 985 | 986 | os-tmpdir@^1.0.1: 987 | version "1.0.2" 988 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 989 | 990 | parseurl@~1.3.1: 991 | version "1.3.1" 992 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 993 | 994 | path-is-absolute@^1.0.0: 995 | version "1.0.1" 996 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 997 | 998 | path-to-regexp@0.1.7: 999 | version "0.1.7" 1000 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 1001 | 1002 | private@^0.1.6, private@~0.1.5: 1003 | version "0.1.6" 1004 | resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" 1005 | 1006 | proxy-addr@~1.1.2: 1007 | version "1.1.2" 1008 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37" 1009 | dependencies: 1010 | forwarded "~0.1.0" 1011 | ipaddr.js "1.1.1" 1012 | 1013 | qs@6.2.0: 1014 | version "6.2.0" 1015 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" 1016 | 1017 | range-parser@~1.2.0: 1018 | version "1.2.0" 1019 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 1020 | 1021 | raw-body@^2.1.0, raw-body@~2.1.7: 1022 | version "2.1.7" 1023 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" 1024 | dependencies: 1025 | bytes "2.4.0" 1026 | iconv-lite "0.4.13" 1027 | unpipe "1.0.0" 1028 | 1029 | regenerate@^1.2.1: 1030 | version "1.3.2" 1031 | resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" 1032 | 1033 | regenerator-runtime@^0.9.5: 1034 | version "0.9.6" 1035 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" 1036 | 1037 | regexpu-core@^2.0.0: 1038 | version "2.0.0" 1039 | resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" 1040 | dependencies: 1041 | regenerate "^1.2.1" 1042 | regjsgen "^0.2.0" 1043 | regjsparser "^0.1.4" 1044 | 1045 | regjsgen@^0.2.0: 1046 | version "0.2.0" 1047 | resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" 1048 | 1049 | regjsparser@^0.1.4: 1050 | version "0.1.5" 1051 | resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" 1052 | dependencies: 1053 | jsesc "~0.5.0" 1054 | 1055 | repeating@^2.0.0: 1056 | version "2.0.1" 1057 | resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 1058 | dependencies: 1059 | is-finite "^1.0.0" 1060 | 1061 | rethinkdbdash@~2.3.0: 1062 | version "2.3.27" 1063 | resolved "https://registry.yarnpkg.com/rethinkdbdash/-/rethinkdbdash-2.3.27.tgz#66de1c6cf13ed89db0c81ee0f060656caddfe1a5" 1064 | dependencies: 1065 | bluebird ">= 3.0.1" 1066 | 1067 | send@0.14.1: 1068 | version "0.14.1" 1069 | resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a" 1070 | dependencies: 1071 | debug "~2.2.0" 1072 | depd "~1.1.0" 1073 | destroy "~1.0.4" 1074 | encodeurl "~1.0.1" 1075 | escape-html "~1.0.3" 1076 | etag "~1.7.0" 1077 | fresh "0.3.0" 1078 | http-errors "~1.5.0" 1079 | mime "1.3.4" 1080 | ms "0.7.1" 1081 | on-finished "~2.3.0" 1082 | range-parser "~1.2.0" 1083 | statuses "~1.3.0" 1084 | 1085 | serve-static@~1.11.1: 1086 | version "1.11.1" 1087 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.1.tgz#d6cce7693505f733c759de57befc1af76c0f0805" 1088 | dependencies: 1089 | encodeurl "~1.0.1" 1090 | escape-html "~1.0.3" 1091 | parseurl "~1.3.1" 1092 | send "0.14.1" 1093 | 1094 | setprototypeof@1.0.2: 1095 | version "1.0.2" 1096 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" 1097 | 1098 | slash@^1.0.0: 1099 | version "1.0.0" 1100 | resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 1101 | 1102 | source-map-support@^0.4.2: 1103 | version "0.4.6" 1104 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.6.tgz#32552aa64b458392a85eab3b0b5ee61527167aeb" 1105 | dependencies: 1106 | source-map "^0.5.3" 1107 | 1108 | source-map@^0.5.0, source-map@^0.5.3: 1109 | version "0.5.6" 1110 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 1111 | 1112 | "statuses@>= 1.3.1 < 2", statuses@~1.3.0: 1113 | version "1.3.1" 1114 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 1115 | 1116 | strip-ansi@^3.0.0: 1117 | version "3.0.1" 1118 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 1119 | dependencies: 1120 | ansi-regex "^2.0.0" 1121 | 1122 | supports-color@^2.0.0: 1123 | version "2.0.0" 1124 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 1125 | 1126 | thinky-export-schema@^2.0.0: 1127 | version "2.0.0" 1128 | resolved "https://registry.yarnpkg.com/thinky-export-schema/-/thinky-export-schema-2.0.0.tgz#f33e14cfde418bc321613e51c0a501f8fdf666f5" 1129 | dependencies: 1130 | lodash.mapvalues "^4.0.1" 1131 | 1132 | thinky@^2.3.2: 1133 | version "2.3.8" 1134 | resolved "https://registry.yarnpkg.com/thinky/-/thinky-2.3.8.tgz#4d3d01fe0aaaa8cd97276965f40dbb4f017300f3" 1135 | dependencies: 1136 | bluebird "~2.10.2" 1137 | rethinkdbdash "~2.3.0" 1138 | validator "~3.22.1" 1139 | 1140 | to-fast-properties@^1.0.1: 1141 | version "1.0.2" 1142 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" 1143 | 1144 | type-is@~1.6.13: 1145 | version "1.6.14" 1146 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" 1147 | dependencies: 1148 | media-typer "0.3.0" 1149 | mime-types "~2.1.13" 1150 | 1151 | unpipe@1.0.0, unpipe@~1.0.0: 1152 | version "1.0.0" 1153 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1154 | 1155 | utils-merge@1.0.0: 1156 | version "1.0.0" 1157 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 1158 | 1159 | validator@~3.22.1: 1160 | version "3.22.2" 1161 | resolved "https://registry.yarnpkg.com/validator/-/validator-3.22.2.tgz#6f297ae67f7f82acc76d0afdb49f18d9a09c18c0" 1162 | 1163 | vary@~1.1.0: 1164 | version "1.1.0" 1165 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" 1166 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-thinky", 3 | "version": "0.4.0-rc-3", 4 | "description": "GraphQL & Relay powered by thinky / RethinkDB", 5 | "repository": "https://github.com/fenos/graphql-thinky", 6 | "main": "lib/index.js", 7 | "scripts": { 8 | "check": "node_modules/.bin/xo src/*.js src/**/*.js", 9 | "test": "npm run check && node_modules/.bin/ava --verbose && rm -rf rethinkdb_data", 10 | "build": "rm -rf lib && BABEL_ENV=production babel src -d lib" 11 | }, 12 | "dependencies": { 13 | "dataloader": "^1.2.0", 14 | "graphql-relay": "^0.4.1", 15 | "lodash": "^4.0.0", 16 | "thinky-export-schema": "^2.0.0" 17 | }, 18 | "devDependencies": { 19 | "ava": "^0.17.0", 20 | "babel": "^6.5.2", 21 | "babel-cli": "^6.9.0", 22 | "babel-eslint": "^6.0.3", 23 | "babel-polyfill": "^6.8.0", 24 | "babel-preset-es2015": "^6.6.0", 25 | "babel-preset-stage-0": "^6.5.0", 26 | "babel-register": "^6.9.0", 27 | "babel-runtime": "^6.0.8", 28 | "chai": "^3.0.0", 29 | "chai-as-promised": "^5.1.0", 30 | "eslint": "^1.7.3", 31 | "graphql": "^0.8.0", 32 | "nyc": "^6.4.4", 33 | "sinon": "^1.15.4", 34 | "sinon-as-promised": "^4.0.0", 35 | "sinon-chai": "^2.8.0", 36 | "thinky": "^2.3.2", 37 | "xo": "^0.15.1" 38 | }, 39 | "peerDependencies": { 40 | "graphql": "^0.8.0", 41 | "thinky": "^2.3.2" 42 | }, 43 | "ava": { 44 | "files": [ 45 | "test/integration/*.test.js", 46 | "test/unit/*.js" 47 | ], 48 | "failFast": true, 49 | "require": [ 50 | "babel-register" 51 | ] 52 | }, 53 | "babel": { 54 | "presets": [ 55 | "es2015", 56 | "stage-0" 57 | ], 58 | "plugins": [ 59 | "transform-runtime" 60 | ], 61 | "ignore": "test/", 62 | "env": { 63 | "development": { 64 | "sourceMaps": "inline" 65 | } 66 | } 67 | }, 68 | "author": "Fabrizio Fenoglio", 69 | "license": "MIT", 70 | "xo": { 71 | "esnext": true, 72 | "ignore": [ 73 | "example/**" 74 | ], 75 | "rules": { 76 | "xo/filename-case": [ 77 | "error", 78 | { 79 | "case": "camelCase" 80 | } 81 | ], 82 | "one-var": 0, 83 | "indent": [ 84 | 2, 85 | 2, 86 | { 87 | "SwitchCase": 1 88 | } 89 | ], 90 | "dot-notation": 0 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/base64.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base 64 3 | * 4 | * @param i 5 | */ 6 | export function base64(i) { 7 | return (new Buffer(i, 'ascii')).toString('base64'); 8 | } 9 | 10 | /** 11 | * Unbase 64 12 | * 13 | * @param i 14 | */ 15 | export function unbase64(i) { 16 | return (new Buffer(i, 'base64')).toString('ascii'); 17 | } 18 | -------------------------------------------------------------------------------- /src/commonArgs.js: -------------------------------------------------------------------------------- 1 | import {GraphQLInt} from 'graphql'; 2 | export default { 3 | offset: { 4 | type: GraphQLInt 5 | }, 6 | skip: { 7 | type: GraphQLInt 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/dataloader/loaderFilter.js: -------------------------------------------------------------------------------- 1 | import {chain, isObject} from 'lodash'; 2 | import { 3 | connectionFromArray 4 | } from 'graphql-relay'; 5 | 6 | class LoaderFilter { 7 | 8 | constructor(results, filtering = {}) { 9 | this.results = results; 10 | this.filtering = { 11 | limit: 40, 12 | orderBy: {}, 13 | filter: {}, 14 | ...filtering || {} 15 | }; 16 | } 17 | 18 | toObject() { 19 | if (isObject(this.results)) { 20 | return this.results; 21 | } 22 | if (Array.isArray(this.results)) { 23 | const result = this.toArray(); 24 | return result[0] || {}; 25 | } 26 | 27 | throw new Error('Couldn\'t transform result to Object'); 28 | } 29 | 30 | /** 31 | * Resolve results with filtering 32 | * applayed 33 | * @returns {Array|*} 34 | */ 35 | toArray() { 36 | let results; 37 | if (!Array.isArray(this.results)) { // eslint-disable-line no-negated-condition 38 | results = [this.results]; 39 | } else { 40 | results = this.results; 41 | } 42 | const {index, offset, orderBy, filter} = this.filtering; 43 | let resultFiltered = chain(results); 44 | 45 | if (orderBy && Object.keys(orderBy).length > 0) { 46 | resultFiltered = resultFiltered.orderBy(orderBy); 47 | } 48 | 49 | if (filter && Object.keys(filter).length > 0) { 50 | resultFiltered = resultFiltered.filter(filter); 51 | } 52 | 53 | resultFiltered = resultFiltered.take(offset - index); 54 | 55 | return resultFiltered.value(); 56 | } 57 | 58 | /** 59 | * Returned a connection from 60 | * array 61 | */ 62 | toConnectionArray(args) { 63 | const results = this.toArray(); 64 | return connectionFromArray(results, args); 65 | } 66 | 67 | /** 68 | * Limit 69 | * @param limit 70 | * @returns {LoaderFilter} 71 | */ 72 | limit(limit) { 73 | this.filtering.limit = limit || this.filtering.limit; 74 | return this; 75 | } 76 | 77 | /** 78 | * Set order by 79 | * @param field 80 | * @param direction 81 | */ 82 | orderBy(field, direction) { 83 | this.filtering.orderBy[field] = direction; 84 | return this; 85 | } 86 | 87 | /** 88 | * 89 | * @param filterDef 90 | */ 91 | filter(filterDef) { 92 | this.filtering.filter = { 93 | ...this.filtering.filter, 94 | ...filterDef 95 | }; 96 | return this; 97 | } 98 | 99 | /** 100 | * Filter if helper 101 | * @param condition 102 | * @param filter 103 | * @returns {LoaderFilter} 104 | */ 105 | filterIf(condition, filter) { 106 | return this.when(condition, () => this.filter(filter)); 107 | } 108 | 109 | /** 110 | * Sometime we use optional 111 | * arguments into our GraphQL queries 112 | * to keep the chain of filters 113 | * we use this function to evaluate 114 | * any filtering if the value is not 115 | * undefined 116 | * @param value 117 | * @param fnEvaluation 118 | * @returns {LoaderFilter} 119 | */ 120 | when(value, fnEvaluation) { 121 | if (value !== undefined) { 122 | fnEvaluation(this); 123 | return this; 124 | } 125 | return this; 126 | } 127 | 128 | /** 129 | * From node args 130 | * 131 | * @param args 132 | * @returns {LoaderFilter} 133 | */ 134 | fromNodeArgs(args) { 135 | this.filter(args.filter); 136 | 137 | this.when(args.orderBy, () => { 138 | this.orderBy.apply(this, args.orderBy); 139 | }); 140 | 141 | this.limit(args.offset); 142 | 143 | return this; 144 | } 145 | } 146 | 147 | export default LoaderFilter; 148 | -------------------------------------------------------------------------------- /src/dataloader/modelLoader.js: -------------------------------------------------------------------------------- 1 | import Dataloader from 'dataloader'; 2 | import {find, uniq, maxBy, flatten} from 'lodash'; 3 | import {buildQuery, buildCount, mapCountToResultSet} from '../queryBuilder'; 4 | import LoaderFilter from './loaderFilter'; 5 | 6 | /** 7 | * Model loader use Dataloader 8 | * to batch and cache DB calls 9 | * using PK or indexed fields 10 | */ 11 | class ModelLoader { 12 | 13 | constructor(model) { 14 | this.model = model; 15 | this.modelName = model.getTableName(); 16 | this._loaders = {}; 17 | } 18 | 19 | /** 20 | * Load by Id 21 | * @param modelId 22 | * @returns {IgnoredPaths|Object|*|void} 23 | */ 24 | loadById(modelId) { 25 | return this.getOrCreateLoader('loadBy', 'id') 26 | .load(modelId).then(results => { 27 | return new LoaderFilter(results); 28 | }); 29 | } 30 | 31 | /** 32 | * Load by any field Name 33 | * @param fieldName 34 | * @param value 35 | * @returns {IgnoredPaths|Object|*|void} 36 | */ 37 | loadBy(fieldName, value) { 38 | return this.getOrCreateLoader('loadBy', fieldName) 39 | .load(value).then(results => { 40 | return new LoaderFilter(results); 41 | }); 42 | } 43 | 44 | /** 45 | * Related loader 46 | * @param relationName 47 | * @param FKID 48 | * @returns {IgnoredPaths|Object|*|void} 49 | */ 50 | async related(relationName, FKID, options) { 51 | const params = this._queryLoaderIdFromParams(options); 52 | 53 | const filterOpt = await this.getOrCreateLoader( 54 | 'query:id', 55 | relationName, 56 | this._queryRelationLoader, 57 | ).load(params); 58 | 59 | return await this.getOrCreateLoader( 60 | `relation:${JSON.stringify(filterOpt)}`, 61 | relationName, 62 | this._queryRelations(relationName, 'id', filterOpt), 63 | ).load(FKID).then(results => { 64 | return new LoaderFilter(results, options); 65 | }); 66 | } 67 | 68 | /** 69 | * Related by field name 70 | * @param relationName 71 | * @param fieldName 72 | * @param FKID 73 | * @returns {IgnoredPaths|Object|*|void} 74 | */ 75 | async relatedByField(relationName, fieldName, FKID, options) { 76 | const params = this._queryLoaderIdFromParams(options); 77 | 78 | const filterOpt = await this.getOrCreateLoader( 79 | `query:${fieldName}`, 80 | relationName, 81 | this._queryRelationLoader, 82 | ).load(params); 83 | 84 | return this.getOrCreateLoader( 85 | `relation:${JSON.stringify(filterOpt)}`, 86 | relationName, 87 | this._queryRelations(relationName, fieldName) 88 | ).load(FKID).then(results => { 89 | return new LoaderFilter(results, options); 90 | }); 91 | } 92 | 93 | /** 94 | * Get or create a Dataloader for this Model 95 | * @param loaderPrefix 96 | * @param fieldName 97 | * @param loaderFn 98 | * @returns {*} 99 | */ 100 | getOrCreateLoader(loaderPrefix, fieldName, loaderFn, loaderOpt = {}) { 101 | const loaderName = `${loaderPrefix}:${fieldName}`; 102 | const checkLoader = this.getLoader(loaderName); 103 | if (checkLoader) { 104 | return checkLoader; 105 | } 106 | const newloaderName = `${this.modelName}:${loaderName}`; 107 | const dataloaderFn = loaderFn || this._queryByField(fieldName); 108 | const Loader = new Dataloader(dataloaderFn, loaderOpt); 109 | this._loaders[newloaderName] = Loader; 110 | return Loader; 111 | } 112 | 113 | /** 114 | * Determine the key of 115 | * the query Loader. 116 | * 117 | * @param filterQuery 118 | * @param filter 119 | * @param attributes 120 | * @param rest 121 | * @returns {*} 122 | * @private 123 | */ 124 | _queryLoaderIdFromParams({filterQuery, filter, attributes, requestedFields, ...rest}) { 125 | if (filterQuery) { 126 | // If the filterQuery flag is enabled 127 | // then i let the filters included 128 | // for the creation of the key loader 129 | rest.filter = filter; 130 | 131 | if (requestedFields) { 132 | rest.attributes = attributes; 133 | } 134 | } else { 135 | // Has been asked to don't execute any filter. 136 | // As such i'm going to put the results into 137 | // the dataloader that doesn't have filters. 138 | // generating the key with empty filters 139 | rest.filter = {}; 140 | rest.attributes = []; 141 | } 142 | return Object.keys(rest).sort() 143 | .reduce((obj, objKey) => { 144 | obj[objKey] = rest[objKey]; 145 | return obj; 146 | }, {}); 147 | } 148 | 149 | /** 150 | * Query relation Loader 151 | * @param queryParams 152 | * @returns {*} 153 | * @private 154 | */ 155 | async _queryRelationLoader(queryParams) { 156 | return queryParams.map(params => { 157 | const same = queryParams.filter(qp => { 158 | return ( 159 | JSON.stringify(params.filter) === JSON.stringify(qp.filter) && 160 | params.index === qp.index 161 | ); 162 | }); 163 | 164 | if (same.length > 0) { 165 | const higherOffset = maxBy(same, 'offset'); 166 | const allAttributes = uniq(flatten(same.map(opts => opts.attributes))); 167 | higherOffset.attributes = allAttributes; 168 | return higherOffset; 169 | } 170 | return params; 171 | }); 172 | } 173 | 174 | /** 175 | * Query model by any field 176 | * @param fieldName 177 | * @returns {function(*=)} 178 | * @private 179 | */ 180 | _queryByField(fieldName) { 181 | return fieldNames => { 182 | const r = this.model._thinky.r; 183 | return this.model.getAll(r.args(uniq(fieldNames)), { 184 | index: fieldName 185 | }).run().then(results => { 186 | return this._mapResults(fieldNames, results, fieldName); 187 | }); 188 | }; 189 | } 190 | 191 | /** 192 | * Query relations 193 | * @param relationName 194 | * @returns {function(*)} 195 | * @private 196 | */ 197 | _queryRelations(relationName, fieldName, nodeAttributes) { 198 | return async FkIds => { 199 | const thinky = this.model._thinky; 200 | const r = thinky.r; 201 | const query = this.model.getAll(r.args(uniq(FkIds)), {index: fieldName}) 202 | .withFields(fieldName).getJoin({ 203 | [relationName]: { 204 | _apply(seq) { 205 | return buildQuery(seq, nodeAttributes, thinky); 206 | } 207 | } 208 | }); 209 | const results = await query.run(); 210 | const resultSet = this._mapResults(FkIds, results, fieldName, relationName); 211 | 212 | // If we need to count result 213 | // I'll do another query 214 | if (nodeAttributes.count) { 215 | const relation = this.model._joins[relationName]; 216 | const FK = relation.rightKey; 217 | const countResults = await this._countRelated( 218 | flatten(resultSet.map(row => row.map(innerRow => innerRow[FK]))), 219 | relationName, 220 | nodeAttributes, 221 | ); 222 | 223 | return mapCountToResultSet(resultSet, countResults, FK); 224 | } 225 | 226 | return resultSet; 227 | }; 228 | } 229 | 230 | /** 231 | * Count related result set 232 | * @param relatedIds 233 | * @param relationName 234 | * @param opts 235 | * @returns {*|void|Promise} 236 | * @private 237 | */ 238 | _countRelated(relatedIds, relationName, opts) { 239 | const relation = this.model._joins[relationName]; 240 | const FK = relation.rightKey; 241 | 242 | return buildCount(relation.model, relatedIds, FK, opts); 243 | } 244 | 245 | /** 246 | * Map Dataloader results 247 | * @param ids 248 | * @param results 249 | * @param fieldName 250 | * @param relationName 251 | * @returns {*} 252 | * @private 253 | */ 254 | _mapResults(ids, results, fieldName, relationName) { 255 | return ids.map(fkId => { 256 | const resultMatch = find(results, {[fieldName]: fkId}); 257 | if (resultMatch) { 258 | if (relationName) { 259 | return resultMatch[relationName]; 260 | } 261 | return resultMatch; 262 | } 263 | return {}; 264 | }); 265 | } 266 | 267 | /** 268 | * Get a loader 269 | * @param loaderName 270 | * @returns {*} 271 | * @private 272 | */ 273 | getLoader(loaderName) { 274 | return this._loaders[`${this.modelName}:${loaderName}`]; 275 | } 276 | 277 | /** 278 | * Get loaders 279 | * @returns {{}|*} 280 | * @private 281 | */ 282 | getLoaders() { 283 | return this._loaders; 284 | } 285 | } 286 | 287 | export default ModelLoader; 288 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {connectionDefinitions, connectionArgs} from 'graphql-relay'; 2 | import resolver from './resolver'; 3 | import typeMapper, {toGraphQLDefinition} from './typeMapper'; 4 | import ModelLoader from './dataloader/modelLoader'; 5 | import LoaderFiler from './dataloader/loaderFilter'; 6 | import {nodeInterfaceMapper} from './relay/nodeDefinition'; 7 | import modelToGQLObjectType from './modelToGqlObjectType'; 8 | import Node from './node'; 9 | import commonArgs from './commonArgs'; 10 | 11 | const defaultOptions = { 12 | maxLimit: 50 13 | }; 14 | 15 | /** 16 | * GraphqlThinky 17 | */ 18 | class GraphqlThinky { 19 | 20 | static typeMapper = typeMapper; 21 | 22 | commonArgs = commonArgs; 23 | 24 | /** 25 | * Graphus constructor accept a Thinky 26 | * Instance 27 | * 28 | * @param thinky 29 | * @param options 30 | */ 31 | constructor(thinky, options = {}) { 32 | this.thinky = thinky; 33 | this.options = Object.assign(defaultOptions, options); 34 | this.loadersKey = options.loadersKey || 'loaders'; 35 | 36 | const nodeMapper = nodeInterfaceMapper(thinky.models); 37 | 38 | this.nodeField = nodeMapper.nodeField; 39 | this.nodeInterface = nodeMapper.nodeInterface; 40 | this.nodeTypeMapper = nodeMapper.nodeTypeMapper; 41 | } 42 | 43 | /** 44 | * Return Model loader 45 | * object of all models 46 | * @returns {*} 47 | */ 48 | getModelLoaders() { 49 | const models = this.thinky.models; 50 | return Object.keys(models).reduce((loadersObj, modelName) => { 51 | const modelLoader = new ModelLoader(models[modelName]); 52 | loadersObj[modelName] = modelLoader; 53 | return loadersObj; 54 | }, {}); 55 | } 56 | 57 | /** 58 | * Resolve a node 59 | * 60 | * @param modelName 61 | * @param related 62 | * @param opts 63 | * @returns {Resolver} 64 | */ 65 | resolve = (modelName, related, opts = {}) => { 66 | const Node = this.node(modelName, related); 67 | return resolver(Node, {...this.options, ...opts}); 68 | }; 69 | 70 | /** 71 | * Create a relay connection 72 | * 73 | * @param modelName 74 | * @param related 75 | * @param opts 76 | * @returns {Resolver} 77 | */ 78 | connect = (modelName, related, {connection, args, ...opts} = {}) => { 79 | /*eslint-disable */ 80 | if (!connection) throw Error('Please provide a connection option.'); 81 | if (!connection.name) throw Error(`Please provide a name for the connection based on Model: ${modelName}.`); 82 | if (!connection.type) throw Error(`Please provide a type for the connection based on Model: ${modelName}.`); 83 | /*eslint-enable */ 84 | 85 | const NodeConnector = this.node(modelName, related, { 86 | ...opts, 87 | connection 88 | }).connect({...this.options, ...opts}); 89 | 90 | return { 91 | type: NodeConnector.connectionType, 92 | args: { 93 | ...NodeConnector.connectionArgs, 94 | ...args || {} 95 | }, 96 | resolve: NodeConnector.resolve 97 | }; 98 | }; 99 | 100 | /** 101 | * Returns a connection definition 102 | * based on a graphQL type. 103 | * This is mostly a helper function 104 | * when constructing connections based 105 | * on dataloader. 106 | * @param gqlType 107 | * @param args 108 | * @returns {{type, args: {}}} 109 | */ 110 | connectTypeDefinition = (gqlType, args) => { 111 | const {connectionType} = connectionDefinitions({nodeType: gqlType}); 112 | return { 113 | type: connectionType, 114 | args: { 115 | ...connectionArgs, 116 | ...args 117 | } 118 | }; 119 | } 120 | 121 | shape = result => { 122 | return new LoaderFiler(result); 123 | } 124 | 125 | /** 126 | * return a graphQL Object Type 127 | * definition from a Thinky Model. 128 | * 129 | * @param model 130 | * @param opts 131 | */ 132 | modelToGraphQLDefinition = (model, opts = {}) => { 133 | if (typeof model === 'string') { 134 | model = this.thinky.models[model]; 135 | } 136 | 137 | if (!model) { 138 | throw new Error(`Model name: ${model} not found.`); 139 | } 140 | 141 | return toGraphQLDefinition(model, opts); 142 | }; 143 | 144 | /** 145 | * Create a GraphQLObjectType based 146 | * to a thinky model. Using this helper 147 | * you'll get free Node type mapping 148 | * for relay 149 | * 150 | * @param model 151 | * @param opts 152 | * @returns {GraphQLObjectType} 153 | */ 154 | createModelType = (model, opts = {}) => { 155 | if (typeof model === 'string') { 156 | model = this.thinky.models[model]; 157 | } 158 | 159 | if (!model) { 160 | throw new Error(`Model name: ${model} not found.`); 161 | } 162 | 163 | // Add node interface if relay is specified 164 | if (opts.globalId === true) { 165 | opts.interfaces = [this.nodeInterface]; 166 | } 167 | 168 | const gqlObjectType = modelToGQLObjectType(model, opts); 169 | 170 | this.nodeTypeMapper.mapTypes({ 171 | [gqlObjectType.name]: gqlObjectType 172 | }); 173 | 174 | return gqlObjectType; 175 | }; 176 | 177 | /** 178 | * Create a node 179 | * 180 | * @param modelName 181 | * @param related 182 | * @param opts 183 | * @returns {Node} 184 | */ 185 | node = (modelName, related, opts = {}) => { 186 | if (!opts.name) { 187 | opts.name = ''; 188 | } 189 | 190 | const connection = opts.connection || {}; 191 | 192 | const models = this.thinky.models; 193 | 194 | let modelTarget = models[modelName], 195 | relation = related; 196 | 197 | if (!modelTarget) { 198 | throw new Error(`Model ${modelName} not found.`); 199 | } 200 | 201 | // Relation is specified as a string 202 | if (typeof related === 'string') { 203 | relation = modelTarget._joins[related]; 204 | 205 | // relation not found can't continue 206 | if (!relation) { 207 | throw new Error( 208 | `Tried to access relation ${related} of the Model ${modelName}, 209 | but relation not found.` 210 | ); 211 | } 212 | 213 | relation.relationName = related; 214 | relation.parentModelName = modelTarget.getTableName(); 215 | } 216 | 217 | modelTarget = (relation) ? relation.model : modelTarget; 218 | return new Node({ 219 | name: related || opts.name, 220 | model: modelTarget, 221 | related: relation, 222 | query: opts.query, 223 | loadersKey: this.loadersKey, 224 | connection: { 225 | ...connection 226 | } 227 | }); 228 | }; 229 | } 230 | 231 | export default GraphqlThinky; 232 | 233 | -------------------------------------------------------------------------------- /src/modelToGqlObjectType.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {GraphQLObjectType} from 'graphql'; 3 | import {toGraphQLDefinition} from './typeMapper'; 4 | 5 | /** 6 | * Create a GraphQLObjectType from a 7 | * a thinky model 8 | * 9 | * @param Model 10 | * @param fields 11 | * @param options 12 | */ 13 | export default function (Model, options = {}) { 14 | const GraphQLDefinition = toGraphQLDefinition(Model, { 15 | exclude: options.exclude, 16 | only: options.only, 17 | map: options.map, 18 | globalId: options.globalId, 19 | allowNull: options.allowNull 20 | }); 21 | 22 | return new GraphQLObjectType({ 23 | name: _.upperFirst(Model.getTableName()), 24 | fields: () => { 25 | const fieldDef = { 26 | ...GraphQLDefinition 27 | }; 28 | 29 | if (typeof options.fields === 'function') { 30 | Object.assign(fieldDef, options.fields()); 31 | } else if (typeof options.fields === 'object') { 32 | Object.assign(fieldDef, options.fields); 33 | } 34 | 35 | return fieldDef; 36 | }, 37 | interfaces: () => options.interfaces || [] 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {buildQuery, buildCount} from './queryBuilder'; 3 | import {resolveConnection} from './relay'; 4 | 5 | /** 6 | * Node class 7 | */ 8 | class Node { 9 | 10 | constructor({model, related = undefined, args = {}, connection = {}, name = '', query = undefined, loadersKey = 'loaders'}) { 11 | assert(model, 'You need to provide a thinky Model'); 12 | 13 | this.model = model; 14 | this.related = related; 15 | this.args = args; 16 | this.connection = connection; 17 | this.name = name; // The name will be populated based to AST name if not provided 18 | this.query = query; 19 | this.loadersKey = loadersKey; 20 | } 21 | 22 | /** 23 | * Resolve node based 24 | * a rethinkDB query 25 | * 26 | * @returns {*} 27 | */ 28 | async queryResolve(loaders) { 29 | this.args.query = this.query; 30 | 31 | let queryResult; 32 | 33 | if (this.args.list) { 34 | const Query = buildQuery(this.model, this.args, this.model._thinky); 35 | queryResult = await Query.run(); 36 | 37 | // If any loader are provided and there is no restriction 38 | // on selecting fields I can safetly cache them into dataloader 39 | // for subsequential calls 40 | if (loaders && !this.args.requestedFields) { 41 | queryResult.forEach(row => { 42 | loaders[this.getModelName()].getOrCreateLoader('loadBy', 'id') 43 | .prime(row.id, row); 44 | }); 45 | } 46 | 47 | if (this.args.count) { 48 | const queryCountResult = await buildCount( 49 | this.model, 50 | [], 51 | null, 52 | this.args 53 | ); 54 | 55 | queryResult = queryResult.map(row => { 56 | row.fullCount = queryCountResult; 57 | return row; 58 | }); 59 | } 60 | } else { 61 | const Query = buildQuery(this.model, { 62 | ...this.args, 63 | offset: false 64 | }, this.model._thinky); 65 | queryResult = await Query.nth(0).default(null).run(); 66 | } 67 | 68 | return queryResult; 69 | } 70 | 71 | /** 72 | * Resolve from tree 73 | * 74 | * @param source 75 | * @returns {*} 76 | */ 77 | fromParentSource(source) { 78 | return source[this.name]; 79 | } 80 | 81 | /** 82 | * Create a relay connection 83 | * 84 | * @returns {{connectionType, edgeType, nodeType, resolveEdge, connectionArgs, resolve}|*} 85 | */ 86 | connect(resolveOptions) { 87 | /*eslint-disable */ 88 | if (!this.connection.name) throw new Error("Please specify a connection name, before call connect on a Node"); 89 | if (!this.connection.type) throw new Error("Please specify a connection type, before call connect on a Node"); 90 | /*eslint-enable */ 91 | 92 | return resolveConnection(this, resolveOptions); 93 | } 94 | 95 | /** 96 | * Resolve Node 97 | * @param treeSource 98 | * @param context 99 | * @returns {*} 100 | */ 101 | async resolve(treeSource, context) { 102 | let result = treeSource; 103 | const loaders = context[this.loadersKey]; 104 | if (!this.isRelated()) { 105 | result = await this.queryResolve(loaders); 106 | } else if (this.isRelated() && treeSource) { 107 | result = this.fromParentSource(treeSource); 108 | 109 | if (!loaders) { 110 | throw new Error(` 111 | GraphQL-thinky couldn't find any loaders set on the GraphQL context 112 | with the key "${this.loadersKey}" 113 | `); 114 | } 115 | 116 | if (!result && treeSource && loaders) { 117 | result = await this.resolveWithLoaders(treeSource, loaders); 118 | } 119 | } 120 | 121 | return result; 122 | } 123 | 124 | /** 125 | * Resolve with loaders 126 | * @param treeSource 127 | * @param loaders 128 | * @returns {*} 129 | */ 130 | async resolveWithLoaders(treeSource, loaders) { 131 | let result; 132 | // Resolve with Loaders from the context 133 | const FkJoin = this.related.leftKey; 134 | this.args.attributes.push(FkJoin); 135 | 136 | if (this.related.type === 'belongsTo') { 137 | const loaderName = this.related.model.getTableName(); 138 | const loader = loaders[loaderName]; 139 | if (!loader) { 140 | throw new Error(` 141 | Loader name "${loaderName}" Not Found for relation 142 | when try to resolve relation ${this.related.relationName} 143 | of the model ${this.getModelName()} 144 | `); 145 | } 146 | result = await loader.loadById(treeSource[FkJoin]); 147 | } else { 148 | const loaderName = this.related.parentModelName; 149 | const loader = loaders[loaderName]; 150 | if (!loader) { 151 | throw new Error(` 152 | Loader name "${loaderName}" Not Found for relation 153 | when try to resolve relation ${this.related.relationName} 154 | of the model ${this.getModelName()}`); 155 | } 156 | result = await loaders[this.related.parentModelName] 157 | .related(this.related.relationName, treeSource[FkJoin], this.args); 158 | } 159 | 160 | if (this.args.list) { 161 | return result.toArray(); 162 | } 163 | return result.toObject(this.args); 164 | } 165 | 166 | /** 167 | * Append args 168 | * 169 | * @param args 170 | */ 171 | appendArgs(args) { 172 | this.args = {...this.args, ...args}; 173 | } 174 | 175 | /** 176 | * Determine if this node is a connection 177 | * 178 | * @returns {string|*} 179 | */ 180 | isConnection() { 181 | return (this.connection.name && this.connection.type); 182 | } 183 | 184 | /** 185 | * Determine if the node is related 186 | * 187 | * @returns {boolean} 188 | */ 189 | isRelated() { 190 | return Boolean(this.related); 191 | } 192 | 193 | /** 194 | * Get model of the Node 195 | * 196 | * @returns {*} 197 | */ 198 | getModel() { 199 | return this.model; 200 | } 201 | 202 | /** 203 | * Get model Name 204 | * 205 | * @return {string} 206 | */ 207 | getModelName() { 208 | return this.model.getTableName(); 209 | } 210 | } 211 | 212 | export default Node; 213 | -------------------------------------------------------------------------------- /src/queryBuilder.js: -------------------------------------------------------------------------------- 1 | import thinkySchema from 'thinky-export-schema'; 2 | import {isFunction, uniq, find, isObject} from 'lodash'; 3 | 4 | /** 5 | * Args to find options 6 | * 7 | * @param args 8 | * @param model 9 | * @param opts 10 | * @returns {{}} 11 | */ 12 | export function argsToFindOptions(args, attributes, model, {maxLimit, requestedFields} = {maxLimit: 50}) { 13 | const result = { 14 | filter: {}, 15 | attributes: [], 16 | limit: undefined, 17 | skip: undefined, 18 | orderBy: undefined 19 | }, 20 | modelSchema = thinkySchema(model), 21 | modelAttributes = Object.keys(modelSchema.fields).concat('id'), 22 | modelRelations = Object.keys(modelSchema.relationships); 23 | 24 | if (args) { 25 | Object.keys(args).forEach(key => { 26 | if (modelAttributes.indexOf(key) !== -1) { 27 | result.filter = result.filter || {}; 28 | result.filter[key] = args[key]; 29 | } 30 | 31 | // Limit arg 32 | if (key === 'offset' && args[key]) { 33 | result.offset = parseInt(args[key], 10); 34 | } 35 | 36 | if (key === 'skip' && args[key]) { 37 | result.index = parseInt(args[key], 10); 38 | } 39 | 40 | if (key === 'order' && args[key]) { 41 | if (args[key].indexOf('reverse:') === 0) { 42 | result.orderBy = [args[key].substring(8), 'DESC']; 43 | } else { 44 | result.orderBy = [args[key], 'ASC']; 45 | } 46 | } 47 | }); 48 | 49 | attributes = Object.keys(attributes).filter(field => { 50 | return (modelRelations.indexOf(field) === -1) && 51 | modelSchema.fields.hasOwnProperty(field) && 52 | modelSchema.fields[field] !== 'Virtual'; 53 | }).concat(['id']); 54 | 55 | if (Array.isArray(requestedFields)) { 56 | attributes = attributes.concat(requestedFields); 57 | } 58 | 59 | result.attributes = uniq(attributes); 60 | 61 | // Set the maxLimit that can be set 62 | // for an offset or use that by default 63 | // if not explicitly disabled 64 | if (maxLimit !== false) { 65 | if (!result.offset) { 66 | result.offset = maxLimit || 100; 67 | } 68 | 69 | if (result.offset > maxLimit) { 70 | result.offset = maxLimit; 71 | } 72 | } 73 | 74 | return result; 75 | } 76 | } 77 | 78 | /** 79 | * Build query 80 | * 81 | * @param seq 82 | * @param args 83 | * @param thinky 84 | * @param options 85 | * @returns {*} 86 | */ 87 | export function buildQuery(seq, args, thinky) { 88 | let Query = seq; 89 | 90 | // Developer can overwrite query per node 91 | if (typeof args.query === 'function') { 92 | Query = args.query(seq, args, thinky); 93 | } else { 94 | // NOTE: selecting only necessary fields will gather a bit of performance 95 | // through your queries, BUT the drawback is that we might 96 | // add another DB query for sub-sequentials GraphQL requests 97 | // when asking for the the same resource. We can't gurantee all the fields 98 | // are available so we don't use Dataloader cache to return the results 99 | if (args.requestedFields) { 100 | if (Array.isArray(args.attributes)) { 101 | Query = seq.withFields(args.attributes); 102 | } else if (isObject(args.attributes)) { 103 | Query = seq.withFields(args.attributes); 104 | } 105 | } 106 | 107 | if (args.filter && args.filterQuery && Object.keys(args.filter).length > 0) { 108 | Object.keys(args.filter).forEach(fieldName => { 109 | if (isFunction(args.filter[fieldName])) { 110 | Query = Query.filter(args.filter[fieldName]); 111 | delete args.filter[fieldName]; 112 | } 113 | }); 114 | 115 | Query = Query.filter(args.filter); 116 | } 117 | 118 | if (args.orderBy && args.orderBy[1] === 'DESC') { 119 | Query = Query.orderBy(thinky.r.desc((args.orderBy[0]))); 120 | } else if (args.orderBy && args.orderBy[0]) { 121 | Query = Query.orderBy(args.orderBy[0]); 122 | } 123 | 124 | if (args.offset) { 125 | Query = Query.slice(args.index, args.offset); 126 | } 127 | } 128 | 129 | return Query; 130 | } 131 | 132 | /** 133 | * Build Count 134 | * @param model 135 | * @param relatedIds 136 | * @param FK 137 | * @param opts 138 | * @returns {*|void|Promise} 139 | */ 140 | export function buildCount(model, relatedIds, FK, opts) { 141 | const thinky = model._thinky; 142 | const r = thinky.r; 143 | opts.offset = false; 144 | opts.orderBy = false; 145 | 146 | if (relatedIds.length > 0 && FK) { 147 | let seq = model.getAll(r.args(uniq(relatedIds)), {index: FK}) 148 | .withFields(['id', FK].concat(Object.keys(opts.filter))); 149 | 150 | seq = buildQuery(seq, opts, thinky); 151 | 152 | return seq.group(FK).ungroup().merge(selectionSet => { 153 | return {fullCount: selectionSet('reduction').count()}; 154 | }).run(); 155 | } 156 | 157 | const seq = buildQuery(model, opts, thinky); 158 | 159 | return seq.count().execute(); 160 | } 161 | 162 | /** 163 | * Map count to result set 164 | * @param resultSet 165 | * @param countResults 166 | * @param FK 167 | * @returns {*} 168 | */ 169 | export function mapCountToResultSet(resultSet, countResults, FK) { 170 | return resultSet.map(row => { 171 | if (Array.isArray(row)) { 172 | return row.map(innerRow => { 173 | const findCount = find(countResults, {group: innerRow[FK]}) || {fullCount: 0}; 174 | innerRow.fullCount = findCount.fullCount; 175 | return innerRow; 176 | }); 177 | } 178 | const findCount = find(countResults, {group: row[FK]}) || {fullCount: 0}; 179 | row.fullCount = findCount.fullCount; 180 | return row; 181 | }); 182 | } 183 | -------------------------------------------------------------------------------- /src/relay/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | connectionFromArray 3 | } from 'graphql-relay'; 4 | 5 | /** 6 | * Check if the type is a connection 7 | * 8 | * @param type 9 | * @returns {boolean|*} 10 | */ 11 | export function isConnection(type) { 12 | return typeof type.name !== 'undefined' && type.name.endsWith('Connection'); 13 | } 14 | 15 | /** 16 | * Handle a connecton from array 17 | * 18 | * @param values 19 | * @param args 20 | */ 21 | export function handleConnection(values, args) { 22 | return connectionFromArray(values, args); 23 | } 24 | 25 | /** 26 | * Node ast 27 | * 28 | * @param connectionAST 29 | * @returns {*|connectionFields.edges|{type, description}|string|Function} 30 | */ 31 | export function nodeAST(connectionAST) { 32 | return connectionAST.fields.edges && 33 | connectionAST.fields.edges.fields.node; 34 | } 35 | 36 | /** 37 | * Get node type 38 | * 39 | * @param connectionType 40 | * @returns {*} 41 | */ 42 | export function nodeType(connectionType) { 43 | return connectionType._fields.edges.type.ofType._fields.node.type; 44 | } 45 | 46 | /** 47 | * Export relay resolver. 48 | */ 49 | export resolveConnection from './resolver'; 50 | -------------------------------------------------------------------------------- /src/relay/nodeDefinition.js: -------------------------------------------------------------------------------- 1 | import { 2 | nodeDefinitions, 3 | fromGlobalId 4 | } from 'graphql-relay'; 5 | 6 | import NodeMapper from './nodeMapper'; 7 | 8 | /** 9 | * Id Global Fetcher 10 | * 11 | * @param thinky 12 | * @param nodeTypeMapper 13 | * @returns {Function} 14 | */ 15 | export function idFetcher(Models, nodeTypeMapper) { 16 | return async(globalId, context) => { 17 | const {type, id} = fromGlobalId(globalId); 18 | 19 | const nodeType = nodeTypeMapper.item(type); 20 | if (nodeType && typeof nodeType.resolve === 'function') { 21 | const res = await Promise.resolve(nodeType.resolve(globalId, context)); 22 | res.__graphqlType__ = type; 23 | return res; 24 | } 25 | 26 | const model = Object.keys(Models).find(model => model === type); 27 | 28 | let result = null; 29 | 30 | if (model) { 31 | result = await Models[model].get(id).run(); 32 | } else { 33 | result = nodeType; 34 | } 35 | 36 | if (result) { 37 | return nodeType.type; 38 | } 39 | 40 | return result; 41 | }; 42 | } 43 | 44 | /** 45 | * Type Resolver 46 | * 47 | * @param nodeTypeMapper 48 | * @returns {Function} 49 | */ 50 | export function typeResolver(nodeTypeMapper) { 51 | return obj => { 52 | const type = obj.__graphqlType__ || 53 | (typeof obj.getModel === 'function') ? 54 | obj.getModel().getModelName() : obj.name; 55 | 56 | if (!type) { 57 | throw new Error(`Unable to determine type of ${typeof obj}. ` + 58 | `Either specify a resolve function in 'NodeTypeMapper' object, or specify '__graphqlType__' property on object.`); 59 | } 60 | 61 | const nodeType = nodeTypeMapper.item(type); 62 | return nodeType && nodeType.type || null; 63 | }; 64 | } 65 | 66 | /** 67 | * Export node definitions 68 | * 69 | * @param Database 70 | * @returns {{nodeTypeMapper: NodeTypeMapper}} 71 | */ 72 | export function nodeInterfaceMapper(Models) { 73 | const nodeTypeMapper = new NodeMapper(); 74 | const nodeObjects = nodeDefinitions( 75 | idFetcher(Models, nodeTypeMapper), 76 | typeResolver(nodeTypeMapper) 77 | ); 78 | 79 | return { 80 | nodeTypeMapper, 81 | ...nodeObjects 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/relay/nodeMapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Node Mapper 4 | |-------------------------------------------------------------------------- 5 | | This instance allow to map the models with relay types 6 | --------------------------------------------------------------------------*/ 7 | class NodeTypeMapper { 8 | constructor() { 9 | this.map = {}; 10 | } 11 | 12 | mapTypes(types) { 13 | Object.entries(types).forEach(([k, v]) => { 14 | this.map[k] = v.type ? v : {type: v}; 15 | }); 16 | } 17 | 18 | item(type) { 19 | return this.map[type]; 20 | } 21 | } 22 | 23 | export default NodeTypeMapper; 24 | -------------------------------------------------------------------------------- /src/relay/resolver.js: -------------------------------------------------------------------------------- 1 | import {connectionDefinitions, connectionArgs} from 'graphql-relay'; 2 | import {GraphQLEnumType, GraphQLList} from 'graphql'; 3 | import simplifyAST from '../simplifyAst'; 4 | import {base64, unbase64} from './../base64.js'; 5 | 6 | const cursorSeparator = '$', 7 | cursorPrefix = `arrayconnection${cursorSeparator}`; 8 | 9 | /** 10 | * Creates a cursor based on the item and the 11 | * index of where is located on the result set. 12 | * needed to identify edges 13 | * 14 | * @param item 15 | * @param index 16 | * @returns {*} 17 | */ 18 | function toCursor(item, index) { 19 | const id = item.id; 20 | return base64(cursorPrefix + id + cursorSeparator + index); 21 | } 22 | 23 | /** 24 | * Decode a cursor into its component parts 25 | * 26 | * @param cursor 27 | * @returns {{id, index}} 28 | */ 29 | function fromCursor(cursor) { 30 | cursor = unbase64(cursor); 31 | cursor = cursor.substring(cursorPrefix.length, cursor.length); 32 | const [id, index] = cursor.split(cursorSeparator); 33 | 34 | return { 35 | id, 36 | index 37 | }; 38 | } 39 | 40 | /** 41 | * Resolve an edge within it's 42 | * cursor, node and source 43 | * 44 | * @param item 45 | * @param index 46 | * @param queriedCursor 47 | * @param args 48 | * @param source 49 | * @returns {{cursor: *, node: *, source: *}} 50 | */ 51 | function resolveEdge(item, index, queriedCursor, args = {}, source) { 52 | if (queriedCursor) { 53 | index = parseInt(queriedCursor.index, 10) + index; 54 | if (index === 0) { 55 | index = 1; 56 | } else { 57 | index++; 58 | } 59 | } 60 | return { 61 | cursor: toCursor(item, index), 62 | node: item, 63 | source 64 | }; 65 | } 66 | 67 | /** 68 | * Return location information 69 | * of an edge 70 | * 71 | * @param resultset 72 | * @param offset 73 | * @param cursor 74 | * @returns {{hasMorePages: boolean, hasPreviousPage: boolean}} 75 | */ 76 | function createEdgeInfo(resultset, offset, index) { 77 | const limit = offset - index; 78 | // retrieve full count from the first edge 79 | // or default 10 80 | let fullCount = resultset[0] && 81 | resultset[0].fullCount && 82 | parseInt(resultset[0].fullCount, 10); 83 | 84 | if (!resultset[0]) { 85 | fullCount = 0; 86 | } 87 | 88 | let hasNextPage = false; 89 | let hasPreviousPage = false; 90 | 91 | if (offset) { 92 | const requested = (index + 1) * limit; 93 | 94 | hasNextPage = requested < fullCount; 95 | hasPreviousPage = (requested > limit); 96 | } 97 | return { 98 | hasNextPage, 99 | hasPreviousPage 100 | }; 101 | } 102 | /** 103 | * Resolve a relay connection 104 | * 105 | * @param Node 106 | * @returns {{connectionType, edgeType, nodeType: *, resolveEdge: resolveEdge, connectionArgs: {orderBy: {type}}, resolve: resolver}} 107 | */ 108 | export default (Node, resolveOpts) => { 109 | const connectionOpts = Node.connection, 110 | connectionName = connectionOpts.name, 111 | nodeType = connectionOpts.type, 112 | userParms = connectionOpts.params || {}; 113 | 114 | connectionOpts.before = connectionOpts.before || (options => options); 115 | connectionOpts.after = connectionOpts.after || (options => options); 116 | 117 | const { 118 | edgeType, 119 | connectionType 120 | } = connectionDefinitions({ 121 | nodeType, 122 | name: connectionName, 123 | connectionFields: connectionOpts.connectionFields, 124 | edgeFields: connectionOpts.edgeFields 125 | }); 126 | 127 | // Define the order of the connection 128 | // To have always a guranteed set of data 129 | // (if not provided) 130 | let orderByEnum; 131 | if (userParms.orderBy === undefined) { 132 | orderByEnum = new GraphQLEnumType({ 133 | name: `${connectionName}ConnectionOrder`, 134 | values: { 135 | ID: {value: ['id', 'ASC']} 136 | } 137 | }); 138 | } else { 139 | orderByEnum = userParms.orderBy; 140 | } 141 | 142 | // Assign the connection arguments 143 | const $connectionArgs = { 144 | ...connectionArgs, 145 | orderBy: { 146 | type: new GraphQLList(orderByEnum) 147 | } 148 | }; 149 | 150 | // We are going to give instruction on how 151 | // the resolver has to retrieve information from 152 | // rethink, then returning it with in the edges,node pattern. 153 | const $resolver = require('./../resolver').default(Node, { 154 | ...resolveOpts, 155 | list: true, 156 | handleConnection: false, 157 | thinky: Node.thinky, 158 | before: (options, parent, args, context) => { 159 | if (args.first || args.last) { 160 | const offset = parseInt(args.first || args.last, 10); 161 | 162 | if (options.count === undefined) { 163 | options.count = true; 164 | } 165 | 166 | if (args.before || args.after) { 167 | const cursor = fromCursor(args.after || args.before); 168 | const startIndex = parseInt(cursor.index, 10); 169 | options.offset = offset + startIndex; 170 | options.index = startIndex; 171 | } else { 172 | options.offset = offset; 173 | options.index = 0; 174 | } 175 | } 176 | 177 | // attach the order into the composition 178 | // stack 179 | let order; 180 | if (!args.orderBy) { 181 | order = [orderByEnum._values[0].value]; 182 | } else if (typeof args.orderBy === 'string') { 183 | order = [orderByEnum._nameLookup[args.orderBy].value]; 184 | } else { 185 | order = args.orderBy; 186 | } 187 | 188 | const orderAttribute = order[0][0]; // Order Attribute 189 | let orderDirection = order[0][1]; // Order Direction 190 | 191 | // Depending on the direction requested 192 | // we sort the result accordently 193 | if (args.last) { 194 | orderDirection = orderDirection === 'ASC' ? 'DESC' : 'ASC'; 195 | } 196 | 197 | // Assign order 198 | options.order = [ 199 | orderAttribute, orderDirection 200 | ]; 201 | 202 | return connectionOpts.before(options, args, root, context); 203 | }, 204 | after: (resultset, {offset, index}, parent, args, root, {source}) => { 205 | let cursor = null; 206 | 207 | // Once we have the result set we decode the cursor 208 | // if given 209 | if (args.after || args.before) { 210 | cursor = fromCursor(args.after || args.before); 211 | } 212 | 213 | // create edges array 214 | const edges = resultset.map((value, idx) => { 215 | // console.log("RESOLVE EDGE", idx); 216 | return resolveEdge(value, idx, cursor, args, source); 217 | }); 218 | 219 | const firstEdge = edges[0], 220 | lastEdge = edges[edges.length - 1]; 221 | 222 | const edgeInfo = createEdgeInfo(resultset, offset, index); 223 | const {hasNextPage, hasPreviousPage} = edgeInfo; 224 | 225 | return { 226 | source, 227 | args, 228 | edges, 229 | pageInfo: { 230 | startCursor: firstEdge ? firstEdge.cursor : null, 231 | endCursor: lastEdge ? lastEdge.cursor : null, 232 | hasPreviousPage, 233 | hasNextPage 234 | } 235 | }; 236 | } 237 | }); 238 | 239 | // Create a wrapper around our custom resolver 240 | // So that it will be executed only if edges are 241 | // returned. 242 | const resolver = (source, args, context, info) => { 243 | if (simplifyAST(info.fieldNodes[0], info).fields.edges) { 244 | return $resolver(source, args, context, info); 245 | } 246 | 247 | return { 248 | source, 249 | args 250 | }; 251 | }; 252 | 253 | return { 254 | connectionType, 255 | edgeType, 256 | nodeType, 257 | resolveEdge, 258 | connectionArgs: $connectionArgs, 259 | resolve: resolver 260 | }; 261 | }; 262 | -------------------------------------------------------------------------------- /src/resolver.js: -------------------------------------------------------------------------------- 1 | import {extend} from 'lodash'; 2 | import {GraphQLList, GraphQLNonNull} from 'graphql'; 3 | import simplifyAST from './simplifyAst'; 4 | import {argsToFindOptions} from './queryBuilder'; 5 | import {isConnection, nodeAST, nodeType} from './relay'; 6 | 7 | /** 8 | * Determine if the GQL node is a list 9 | * @param gqlTye 10 | * @returns {*} 11 | * @private 12 | */ 13 | function _isList(gqlTye) { 14 | if (gqlTye instanceof GraphQLList) { 15 | return true; 16 | } 17 | 18 | if (gqlTye instanceof GraphQLNonNull) { 19 | return _isList(gqlTye.ofType); 20 | } 21 | 22 | return false; 23 | } 24 | 25 | /** 26 | * 27 | * @param Node 28 | * @param opts 29 | * @returns {function(*=, *=, *=, *=)} 30 | */ 31 | export default function resolver(Node, {before, after, ...opts} = {}) { 32 | if (before === undefined) { 33 | before = async opts => opts; 34 | } 35 | if (after === undefined) { 36 | after = async opts => opts; 37 | } 38 | 39 | const Model = Node.getModel(); 40 | 41 | /** 42 | * Resolver GraphQL 43 | * 44 | * @param source 45 | * @param args 46 | * @param context 47 | * @param info 48 | * @constructor 49 | */ 50 | const Resolver = async(source, args, context, info) => { 51 | Node.name = Node.name || info.fieldName; 52 | 53 | let simplyAST = simplifyAST(info.fieldNodes[0], info); 54 | let requestedFields = simplyAST.fields; 55 | let type = info.returnType; 56 | 57 | const connection = isConnection(info.returnType); 58 | if (connection) { 59 | simplyAST = nodeAST(simplyAST); 60 | requestedFields = simplyAST.fields; 61 | type = nodeType(type); 62 | } 63 | 64 | const findOptions = argsToFindOptions(args, requestedFields, Model, opts); 65 | 66 | let nodeAttributes = extend({ 67 | list: connection || _isList(type), 68 | filterQuery: true, 69 | requestedFields: false, 70 | attributes: [], 71 | filter: {}, 72 | index: 0, 73 | offset: opts.maxLimit, 74 | skip: undefined, 75 | orderBy: undefined, 76 | count: undefined, 77 | ...findOptions 78 | }, opts); 79 | 80 | // Before Resolve is triggered, good for permission checks 81 | // and manipulating query exec 82 | nodeAttributes = await before(nodeAttributes, source, args, context, { 83 | ...info, 84 | ast: simplyAST, 85 | type, 86 | source 87 | }); 88 | 89 | type = type.ofType || type; 90 | Node.appendArgs(nodeAttributes); 91 | 92 | const result = await Node.resolve(source, context || {}); 93 | 94 | return await after(result, nodeAttributes, source, args, context, { 95 | ...info, 96 | ast: simplyAST, 97 | type, 98 | source 99 | }); 100 | }; 101 | 102 | return Resolver; 103 | } 104 | -------------------------------------------------------------------------------- /src/simplifyAst.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Deep merge 3 | * 4 | * @param a 5 | * @param b 6 | * @returns {*} 7 | */ 8 | function deepMerge(a, b) { 9 | Object.keys(b).forEach(key => { 10 | if (['fields', 'args'].indexOf(key) !== -1) { 11 | return; 12 | } 13 | 14 | if (a[key] && b[key] && typeof a[key] === 'object' && typeof b[key] === 'object') { 15 | a[key] = deepMerge(a[key], b[key]); 16 | } else { 17 | a[key] = b[key]; 18 | } 19 | }); 20 | 21 | if (a.fields && b.fields) { 22 | a.fields = deepMerge(a.fields, b.fields); 23 | } else if (a.fields || b.fields) { 24 | a.fields = a.fields || b.fields; 25 | } 26 | 27 | return a; 28 | } 29 | 30 | /** 31 | * 32 | * @param info 33 | * @returns {*|boolean} 34 | */ 35 | export function hasFragments(info) { 36 | return info.fragments && Object.keys(info.fragments).length > 0; 37 | } 38 | 39 | /** 40 | * 41 | * @param info 42 | * @param ast 43 | * @returns {*|boolean|*|boolean} 44 | */ 45 | export function isFragment(info, ast) { 46 | return hasFragments(info) && info.fragments[ast.name.value] && ast.kind !== 'FragmentDefinition'; 47 | } 48 | 49 | /** 50 | * 51 | * @param ast 52 | * @param info 53 | * @param parent 54 | * @returns {*} 55 | */ 56 | export default function simplifyAST(ast, info, parent) { 57 | let selections; 58 | info = info || {}; 59 | 60 | if (ast.selectionSet) { 61 | selections = ast.selectionSet.selections; 62 | } 63 | 64 | if (Array.isArray(ast)) { 65 | selections = ast; 66 | } 67 | 68 | if (isFragment(info, ast)) { 69 | return simplifyAST(info.fragments[ast.name.value], info); 70 | } 71 | 72 | if (!selections) { 73 | return { 74 | fields: {}, 75 | args: {} 76 | }; 77 | } 78 | 79 | return selections.reduce((simpleAST, selection) => { 80 | if (selection.kind === 'FragmentSpread' || selection.kind === 'InlineFragment') { 81 | simpleAST = deepMerge( 82 | simpleAST, simplifyAST(selection, info) 83 | ); 84 | return simpleAST; 85 | } 86 | 87 | const name = selection.name.value, 88 | alias = selection.alias && selection.alias.value, 89 | key = alias || name; 90 | 91 | simpleAST.fields[key] = simpleAST.fields[key] || {}; 92 | simpleAST.fields[key] = deepMerge( 93 | simpleAST.fields[key], simplifyAST(selection, info, simpleAST.fields[key]) 94 | ); 95 | 96 | if (alias) { 97 | simpleAST.fields[key].key = name; 98 | } 99 | 100 | simpleAST.fields[key].args = selection.arguments.reduce((args, arg) => { 101 | args[arg.name.value] = arg.value.value; 102 | return args; 103 | }, {}); 104 | 105 | if (parent) { 106 | Object.defineProperty(simpleAST.fields[key], '$parent', {value: parent, enumerable: false}); 107 | } 108 | 109 | return simpleAST; 110 | }, { 111 | fields: {}, 112 | args: {} 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /src/typeMapper.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLString, 4 | GraphQLBoolean, 5 | GraphQLEnumType, 6 | GraphQLList, 7 | GraphQLObjectType, 8 | GraphQLNonNull 9 | } from 'graphql'; 10 | 11 | import {globalIdField} from 'graphql-relay'; 12 | import {type} from 'thinky'; 13 | import {upperFirst} from 'lodash'; 14 | 15 | let customTypeMapper; 16 | 17 | /** 18 | * A function to set a custom mapping of types 19 | * @param {Function} mapFunc 20 | */ 21 | export function mapType(mapFunc) { 22 | customTypeMapper = mapFunc; 23 | } 24 | 25 | /** 26 | * Create a graphql definition based to a 27 | * thinky model 28 | * 29 | * @param thinkyModel 30 | * @returns {*} 31 | */ 32 | export function toGraphQLDefinition(thinkyModel, opts = {}) { 33 | opts.exclude = opts.exclude || []; 34 | opts.only = opts.only || null; 35 | opts.map = opts.map || {}; 36 | opts.globalId = (opts.globalId === undefined) ? false : opts.globalId; 37 | opts.allowNull = (opts.allowNull === undefined) ? true : opts.allowNull; 38 | 39 | const modelSchema = thinkyModel._schema._schema, 40 | modelName = thinkyModel.getTableName(); 41 | 42 | // Generate the GraphQL Definition Object 43 | const graphQLDefinition = Object.keys(modelSchema).reduce((gqlDef, key) => { 44 | // Skip Excluded 45 | if (opts.exclude.indexOf(key) !== -1) { 46 | return gqlDef; 47 | } 48 | 49 | // Only those fields 50 | if (opts.only && opts.only.indexOf(key) === -1) { 51 | return gqlDef; 52 | } 53 | 54 | const modelType = modelSchema[key]; 55 | const name = (type.isObject(modelType)) ? upperFirst(modelName) + upperFirst(key) : key; 56 | const graphQLType = attributeToGraphQLType(modelType, name); 57 | 58 | if (graphQLType) { 59 | // Map the field if specified 60 | if (opts.map.hasOwnProperty(key)) { 61 | if (typeof opts.map === 'function') { 62 | key = opts.map(key); 63 | } else { 64 | key = opts.map[key]; 65 | } 66 | } 67 | 68 | // If the attribute is a "not nullable" field, then i'll wrap it 69 | // with a GraphQLNotNull instance 70 | if (opts.allowNull && modelType._options.enforce_type === 'strict') { 71 | gqlDef[key] = { 72 | type: new GraphQLNonNull(graphQLType) 73 | }; 74 | } else { 75 | gqlDef[key] = { 76 | type: graphQLType 77 | }; 78 | } 79 | } 80 | 81 | return gqlDef; 82 | }, {}); 83 | 84 | // Add relay global id if requested 85 | if (opts.globalId) { 86 | const field = `${modelName.toLowerCase()}ID`; 87 | graphQLDefinition.id = globalIdField(upperFirst(modelName), instance => instance.id); 88 | graphQLDefinition[field] = { 89 | type: GraphQLString, 90 | resolve: source => { 91 | return source.id; 92 | } 93 | }; 94 | } else { 95 | graphQLDefinition.id = { 96 | type: GraphQLString 97 | }; 98 | } 99 | 100 | return graphQLDefinition; 101 | } 102 | 103 | /** 104 | * Accept a thinky type and return 105 | * a graphQL Type 106 | * 107 | * @param attributeType 108 | * @param name 109 | */ 110 | export function attributeToGraphQLType(attributeType, name) { 111 | const schema = attributeType._schema, 112 | schemaKeys = schema ? Object.keys(schema) : [], 113 | enumKeys = (attributeType._enum) ? Object.keys(attributeType._enum) : []; 114 | 115 | // User did create a custom type mapper 116 | // return it if it is truthy 117 | if (customTypeMapper) { 118 | const result = customTypeMapper(attributeType); 119 | if (result) { 120 | return result; 121 | } 122 | } 123 | 124 | // ENUM type 125 | if (enumKeys.length > 0) { 126 | const specialChars = /[^a-z\d_]/i; 127 | 128 | return new GraphQLEnumType({ 129 | name: `${name}EnumType`, 130 | values: enumKeys.reduce((obj, enumKey) => { 131 | const value = enumKey; 132 | let sanitizedValue = value; 133 | if (specialChars.test(value)) { 134 | sanitizedValue = value.split(specialChars).reduce((reduced, val, idx) => { 135 | let newVal = val; 136 | if (idx > 0) { 137 | newVal = `${val[0].toUpperCase()}${val.slice(1)}`; 138 | } 139 | return `${reduced}${newVal}`; 140 | }); 141 | } 142 | obj[sanitizedValue] = {value}; 143 | return obj; 144 | }, {}) 145 | }); 146 | } 147 | 148 | // Bolean Type 149 | if (type.isBoolean(attributeType)) { 150 | return GraphQLBoolean; 151 | } 152 | 153 | // String Type 154 | if (type.isString(attributeType) || 155 | type.isDate(attributeType) || 156 | type.isPoint(attributeType)) { 157 | return GraphQLString; 158 | } 159 | 160 | // Number Type TODO: what about if it is float? 161 | if (type.isNumber(attributeType)) { 162 | return GraphQLInt; 163 | } 164 | 165 | // Object type 166 | if (type.isObject(attributeType)) { 167 | if (schemaKeys.length === 0) { 168 | console.info('Attrbute type object doesnt have a schema, skip...'); 169 | return; 170 | } 171 | 172 | const graphQLTypeDef = {}; 173 | schemaKeys.forEach(attribute => { 174 | graphQLTypeDef[attribute] = {}; 175 | 176 | const graphQLType = attributeToGraphQLType(schema[attribute], name); 177 | 178 | if (graphQLType) { 179 | graphQLTypeDef[attribute]['type'] = graphQLType; 180 | } 181 | }); 182 | 183 | if (!name) { 184 | throw new Error('Specify a name for the Object Attribute type'); 185 | } 186 | 187 | return new GraphQLObjectType({ 188 | name: upperFirst(name), 189 | fields: { 190 | ...graphQLTypeDef 191 | } 192 | }); 193 | } 194 | 195 | // Array type 196 | if (type.isArray(attributeType)) { 197 | const schemaArray = attributeType._schema; 198 | const arrayType = attributeToGraphQLType(schemaArray, name); 199 | return new GraphQLList(arrayType); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /test/helpers/db/index.js: -------------------------------------------------------------------------------- 1 | import thinky from './thinky'; 2 | import models from './models'; 3 | 4 | /** 5 | * Clear database 6 | * 7 | * @returns {Promise} 8 | */ 9 | async function clearDB() { 10 | 11 | const tables = await thinky.r.tableList().run(); 12 | var deleted = []; 13 | 14 | tables.forEach((table) => { 15 | const deletePromise = thinky.r.table(table).delete().run(); 16 | deleted.push(deletePromise); 17 | }); 18 | 19 | return Promise.all(deleted); 20 | } 21 | 22 | /** 23 | * Drop DB in use 24 | * 25 | * @returns {*} 26 | */ 27 | function dropDB() { 28 | return thinky.r.dbDrop(thinky._config.db).run(); 29 | } 30 | 31 | export default { 32 | instance: thinky, 33 | models, 34 | clearDB, 35 | dropDB 36 | }; -------------------------------------------------------------------------------- /test/helpers/db/models.js: -------------------------------------------------------------------------------- 1 | import DB from './thinky'; 2 | 3 | const type = DB.type; 4 | 5 | const User = DB.createModel('user', { 6 | name: type.string(), 7 | username: type.string(), 8 | virtualName: type.virtual().default(function() { 9 | return this.name+"Virtual"; 10 | }) 11 | }); 12 | 13 | const Task = DB.createModel('task', { 14 | title: type.string(), 15 | completed: type.boolean(), 16 | assignee_id: type.string() 17 | }); 18 | 19 | const Tag = DB.createModel('tag', { 20 | name: type.string(), 21 | description: type.string() 22 | }); 23 | 24 | User.hasMany(Task,'tasks','id','assignee_id'); 25 | 26 | Task.hasAndBelongsToMany(Tag, "tags", "id", "id"); 27 | Tag.hasAndBelongsToMany(Task, "tasks", "id", "id"); 28 | 29 | 30 | export default { 31 | User, 32 | Task, 33 | Tag 34 | }; -------------------------------------------------------------------------------- /test/helpers/db/thinky.js: -------------------------------------------------------------------------------- 1 | import thinky from 'thinky'; 2 | 3 | export default thinky({ 4 | db: "think_graph_test_" + process.pid 5 | }); -------------------------------------------------------------------------------- /test/helpers/graphql/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLObjectType, 4 | GraphQLNonNull, 5 | GraphQLString, 6 | graphql, 7 | } from 'graphql'; 8 | 9 | import loaders from './loaders'; 10 | 11 | /** 12 | * 13 | * Generate Schema 14 | * 15 | * @param fields 16 | */ 17 | function createSchema(fields,node) { 18 | 19 | return new GraphQLSchema({ 20 | query: new GraphQLObjectType({ 21 | name: 'RootQueryType', 22 | fields: { 23 | ...fields 24 | } 25 | }), 26 | node: node 27 | }); 28 | } 29 | 30 | /** 31 | * User type 32 | * 33 | * @param fields 34 | */ 35 | function userType(fields, name, nodeInterface) { 36 | return new GraphQLObjectType({ 37 | name: name || 'User', 38 | description: 'A user', 39 | fields: { 40 | id: { 41 | type: new GraphQLNonNull(GraphQLString), 42 | }, 43 | name: { 44 | type: GraphQLString, 45 | }, 46 | username: { 47 | type: GraphQLString 48 | }, 49 | virtualName: { 50 | type: GraphQLString 51 | }, 52 | ...fields 53 | }, 54 | interfaces: () => nodeInterface || [] 55 | }); 56 | } 57 | 58 | /** 59 | * Task Type 60 | * 61 | * @param fields 62 | */ 63 | export function taskType(fields,nodeInterface) { 64 | return new GraphQLObjectType({ 65 | name: 'Task', 66 | description: 'A Task', 67 | fields: () => { 68 | 69 | if (typeof fields === 'function') { 70 | fields = fields(); 71 | } 72 | 73 | return { 74 | id: { 75 | type: new GraphQLNonNull(GraphQLString), 76 | }, 77 | title: { 78 | type: GraphQLString, 79 | }, 80 | description: { 81 | type: GraphQLString 82 | }, 83 | assignee_id: { 84 | type: new GraphQLNonNull(GraphQLString) 85 | }, 86 | ...fields 87 | } 88 | }, 89 | interfaces: () => nodeInterface || [] 90 | }); 91 | } 92 | 93 | /** 94 | * Tag type 95 | * 96 | * @param fields 97 | */ 98 | export function tagType(fields, nodeInterface) { 99 | 100 | return new GraphQLObjectType({ 101 | name: 'Tag', 102 | description: 'A Tag', 103 | fields: () => { 104 | if (typeof fields === 'function') { 105 | fields = fields(); 106 | } 107 | 108 | return { 109 | id: { 110 | type: new GraphQLNonNull(GraphQLString), 111 | } 112 | , 113 | name: { 114 | type: GraphQLString, 115 | }, 116 | description: { 117 | type: GraphQLString 118 | }, 119 | ...fields 120 | } 121 | }, 122 | interfaces: () => nodeInterface || [] 123 | }); 124 | } 125 | 126 | export function executeQuery(schema,query) { 127 | return graphql(schema,query,null,{loaders}); 128 | } 129 | 130 | export default { 131 | createSchema, 132 | userType, 133 | taskType, 134 | tagType, 135 | executeQuery 136 | } -------------------------------------------------------------------------------- /test/helpers/graphql/loaders.js: -------------------------------------------------------------------------------- 1 | import thinky from '../db/thinky'; 2 | import ModelLoader from '../../../src/dataloader/modelLoader'; 3 | 4 | const models = thinky.models; 5 | export default Object.keys(models).reduce((loadersObj, modelName) => { 6 | modelName = models[modelName]._name; 7 | const model = models[modelName]; 8 | const modelLoader = new ModelLoader(model); 9 | loadersObj[modelName] = modelLoader; 10 | return loadersObj; 11 | }, {}); -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require("babel-polyfill"); 3 | 4 | module.exports = { 5 | DB: require('./db').default, 6 | graphql: require('./graphql').default, 7 | loaders: require('./graphql/loaders').default 8 | }; -------------------------------------------------------------------------------- /test/integration/graphql-thinky.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expect} from 'chai'; 3 | import {DB, graphql as Graph} from './../helpers'; 4 | import GraphqlThinky from './../../src/index'; 5 | import { 6 | graphql, 7 | GraphQLList, 8 | GraphQLInt, 9 | GraphQLString, 10 | GraphQLEnumType, 11 | GraphQLNonNull 12 | } from 'graphql'; 13 | import _ from 'lodash'; 14 | 15 | 16 | test.beforeEach(async function (t) { 17 | 18 | t.context.graphqlThinky = new GraphqlThinky(DB); 19 | 20 | const users = t.context.users = await DB.models.User.save([ 21 | { name: 'jhon', username: 'doe' }, 22 | { name: 'fabri', username: 'fenos' }, 23 | { name: 'will', username: 'red' }, 24 | { name: 'smith', username: 'blue' }, 25 | { name: 'paul', username: 'orange' }, 26 | { name: 'tesla', username: 'ele' }, 27 | ]); 28 | 29 | const tasks = []; 30 | users.forEach((user, key) => { 31 | tasks.push({ 32 | title: 'My task' + key, 33 | number: key, 34 | description: 'My duty' + key, 35 | assignee_id: user.id 36 | }); 37 | }); 38 | 39 | return t.context.tasks = await DB.models.Task.save(tasks); 40 | }); 41 | 42 | test.afterEach(async(t) => { 43 | return await DB.clearDB(); 44 | }); 45 | 46 | test.after('cleanup', async function () { 47 | await DB.dropDB(); 48 | return await DB.instance.r.getPool().drain(); 49 | }); 50 | 51 | test.serial('it resolve a list of users', async(t) => { 52 | const { resolve } = t.context.graphqlThinky; 53 | 54 | const userType = Graph.userType(); 55 | const schema = Graph.createSchema({ 56 | users: { 57 | type: new GraphQLList(userType), 58 | resolve: resolve('User') 59 | } 60 | }); 61 | 62 | const result = await Graph.executeQuery(schema, ` 63 | { 64 | users { 65 | name 66 | } 67 | } 68 | `); 69 | 70 | if (result.errors) throw new Error(result.errors[0].stack); 71 | 72 | expect(result.data.users).to.have.lengthOf(t.context.users.length); 73 | 74 | result.data.users.forEach((user) => { 75 | expect(user).to.have.property('name').that.is.a('string'); 76 | }); 77 | }); 78 | 79 | test.serial('it resolve a list of users with related tasks', async(t) => { 80 | const { resolve } = t.context.graphqlThinky; 81 | 82 | const taskType = Graph.taskType(); 83 | const userType = Graph.userType({ 84 | tasks: { 85 | type: new GraphQLList(taskType), 86 | resolve: resolve('User','tasks') 87 | } 88 | }); 89 | const schema = Graph.createSchema({ 90 | users: { 91 | type: new GraphQLList(userType), 92 | resolve: resolve('User') 93 | } 94 | }); 95 | 96 | const result = await Graph.executeQuery(schema, ` 97 | { 98 | users { 99 | name 100 | 101 | tasks { 102 | title 103 | } 104 | } 105 | } 106 | `); 107 | 108 | expect(result.data.users).to.have.lengthOf(t.context.users.length); 109 | 110 | result.data.users.forEach((user) => { 111 | expect(user).to.have.property('name').that.is.a('string'); 112 | 113 | user.tasks.forEach((task) => { 114 | expect(task).to.have.property('title').that.is.a('string') 115 | }); 116 | }); 117 | }); 118 | 119 | test.serial('it resolve a list of users with related tasks given ast name different then relation name', async(t) => { 120 | const { resolve } = t.context.graphqlThinky; 121 | 122 | const taskType = Graph.taskType(); 123 | const userType = Graph.userType({ 124 | tasksList: { 125 | type: new GraphQLList(taskType), 126 | resolve: resolve('User','tasks') 127 | } 128 | }); 129 | const schema = Graph.createSchema({ 130 | users: { 131 | type: new GraphQLList(userType), 132 | resolve: resolve('User') 133 | } 134 | }); 135 | 136 | const result = await Graph.executeQuery(schema, ` 137 | { 138 | users { 139 | name 140 | 141 | tasksList { 142 | title 143 | } 144 | } 145 | } 146 | `); 147 | 148 | expect(result.data.users).to.have.lengthOf(t.context.users.length); 149 | 150 | result.data.users.forEach((user) => { 151 | expect(user).to.have.property('name').that.is.a('string'); 152 | 153 | user.tasksList.forEach((task) => { 154 | expect(task).to.have.property('title').that.is.a('string') 155 | }); 156 | }); 157 | }); 158 | 159 | test.serial('it resolve a connection of users', async(t) => { 160 | 161 | const { connect } = t.context.graphqlThinky; 162 | const userType = Graph.userType(); 163 | 164 | const schema = Graph.createSchema({ 165 | users: { 166 | ...connect('User', null, { 167 | connection: { 168 | name: 'UserConnection', 169 | type: userType 170 | } 171 | }) 172 | } 173 | }); 174 | 175 | const result = await Graph.executeQuery(schema, ` 176 | { 177 | users { 178 | edges { 179 | node { 180 | name 181 | } 182 | } 183 | } 184 | } 185 | `); 186 | 187 | expect(result.data.users.edges).to.have.lengthOf(t.context.users.length); 188 | 189 | result.data.users.edges.forEach((user) => { 190 | expect(user.node).to.have.property('name').that.is.a('string'); 191 | }); 192 | }); 193 | 194 | test.serial('it resolve a connection of users and related connection tasks', async(t) => { 195 | 196 | const { connect } = t.context.graphqlThinky; 197 | const taskType = Graph.taskType({}); 198 | 199 | const userType = Graph.userType({ 200 | tasksConnection: { 201 | ...connect('User','tasks', { 202 | connection: { 203 | name: 'UserTask', 204 | type: taskType 205 | } 206 | }) 207 | } 208 | }); 209 | 210 | const schema = await Graph.createSchema({ 211 | usersConnection: { 212 | ...connect('User', null, { 213 | connection: { 214 | name: 'UserConnection', 215 | type: userType 216 | } 217 | }) 218 | } 219 | }); 220 | 221 | const result = await Graph.executeQuery(schema, ` 222 | { 223 | usersConnection { 224 | edges { 225 | node { 226 | name 227 | 228 | tasksConnection { 229 | edges { 230 | node { 231 | title 232 | } 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | `); 240 | 241 | expect(result.data.usersConnection.edges).to.have.lengthOf(t.context.users.length); 242 | 243 | result.data.usersConnection.edges.forEach((user) => { 244 | expect(user.node).to.have.property('name').that.is.a('string'); 245 | user.node.tasksConnection.edges.forEach((task) => { 246 | expect(task.node).to.have.property('title').that.is.a('string'); 247 | }); 248 | }); 249 | }); -------------------------------------------------------------------------------- /test/integration/graphus.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {DB,graphql} from './../helpers'; 3 | import Graphus from './../../src/index'; 4 | import Node from '../../src/node'; 5 | import {expect} from 'chai'; 6 | 7 | test.beforeEach((t) => { 8 | t.context.graphus = new Graphus(DB); 9 | }); 10 | 11 | test('it should allow to register types into nodeTypeMapper', (t) => { 12 | 13 | const {nodeTypeMapper} = t.context.graphus; 14 | const userType = graphql.userType(); 15 | 16 | nodeTypeMapper.mapTypes({ 17 | 'user': userType 18 | }); 19 | 20 | expect(nodeTypeMapper.map.user.type).to.be.deep.equal(userType); 21 | }); 22 | 23 | test('it should create a node based to a model', (t) => { 24 | const graphus = t.context.graphus; 25 | 26 | const node = graphus.node('User'); 27 | expect(node).to.be.an.instanceof(Node); 28 | }); 29 | 30 | test('it should create a connection based to a model', (t) => { 31 | 32 | const graphus = t.context.graphus; 33 | const userType = graphql.userType(); 34 | 35 | const connection = graphus.connect('User',null,{ 36 | connection: { 37 | name: 'UserConnection', 38 | type: userType 39 | } 40 | }); 41 | 42 | expect(connection).to.have.property('type'); 43 | expect(connection).to.have.property('args'); 44 | expect(connection).to.have.property('resolve'); 45 | }); -------------------------------------------------------------------------------- /test/integration/relay.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expect} from 'chai'; 3 | import {DB,graphql as Graph} from './../helpers'; 4 | import { 5 | graphql, 6 | GraphQLList, 7 | GraphQLInt, 8 | GraphQLString, 9 | GraphQLEnumType, 10 | GraphQLNonNull 11 | } from 'graphql'; 12 | import _ from 'lodash'; 13 | import {resolveConnection} from './../../src/relay'; 14 | import {nodeInterfaceMapper} from './../../src/relay/nodeDefinition'; 15 | import resolver from './../../src/resolver'; 16 | import Node from '../../src/node'; 17 | 18 | test.beforeEach(async function(t) { 19 | 20 | const users = t.context.users = await DB.models.User.save([ 21 | {name: 'jhon',username: 'doe'}, 22 | {name: 'fabri',username: 'fenos'}, 23 | {name: 'will',username: 'red'}, 24 | {name: 'smith',username: 'blue'}, 25 | {name: 'paul',username: 'orange'}, 26 | {name: 'tesla',username: 'ele'}, 27 | ]); 28 | 29 | const tasks = []; 30 | users.forEach((user,key) => { 31 | tasks.push({title: 'My task'+key, description: 'My duty'+key, assignee_id: user.id}); 32 | }); 33 | 34 | const { 35 | nodeInterface, 36 | nodeField, 37 | nodeTypeMapper 38 | } = nodeInterfaceMapper(DB.models); 39 | 40 | t.context.nodeField = nodeField; 41 | t.context.nodeTypeMapper = nodeTypeMapper; 42 | t.context.nodeInterface = nodeInterface; 43 | 44 | nodeTypeMapper.mapTypes({ 45 | ['User']: { type: Graph.userType() }, 46 | ['Task']: { type: Graph.taskType() } 47 | }); 48 | 49 | return t.context.tasks = await DB.models.Task.save(tasks); 50 | }); 51 | 52 | test.afterEach.always(async (t) => { 53 | return await DB.clearDB(); 54 | }); 55 | 56 | test.after('cleanup' ,async function() { 57 | await DB.dropDB(); 58 | return await DB.instance.r.getPool().drain(); 59 | }); 60 | 61 | test.serial('should return a list Of edges and nodes', async (t) => { 62 | 63 | const userType = Graph.userType(); 64 | 65 | const UserConnector = new Node({ 66 | model: DB.models.User, 67 | connection: { 68 | args: { 69 | thiky: DB.instance 70 | }, 71 | name: 'UserConnection', 72 | type: userType 73 | } 74 | }).connect(); 75 | 76 | const schema = Graph.createSchema({ 77 | users: { 78 | type: UserConnector.connectionType, 79 | args: { 80 | ...UserConnector.connectionArgs 81 | }, 82 | resolve: UserConnector.resolve 83 | } 84 | }); 85 | 86 | const result = await Graph.executeQuery(schema, ` 87 | { 88 | users { 89 | edges { 90 | node { 91 | name 92 | } 93 | } 94 | } 95 | } 96 | `); 97 | 98 | if (result.errors) throw new Error(result.errors[0].stack); 99 | 100 | result.data.users.edges.forEach((user) => { 101 | expect(user.node).to.have.property('name').that.is.a('string'); 102 | }); 103 | }); 104 | 105 | test.serial('should limit a connection of results', async (t) => { 106 | 107 | const tasks = []; 108 | 109 | // 2 tasks for each users 110 | t.context.users.forEach((user,key) => { 111 | tasks.push({title: 'My task'+key, description: 'My duty'+key, assignee_id: user.id}); 112 | }); 113 | 114 | t.context.tasks = await DB.models.Task.save(tasks); 115 | 116 | const User = new Node({ 117 | model: DB.models.User 118 | }); 119 | 120 | const TaskConnector = new Node({ 121 | model: DB.models.Task, 122 | related: { 123 | ...DB.models.User._joins.tasks, 124 | parentModelName: 'user', 125 | relationName: 'tasks', 126 | }, 127 | connection: { 128 | name: 'UserTaskConnection', 129 | type: Graph.taskType() 130 | } 131 | }).connect(); 132 | 133 | const userType = Graph.userType({ 134 | tasks: { 135 | type: TaskConnector.connectionType, 136 | args: { 137 | ...TaskConnector.connectionArgs 138 | }, 139 | resolve: TaskConnector.resolve 140 | } 141 | }); 142 | 143 | const schema = Graph.createSchema({ 144 | users: { 145 | type: new GraphQLList(userType), 146 | resolve: resolver(User) 147 | } 148 | }); 149 | 150 | const result = await Graph.executeQuery(schema, ` 151 | { 152 | users { 153 | name 154 | tasks(first: 2) { 155 | edges { 156 | node { 157 | title 158 | } 159 | } 160 | } 161 | } 162 | } 163 | `); 164 | 165 | if (result.errors) throw new Error(result.errors[0].stack); 166 | 167 | result.data.users.forEach((user) => { 168 | user.tasks.edges.forEach((task) => { 169 | expect(task.node).to.have.property('title'); 170 | }) 171 | }); 172 | 173 | result.data.users.forEach((user) => { 174 | expect(user.tasks.edges).to.have.lengthOf(2); 175 | }); 176 | }); 177 | 178 | test.serial('should paginate a connection of results', async (t) => { 179 | 180 | const tasks = []; 181 | const user = _.sample(t.context.users); 182 | 183 | // 4 tasks for the user 184 | await DB.models.Task.delete().run(); 185 | [1,2,3,4].forEach((_,key) => { 186 | tasks.push({title: 'My task'+key, description: 'My duty'+key, assignee_id: user.id}); 187 | }); 188 | 189 | await DB.models.Task.save(tasks); 190 | 191 | const User = new Node({ 192 | model: DB.models.User 193 | }); 194 | 195 | const TaskConnector = new Node({ 196 | model: DB.models.Task, 197 | related: { 198 | ...DB.models.User._joins.tasks, 199 | parentModelName: 'user', 200 | relationName: 'tasks', 201 | }, 202 | connection: { 203 | name: 'UserTaskConnection', 204 | type: Graph.taskType() 205 | } 206 | }).connect(); 207 | 208 | const userType = Graph.userType({ 209 | tasks: { 210 | type: TaskConnector.connectionType, 211 | args: { 212 | ...TaskConnector.connectionArgs 213 | }, 214 | resolve: TaskConnector.resolve 215 | } 216 | }); 217 | 218 | const schema = Graph.createSchema({ 219 | user: { 220 | args: { 221 | id: { 222 | type: GraphQLString 223 | } 224 | }, 225 | type: userType, 226 | resolve: resolver(User) 227 | } 228 | }); 229 | 230 | const firstDataSet = await Graph.executeQuery(schema, ` 231 | { 232 | user (id: "${user.id}") { 233 | name 234 | tasks(first: 2) { 235 | pageInfo { 236 | hasNextPage 237 | hasPreviousPage 238 | startCursor 239 | endCursor 240 | } 241 | edges { 242 | cursor 243 | node { 244 | title 245 | } 246 | } 247 | } 248 | } 249 | } 250 | `); 251 | 252 | if (firstDataSet.errors) throw new Error(firstDataSet.errors[0].stack); 253 | 254 | expect(firstDataSet.data.user.tasks.edges).to.have.lengthOf(2); 255 | expect(firstDataSet.data.user.tasks.pageInfo.hasNextPage).to.be.equal(true); 256 | expect(firstDataSet.data.user.tasks.pageInfo.hasPreviousPage).to.be.equal(false); 257 | 258 | const lastTaskIndex = firstDataSet.data.user.tasks.edges.length - 1; 259 | const cusor = firstDataSet.data.user.tasks.edges[lastTaskIndex].cursor; 260 | 261 | const secondDataSet = await Graph.executeQuery(schema, ` 262 | { 263 | user (id: "${user.id}") { 264 | name 265 | tasks(first: 2, after: "${cusor}") { 266 | pageInfo { 267 | hasNextPage 268 | hasPreviousPage 269 | startCursor 270 | endCursor 271 | } 272 | edges { 273 | cursor 274 | node { 275 | title 276 | } 277 | } 278 | } 279 | } 280 | } 281 | `); 282 | 283 | expect(secondDataSet.data.user.tasks.edges).to.not.include.members(firstDataSet.data.user.tasks.edges); 284 | expect(secondDataSet.data.user.tasks.edges[0].cursor).to.not.equal(cusor); 285 | 286 | expect(secondDataSet.data.user.tasks.pageInfo.hasNextPage).to.be.equal(false); 287 | expect(secondDataSet.data.user.tasks.pageInfo.hasPreviousPage).to.be.equal(true); 288 | }); 289 | 290 | test.serial('should paginate a connection of results, asserting page info based to 2:6', async (t) => { 291 | 292 | const tasks = []; 293 | const user = _.sample(t.context.users); 294 | 295 | // 4 tasks for the user 296 | await DB.models.Task.delete().run(); 297 | [1,2,3,4,5,6].forEach((_,key) => { 298 | tasks.push({title: 'My task'+key, description: 'My duty'+key, assignee_id: user.id}); 299 | }); 300 | 301 | await DB.models.Task.save(tasks); 302 | 303 | const User = new Node({ 304 | model: DB.models.User 305 | }); 306 | 307 | const TaskConnector = new Node({ 308 | model: DB.models.Task, 309 | related: { 310 | ...DB.models.User._joins.tasks, 311 | parentModelName: 'user', 312 | relationName: 'tasks' 313 | }, 314 | connection: { 315 | name: 'UserTaskConnection', 316 | type: Graph.taskType() 317 | } 318 | }).connect(); 319 | 320 | const userType = Graph.userType({ 321 | tasks: { 322 | type: TaskConnector.connectionType, 323 | args: { 324 | ...TaskConnector.connectionArgs 325 | }, 326 | resolve: TaskConnector.resolve 327 | } 328 | }); 329 | 330 | const schema = Graph.createSchema({ 331 | user: { 332 | args: { 333 | id: { 334 | type: GraphQLString 335 | } 336 | }, 337 | type: userType, 338 | resolve: resolver(User) 339 | } 340 | }); 341 | 342 | const firstDataSet = await Graph.executeQuery(schema, ` 343 | { 344 | user (id: "${user.id}") { 345 | name 346 | tasks(first: 2) { 347 | pageInfo { 348 | hasNextPage 349 | hasPreviousPage 350 | startCursor 351 | endCursor 352 | } 353 | edges { 354 | cursor 355 | node { 356 | title 357 | } 358 | } 359 | } 360 | } 361 | } 362 | `); 363 | 364 | if (firstDataSet.errors) throw new Error(firstDataSet.errors[0].stack); 365 | 366 | expect(firstDataSet.data.user.tasks.edges).to.have.lengthOf(2); 367 | expect(firstDataSet.data.user.tasks.pageInfo.hasNextPage).to.be.equal(true); 368 | expect(firstDataSet.data.user.tasks.pageInfo.hasPreviousPage).to.be.equal(false); 369 | 370 | 371 | let lastTaskIndex = firstDataSet.data.user.tasks.edges.length - 1; 372 | let cusor = firstDataSet.data.user.tasks.edges[lastTaskIndex].cursor; 373 | 374 | const secondDataSet = await Graph.executeQuery(schema, ` 375 | { 376 | user (id: "${user.id}") { 377 | name 378 | tasks(first: 2, after: "${cusor}") { 379 | pageInfo { 380 | hasNextPage 381 | hasPreviousPage 382 | startCursor 383 | endCursor 384 | } 385 | edges { 386 | cursor 387 | node { 388 | title 389 | } 390 | } 391 | } 392 | } 393 | } 394 | `); 395 | 396 | expect(secondDataSet.data.user.tasks.edges).to.not.include.members(firstDataSet.data.user.tasks.edges); 397 | expect(secondDataSet.data.user.tasks.edges[0].cursor).to.not.equal(cusor); 398 | 399 | expect(secondDataSet.data.user.tasks.pageInfo.hasNextPage).to.be.equal(true); 400 | expect(secondDataSet.data.user.tasks.pageInfo.hasPreviousPage).to.be.equal(true); 401 | 402 | 403 | lastTaskIndex = secondDataSet.data.user.tasks.edges.length - 1; 404 | cusor = secondDataSet.data.user.tasks.edges[lastTaskIndex].cursor; 405 | 406 | const thridDataSet = await Graph.executeQuery(schema, ` 407 | { 408 | user (id: "${user.id}") { 409 | name 410 | tasks(first: 2, after: "${cusor}") { 411 | pageInfo { 412 | hasNextPage 413 | hasPreviousPage 414 | startCursor 415 | endCursor 416 | } 417 | edges { 418 | cursor 419 | node { 420 | title 421 | } 422 | } 423 | } 424 | } 425 | } 426 | `); 427 | 428 | expect(thridDataSet.data.user.tasks.edges).to.not.include.members(secondDataSet.data.user.tasks.edges); 429 | expect(thridDataSet.data.user.tasks.edges[0].cursor).to.not.equal(cusor); 430 | 431 | expect(thridDataSet.data.user.tasks.pageInfo.hasNextPage).to.be.equal(false); 432 | expect(thridDataSet.data.user.tasks.pageInfo.hasPreviousPage).to.be.equal(true); 433 | }); 434 | 435 | test.serial('should order a connection of results', async (t) => { 436 | 437 | const tasks = []; 438 | const user = _.sample(t.context.users); 439 | 440 | // 2 tasks for each users 441 | t.context.users.forEach((user,key) => { 442 | tasks.push({title: 'My task'+key, description: 'My duty'+key, assignee_id: user.id}); 443 | }); 444 | 445 | t.context.tasks = await DB.models.Task.save(tasks); 446 | 447 | const TaskConnector = new Node({ 448 | model: DB.models.Task, 449 | related: { 450 | ...DB.models.User._joins.tasks, 451 | parentModelName: 'user', 452 | relationName: 'tasks', 453 | }, 454 | connection: { 455 | name: 'UserTaskConnection', 456 | type: Graph.taskType(), 457 | params: { 458 | orderBy: new GraphQLEnumType({ 459 | name: 'UserTaskConnectionOrder', 460 | values: { 461 | NAME: {value: ['title','DESC']} 462 | } 463 | }) 464 | } 465 | } 466 | }).connect(); 467 | 468 | const User = new Node({ 469 | model: DB.models.User 470 | }); 471 | 472 | const userType = Graph.userType({ 473 | tasks: { 474 | type: TaskConnector.connectionType, 475 | args: { 476 | ...TaskConnector.connectionArgs 477 | }, 478 | resolve: TaskConnector.resolve 479 | } 480 | }); 481 | 482 | const schema = Graph.createSchema({ 483 | user: { 484 | args: { 485 | id: { 486 | type: GraphQLString 487 | } 488 | }, 489 | type: userType, 490 | resolve: resolver(User, {thinky: DB.instance}) 491 | } 492 | }); 493 | 494 | const result = await Graph.executeQuery(schema, ` 495 | { 496 | user(id: "${user.id}") { 497 | name 498 | tasks(orderBy: NAME) { 499 | edges { 500 | node { 501 | title 502 | } 503 | } 504 | } 505 | } 506 | } 507 | `); 508 | 509 | if (result.errors) throw new Error(result.errors[0].stack); 510 | 511 | const resultOrderedDesc = await DB.models.Task.filter({assignee_id: user.id}).pluck('title').orderBy(DB.instance.r.desc('title')).run().map((task) => { 512 | return {node: {...task}}; 513 | }); 514 | 515 | expect(result.data.user.tasks.edges).to.be.deep.equal(resultOrderedDesc); 516 | }); 517 | 518 | test.serial('should filter a connection of results', async (t) => { 519 | 520 | const tasks = []; 521 | let user = await _.sample(t.context.users); 522 | 523 | 524 | // 2 tasks for each users 525 | t.context.users.forEach((user,key) => { 526 | tasks.push({title: 'My task'+key+user.id, description: 'My duty'+key, assignee_id: user.id}); 527 | }); 528 | 529 | t.context.tasks = await DB.models.Task.save(tasks); 530 | 531 | user = await user.getModel().get(user.id).getJoin({ tasks: true}).run(); 532 | 533 | const TaskConnector = new Node({ 534 | model: DB.models.Task, 535 | related: { 536 | ...DB.models.User._joins.tasks, 537 | parentModelName: 'user', 538 | relationName: 'tasks', 539 | }, 540 | thinky: DB.instance, 541 | connection: { 542 | name: 'UserTaskConnection', 543 | type: Graph.taskType(), 544 | params: { 545 | filters: { 546 | title: true 547 | } 548 | } 549 | } 550 | }).connect(); 551 | 552 | const User = new Node({ 553 | model: DB.models.User 554 | }); 555 | 556 | const userType = Graph.userType({ 557 | tasks: { 558 | type: TaskConnector.connectionType, 559 | args: { 560 | ...TaskConnector.connectionArgs, 561 | title: { 562 | type: GraphQLString 563 | } 564 | }, 565 | resolve: TaskConnector.resolve 566 | } 567 | }); 568 | 569 | const schema = Graph.createSchema({ 570 | user: { 571 | args: { 572 | id: { 573 | type: GraphQLString 574 | } 575 | }, 576 | type: userType, 577 | resolve: resolver(User) 578 | } 579 | }); 580 | 581 | const result = await Graph.executeQuery(schema, ` 582 | { 583 | user(id: "${user.id}") { 584 | name 585 | tasks(title: "${user.tasks[0].title}") { 586 | edges { 587 | node { 588 | title 589 | } 590 | } 591 | } 592 | } 593 | } 594 | `); 595 | 596 | if (result.errors) throw new Error(result.errors[0].stack); 597 | 598 | const tasksFormatted = user.tasks.map((task) => { 599 | return {node: {title: task.title}}; 600 | }); 601 | 602 | expect(result.data.user.tasks.edges[0]).to.be.deep.equal(tasksFormatted[0]); 603 | expect(result.data.user.tasks.edges.length).equal(1); 604 | }); 605 | 606 | test.serial('allow to list 2 connections', async (t) => { 607 | 608 | const tasks = []; 609 | // 2 tasks for each users 610 | t.context.users.forEach((user,key) => { 611 | tasks.push({title: 'My task'+key+user.id, description: 'My duty'+key, assignee_id: user.id}); 612 | }); 613 | 614 | t.context.tasks = await DB.models.Task.save(tasks); 615 | 616 | const TaskConnector = new Node({ 617 | model: DB.models.Task, 618 | related: { 619 | ...DB.models.User._joins.tasks, 620 | parentModelName: 'user', 621 | relationName: 'tasks', 622 | }, 623 | thinky: DB.instance, 624 | connection: { 625 | name: 'UserTaskConnection', 626 | type: Graph.taskType(), 627 | params: { 628 | filters: { 629 | title: true 630 | } 631 | } 632 | } 633 | }).connect(); 634 | 635 | const userType = Graph.userType({ 636 | tasks: { 637 | type: TaskConnector.connectionType, 638 | args: TaskConnector.connectionArgs, 639 | resolve: TaskConnector.resolve 640 | } 641 | }); 642 | 643 | const UserConnection = new Node({ 644 | model: DB.models.User, 645 | connection: { 646 | name: 'UserConnection', 647 | type: userType, 648 | } 649 | }).connect(); 650 | 651 | const schema = Graph.createSchema({ 652 | users: { 653 | type: UserConnection.connectionType, 654 | args:UserConnection.connectionArgs, 655 | resolve: UserConnection.resolve 656 | } 657 | }); 658 | 659 | const result = await Graph.executeQuery(schema, ` 660 | { 661 | users { 662 | edges { 663 | node { 664 | name 665 | tasks { 666 | edges { 667 | node { 668 | title 669 | } 670 | } 671 | } 672 | } 673 | } 674 | } 675 | } 676 | `); 677 | 678 | if (result.errors) throw new Error(result.errors[0].stack); 679 | 680 | result.data.users.edges.forEach((user) => { 681 | expect(user.node).to.be.defined; 682 | 683 | user.node.tasks.edges.forEach((task) => { 684 | expect(task.node).to.be.defined; 685 | }) 686 | }); 687 | }); 688 | -------------------------------------------------------------------------------- /test/integration/resolver.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expect} from 'chai'; 3 | import {DB,graphql as Graph} from './../helpers'; 4 | import { 5 | graphql, 6 | GraphQLList, 7 | GraphQLInt, 8 | GraphQLString, 9 | GraphQLEnumType, 10 | GraphQLNonNull 11 | } from 'graphql'; 12 | import _ from 'lodash'; 13 | import resolver from './../../src/resolver'; 14 | import Node from '../../src/node'; 15 | 16 | test.beforeEach(async function(t) { 17 | 18 | const users = t.context.users = await DB.models.User.save([ 19 | {name: 'jhon',username: 'doe'}, 20 | {name: 'fabri',username: 'fenos'}, 21 | {name: 'will',username: 'red'}, 22 | {name: 'smith',username: 'blue'}, 23 | {name: 'paul',username: 'orange'}, 24 | {name: 'tesla',username: 'ele'}, 25 | ]); 26 | 27 | const tasks = []; 28 | users.forEach((user,key) => { 29 | tasks.push({ 30 | title: 'My task'+key, 31 | number: key, 32 | description: 'My duty'+key, 33 | assignee_id: user.id 34 | }); 35 | }); 36 | 37 | return t.context.tasks = await DB.models.Task.save(tasks); 38 | }); 39 | 40 | test.afterEach(async (t) => { 41 | return await DB.clearDB(); 42 | }); 43 | 44 | test.after('cleanup' ,async function() { 45 | await DB.dropDB(); 46 | return await DB.instance.r.getPool().drain(); 47 | }); 48 | 49 | test.serial('should return a list (Array) of records given a Thinky model', async (t) => { 50 | 51 | const userType = Graph.userType({ 52 | tasks: { 53 | type: Graph.taskType() 54 | } 55 | }); 56 | 57 | const UserResolver = new Node({ 58 | model: DB.models.User 59 | }); 60 | 61 | const schema = Graph.createSchema({ 62 | users: { 63 | type: new GraphQLList(userType), 64 | resolve: resolver(UserResolver) 65 | } 66 | }); 67 | 68 | const result = await graphql(schema, ` 69 | { 70 | users { 71 | name 72 | } 73 | } 74 | `); 75 | 76 | if (result.errors) throw new Error(result.errors[0].stack); 77 | 78 | const users = t.context.users; 79 | expect(result.data.users).to.have.length.above(0); 80 | 81 | const usersNames = users.map(user => ({name: user.name})); 82 | // As the GraphQL query doesn't specify an ordering, 83 | // the order of the two lists can not be asserted. 84 | expect(result.data.users).to.deep.have.members(usersNames); 85 | }); 86 | 87 | test.serial('should return a single record (Object) given a Thinky model', async (t) => { 88 | 89 | const userType = Graph.userType(); 90 | 91 | const UserResolver = new Node({ 92 | model: DB.models.User 93 | }); 94 | 95 | const schema = Graph.createSchema({ 96 | user: { 97 | type: userType, 98 | resolve: resolver(UserResolver) 99 | } 100 | }); 101 | 102 | const result = await graphql(schema, ` 103 | { 104 | user { 105 | name 106 | username 107 | virtualName 108 | } 109 | } 110 | `); 111 | 112 | if (result.errors) throw new Error(result.errors[0].stack); 113 | 114 | expect(result.data.user).to.have.property('name') 115 | .that.is.a('string'); 116 | 117 | expect(result.data.user).to.have.property('username') 118 | .that.is.a('string'); 119 | 120 | expect(result.data.user).to.have.property('virtualName') 121 | .that.is.a('string'); 122 | }); 123 | 124 | test.serial('should find a record by id', async(t) => { 125 | 126 | const userType = Graph.userType(); 127 | const user = _.sample(t.context.users); 128 | 129 | const UserResolver = new Node({ 130 | name: 'users', 131 | model: DB.models.User 132 | }); 133 | 134 | const schema = Graph.createSchema({ 135 | user: { 136 | args: { 137 | id: { 138 | type: GraphQLString 139 | } 140 | }, 141 | type: userType, 142 | resolve: resolver(UserResolver) 143 | } 144 | }); 145 | 146 | const result = await graphql(schema, ` 147 | { 148 | user (id: "${user.id}") { 149 | id 150 | name 151 | username 152 | } 153 | } 154 | `); 155 | 156 | if (result.errors) throw new Error(result.errors[0].stack); 157 | 158 | expect(result.data).to.deep.equal({ 159 | user: { 160 | id: user.id, 161 | name: user.name, 162 | username: user.username 163 | } 164 | }); 165 | }); 166 | 167 | test.serial('should allow to manipulate query options', async(t) => { 168 | 169 | const userType = Graph.userType(); 170 | const user = _.find(t.context.users,{name: 'jhon'}); 171 | 172 | const UserResolver = new Node({ 173 | model: DB.models.User 174 | }); 175 | 176 | const schema = Graph.createSchema({ 177 | user: { 178 | type: userType, 179 | resolve: resolver(UserResolver, { 180 | before: (options,args) => { 181 | options.filter = options.filter || {}; 182 | options.filter.name = 'jhon'; 183 | return options; 184 | } 185 | }) 186 | } 187 | }); 188 | 189 | const result = await graphql(schema, ` 190 | { 191 | user { 192 | id 193 | name 194 | username 195 | } 196 | } 197 | `); 198 | 199 | if (result.errors) throw new Error(result.errors[0].stack); 200 | 201 | expect(result.data).to.deep.equal({ 202 | user: { 203 | id: user.id, 204 | name: user.name, 205 | username: user.username 206 | } 207 | }); 208 | }); 209 | 210 | test.serial('should limit results when limit arg is requested', async(t) => { 211 | 212 | const userType = Graph.userType(); 213 | 214 | const UserResolver = new Node({ 215 | model: DB.models.User 216 | }); 217 | 218 | const schema = Graph.createSchema({ 219 | users: { 220 | args: { 221 | offset: { 222 | type: GraphQLInt 223 | } 224 | }, 225 | type: new GraphQLList(userType), 226 | resolve: resolver(UserResolver, { 227 | list: true 228 | }) 229 | } 230 | }); 231 | 232 | const result = await graphql(schema, ` 233 | { 234 | users(offset: 2) { 235 | id 236 | name 237 | username 238 | } 239 | } 240 | `); 241 | 242 | if (result.errors) throw new Error(result.errors[0].stack); 243 | 244 | expect(result.data.users).to.have.lengthOf(2); 245 | }); 246 | 247 | test.serial('should add filter to a query', async(t) => { 248 | 249 | const userType = Graph.userType(); 250 | 251 | const UserResolver = new Node({ 252 | model: DB.models.User 253 | }); 254 | 255 | const schema = Graph.createSchema({ 256 | users: { 257 | type: new GraphQLList(userType), 258 | resolve: resolver(UserResolver, { 259 | before: (opts) => { 260 | opts.filter.name = 'fabri'; 261 | return opts; 262 | } 263 | }) 264 | } 265 | }); 266 | 267 | const result = await graphql(schema, ` 268 | { 269 | users { 270 | id 271 | name 272 | username 273 | } 274 | } 275 | `); 276 | 277 | if (result.errors) throw new Error(result.errors[0].stack); 278 | 279 | expect(result.data.users[0]).to.have.property('name').that.is.equal('fabri');// 280 | }); 281 | 282 | test.serial('should add filter to a nested query', async(t) => { 283 | 284 | const UserResolver = new Node({ 285 | model: DB.models.User 286 | }); 287 | 288 | const TaskResolver = new Node({ 289 | model: DB.models.Task, 290 | related: { 291 | ...DB.models.User._joins.tasks, 292 | parentModelName: 'user', 293 | } 294 | }); 295 | 296 | const userType = Graph.userType({ 297 | tasks: { 298 | type: new GraphQLList(Graph.taskType()), 299 | resolve: resolver(TaskResolver, { 300 | before: (opts) => { 301 | opts.filter.title = 'My task0'; 302 | return opts; 303 | } 304 | }) 305 | } 306 | }); 307 | 308 | const schema = Graph.createSchema({ 309 | users: { 310 | type: new GraphQLList(userType), 311 | resolve: resolver(UserResolver) 312 | } 313 | }); 314 | 315 | const result = await Graph.executeQuery(schema, ` 316 | { 317 | users { 318 | id 319 | name 320 | username 321 | 322 | tasks { 323 | title 324 | } 325 | } 326 | } 327 | `); 328 | 329 | if (result.errors) throw new Error(result.errors[0].stack); 330 | 331 | result.data.users.forEach((user) => { 332 | user.tasks.forEach((task) => { 333 | expect(task).to.have.property('title').that.is.equal('My task0'); 334 | }) 335 | }); 336 | }); 337 | 338 | test.serial('should order results by name ', async(t) => { 339 | 340 | const userType = Graph.userType(); 341 | 342 | const UserResolver = new Node({ 343 | model: DB.models.User 344 | }); 345 | 346 | const schema = Graph.createSchema({ 347 | users: { 348 | args: { 349 | order: { 350 | type: GraphQLString 351 | } 352 | }, 353 | type: new GraphQLList(userType), 354 | resolve: resolver(UserResolver) 355 | } 356 | }); 357 | 358 | const result = await Graph.executeQuery(schema, ` 359 | { 360 | users(order: "name") { 361 | id 362 | name 363 | username 364 | virtualName 365 | } 366 | } 367 | `); 368 | 369 | if (result.errors) throw new Error(result.errors[0].stack); 370 | 371 | let orderedUser = t.context.users.map(function(user) { 372 | return {...user}; 373 | }); 374 | orderedUser = _.orderBy(orderedUser,'name'); 375 | 376 | expect(result.data.users).to.deep.have.equal(orderedUser); 377 | }); 378 | 379 | test.serial('should order results to desc', async(t) => { 380 | 381 | const userType = Graph.userType(); 382 | 383 | const UserResolver = new Node({ 384 | name: 'users', 385 | model: DB.models.User 386 | }); 387 | 388 | const schema = Graph.createSchema({ 389 | users: { 390 | args: { 391 | order: { 392 | type: GraphQLString 393 | } 394 | }, 395 | type: new GraphQLList(userType), 396 | resolve: resolver(UserResolver,{thinky: DB.instance}) 397 | } 398 | }); 399 | 400 | const result = await graphql(schema, ` 401 | { 402 | users(order: "reverse:name") { 403 | id 404 | name 405 | username 406 | virtualName 407 | } 408 | } 409 | `); 410 | 411 | if (result.errors) throw new Error(result.errors[0].stack); 412 | 413 | let orderedUser = t.context.users.map(function(user) { 414 | return {...user}; 415 | }); 416 | orderedUser = _.orderBy(orderedUser,'name','desc'); 417 | expect(result.data.users).to.deep.have.equal(orderedUser); 418 | }); 419 | 420 | test.serial('should allow manipulate the query results', async(t) => { 421 | 422 | const userType = Graph.userType(); 423 | 424 | const UserResolver = new Node({ 425 | model: DB.models.User 426 | }); 427 | 428 | const schema = Graph.createSchema({ 429 | users: { 430 | type: new GraphQLList(userType), 431 | resolve: resolver(UserResolver, { 432 | after: (result,args,context) => { 433 | return result.map(function () { 434 | return { 435 | name: 'GRAPH' 436 | }; 437 | }); 438 | } 439 | }) 440 | } 441 | }); 442 | 443 | const result = await graphql(schema, ` 444 | { 445 | users { 446 | name 447 | } 448 | } 449 | `); 450 | 451 | if (result.errors) throw new Error(result.errors[0].stack); 452 | 453 | const users = t.context.users; 454 | 455 | expect(result.data.users).to.have.length(users.length); 456 | 457 | result.data.users.forEach(function (user) { 458 | expect(user.name).to.equal('GRAPH'); 459 | }); 460 | }); 461 | 462 | test.serial('should allow for nested fetching', async(t) => { 463 | 464 | const UserResolver = new Node({ 465 | name: 'users', 466 | model: DB.models.User 467 | }); 468 | 469 | const TaskResolver = new Node({ 470 | model: DB.models.Task, 471 | related: { 472 | ...DB.models.User._joins.tasks, 473 | parentModelName: 'user', 474 | relationName: 'tasks' 475 | } 476 | }); 477 | 478 | const userType = Graph.userType({ 479 | tasks: { 480 | type: new GraphQLList(Graph.taskType()), 481 | resolve: resolver(TaskResolver) 482 | } 483 | }); 484 | 485 | const user = t.context.users[0]; 486 | 487 | const schema = Graph.createSchema({ 488 | user: { 489 | type: userType, 490 | args: { 491 | id: { 492 | type: new GraphQLNonNull(GraphQLString) 493 | } 494 | }, 495 | resolve: resolver(UserResolver), 496 | } 497 | }); 498 | 499 | const result = await Graph.executeQuery(schema, ` 500 | { 501 | user(id: "${user.id}") { 502 | name 503 | tasks { 504 | title 505 | } 506 | } 507 | } 508 | `); 509 | 510 | if (result.errors) throw new Error(result.errors[0].stack); 511 | 512 | expect(result.data.user.tasks).to.have.length.within(0,1000); 513 | 514 | }); 515 | 516 | test.serial('should allow for nested recursive fetching', async(t) => { 517 | 518 | const tags = []; 519 | t.context.tasks.forEach( (task, key) => { 520 | const Tag = new DB.models.Tag({ 521 | name: 'tag' + key, 522 | description: 'tag-n-' + key 523 | }); 524 | task.tags = [Tag]; 525 | tags.push(task.saveAll({tags: true})); 526 | }); 527 | 528 | await Promise.all(tags); 529 | 530 | const User = DB.models.User; 531 | const Task = DB.models.Task; 532 | const Tag = DB.models.Tag; 533 | 534 | const UserResolver = new Node({ 535 | model: User 536 | }); 537 | 538 | const TagResolver = new Node({ 539 | model: Tag, 540 | related: { 541 | ...Task._joins.tags, 542 | parentModelName: 'task', 543 | relationName: 'tags' 544 | } 545 | }); 546 | 547 | const TaskResolver = new Node({ 548 | model: Task, 549 | related: { 550 | ...User._joins.tasks, 551 | parentModelName: 'user', 552 | relationName: 'tasks' 553 | } 554 | }); 555 | 556 | const userType = Graph.userType({ 557 | tasks: { 558 | type: new GraphQLList(Graph.taskType({ 559 | tags: { 560 | type: new GraphQLList(Graph.tagType()), 561 | resolve: resolver(TagResolver,{ 562 | before: (opts) => { 563 | opts.order = ['name','ASC']; 564 | return opts; 565 | } 566 | }) 567 | } 568 | })), 569 | resolve: resolver(TaskResolver) 570 | } 571 | }); 572 | 573 | const user = t.context.users[0]; 574 | 575 | const schema = Graph.createSchema({ 576 | user: { 577 | type: userType, 578 | resolve: resolver(UserResolver), 579 | args: { 580 | id: { 581 | type: new GraphQLNonNull(GraphQLString) 582 | } 583 | } 584 | } 585 | }); 586 | 587 | const result = await Graph.executeQuery(schema, ` 588 | { 589 | user(id: "${user.id}") { 590 | name 591 | tasks { 592 | title 593 | tags { 594 | name 595 | } 596 | } 597 | } 598 | } 599 | `); 600 | 601 | if (result.errors) throw new Error(result.errors[0].stack); 602 | 603 | result.data.user.tasks.forEach((task) => { 604 | task.tags.forEach((tag,key) => { 605 | expect(tag).to.deep.equal({ 606 | name: 'tag' + key 607 | }); 608 | }); 609 | }); 610 | 611 | }); 612 | 613 | test.serial('should allow for nested recursive fetching 3 level', async(t) => { 614 | 615 | const tags = []; 616 | t.context.tasks.forEach((task, key) => { 617 | const Tag = new DB.models.Tag({ 618 | name: 'tag' + key, 619 | description: 'tag-n-' + key 620 | }); 621 | task.tags = [Tag]; 622 | tags.push(task.saveAll({ tags: true })); 623 | }); 624 | 625 | await Promise.all(tags); 626 | 627 | const User = DB.models.User; 628 | const Task = DB.models.Task; 629 | const Tag = DB.models.Tag; 630 | 631 | const UserResolver = new Node({ 632 | model: User 633 | }); 634 | 635 | const TagTaskResolver = new Node({ 636 | model: Tag, 637 | related: { 638 | ...Tag._joins.tasks, 639 | parentModelName: 'task', 640 | relationName: 'tags' 641 | } 642 | }); 643 | 644 | const UserTaskResolver = new Node({ 645 | model: Task, 646 | related: { 647 | ...User._joins.tasks, 648 | parentModelName: 'user', 649 | relationName: 'tasks' 650 | } 651 | }); 652 | 653 | const TaskTagResolver = new Node({ 654 | model: Task, 655 | related: { 656 | ...Tag._joins.tasks, 657 | parentModelName: 'tag', 658 | relationName: 'tasks' 659 | } 660 | }); 661 | 662 | let TaskType = Graph.taskType({ 663 | tags: { 664 | type: new GraphQLList(Graph.tagType(() => { 665 | return { 666 | tasks: ({ 667 | args: { 668 | limit: { 669 | type: GraphQLInt 670 | } 671 | }, 672 | type: new GraphQLList(TaskType), 673 | resolve: resolver(TaskTagResolver) 674 | }) 675 | } 676 | })), 677 | resolve: resolver(TagTaskResolver) 678 | } 679 | }); 680 | 681 | const userType = Graph.userType({ 682 | tasks: { 683 | args: { 684 | limit: { 685 | type: GraphQLInt 686 | } 687 | }, 688 | type: new GraphQLList(TaskType), 689 | resolve: resolver(UserTaskResolver) 690 | } 691 | }); 692 | 693 | const user = t.context.users[0]; 694 | 695 | const schema = Graph.createSchema({ 696 | user: { 697 | type: userType, 698 | resolve: resolver(UserResolver, { 699 | thinky: DB.instance 700 | }), 701 | args: { 702 | id: { 703 | type: new GraphQLNonNull(GraphQLString) 704 | } 705 | } 706 | } 707 | }); 708 | 709 | const result = await Graph.executeQuery(schema, ` 710 | { 711 | user(id: "${user.id}") { 712 | name 713 | tasks { 714 | title 715 | tags { 716 | name 717 | tasks(limit: 5) { 718 | title 719 | } 720 | } 721 | } 722 | } 723 | } 724 | `); 725 | 726 | if (result.errors) throw new Error(result.errors[0].stack); 727 | 728 | result.data.user.tasks.forEach((task) => { 729 | task.tags.forEach((tag, key) => { 730 | tag.tasks.forEach((task1) => { 731 | expect(task1).to.include.keys('title'); 732 | }); 733 | }); 734 | }); 735 | }); 736 | 737 | test.serial("it should handle empty results", async(t) => { 738 | 739 | const UserModel = DB.models.User; 740 | await UserModel.delete(); 741 | 742 | const UserResolver = new Node({ 743 | model: UserModel 744 | }); 745 | 746 | const userType = Graph.userType(); 747 | 748 | const schema = Graph.createSchema({ 749 | users: { 750 | type: new GraphQLList(userType), 751 | resolve: resolver(UserResolver, { 752 | thinky: DB.instance 753 | }) 754 | } 755 | }); 756 | 757 | const result = await graphql(schema, ` 758 | { 759 | users { 760 | name 761 | } 762 | } 763 | `, null); 764 | 765 | expect(result.data.users).to.be.empty; 766 | }); 767 | 768 | test.serial("it should fetch default attributes", async(t) => { 769 | 770 | const UserModel = DB.models.User; 771 | 772 | const UserResolver = new Node({ 773 | model: UserModel 774 | }); 775 | 776 | const userType = Graph.userType(); 777 | 778 | const schema = Graph.createSchema({ 779 | users: { 780 | type: new GraphQLList(userType), 781 | resolve: resolver(UserResolver, { 782 | requestedFields: ['surname'], 783 | }) 784 | } 785 | }); 786 | 787 | const result = await graphql(schema, ` 788 | { 789 | users { 790 | name 791 | } 792 | } 793 | `, null); 794 | 795 | result.data.users.forEach(user => { 796 | expect(user.name).to.be.a('string'); 797 | expect(user.surname).to.be.a('string'); 798 | }); 799 | }); 800 | 801 | test.serial("it should not allow limiting result more then the default limit to prevent hacks", async(t) => { 802 | 803 | const UserModel = DB.models.User; 804 | 805 | const UserResolver = new Node({ 806 | model: UserModel 807 | }); 808 | 809 | const userType = Graph.userType(); 810 | 811 | const schema = Graph.createSchema({ 812 | users: { 813 | type: new GraphQLList(userType), 814 | args: { 815 | limit: { 816 | type: GraphQLInt 817 | } 818 | }, 819 | resolve: resolver(UserResolver, { 820 | maxLimit: 2, 821 | thinky: DB.instance 822 | }) 823 | } 824 | }); 825 | 826 | const result = await Graph.executeQuery(schema, ` 827 | { 828 | users(limit: 100000) { 829 | name 830 | } 831 | } 832 | `, null); 833 | 834 | expect(result.data.users).to.have.lengthOf(2); 835 | }); 836 | 837 | test.serial("it should allow to overwrite and compose a custom query on each Node", async(t) => { 838 | 839 | const UserModel = DB.models.User; 840 | const TaskModel = DB.models.Task; 841 | 842 | const user = await UserModel.get(t.context.users[0].id).getJoin({tasks: true}).run(); 843 | const task = user.tasks[0]; 844 | 845 | const UserResolver = new Node({ 846 | model: UserModel, 847 | query: (seq,args,thinky) => { 848 | return seq.filter({id: user.id}); 849 | } 850 | }); 851 | 852 | const TaskResolver = new Node({ 853 | model: TaskModel, 854 | query: (seq) => { 855 | return seq.filter({id: task.id}); 856 | } 857 | }); 858 | 859 | const userType = Graph.userType({ 860 | tasks: { 861 | type: new GraphQLList(Graph.taskType()), 862 | resolve: resolver(TaskResolver,{ 863 | 864 | }) 865 | } 866 | }); 867 | 868 | const schema = Graph.createSchema({ 869 | users: { 870 | type: new GraphQLList(userType), 871 | resolve: resolver(UserResolver, { 872 | thinky: DB.instance 873 | }) 874 | } 875 | }); 876 | 877 | const result = await Graph.executeQuery(schema, ` 878 | { 879 | users { 880 | id 881 | name 882 | tasks { 883 | id 884 | title 885 | } 886 | } 887 | } 888 | `, null); 889 | 890 | expect(result.data.users).to.have.lengthOf(1); 891 | expect(result.data.users[0].id).to.equal(user.id); 892 | expect(result.data.users[0].tasks[0].id).to.equal(task.id); 893 | }); -------------------------------------------------------------------------------- /test/unit/modelToGQLObjectType.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { 3 | GraphQLObjectType, 4 | GraphQLString, 5 | GraphQLInt 6 | } from 'graphql'; 7 | 8 | import {expect} from 'chai'; 9 | import modelToGQLObjectType from '../../src/modelToGqlObjectType'; 10 | import thinky from './../helpers/db/thinky'; 11 | const type = thinky.type; 12 | 13 | test('it should create a GraphQLObjectType from a Thinky Model', () => { 14 | 15 | const Model = thinky.createModel('car1', { 16 | brand: type.string(), 17 | name: type.string() 18 | }); 19 | 20 | const result = modelToGQLObjectType(Model); 21 | const fields = result._typeConfig.fields(); 22 | 23 | expect(result).to.be.an.instanceof(GraphQLObjectType); 24 | expect(result.name).to.be.equal('Car1'); 25 | expect(fields.id.type).to.be.equal(GraphQLString); 26 | expect(fields.brand.type).to.be.equal(GraphQLString); 27 | expect(fields.name.type).to.be.equal(GraphQLString); 28 | }); 29 | 30 | test('it should allow to extend the ObjectType Definition', () => { 31 | 32 | const Model = thinky.createModel('car2', { 33 | brand: type.string(), 34 | name: type.string() 35 | }); 36 | 37 | const result = modelToGQLObjectType(Model, { 38 | fields: () => ({ 39 | wheels: { 40 | type: GraphQLInt 41 | } 42 | }) 43 | }); 44 | 45 | const fields = result._typeConfig.fields(); 46 | 47 | expect(fields.wheels.type).to.be.equal(GraphQLInt); 48 | }); 49 | 50 | test('it should allow to use the TypeMapper option to define the GraphQLObjectType', () => { 51 | 52 | const Model = thinky.createModel('car3', { 53 | brand: type.string(), 54 | name: type.string() 55 | }); 56 | 57 | const result = modelToGQLObjectType(Model, { 58 | fields: { 59 | wheels: { 60 | type: GraphQLInt 61 | } 62 | }, 63 | exclude: ['brand'] 64 | }); 65 | 66 | const fields = result._typeConfig.fields(); 67 | 68 | expect(fields.name.type).to.be.equal(GraphQLString); 69 | expect(fields.brand).to.be.undefined; 70 | }); 71 | 72 | test('it should allow to overwrite a field from an automatic generated one', () => { 73 | 74 | const Model = thinky.createModel('car4', { 75 | brand: type.string(), 76 | name: type.string() 77 | }); 78 | 79 | const result = modelToGQLObjectType(Model, { 80 | fields: { 81 | brand: { 82 | type: GraphQLInt 83 | } 84 | } 85 | }); 86 | 87 | const fields = result._typeConfig.fields(); 88 | 89 | expect(fields.brand.type).to.be.equal(GraphQLInt); 90 | }); -------------------------------------------------------------------------------- /test/unit/simplyAST.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import simplifyAST from '../../src/simplifyAst'; 3 | import {parse as parser} from 'graphql/language/parser'; 4 | import {expect} from 'chai'; 5 | 6 | const parse = function (query) { 7 | return parser(query).definitions[0]; 8 | }; 9 | 10 | test('should simplify a basic nested structure', () => { 11 | expect(simplifyAST(parse(` 12 | { 13 | users { 14 | name 15 | projects { 16 | name 17 | } 18 | } 19 | } 20 | `))).to.deep.equal({ 21 | args: {}, 22 | fields: { 23 | users: { 24 | args: {}, 25 | fields: { 26 | name: { 27 | args: {}, 28 | fields: {} 29 | }, 30 | projects: { 31 | args: {}, 32 | fields: { 33 | name: { 34 | args: {}, 35 | fields: {} 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | }); 43 | }); 44 | 45 | test('should simplify a basic structure with args', () => { 46 | expect(simplifyAST(parse(` 47 | { 48 | user(id: 1) { 49 | name 50 | } 51 | } 52 | `))).to.deep.equal({ 53 | args: {}, 54 | fields: { 55 | user: { 56 | args: { 57 | id: "1" 58 | }, 59 | fields: { 60 | name: { 61 | args: {}, 62 | fields: {} 63 | } 64 | } 65 | } 66 | } 67 | }); 68 | }); 69 | 70 | test('should simplify a basic structure with an inline fragment', () => { 71 | expect(simplifyAST(parse(` 72 | { 73 | user { 74 | ... on User { 75 | name 76 | } 77 | } 78 | } 79 | `))).to.deep.equal({ 80 | args: {}, 81 | fields: { 82 | user: { 83 | args: {}, 84 | fields: { 85 | name: { 86 | args: {}, 87 | fields: {} 88 | } 89 | } 90 | } 91 | } 92 | }); 93 | }); 94 | 95 | test('should expose a $parent', () => { 96 | var ast = simplifyAST(parse(` 97 | { 98 | users { 99 | name 100 | projects(first: 1) { 101 | nodes { 102 | name 103 | } 104 | } 105 | } 106 | } 107 | `)); 108 | 109 | expect(ast.fields.users.fields.projects.fields.nodes.$parent).to.be.ok; 110 | expect(ast.fields.users.fields.projects.fields.nodes.$parent.args).to.deep.equal({ 111 | first: '1' 112 | }); 113 | }); 114 | 115 | test('should simplify a nested structure at the lowest level', () => { 116 | expect(simplifyAST(parse(` 117 | { 118 | users { 119 | name 120 | projects { 121 | node { 122 | name 123 | } 124 | node { 125 | id 126 | } 127 | } 128 | } 129 | } 130 | `))).to.deep.equal({ 131 | args: {}, 132 | fields: { 133 | users: { 134 | args: {}, 135 | fields: { 136 | name: { 137 | args: {}, 138 | fields: {} 139 | }, 140 | projects: { 141 | args: {}, 142 | fields: { 143 | node: { 144 | args: {}, 145 | fields: { 146 | name: { 147 | args: {}, 148 | fields: {} 149 | }, 150 | id: { 151 | args: {}, 152 | fields: {} 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | }); 162 | }); 163 | 164 | test('should simplify a nested structure duplicated at a high level', () => { 165 | expect(simplifyAST(parse(` 166 | { 167 | users { 168 | name 169 | projects { 170 | node { 171 | name 172 | } 173 | } 174 | projects { 175 | node { 176 | id 177 | } 178 | } 179 | } 180 | } 181 | `))).to.deep.equal({ 182 | args: {}, 183 | fields: { 184 | users: { 185 | args: {}, 186 | fields: { 187 | name: { 188 | args: {}, 189 | fields: {} 190 | }, 191 | projects: { 192 | args: {}, 193 | fields: { 194 | node: { 195 | args: {}, 196 | fields: { 197 | name: { 198 | args: {}, 199 | fields: {} 200 | }, 201 | id: { 202 | args: {}, 203 | fields: {} 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | }); 213 | }); 214 | 215 | test('should simplify a structure with aliases', () => { 216 | expect(simplifyAST(parse(` 217 | { 218 | luke: human(id: "1000") { 219 | name 220 | } 221 | leia: human(id: "1003") { 222 | firstName: name 223 | } 224 | } 225 | `))).to.deep.equal({ 226 | args: {}, 227 | fields: { 228 | luke: { 229 | key: "human", 230 | args: { 231 | id: "1000" 232 | }, 233 | fields: { 234 | name: { 235 | args: {}, 236 | fields: {} 237 | } 238 | } 239 | }, 240 | leia: { 241 | key: "human", 242 | args: { 243 | id: "1003" 244 | }, 245 | fields: { 246 | firstName: { 247 | key: "name", 248 | args: {}, 249 | fields: {} 250 | } 251 | } 252 | } 253 | } 254 | }) 255 | }); -------------------------------------------------------------------------------- /test/unit/typeMapper.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import thinky from './../helpers/db/thinky'; 4 | import thinkySchema from 'thinky-export-schema'; 5 | import {expect} from 'chai'; 6 | import {toGraphQLDefinition,attributeToGraphQLType} from './../../src/typeMapper'; 7 | import { 8 | GraphQLString, 9 | GraphQLBoolean, 10 | GraphQLInt, 11 | GraphQLObjectType, 12 | GraphQLList, 13 | GraphQLEnumType, 14 | GraphQLNonNull 15 | } from 'graphql'; 16 | 17 | import { 18 | toGlobalId 19 | } from 'graphql-relay'; 20 | 21 | const thinkyType = thinky.type; 22 | 23 | 24 | test('it should return a GraphQLString for types: [string, date, id]', () => { 25 | 26 | const resultWithString = attributeToGraphQLType(thinkyType.string()); 27 | const resultWithDate = attributeToGraphQLType(thinkyType.date()); 28 | const resultWithID = attributeToGraphQLType(thinkyType.id()); 29 | 30 | expect(resultWithString).to.be.equal(GraphQLString); 31 | expect(resultWithDate).to.be.equal(GraphQLString); 32 | expect(resultWithID).to.be.equal(GraphQLString); 33 | 34 | }); 35 | 36 | test('it should return a GraphQLBoolean for types: [boolean]', () => { 37 | 38 | const result = attributeToGraphQLType(thinkyType.boolean()); 39 | 40 | expect(result).to.be.equal(GraphQLBoolean); 41 | }); 42 | 43 | 44 | test('it should return a GraphQLInt for types: [number]', () => { 45 | 46 | const result = attributeToGraphQLType(thinkyType.number()); 47 | 48 | expect(result).to.be.equal(GraphQLInt); 49 | }); 50 | 51 | test('it should return a GraphQLObjectType for types: [object]', () => { 52 | 53 | const result = attributeToGraphQLType(thinkyType.object().schema({ 54 | profile: thinkyType.string() 55 | }),'images'); 56 | 57 | expect(result).to.be.an.instanceof(GraphQLObjectType); 58 | expect(result._typeConfig.fields.profile.type).to.be.equal(GraphQLString); 59 | }); 60 | 61 | test('it should throw exception if the name of a object is not Specified', () => { 62 | 63 | const result = () => { 64 | return attributeToGraphQLType(thinkyType.object().schema({ 65 | profile: thinkyType.string() 66 | })); 67 | }; 68 | 69 | expect(result).to.throw('Specify a name for the Object Attribute type'); 70 | }); 71 | 72 | test('it should return a GraphQLList for types: [array]', () => { 73 | 74 | const result = attributeToGraphQLType( 75 | thinkyType.array().schema(thinkyType.number()) 76 | ); 77 | 78 | expect(result).to.be.an.instanceof(GraphQLList); 79 | }); 80 | 81 | test('it should return a GraphQLList of GraphQLObjectType for type: [array]', () => { 82 | 83 | const result = attributeToGraphQLType( 84 | thinkyType.array().schema(thinkyType.object().schema({ 85 | images: thinkyType.string() 86 | })),'images' 87 | ); 88 | 89 | expect(result).to.be.an.instanceof(GraphQLList); 90 | }); 91 | 92 | test('it should return a GraphQLENUM for type: [string,enum]', () => { 93 | 94 | const result = attributeToGraphQLType( 95 | thinkyType.string().enum(['yes','no']) 96 | ); 97 | 98 | expect(result).to.be.an.instanceof(GraphQLEnumType); 99 | }); 100 | 101 | 102 | test("it transform a thinky model with string attrs, to a GraphQL definition", () => { 103 | 104 | const Model = thinky.createModel('user-test', { 105 | name: thinkyType.string(), 106 | surname: thinkyType.string() 107 | }); 108 | 109 | const GraphQLFields = toGraphQLDefinition(Model); 110 | 111 | Object.keys(GraphQLFields).forEach((key) => { 112 | expect(GraphQLFields[key].type).to.be.equal(GraphQLString); 113 | }); 114 | }); 115 | 116 | test("it transform a thinky model to a GraphQL definition", () => { 117 | 118 | const Model = thinky.createModel('usertest1', { 119 | name: thinkyType.string(), 120 | surname: thinkyType.string(), 121 | images: thinkyType.object().schema({ 122 | profile: thinkyType.string(), 123 | cover: thinkyType.string() 124 | }), 125 | social: thinkyType.array().schema(thinkyType.string()), 126 | myVirtual: thinkyType.virtual(), 127 | myAny: thinkyType.any() 128 | }); 129 | 130 | const GraphQLFields = toGraphQLDefinition(Model); 131 | expect(GraphQLFields['id'].type).to.be.equal(GraphQLString); 132 | expect(GraphQLFields['name'].type).to.be.equal(GraphQLString); 133 | expect(GraphQLFields['surname'].type).to.be.equal(GraphQLString); 134 | expect(GraphQLFields['images'].type).to.be.an.instanceof(GraphQLObjectType); 135 | expect(GraphQLFields['images'].type._typeConfig.fields.profile.type).to.be.equal(GraphQLString); 136 | expect(GraphQLFields['images'].type._typeConfig.fields.cover.type).to.be.equal(GraphQLString); 137 | expect(GraphQLFields['social'].type).to.be.an.instanceof(GraphQLList); 138 | 139 | // Virtual and ANY are not converted to a graphql definition 140 | // because we can't strongly type them. 141 | expect(GraphQLFields['myVirtual']).to.be.undefined; 142 | expect(GraphQLFields['myAny']).to.be.undefined; 143 | }); 144 | 145 | test("it should add GraphQLNotNull type to required fields", () => { 146 | 147 | const Model = thinky.createModel('user-test-5', { 148 | name: thinkyType.string().allowNull(false), 149 | surname: thinkyType.string(), 150 | email: thinkyType.string() 151 | }); 152 | 153 | const GraphQLFields = toGraphQLDefinition(Model); 154 | 155 | expect(GraphQLFields['name'].type).to.be.instanceof(GraphQLNonNull); 156 | }); 157 | 158 | test("it should by pass the not null definition", () => { 159 | 160 | const Model = thinky.createModel('user-test-7', { 161 | name: thinkyType.string().allowNull(false), 162 | surname: thinkyType.string(), 163 | email: thinkyType.string() 164 | }); 165 | 166 | const GraphQLFields = toGraphQLDefinition(Model, { 167 | allowNull: false 168 | }); 169 | 170 | expect(GraphQLFields['name'].type).to.be.not.instanceof(GraphQLNonNull); 171 | expect(GraphQLFields['name'].type).to.be.equal(GraphQLString); 172 | }); 173 | 174 | test("it should exclude the specified attributes from the GraphQL Definition", () => { 175 | 176 | const Model = thinky.createModel('user-test-2', { 177 | name: thinkyType.string(), 178 | surname: thinkyType.string(), 179 | email: thinkyType.string() 180 | }); 181 | 182 | const GraphQLFields = toGraphQLDefinition(Model, { 183 | exclude: ['name','surname'] 184 | }); 185 | 186 | expect(GraphQLFields['name']).to.be.undefined 187 | expect(GraphQLFields['surname']).to.be.undefined; 188 | expect(GraphQLFields['email'].type).to.be.equal(GraphQLString); 189 | 190 | }); 191 | 192 | test("it should convert only the specified attributes to a GraphQL Definition", () => { 193 | 194 | const Model = thinky.createModel('user-test-3', { 195 | name: thinkyType.string(), 196 | surname: thinkyType.string(), 197 | email: thinkyType.string() 198 | }); 199 | 200 | const GraphQLFields = toGraphQLDefinition(Model, { 201 | only: ['name','surname'] 202 | }); 203 | 204 | expect(GraphQLFields['name'].type).to.be.equal(GraphQLString); 205 | expect(GraphQLFields['surname'].type).to.be.equal(GraphQLString); 206 | expect(GraphQLFields['email']).to.be.undefined; 207 | 208 | }); 209 | 210 | test("it should map fields", () => { 211 | 212 | const Model = thinky.createModel('user-test-4', { 213 | name: thinkyType.string(), 214 | surname: thinkyType.string(), 215 | email: thinkyType.string() 216 | }); 217 | 218 | const GraphQLFields = toGraphQLDefinition(Model, { 219 | map: { 220 | email: 'EMAIL' 221 | } 222 | }); 223 | 224 | expect(GraphQLFields['EMAIL'].type).to.be.equal(GraphQLString); 225 | expect(GraphQLFields['email']).to.be.undefined; 226 | }); 227 | 228 | test("it should add a relay global ID", () => { 229 | 230 | const Model = thinky.createModel('user-test-6', { 231 | name: thinkyType.string(), 232 | surname: thinkyType.string(), 233 | email: thinkyType.string() 234 | }); 235 | 236 | const GraphQLFields = toGraphQLDefinition(Model, { 237 | globalId: true 238 | }); 239 | 240 | expect(GraphQLFields.id.resolve).to.be.ok; 241 | expect(GraphQLFields.id.type.ofType.name).to.equal('ID'); 242 | expect(GraphQLFields.id.resolve({ 243 | id: 'hello' 244 | })).to.equal(toGlobalId('User-test-6', 'hello')); 245 | 246 | expect(GraphQLFields['user-test-6ID'].type).to.be.deep.equal(GraphQLString); 247 | }); --------------------------------------------------------------------------------