├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── demo ├── .babelrc ├── .gitignore ├── .sequelizerc ├── package-lock.json ├── package.json └── server │ ├── config │ └── database.js │ ├── graphql │ ├── queryLoaderConfig.js │ ├── resolvers │ │ ├── article.js │ │ ├── category.js │ │ ├── index.js │ │ └── user.js │ └── schemas │ │ ├── article.js │ │ ├── category.js │ │ ├── comment.js │ │ ├── index.js │ │ └── user.js │ ├── index.js │ ├── migrations │ ├── 20190204140700-create-user.js │ ├── 20190204140701-create-category.js │ ├── 20190204140742-create-article.js │ └── 20190216132118-create-comment.js │ ├── models │ ├── article.js │ ├── category.js │ ├── comment.js │ ├── index.js │ └── user.js │ └── seeders │ └── dbSeed.js ├── jestconfig.json ├── package-lock.json ├── package.json ├── src ├── __mocks__ │ ├── getArticlesArgsInfo.ts │ ├── getArticlesInfo.ts │ ├── getArticlesWithIncludesInfo.ts │ ├── getCategoryDeepInfo.ts │ ├── getCategoryInfo.ts │ └── models │ │ ├── articleModel.ts │ │ ├── categoryModel.ts │ │ ├── commentModel.ts │ │ └── userModel.ts ├── __tests__ │ └── queryLoader.spec.ts ├── index.ts ├── queryLoader.ts └── types.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | # General settings for whole project 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | # Format specific overrides 10 | [*.md] 11 | max_line_length = 0 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | /coverage 4 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - "10.15.0" 5 | before_script: 6 | - npm install 7 | script: 8 | - npm test 9 | - npm run coverage -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Samuel Osuh and all collaborators 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-sequelize-query-loader 2 | 3 | Convert GraphQL Query to query options for Sequelize models facilitating eagerloading of associated resources. 4 | 5 | [![codecov](https://codecov.io/gh/jsamchineme/graphql-sequelize-query-loader/branch/master/graph/badge.svg)](https://codecov.io/gh/jsamchineme/graphql-sequelize-query-loader) 6 | [![Build Status](https://travis-ci.com/jsamchineme/graphql-sequelize-query-loader.svg?branch=master)](https://travis-ci.com/jsamchineme/graphql-sequelize-query-loader) 7 | [![codebeat badge](https://codebeat.co/badges/0c5b767b-a1e4-4f4f-9feb-5911690d1077)](https://codebeat.co/projects/github-com-jsamchineme-graphql-sequelize-query-loader-master) 8 | [![License](https://badgen.net/github/license/jsamchineme/graphql-sequelize-query-loader)](https://github.com/jsamchineme/graphql-sequelize-query-loader/blob/master/LICENCE) 9 | 10 | ### Overview 11 | Essentially, the tool expects that all sequelize models have set on them the appropriate associations to all related resources. Then, when given a GraphQL query, it parses the info object made available as a parameter in GraphQL Query Resolvers, producing an object which includes ONLY the selected resources and with ONLY the specified attributes. 12 |
13 | This solves for the `N + 1` problem with database querying, as well as the issue of `over-fetching` of table attributes in result sets. 14 | 15 | ## Installation 16 | `$ npm install --save graphql-sequelize-query-loader` 17 | 18 | ## Pre-requisites 19 | In order to use the helper to eagerload related entities, you need to have setup the associations on the Sequelize Models. 20 | 21 | ## Features 22 | - maps the selected fields (in all models found in a GraphQL query) to the `attributes` option for Sequelize `Model.Find 23 | - maps included models found in the GraphQL query to `include` option properties 24 | - converts `scope` argument in GraphQL query and turns them to `where` option properties 25 | 26 | ## Usage 27 | ```js 28 | import queryLoader from 'graphql-sequelize-query-loader'; 29 | import models from 'path/to/sequelize/models'; 30 | 31 | /** 32 | * dictionary of what sequelize models respectively match the named resources 33 | * captured on the graphql schema 34 | */ 35 | const includeModels = { 36 | articles: models.Article, 37 | article: models.Article, 38 | owner: models.User, 39 | category: models.category, 40 | comments: models.Comment, 41 | }; 42 | 43 | /* 44 | * Initiliase the loader with "includeModels", 45 | * a map of all models referenced in GraphQL with their respective Sequelize Model 46 | */ 47 | queryLoader.init({ includeModels }); 48 | 49 | /** 50 | * GraphQL 51 | */ 52 | Query: { 53 | articles: async (parent, args, { models }, info) => { 54 | const { Article } = models; 55 | const queryOptions = queryLoader.getFindOptions({ model: Article, info }); 56 | const articles = await Article.findAll(queryOptions); 57 | return articles; 58 | }, 59 | }, 60 | ``` 61 | 62 | ## Examples 63 | You can find examples in the [demo](https://github.com/jsamchineme/graphql-sequelize-query-loader/tree/add-example-project/demo) directory. 64 | It contains migrations, models and seeds setup for testing out the app. 65 |
It also contains graphql schemas and resolvers with examples of how the `queryLoader` utility is used 66 | 67 | ## Testing the Demo 68 | On the [git repo](https://github.com/jsamchineme/graphql-sequelize-query-loader/tree/add-example-project/demo) 69 | You can quickly test the demo and play with the code using `Gitpod`. 70 |
Gitpod is a full blown IDE created on the fly from a git repository. 71 |
It allows us to test a github project with just the browser. 72 | Learn [more about gitpod](https://www.gitpod.io/) OR [install the chrome extension](https://chrome.google.com/webstore/detail/gitpod-online-ide/dodmmooeoklaejobgleioelladacbeki) 73 | 74 | - clone the repo, if you want to test locally 75 | - check into the `demo` directory. `$cd demo` 76 | - environment should be set to `development`. This is already setup in the package.json scripts
77 | - setup `DATABASE_URL`: You can use this database created with elephantsql.
78 | `postgres://cigjzbzt:krzc-48qlH4hfj0HM5Oid_rfxN9uLbuf@raja.db.elephantsql.com:5432/cigjzbzt` 79 | - `npm install` 80 | - run migrations and seeds - `npm run db:seed` (optional): This will create tables and seed them with dummy data. If you are testing the project locally, you will definitely need to this the first time, but if you are testing it online with gitpod, the database has already been migrated and seeded, so you don't have to do this step. 81 | - start the development server: `npm start` 82 | - send GraphQL queries to the `/graphql` endpoint.
83 | 84 | Examples queries 85 | ```js 86 | { 87 | articles(scope: 'id|gt|2') { 88 | id 89 | title 90 | } 91 | } 92 | 93 | { 94 | articles { 95 | id 96 | title 97 | owner { 98 | firstname 99 | lastname 100 | } 101 | comments { 102 | id 103 | body 104 | } 105 | } 106 | } 107 | 108 | { 109 | categories { 110 | name 111 | articles { 112 | id 113 | title 114 | owner { 115 | firstname 116 | lastname 117 | } 118 | comments { 119 | id 120 | body 121 | } 122 | } 123 | } 124 | } 125 | ``` 126 | **To get a better view of this utility's value**, check the terminal to see the number of queries executed by Sequelize. 127 | You will find that all the queries above execute JUST ONE query. This could have been over tens of queries given very large tables, and with code not so well written. 128 |
129 |
130 | 131 | 132 | ## Unit Tests 133 | 134 | `npm test` 135 |

136 | You can find the test cases in the [`/src/__tests__`](https://github.com/jsamchineme/graphql-sequelize-query-loader/blob/add-example-project/src/__tests__/queryLoader.spec.ts) directory 137 |

138 | 139 | ## Scope Argument 140 | 141 | This allows us to query models and return records matching certain conditions. 142 | Currently, we support a small set of Sequelize operators with the `scope` argument 143 | 144 | ### Usage 145 | 146 | In a GraphQL query, provide `scope` argument with the following pattern 147 | `field|operator|value` 148 | 149 | Example 150 | ```js 151 | articles(scope: 'title|like|%graphql%') { 152 | id 153 | title 154 | } 155 | ``` 156 | 157 | ### Supported Scope Operators 158 | eq, gt, gte, like, lt, lte, ne 159 | 160 |
161 | 162 | ## Author 163 | Samuel Osuh @jsamchineme 164 | -------------------------------------------------------------------------------- /demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /demo/.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 'config': path.resolve('server/config', 'database.js'), 5 | 'models-path': path.resolve('server', 'models'), 6 | 'seeders-path': path.resolve('server', 'seeders'), 7 | 'migrations-path': path.resolve('server', 'migrations') 8 | }; 9 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-pro", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development babel-node server --preset-env", 8 | "migrate": "cross-env NODE_ENV=development sequelize db:migrate", 9 | "db:seed": "npm run migration && cross-env NODE_ENV=development babel-node server/seeders/dbSeed.js --preset-env", 10 | "migrate:reset": "sequelize db:migrate:undo:all", 11 | "migration": "npm run migrate:reset && npm run migrate" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/cli": "^7.2.3", 17 | "@babel/core": "^7.2.2", 18 | "@babel/node": "^7.2.2", 19 | "@babel/preset-env": "^7.3.1", 20 | "cross-env": "^5.2.0", 21 | "sequelize-cli": "^5.5.0" 22 | }, 23 | "dependencies": { 24 | "apollo-boost": "^0.1.28", 25 | "apollo-server-express": "^2.4.2", 26 | "body-parser": "^1.18.3", 27 | "cors": "^2.8.5", 28 | "dotenv": "^6.2.0", 29 | "express": "^4.16.4", 30 | "faker": "^4.1.0", 31 | "graphql": "^14.1.1", 32 | "graphql-sequelize-query-loader": "^1.0.5", 33 | "graphql-tag": "^2.10.1", 34 | "morgan": "^1.9.1", 35 | "pg": "^7.8.0", 36 | "pg-hstore": "^2.3.2", 37 | "sequelize": "^5.8.10" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /demo/server/config/database.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | development: { 5 | url: process.env.DATABASE_URL, 6 | dialect: 'postgres' 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /demo/server/graphql/queryLoaderConfig.js: -------------------------------------------------------------------------------- 1 | import queryLoader from 'graphql-sequelize-query-loader'; 2 | import models from '../models'; 3 | 4 | /** 5 | * dictionary of what sequelize models respectively match the named resources 6 | * captured on the graphql schema 7 | */ 8 | const includeModels = { 9 | articles: models.Article, 10 | article: models.Article, 11 | owner: models.User, 12 | category: models.category, 13 | comments: models.Comment, 14 | me: models.User 15 | }; 16 | 17 | queryLoader.init({ includeModels }); 18 | 19 | 20 | export default queryLoader; 21 | -------------------------------------------------------------------------------- /demo/server/graphql/resolvers/article.js: -------------------------------------------------------------------------------- 1 | import queryLoader from '../queryLoaderConfig'; 2 | 3 | 4 | export default { 5 | Query: { 6 | articles: async (parent, args, { models }, info) => { 7 | const { Article } = models; 8 | // query options prepare attributes and associated model includes 9 | const queryOptions = queryLoader.getFindOptions({ model: Article, info }); 10 | 11 | const articles = await Article.findAll(queryOptions); 12 | return articles; 13 | }, 14 | article: async (parent, { id }, { models }, info) => { 15 | const { Article } = models; 16 | // query options prepare attributes and associated model includes 17 | const queryOptions = queryLoader.getFindOptions({ model: Article, info }); 18 | 19 | const article = await Article.findByPk(id, queryOptions); 20 | return article; 21 | }, 22 | }, 23 | Article: { 24 | owner: (parent) => { 25 | const { owner } = parent; 26 | return owner; 27 | }, 28 | category: (parent) => { 29 | const { category } = parent; 30 | return category; 31 | }, 32 | comments: (parent) => { 33 | const { comments } = parent; 34 | return comments; 35 | } 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /demo/server/graphql/resolvers/category.js: -------------------------------------------------------------------------------- 1 | import queryLoader from '../queryLoaderConfig'; 2 | 3 | export default { 4 | Query: { 5 | categories: async (parent, args, { models }, info) => { 6 | const { Category } = models; 7 | const queryOptions = queryLoader.getFindOptions({ model: Category, info }); 8 | 9 | const categories = await Category.findAll(queryOptions); 10 | return categories; 11 | }, 12 | category: async (parent, { id }, { models }, info) => { 13 | const { Category } = models; 14 | const queryOptions = queryLoader.getFindOptions({ model: Category, info }); 15 | 16 | const category = await Category.findByPk(id, queryOptions); 17 | return category; 18 | } 19 | }, 20 | Category: { 21 | articles: async (parent) => { 22 | const { articles } = parent; 23 | return articles; 24 | } 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /demo/server/graphql/resolvers/index.js: -------------------------------------------------------------------------------- 1 | import userResolvers from './user'; 2 | import articleResolvers from './article'; 3 | import categoryResolvers from './category'; 4 | 5 | export default [userResolvers, articleResolvers, categoryResolvers]; 6 | -------------------------------------------------------------------------------- /demo/server/graphql/resolvers/user.js: -------------------------------------------------------------------------------- 1 | import queryLoader from '../queryLoaderConfig'; 2 | 3 | export default { 4 | Query: { 5 | user: (parent, { id }, { models }, info) => { 6 | const { User } = models; 7 | // query options prepare attributes and associated model includes 8 | const queryOptions = queryLoader.getFindOptions({ model: User, info }); 9 | 10 | const user = User.findByPk(id, queryOptions); 11 | return user; 12 | }, 13 | users: (parent, args, { models }, info) => { 14 | const { User } = models; 15 | // query options prepare attributes and associated model includes 16 | const queryOptions = queryLoader.getFindOptions({ model: User, info }); 17 | 18 | const users = User.findAll(queryOptions); 19 | return users; 20 | }, 21 | }, 22 | User: { 23 | articles: async (parent) => { 24 | const { articles } = parent; 25 | 26 | return articles; 27 | } 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /demo/server/graphql/schemas/article.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default gql` 4 | extend type Query { 5 | article(id: ID!): Article! 6 | articles(scope: String): [Article!]! 7 | } 8 | 9 | type Article { 10 | id: ID! 11 | body: String 12 | title: String 13 | slug: String 14 | description: String 15 | authorId: String 16 | categoryId: Int! 17 | owner: User! 18 | category: Category! 19 | comments(scope: String): [Comment!]! 20 | } 21 | 22 | extend type Mutation { 23 | createArticle( 24 | title: String! 25 | description: String! 26 | body: String! 27 | categoryId: Int! 28 | ): Article! 29 | 30 | deleteArticle(id: ID!): Boolean! 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /demo/server/graphql/schemas/category.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default gql` 4 | extend type Query { 5 | category(id: ID!): Category! 6 | categories: [Category!]! 7 | } 8 | 9 | type Category { 10 | id: ID! 11 | name: String 12 | createdAt: String 13 | updatedAt: String 14 | articles(scope: String): [Article!]! 15 | } 16 | 17 | extend type Mutation { 18 | createCategory( 19 | name: String! 20 | ): Category! 21 | 22 | deleteCategory(id: ID!): Boolean! 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /demo/server/graphql/schemas/comment.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default gql` 4 | extend type Query { 5 | comment(id: ID!): Comment! 6 | comments(scope: String): [Comment!]! 7 | } 8 | 9 | type Comment { 10 | id: ID! 11 | name: String 12 | userId: String 13 | articleId: Int 14 | body: String 15 | owner: User 16 | createdAt: String 17 | updatedAt: String 18 | articles: [Article!] 19 | } 20 | 21 | extend type Mutation { 22 | createComment( 23 | articleId: Int! 24 | body: String! 25 | ): Comment! 26 | 27 | deleteComment(id: ID!): Boolean! 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /demo/server/graphql/schemas/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | import userSchema from './user'; 4 | import articleSchema from './article'; 5 | import commentSchema from './comment'; 6 | import categorySchema from './category'; 7 | 8 | const linkSchema = gql` 9 | type Query { 10 | _: Boolean 11 | } 12 | 13 | type Mutation { 14 | _: Boolean 15 | } 16 | 17 | type Subscription { 18 | _: Boolean 19 | } 20 | `; 21 | 22 | export default [linkSchema, userSchema, articleSchema, commentSchema, categorySchema]; 23 | -------------------------------------------------------------------------------- /demo/server/graphql/schemas/user.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default gql` 4 | extend type Query { 5 | me: User 6 | user(id: ID!): User 7 | users: [User!] 8 | } 9 | 10 | type User { 11 | id: ID! 12 | userId: ID 13 | firstname: String 14 | lastname: String 15 | username: String! 16 | articles(id: ID, scope: String): [Article!]! 17 | } 18 | 19 | extend type Mutation { 20 | createUser( 21 | firstname: String 22 | lastname: String 23 | username: String 24 | ): User! 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /demo/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { ApolloServer } from 'apollo-server-express'; 3 | import cors from 'cors'; 4 | import schema from './graphql/schemas'; 5 | import resolvers from './graphql/resolvers'; 6 | import models from './models'; 7 | 8 | const app = express(); 9 | app.use(cors()); 10 | app.use(express.urlencoded({ extended: true })); 11 | 12 | const start = async () => { 13 | const server = new ApolloServer({ 14 | typeDefs: schema, 15 | resolvers, 16 | context: { 17 | models, 18 | } 19 | }); 20 | 21 | server.applyMiddleware({ app, path: '/graphql' }); 22 | 23 | const port = 8001; 24 | app.listen({ port }, () => { 25 | console.log(`🚀 Apollo Server on http://localhost:${port}${server.graphqlPath}`); 26 | }); 27 | }; 28 | 29 | start(); 30 | -------------------------------------------------------------------------------- /demo/server/migrations/20190204140700-create-user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Users', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER, 9 | }, 10 | firstname: { 11 | type: Sequelize.STRING 12 | }, 13 | lastname: { 14 | type: Sequelize.STRING 15 | }, 16 | username: { 17 | allowNull: false, 18 | type: Sequelize.STRING, 19 | unique: true 20 | }, 21 | createdAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE 24 | }, 25 | updatedAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | } 29 | }); 30 | }, 31 | down: queryInterface => queryInterface.dropTable('Users') 32 | }; 33 | -------------------------------------------------------------------------------- /demo/server/migrations/20190204140701-create-category.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Categories', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER 9 | }, 10 | name: { 11 | allowNull: false, 12 | type: Sequelize.STRING 13 | }, 14 | createdAt: { 15 | allowNull: false, 16 | type: Sequelize.DATE 17 | }, 18 | updatedAt: { 19 | allowNull: false, 20 | type: Sequelize.DATE 21 | } 22 | }); 23 | }, 24 | down: (queryInterface, Sequelize) => queryInterface.dropTable('Categories') 25 | }; -------------------------------------------------------------------------------- /demo/server/migrations/20190204140742-create-article.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Articles', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER 9 | }, 10 | title: { 11 | allowNull: false, 12 | type: Sequelize.STRING 13 | }, 14 | slug: { 15 | allowNull: false, 16 | type: Sequelize.STRING 17 | }, 18 | description: { 19 | type: Sequelize.STRING 20 | }, 21 | body: { 22 | allowNull: false, 23 | type: Sequelize.STRING 24 | }, 25 | authorId: { 26 | allowNull: false, 27 | type: Sequelize.INTEGER, 28 | onDelete: 'CASCADE', 29 | references: { 30 | model: 'Users', 31 | key: 'id' 32 | } 33 | }, 34 | categoryId: { 35 | allowNull: false, 36 | type: Sequelize.INTEGER, 37 | onDelete: 'CASCADE', 38 | references: { 39 | model: 'Categories', 40 | key: 'id' 41 | } 42 | }, 43 | createdAt: { 44 | allowNull: false, 45 | type: Sequelize.DATE 46 | }, 47 | updatedAt: { 48 | allowNull: false, 49 | type: Sequelize.DATE 50 | } 51 | }); 52 | }, 53 | down: (queryInterface) => queryInterface.dropTable('Articles') 54 | }; -------------------------------------------------------------------------------- /demo/server/migrations/20190216132118-create-comment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Comments', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER 9 | }, 10 | userId: { 11 | type: Sequelize.INTEGER, 12 | onDelete: 'CASCADE', 13 | references: { 14 | model: 'Users', 15 | key: 'id' 16 | } 17 | }, 18 | articleId: { 19 | type: Sequelize.INTEGER, 20 | onDelete: 'CASCADE', 21 | references: { 22 | model: 'Articles', 23 | key: 'id', 24 | } 25 | }, 26 | body: { 27 | allowNull: false, 28 | type: Sequelize.STRING 29 | }, 30 | createdAt: { 31 | allowNull: false, 32 | type: Sequelize.DATE 33 | }, 34 | updatedAt: { 35 | allowNull: false, 36 | type: Sequelize.DATE 37 | } 38 | }); 39 | }, 40 | down: queryInterface => queryInterface.dropTable('Comments') 41 | }; 42 | -------------------------------------------------------------------------------- /demo/server/models/article.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Article = sequelize.define('Article', { 3 | title: { 4 | allowNull: false, 5 | type: DataTypes.STRING 6 | }, 7 | slug: { 8 | allowNull: false, 9 | type: DataTypes.STRING 10 | }, 11 | description: { 12 | type: DataTypes.STRING 13 | }, 14 | body: { 15 | allowNull: false, 16 | type: DataTypes.TEXT 17 | }, 18 | authorId: DataTypes.UUID, 19 | categoryId: { 20 | allowNull: false, 21 | type: DataTypes.INTEGER, 22 | }, 23 | }, {}); 24 | Article.associate = (models) => { 25 | Article.belongsTo(models.User, { 26 | foreignKey: 'authorId', 27 | as: 'owner' 28 | }); 29 | Article.hasMany(models.Comment, { 30 | foreignKey: 'articleId', 31 | as: 'comments' 32 | }); 33 | }; 34 | return Article; 35 | }; 36 | -------------------------------------------------------------------------------- /demo/server/models/category.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Category = sequelize.define('Category', { 3 | name: DataTypes.STRING 4 | }, {}); 5 | Category.associate = function (models) { 6 | Category.hasMany(models.Article, { 7 | foreignKey: 'categoryId', 8 | as: 'articles' 9 | }); 10 | }; 11 | return Category; 12 | }; 13 | -------------------------------------------------------------------------------- /demo/server/models/comment.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Comment = sequelize.define('Comment', { 3 | userId: { 4 | allowNull: false, 5 | type: DataTypes.INTEGER, 6 | references: { 7 | model: 'User', 8 | key: 'id', 9 | as: 'userId' 10 | } 11 | }, 12 | articleId: DataTypes.INTEGER, 13 | body: DataTypes.TEXT 14 | }, {}); 15 | Comment.associate = (models) => { 16 | Comment.belongsTo(models.User, { 17 | foreignKey: 'userId', 18 | as: 'owner' 19 | }); 20 | }; 21 | return Comment; 22 | }; 23 | -------------------------------------------------------------------------------- /demo/server/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | 5 | const basename = path.basename(__filename); 6 | const env = process.env.NODE_ENV || 'development'; 7 | const config = require(`${__dirname}/../config/database.js`)[env]; 8 | const db = {}; 9 | 10 | const sequelize = new Sequelize(config.url, config); 11 | 12 | fs 13 | .readdirSync(__dirname) 14 | .filter((file) => { 15 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 16 | }) 17 | .forEach((file) => { 18 | const model = sequelize.import(path.join(__dirname, file)); 19 | db[model.name] = model; 20 | }); 21 | 22 | Object.keys(db).forEach((modelName) => { 23 | if (db[modelName].associate) { 24 | db[modelName].associate(db); 25 | } 26 | }); 27 | 28 | db.sequelize = sequelize; 29 | db.Sequelize = Sequelize; 30 | 31 | module.exports = db; 32 | -------------------------------------------------------------------------------- /demo/server/models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const User = sequelize.define('User', { 3 | firstname: DataTypes.STRING, 4 | lastname: DataTypes.STRING, 5 | username: { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | unique: true, 9 | } 10 | }, {}); 11 | User.associate = (models) => { 12 | User.hasMany(models.Article, { 13 | foreignKey: 'authorId', 14 | as: 'articles', 15 | onDelete: 'CASCADE' 16 | }); 17 | }; 18 | 19 | return User; 20 | }; 21 | -------------------------------------------------------------------------------- /demo/server/seeders/dbSeed.js: -------------------------------------------------------------------------------- 1 | import models from '../models'; 2 | import faker from 'faker'; 3 | 4 | const { User, Category, Article, Comment } = models; 5 | 6 | let categories; 7 | let articles; 8 | let users; 9 | let comments; 10 | 11 | 12 | const seedCategories = async () => { 13 | const dummyCategories = [ 14 | { name: 'technology' }, 15 | { name: 'fashion' }, 16 | ]; 17 | 18 | const categoriesPromise = dummyCategories.map(async (data) => { 19 | return await Category.create(data); 20 | }); 21 | categories = await Promise.all(categoriesPromise); 22 | }; 23 | 24 | const seedUsers = async () => { 25 | const createdUsers = []; 26 | for(let i=1; i <= 2; i++) { 27 | const firstname = faker.name.firstName(); 28 | const lastname = faker.name.lastName(); 29 | const username = firstname.toLowerCase() + '.' + lastname.toLowerCase(); 30 | const userData = { firstname, lastname, username }; 31 | const newUser = await User.create(userData); 32 | createdUsers.push(newUser); 33 | } 34 | users = await Promise.all(createdUsers); 35 | } 36 | 37 | const seedArticles = async () => { 38 | const articleSeedsPromise = []; 39 | 40 | // create 20 articles 41 | for(let a=1; a <= 20; a++) { 42 | const user = faker.random.arrayElement(users); 43 | const category = faker.random.arrayElement(categories); 44 | const title = faker.random.words(10); 45 | const slug = title.replace(/\s+/g, '-').toLowerCase(); 46 | 47 | const newArticle = await Article.create({ 48 | title, 49 | slug, 50 | description: faker.random.words(10), 51 | body: faker.random.words(10), 52 | authorId: user.id, 53 | categoryId: category.id 54 | }); 55 | 56 | articleSeedsPromise.push(newArticle); 57 | } 58 | articles = await Promise.all(articleSeedsPromise); 59 | }; 60 | 61 | 62 | const seedComments = async () => { 63 | // create 30 random comments 64 | for(let i=1; i <= 30; i++) { 65 | const user = faker.random.arrayElement(users); 66 | const article = faker.random.arrayElement(articles); 67 | await Comment.create({ 68 | userId: user.id, 69 | articleId: article.id, 70 | body: faker.random.words(10) 71 | }); 72 | } 73 | } 74 | 75 | 76 | const seedDatabase = async () => { 77 | await seedCategories(); 78 | await seedUsers(); 79 | await seedArticles(); 80 | await seedComments(); 81 | } 82 | 83 | seedDatabase(); -------------------------------------------------------------------------------- /jestconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.(t|j)sx?$": "ts-jest" 4 | }, 5 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 6 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], 7 | "collectCoverage": true, 8 | "globals": { 9 | "ts-jest": { 10 | "diagnostics": false 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-sequelize-query-loader", 3 | "version": "1.0.6", 4 | "description": "Convert GraphQL Query to query options for Sequelize models facilitating eagerloading of associated resources.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "jest --config jestconfig.json", 9 | "build": "tsc", 10 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 11 | "lint": "tslint -p tsconfig.json", 12 | "prepublishOnly": "npm test && npm run lint", 13 | "preversion": "npm run lint", 14 | "version": "npm run format && git add -A src", 15 | "coverage": "codecov", 16 | "postversion": "git push -f && git push --tags" 17 | }, 18 | "files": [ 19 | "lib/**/*" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/jsamchineme/graphql-sequelize-query-loader" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/jsamchineme/graphql-sequelize-query-loader" 27 | }, 28 | "homepage": "https://github.com/jsamchineme/graphql-sequelize-query-loader", 29 | "license": "MIT", 30 | "keywords": [ 31 | "sequelize", 32 | "graphql", 33 | "graphql-sequelize", 34 | "graphql utils", 35 | "query-loader" 36 | ], 37 | "author": "jsamchineme", 38 | "devDependencies": { 39 | "@types/bluebird": "^3.5.27", 40 | "@types/graphql": "^14.2.1", 41 | "@types/jest": "^24.0.15", 42 | "@types/validator": "^10.11.1", 43 | "codecov": "^3.5.0", 44 | "jest": "^24.8.0", 45 | "prettier": "^1.18.2", 46 | "ts-jest": "^24.0.2", 47 | "tslint": "^5.17.0", 48 | "tslint-config-prettier": "^1.18.0", 49 | "typescript": "^3.5.2" 50 | }, 51 | "dependencies": { 52 | "graphql": "^14.3.1", 53 | "sequelize": "^5.8.10" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/__mocks__/getArticlesArgsInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLResolveInfo 3 | } from 'graphql'; 4 | 5 | /** 6 | * A mock of the structure of the info object produced when a query 7 | * like this is sent from graphql 8 | * ```js 9 | * articles(scope: "id|gt|2") { 10 | * id 11 | * title 12 | * } 13 | */ 14 | 15 | const getArticlesArgsInfo: GraphQLResolveInfo | any = { 16 | fieldName: 'articles', 17 | fieldNodes: [{ 18 | alias: undefined, 19 | arguments: [{ 20 | kind: 'Argument', 21 | name: { 22 | kind: 'Name', 23 | value: 'scope', 24 | }, 25 | value: { 26 | block: false, 27 | kind: 'StringValue', 28 | value: 'id|gt|2', 29 | }, 30 | }], 31 | directives: [], 32 | kind: 'Field', 33 | selectionSet: { 34 | kind: 'SelectionSet', 35 | selections: [{ 36 | alias: undefined, 37 | arguments: [], 38 | directives: [], 39 | kind: 'Field', 40 | name: { 41 | kind: 'Name', 42 | value: 'id', 43 | }, 44 | selectionSet: undefined, 45 | }, 46 | { 47 | alias: undefined, 48 | arguments: [], 49 | directives: [], 50 | kind: 'Field', 51 | name: { 52 | kind: 'Name', 53 | value: 'title', 54 | }, 55 | selectionSet: undefined, 56 | } 57 | ], 58 | }, 59 | }], 60 | }; 61 | 62 | export default getArticlesArgsInfo; 63 | 64 | -------------------------------------------------------------------------------- /src/__mocks__/getArticlesInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLResolveInfo 3 | } from 'graphql'; 4 | 5 | /** 6 | * A mock of the structure of the info object produced when a query 7 | * like this is sent from graphql 8 | * ```js 9 | * articles { 10 | * id 11 | * title 12 | * } 13 | */ 14 | 15 | const getArticlesInfo: GraphQLResolveInfo | any = { 16 | fieldName: 'articles', 17 | fieldNodes: [{ 18 | alias: undefined, 19 | arguments: [], 20 | directives: [], 21 | kind: 'Field', 22 | selectionSet: { 23 | kind: 'SelectionSet', 24 | selections: [{ 25 | alias: undefined, 26 | arguments: [], 27 | directives: [], 28 | kind: 'Field', 29 | name: { 30 | kind: 'Name', 31 | value: 'id', 32 | }, 33 | selectionSet: undefined, 34 | }, 35 | { 36 | alias: undefined, 37 | arguments: [], 38 | directives: [], 39 | kind: 'Field', 40 | name: { 41 | kind: 'Name', 42 | value: 'title', 43 | }, 44 | selectionSet: undefined, 45 | } 46 | ], 47 | }, 48 | }], 49 | }; 50 | 51 | export default getArticlesInfo; -------------------------------------------------------------------------------- /src/__mocks__/getArticlesWithIncludesInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLResolveInfo 3 | } from 'graphql'; 4 | 5 | /** 6 | * A mock of the structure of the info object produced when a query 7 | * like this is sent from graphql 8 | * ```js 9 | * articles(scope: "id|gt|2") { 10 | * id 11 | * title 12 | * owner { 13 | * firstname 14 | * lastname 15 | * } 16 | * comments { 17 | * id 18 | * body 19 | * } 20 | * } 21 | */ 22 | 23 | const getArticlesWithIncludesInfo: GraphQLResolveInfo | any = { 24 | fieldName: 'articles', 25 | fieldNodes: [{ 26 | alias: undefined, 27 | arguments: [{ 28 | kind: 'Argument', 29 | name: { 30 | kind: 'Name', 31 | value: 'scope', 32 | }, 33 | value: { 34 | block: false, 35 | kind: 'StringValue', 36 | value: 'id|gt|2', 37 | }, 38 | }], 39 | directives: [], 40 | kind: 'Field', 41 | selectionSet: { 42 | kind: 'SelectionSet', 43 | selections: [{ 44 | arguments: [], 45 | kind: 'Field', 46 | name: { 47 | kind: 'Name', 48 | value: 'id', 49 | }, 50 | selectionSet: undefined, 51 | }, 52 | { 53 | arguments: [], 54 | kind: 'Field', 55 | name: { 56 | kind: 'Name', 57 | value: 'title', 58 | }, 59 | selectionSet: undefined, 60 | }, 61 | { 62 | arguments: [], 63 | kind: 'Field', 64 | name: { 65 | kind: 'Name', 66 | value: 'owner', 67 | }, 68 | selectionSet: { 69 | kind: 'SelectionSet', 70 | selections: [{ 71 | arguments: [], 72 | kind: 'Field', 73 | name: { 74 | kind: 'Name', 75 | value: 'firstname', 76 | }, 77 | selectionSet: undefined, 78 | }, 79 | { 80 | arguments: [], 81 | kind: 'Field', 82 | name: { 83 | kind: 'Name', 84 | value: 'lastname', 85 | }, 86 | selectionSet: undefined, 87 | } 88 | ], 89 | }, 90 | }, 91 | { 92 | arguments: [], 93 | kind: 'Field', 94 | name: { 95 | kind: 'Name', 96 | value: 'comments', 97 | }, 98 | selectionSet: { 99 | kind: 'SelectionSet', 100 | selections: [{ 101 | arguments: [], 102 | kind: 'Field', 103 | name: { 104 | kind: 'Name', 105 | value: 'id', 106 | }, 107 | selectionSet: undefined, 108 | }, 109 | { 110 | arguments: [], 111 | kind: 'Field', 112 | name: { 113 | kind: 'Name', 114 | value: 'body', 115 | }, 116 | selectionSet: undefined, 117 | } 118 | ], 119 | }, 120 | } 121 | ], 122 | }, 123 | }], 124 | }; 125 | 126 | export default getArticlesWithIncludesInfo; -------------------------------------------------------------------------------- /src/__mocks__/getCategoryDeepInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLResolveInfo 3 | } from 'graphql'; 4 | 5 | /** 6 | * A mock of the structure of the info object produced when a query 7 | * like this is sent from graphql 8 | * ```js 9 | * categories { 10 | * id 11 | * name 12 | * articles(scope: 'id|gt|2 && body|like|%dummy%') { 13 | * id 14 | * title 15 | * owner { 16 | * firstname 17 | * lastname 18 | * } 19 | * comments { 20 | * id 21 | * body 22 | * } 23 | * } 24 | * } 25 | */ 26 | 27 | const getCategoryDeeperInfo: GraphQLResolveInfo | any = { 28 | fieldName: 'categories', 29 | fieldNodes: [{ 30 | alias: undefined, 31 | arguments: [], 32 | directives: [], 33 | kind: 'Field', 34 | selectionSet: { 35 | kind: 'SelectionSet', 36 | selections: [{ 37 | alias: undefined, 38 | arguments: [], 39 | directives: [], 40 | kind: 'Field', 41 | name: { 42 | kind: 'Name', 43 | value: 'id', 44 | }, 45 | selectionSet: undefined, 46 | }, 47 | { 48 | alias: undefined, 49 | arguments: [], 50 | directives: [], 51 | kind: 'Field', 52 | name: { 53 | kind: 'Name', 54 | value: 'name', 55 | }, 56 | selectionSet: undefined, 57 | }, 58 | { 59 | alias: undefined, 60 | arguments: [{ 61 | kind: 'Argument', 62 | name: { 63 | kind: 'Name', 64 | value: 'scope', 65 | }, 66 | value: { 67 | block: false, 68 | kind: 'StringValue', 69 | value: 'id|gt|2 && body|like|%dummy%', 70 | }, 71 | }], 72 | directives: [], 73 | kind: 'Field', 74 | name: { 75 | kind: 'Name', 76 | value: 'articles', 77 | }, 78 | selectionSet: { 79 | kind: 'SelectionSet', 80 | selections: [{ 81 | arguments: [], 82 | kind: 'Field', 83 | name: { 84 | kind: 'Name', 85 | value: 'id', 86 | }, 87 | selectionSet: undefined, 88 | }, 89 | { 90 | arguments: [], 91 | kind: 'Field', 92 | name: { 93 | kind: 'Name', 94 | value: 'title', 95 | }, 96 | selectionSet: undefined, 97 | }, 98 | { 99 | arguments: [], 100 | kind: 'Field', 101 | name: { 102 | kind: 'Name', 103 | value: 'owner', 104 | }, 105 | selectionSet: { 106 | kind: 'SelectionSet', 107 | selections: [{ 108 | arguments: [], 109 | kind: 'Field', 110 | name: { 111 | kind: 'Name', 112 | value: 'firstname', 113 | }, 114 | selectionSet: undefined, 115 | }, 116 | { 117 | arguments: [], 118 | kind: 'Field', 119 | name: { 120 | kind: 'Name', 121 | value: 'lastname', 122 | }, 123 | selectionSet: undefined, 124 | } 125 | ], 126 | }, 127 | }, 128 | { 129 | arguments: [], 130 | kind: 'Field', 131 | name: { 132 | kind: 'Name', 133 | value: 'comments', 134 | }, 135 | selectionSet: { 136 | kind: 'SelectionSet', 137 | selections: [{ 138 | arguments: [], 139 | kind: 'Field', 140 | name: { 141 | kind: 'Name', 142 | value: 'id', 143 | }, 144 | selectionSet: undefined, 145 | }, 146 | { 147 | arguments: [], 148 | kind: 'Field', 149 | name: { 150 | kind: 'Name', 151 | value: 'body', 152 | }, 153 | selectionSet: undefined, 154 | } 155 | ], 156 | }, 157 | } 158 | ], 159 | }, 160 | } 161 | ], 162 | }, 163 | }], 164 | }; 165 | 166 | export default getCategoryDeeperInfo; -------------------------------------------------------------------------------- /src/__mocks__/getCategoryInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLResolveInfo 3 | } from 'graphql'; 4 | 5 | /** 6 | * A mock of the structure of the info object produced when a query 7 | * like this is sent from graphql 8 | * ```js 9 | * categories { 10 | * id 11 | * name 12 | * articles { 13 | * id 14 | * title 15 | * owner { 16 | * firstname 17 | * lastname 18 | * } 19 | * comments { 20 | * id 21 | * body 22 | * } 23 | * } 24 | * } 25 | */ 26 | 27 | const getCategoryInfo: GraphQLResolveInfo | any = { 28 | fieldName: 'categories', 29 | fieldNodes: [{ 30 | alias: undefined, 31 | arguments: [], 32 | directives: [], 33 | kind: 'Field', 34 | selectionSet: { 35 | kind: 'SelectionSet', 36 | selections: [{ 37 | alias: undefined, 38 | arguments: [], 39 | directives: [], 40 | kind: 'Field', 41 | name: { 42 | kind: 'Name', 43 | value: 'id', 44 | }, 45 | selectionSet: undefined, 46 | }, 47 | { 48 | alias: undefined, 49 | arguments: [], 50 | directives: [], 51 | kind: 'Field', 52 | name: { 53 | kind: 'Name', 54 | value: 'name', 55 | }, 56 | selectionSet: undefined, 57 | }, 58 | { 59 | alias: undefined, 60 | arguments: [], 61 | directives: [], 62 | kind: 'Field', 63 | name: { 64 | kind: 'Name', 65 | value: 'articles', 66 | }, 67 | selectionSet: { 68 | kind: 'SelectionSet', 69 | selections: [{ 70 | arguments: [], 71 | kind: 'Field', 72 | name: { 73 | kind: 'Name', 74 | value: 'id', 75 | }, 76 | selectionSet: undefined, 77 | }, 78 | { 79 | arguments: [], 80 | kind: 'Field', 81 | name: { 82 | kind: 'Name', 83 | value: 'title', 84 | }, 85 | selectionSet: undefined, 86 | }, 87 | { 88 | arguments: [], 89 | kind: 'Field', 90 | name: { 91 | kind: 'Name', 92 | value: 'owner', 93 | }, 94 | selectionSet: { 95 | kind: 'SelectionSet', 96 | selections: [{ 97 | arguments: [], 98 | kind: 'Field', 99 | name: { 100 | kind: 'Name', 101 | value: 'firstname', 102 | }, 103 | selectionSet: undefined, 104 | }, 105 | { 106 | arguments: [], 107 | kind: 'Field', 108 | name: { 109 | kind: 'Name', 110 | value: 'lastname', 111 | }, 112 | selectionSet: undefined, 113 | } 114 | ], 115 | }, 116 | }, 117 | { 118 | arguments: [], 119 | kind: 'Field', 120 | name: { 121 | kind: 'Name', 122 | value: 'comments', 123 | }, 124 | selectionSet: { 125 | kind: 'SelectionSet', 126 | selections: [{ 127 | arguments: [], 128 | kind: 'Field', 129 | name: { 130 | kind: 'Name', 131 | value: 'id', 132 | }, 133 | selectionSet: undefined, 134 | }, 135 | { 136 | arguments: [], 137 | kind: 'Field', 138 | name: { 139 | kind: 'Name', 140 | value: 'body', 141 | }, 142 | selectionSet: undefined, 143 | } 144 | ], 145 | }, 146 | } 147 | ], 148 | }, 149 | } 150 | ], 151 | }, 152 | }], 153 | }; 154 | 155 | export default getCategoryInfo; -------------------------------------------------------------------------------- /src/__mocks__/models/articleModel.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "sequelize/types"; 2 | 3 | const articleModel: Model | any = { 4 | rawAttributes: { 5 | authorId: { 6 | type: 'string' 7 | }, 8 | body: { 9 | type: 'string' 10 | }, 11 | categoryId: { 12 | type: 'integer', 13 | }, 14 | description: { 15 | type: 'string' 16 | }, 17 | id: { 18 | type: 'integer' 19 | }, 20 | slug: { 21 | type: 'string' 22 | }, 23 | title: { 24 | type: 'string' 25 | }, 26 | } 27 | }; 28 | 29 | export default articleModel; 30 | -------------------------------------------------------------------------------- /src/__mocks__/models/categoryModel.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "sequelize/types"; 2 | 3 | const categoryModel: Model | any = { 4 | rawAttributes: { 5 | id: { 6 | type: 'integer' 7 | }, 8 | name: { 9 | type: 'string' 10 | }, 11 | } 12 | }; 13 | 14 | export default categoryModel; 15 | -------------------------------------------------------------------------------- /src/__mocks__/models/commentModel.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "sequelize/types"; 2 | 3 | const commentModel: Model | any = { 4 | rawAttributes: { 5 | articleId: { 6 | type: 'integer' 7 | }, 8 | body: { 9 | type: 'string' 10 | }, 11 | id: { 12 | type: 'integer' 13 | }, 14 | } 15 | }; 16 | 17 | export default commentModel; 18 | -------------------------------------------------------------------------------- /src/__mocks__/models/userModel.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "sequelize/types"; 2 | 3 | const userModel: Model | any = { 4 | rawAttributes: { 5 | firstname: { 6 | type: 'string' 7 | }, 8 | id: { 9 | type: 'integer' 10 | }, 11 | lastname: { 12 | type: 'string' 13 | }, 14 | } 15 | }; 16 | 17 | export default userModel; 18 | -------------------------------------------------------------------------------- /src/__tests__/queryLoader.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | import { Op } from 'sequelize'; 3 | import getArticlesArgsInfoMock from '../__mocks__/getArticlesArgsInfo'; 4 | import getArticlesInfoMock from '../__mocks__/getArticlesInfo'; 5 | import getArticlesWithIncludesInfoMock from '../__mocks__/getArticlesWithIncludesInfo'; 6 | import getCategoryDeepInfoMock from '../__mocks__/getCategoryDeepInfo'; 7 | import getCategoryInfoMock from '../__mocks__/getCategoryInfo'; 8 | import articleModelMock from '../__mocks__/models/articleModel'; 9 | import commentModelMock from '../__mocks__/models/commentModel'; 10 | import userModelMock from '../__mocks__/models/userModel'; 11 | import queryLoader from '../index'; 12 | import { IWhereConstraints } from '../types'; 13 | 14 | const includeModels = { 15 | article: articleModelMock, 16 | articles: articleModelMock, 17 | category: userModelMock, 18 | comments: commentModelMock, 19 | owner: userModelMock, 20 | }; 21 | 22 | describe('queryLoader', () => { 23 | describe('queryLoader.init()', () => { 24 | it('initialises loader with include models', () => { 25 | queryLoader.init({ includeModels }); 26 | expect(queryLoader.includeModels.article).toEqual(articleModelMock); 27 | expect(typeof queryLoader === 'object').toBe(true); 28 | }); 29 | 30 | it('throws an error when include models are not supplied', () => { 31 | try { 32 | (queryLoader as any).init({}); 33 | } catch(error) { 34 | expect(error.message).toEqual('Please supply parameter property :includeModels. Check the docs'); 35 | } 36 | }); 37 | }); 38 | 39 | describe('queryLoader.getSelectedAttributes()', () => { 40 | it('returns the selected attributes', () => { 41 | const info = getArticlesInfoMock as GraphQLResolveInfo 42 | const selectedAttributes = queryLoader.getSelectedAttributes({ 43 | model: articleModelMock, 44 | selections: info.fieldNodes[0].selectionSet.selections 45 | }); 46 | const expectedAttributes = ['id', 'title'] 47 | 48 | expect(selectedAttributes).toEqual(expectedAttributes); 49 | }); 50 | }); 51 | 52 | describe('queryLoader.turnArgsToWhere()', () => { 53 | it('returns "undefined" when query lacks the "scope" argument', () => { 54 | const info = getArticlesInfoMock as GraphQLResolveInfo 55 | const whereConstraints = queryLoader.turnArgsToWhere(info.fieldNodes[0].arguments); 56 | const expectedWhereConstraints = {} 57 | expect(whereConstraints).toEqual(expectedWhereConstraints); 58 | }); 59 | 60 | it('returns objects with properties corresponding to query when "scope" argument is provided', () => { 61 | const info = getArticlesArgsInfoMock as GraphQLResolveInfo 62 | const whereConstraints = queryLoader.turnArgsToWhere(info.fieldNodes[0].arguments); 63 | const expectedWhereConstraints: IWhereConstraints = { 64 | id: { 65 | [Op.gt]: '2' 66 | } 67 | } 68 | expect(whereConstraints).toEqual(expectedWhereConstraints); 69 | }); 70 | }); 71 | 72 | describe('queryLoader.getValidScopeString()', () => { 73 | it('throws error when incorrect parts are supplied', () => { 74 | let scopeString = 'id|gt| '; 75 | try { 76 | queryLoader.getValidScopeString(scopeString); 77 | } catch(error) { 78 | expect(error.message).toEqual(`Incorrect Parts supplied for scope: ${scopeString}`); 79 | } 80 | 81 | scopeString = 'id|gt'; 82 | try { 83 | queryLoader.getValidScopeString(scopeString); 84 | } catch(error) { 85 | expect(error.message).toEqual(`Incorrect Parts supplied for scope: ${scopeString}`); 86 | } 87 | }); 88 | 89 | it('returns appropriate array value when correct parts are supplied', () => { 90 | const scopeString = 'id|gt|1'; 91 | expect(queryLoader.getValidScopeString(scopeString)).toEqual(['id', 'gt', '1']); 92 | }); 93 | }); 94 | 95 | describe('queryLoader.getSelectedIncludes()', () => { 96 | it('returns an empty array when query has no included models', () => { 97 | const info = getArticlesArgsInfoMock as GraphQLResolveInfo 98 | const includes = queryLoader.getSelectedIncludes({ 99 | model: articleModelMock, 100 | selections: info.fieldNodes[0].selectionSet.selections 101 | }); 102 | expect(includes).toEqual([]); 103 | }); 104 | 105 | it('returns a non-empty array when query has an/some included model(s)', () => { 106 | const info = getArticlesWithIncludesInfoMock as GraphQLResolveInfo 107 | const includes = queryLoader.getSelectedIncludes({ 108 | model: articleModelMock, 109 | selections: info.fieldNodes[0].selectionSet.selections 110 | }); 111 | expect(includes.length).toEqual(2); 112 | }); 113 | 114 | it('handles cases where included models next/include other related models', () => { 115 | const info = getCategoryInfoMock as GraphQLResolveInfo 116 | const includes = queryLoader.getSelectedIncludes({ 117 | model: articleModelMock, 118 | selections: info.fieldNodes[0].selectionSet.selections 119 | }); 120 | /** 121 | * structures expected 122 | * ```js 123 | * [{ 124 | * model: Article, 125 | * as: 'articles', 126 | * attributes: ['id', 'title'], 127 | * required: false, 128 | * include: [{ 129 | * model: User, 130 | * as: 'owner', 131 | * attributes: ['firstname', 'lastname'], 132 | * required: false, 133 | * include: [] 134 | * }, 135 | * { 136 | * model: Comment, 137 | * as: 'comments', 138 | * attributes: ['id', 'body'], 139 | * required: false, 140 | * include: [] 141 | * }] 142 | * }] 143 | */ 144 | expect(includes.length).toEqual(1); 145 | expect(includes[0].include.length).toEqual(2); 146 | }); 147 | }); 148 | 149 | describe('queryLoader.getFindOptions()', () => { 150 | it('returns object without "include" property, when graphql query lacks included selections', () => { 151 | const info = getArticlesInfoMock as GraphQLResolveInfo 152 | const options = queryLoader.getFindOptions({ 153 | info, 154 | model: articleModelMock, 155 | }); 156 | expect(options.include).toEqual(undefined); 157 | }); 158 | 159 | it('returns object without "where" when graphql query lacks "scope" argument', () => { 160 | const info = getArticlesInfoMock as GraphQLResolveInfo 161 | const options = queryLoader.getFindOptions({ 162 | info, 163 | model: articleModelMock, 164 | }); 165 | expect(options.where).toBe(undefined); 166 | }); 167 | 168 | it('returns object with "include" property, when graphql query has included selections', () => { 169 | const info = getArticlesWithIncludesInfoMock as GraphQLResolveInfo 170 | const options = queryLoader.getFindOptions({ 171 | info, 172 | model: articleModelMock, 173 | }); 174 | expect(options.include.length).toEqual(2); 175 | }); 176 | 177 | it('returns object with "where" property, when graphql query has "scope" argument', () => { 178 | const info = getArticlesArgsInfoMock as GraphQLResolveInfo 179 | const options = queryLoader.getFindOptions({ 180 | info, 181 | model: articleModelMock, 182 | }); 183 | const expectedWhereConstraints: IWhereConstraints = { 184 | id: { 185 | [Op.gt]: '2' 186 | } 187 | }; 188 | expect(options.where).toEqual(expectedWhereConstraints); 189 | }); 190 | 191 | it('gets findOptions and whereConstraints for deeper shapes', () => { 192 | const info = getCategoryDeepInfoMock as GraphQLResolveInfo 193 | const options = queryLoader.getFindOptions({ 194 | info, 195 | model: articleModelMock, 196 | }); 197 | 198 | const expectedStructure = { 199 | attributes: ['id'], 200 | include: [{ 201 | as: 'articles', 202 | attributes: ['id', 'title'], 203 | include: [{ 204 | as: 'owner', 205 | attributes: ['firstname', 'lastname'], 206 | model: userModelMock, 207 | required: false, 208 | }, 209 | { 210 | as: 'comments', 211 | attributes: ['id', 'body'], 212 | model: commentModelMock, 213 | required: false, 214 | }], 215 | model: articleModelMock, 216 | required: false, 217 | where: { 218 | body: { 219 | [Op.like]: '%dummy%' 220 | }, 221 | id: { 222 | [Op.gt]: '2' 223 | }, 224 | }, 225 | }] 226 | } 227 | 228 | expect(options).toEqual(expectedStructure); 229 | }); 230 | }); 231 | }); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as queryLoader from './queryLoader'; 2 | 3 | export default queryLoader; -------------------------------------------------------------------------------- /src/queryLoader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldNode, 3 | SelectionNode, 4 | SelectionSetNode, 5 | StringValueNode, 6 | } from 'graphql'; 7 | 8 | import { 9 | FindOptions, 10 | IncludeOptions, 11 | Model as SequelizeModel, 12 | Op, 13 | } from 'sequelize'; 14 | 15 | import { 16 | IQueryLoader, 17 | ISequelizeOperators, 18 | IWhereConstraints, 19 | SelectedAttributes, 20 | SelectedIncludes, 21 | } from './types'; 22 | 23 | 24 | /** 25 | * Dictionary of available query scope operators 26 | * and their equivalent sequelize operators 27 | */ 28 | const sequelizeOperators: ISequelizeOperators = { 29 | eq: Op.eq, 30 | gt: Op.gt, 31 | gte: Op.gte, 32 | like: Op.like, 33 | lt: Op.lt, 34 | lte: Op.lte, 35 | ne: Op.ne, 36 | }; 37 | 38 | 39 | const queryLoader: IQueryLoader = { 40 | includeModels: {}, 41 | 42 | /** 43 | * Initialise the queryLoader utility 44 | * 45 | * @param options - configuration options used for the initializing utility 46 | * @param options.includeModels - object containing included Models as pairs of `modelName`: `SequelizeModel` 47 | */ 48 | init({ includeModels }) { 49 | if(includeModels === undefined) { 50 | throw new Error('Please supply parameter property :includeModels. Check the docs'); 51 | } 52 | this.includeModels = includeModels; 53 | }, 54 | 55 | /** 56 | * Returns the options that should be supplied to 57 | * a Sequelize `Find` or `FindAll` method call 58 | * 59 | * @remarks 60 | * A GraphQL Query with this structure 61 | * ```js 62 | * categories { 63 | * name 64 | * articles(scope: "id|gt|2") { 65 | * id 66 | * title 67 | * owner { 68 | * id 69 | * lastname 70 | * } 71 | * comments { 72 | * id 73 | * body 74 | * } 75 | * } 76 | * } 77 | * ``` 78 | * is converted to a `findOptions` object, in this structure 79 | * forming ONE SINGLE QUERY that will be executed against the database 80 | * with sequelize 81 | * ```js 82 | * { 83 | * attributes: ['name'], 84 | * include: [{ 85 | * model: Article, 86 | * as: 'articles', 87 | * attributes: ['id', 'title'], 88 | * required: false, 89 | * where: { 90 | * id: { 91 | * [Symbol(gt)]: '2' 92 | * } 93 | * }, 94 | * include: [{ 95 | * model: User, 96 | * as: 'owner', 97 | * attributes: ['id', 'lastname'], 98 | * required: false, 99 | * include: [] 100 | * }, 101 | * { 102 | * model: Comment, 103 | * as: 'comments', 104 | * attributes: ['id', 'body'], 105 | * required: false, 106 | * include: [] 107 | * }] 108 | * }] 109 | * } 110 | * ``` 111 | * @param model - model 112 | * @param info - the info meta property passed from graphql. 113 | * It has a structure that we can parse or analyse, to determine the attributes to be selected from the database 114 | * as well as the associated models to be included using sequelize include 115 | * @returns the query options to be applied to the Find method call 116 | * 117 | */ 118 | getFindOptions({ model, info }): object { 119 | const selections = (info.fieldNodes[0].selectionSet as SelectionSetNode).selections; 120 | const selectedAttributes = this.getSelectedAttributes({ model, selections }); 121 | const queryIncludes = this.getSelectedIncludes({ model, selections }); 122 | 123 | const findOptions: FindOptions = { 124 | attributes: selectedAttributes, 125 | }; 126 | 127 | const selectionArguments = info.fieldNodes[0].arguments || []; 128 | const whereAttributes = this.turnArgsToWhere(selectionArguments); 129 | 130 | if (queryIncludes.length) { 131 | findOptions.include = queryIncludes; 132 | } 133 | if (Object.keys(whereAttributes).length) { 134 | findOptions.where = whereAttributes; 135 | } 136 | 137 | return findOptions; 138 | }, 139 | 140 | /** 141 | * Return an array of all the includes to be carried out 142 | * based on the schema sent in the request from graphql 143 | * 144 | * @param selections - an array of the selection nodes for each field in the schema. 145 | * @returns the array that should contain all model and association-model includes 146 | */ 147 | prepareIncludes({ selections = [] }): SelectedIncludes { 148 | const includes: SelectedIncludes = []; 149 | const includedModelSelections: SelectionNode[] = 150 | selections.filter((selection) => (selection as FieldNode).selectionSet !== undefined); 151 | 152 | const hasFieldWithSelectionSet = includedModelSelections !== undefined; 153 | 154 | includedModelSelections.forEach((item) => { 155 | const selection = item as FieldNode; 156 | const fieldName: string = selection.name.value; 157 | const includedModel: SequelizeModel | any = this.getIncludeModel(fieldName); 158 | const selectionSet = selection.selectionSet || { selections: undefined }; 159 | 160 | const selectedAttributes: SelectedAttributes = this.getSelectedAttributes({ 161 | model: includedModel, 162 | selections: selectionSet.selections 163 | }); 164 | 165 | let queryIncludes : IncludeOptions[] = []; 166 | if (hasFieldWithSelectionSet) { 167 | const fieldSelectionSet = selection.selectionSet || { selections: undefined }; 168 | const currentSelections = fieldSelectionSet.selections; 169 | queryIncludes = this.getSelectedIncludes({ model: includedModel, selections: currentSelections }); 170 | } 171 | 172 | const selectionArguments = selection.arguments || []; 173 | const whereAttributes = this.turnArgsToWhere(selectionArguments); 174 | 175 | const includeOption: IncludeOptions = { 176 | as: fieldName, 177 | attributes: selectedAttributes, 178 | model: includedModel, 179 | required: false 180 | }; 181 | 182 | if (Object.keys(whereAttributes).length) { 183 | includeOption.where = whereAttributes; 184 | } 185 | if (queryIncludes.length) { 186 | includeOption.include = queryIncludes; 187 | } 188 | 189 | includes.push(includeOption); 190 | }); 191 | return includes; 192 | }, 193 | 194 | /** 195 | * Return an array of all the includes to be carried out 196 | * based on the schema sent in the request from graphql 197 | * 198 | * @remarks 199 | * This method is called `recursively` to prepare the included models 200 | * for all nodes with nested or associated resource(s) 201 | * 202 | * @param model - a specific model that should be checked for selected includes 203 | * @param selections - an array of the selection nodes for each field in the schema. 204 | * @returns the array that should contain all model and association-model includes 205 | */ 206 | getSelectedIncludes({ model, selections = [] }) { 207 | let includes: SelectedIncludes = []; 208 | const includedModelSections = selections.filter(item => (item as FieldNode).selectionSet !== undefined); 209 | 210 | /** 211 | * hasFieldWithSelectionSet is used to assert that the top level resource 212 | * in the selectionSet has a child field which is a model 213 | */ 214 | const hasFieldWithSelectionSet = includedModelSections !== undefined; 215 | 216 | if (hasFieldWithSelectionSet) { 217 | includes = this.prepareIncludes({ model, selections }); 218 | } 219 | return includes 220 | }, 221 | 222 | getIncludeModel(modelKeyName: string) { 223 | return this.includeModels[modelKeyName]; 224 | }, 225 | 226 | getSelectedAttributes({ model, selections = [] }) { 227 | /** 228 | * Request schema can sometimes have fields that do not exist in the table for the Model requested. 229 | * Here, we get all model attributes and check the request schema for fields that exist as 230 | * attributes for that model. 231 | * Those are the attributes that should be passed to the sequelise "select" query 232 | */ 233 | 234 | // Initialise the list of selected attributes 235 | const selectedAttributes: SelectedAttributes = []; 236 | 237 | // Get the field names for the model 238 | const modelAttributes = Object.keys((model).rawAttributes); 239 | 240 | selections.forEach((item) => { 241 | const selection = item as FieldNode; 242 | const fieldName = selection.name.value; 243 | const isModelAttribute = modelAttributes.find(attr => attr === fieldName); 244 | const hasSubSelection = selection.selectionSet !== undefined; 245 | 246 | if (isModelAttribute && !hasSubSelection) { 247 | selectedAttributes.push(fieldName); 248 | } 249 | }); 250 | 251 | return selectedAttributes; 252 | }, 253 | 254 | turnArgsToWhere(fieldArguments) { 255 | /** 256 | * With any of the included models, the query can have arguments, 257 | * Here we convert the arguments into a data structure which will 258 | * serve as the WHERE property to be supplied to the sequelize query 259 | */ 260 | 261 | let whereConstraints: IWhereConstraints = {}; 262 | if (fieldArguments.length) { 263 | whereConstraints = this.getWhereConstraints(fieldArguments) as IWhereConstraints; 264 | } 265 | return whereConstraints; 266 | }, 267 | 268 | getWhereConstraints(fieldArguments) { 269 | const whereOption: IWhereConstraints = {}; 270 | const scopeFieldArgument = fieldArguments.find(arg => arg.name.value === 'scope'); 271 | 272 | if(scopeFieldArgument !== undefined) { 273 | const argumentValueNode = scopeFieldArgument.value as StringValueNode; 274 | const argumentString = argumentValueNode.value; 275 | /** 276 | * we split with `&&` because we can multiple constraints 277 | * for example we can the scope argument as a string like the following 278 | * `id|like|%introduction% && published|eq|true` 279 | * 280 | * This would be the case for a GraphQL query like the one below 281 | * ```js 282 | * articles(scope: "id|like|%introduction% && published|eq|true") { 283 | * id 284 | * body 285 | * } 286 | */ 287 | const whereComparisons = argumentString.split('&&'); 288 | 289 | whereComparisons.forEach((fieldConditionString) => { 290 | const splitString = this.getValidScopeString(fieldConditionString); 291 | const field = splitString[0].trim(); 292 | const operation = splitString[1].trim(); 293 | const value = splitString[2].trim(); 294 | const sequelizeOperator = sequelizeOperators[operation]; 295 | whereOption[field] = { [sequelizeOperator]: value }; 296 | }); 297 | } 298 | 299 | return whereOption; 300 | }, 301 | 302 | /** 303 | * Validate the scope argument string to be sure 304 | * there are no errors. 305 | * @param splitString - scope string to be checked 306 | */ 307 | getValidScopeString(fieldConditionString) { 308 | const splitString = fieldConditionString.split('|'); 309 | if(splitString.length < 3) { 310 | throw Error(`Incorrect Parts supplied for scope: ${fieldConditionString}`); 311 | } 312 | const field = splitString[0].trim(); 313 | const operation = splitString[1].trim(); 314 | const value = splitString[2].trim(); 315 | 316 | if(field === "" || operation === "" || value === "") { 317 | throw Error(`Incorrect Parts supplied for scope: ${fieldConditionString}`); 318 | } 319 | return splitString; 320 | } 321 | } 322 | 323 | export = queryLoader; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentNode, GraphQLResolveInfo, SelectionNode } from 'graphql'; 2 | import { FindOptions, IncludeOptions, Model as SequelizeModel } from "sequelize/types"; 3 | 4 | /** 5 | * Object containing all loaded models 6 | * from sequelize 7 | * 8 | * eg 9 | * ```js 10 | * { 11 | * Article: SequelizeModel, 12 | * Category: SequelizeModel, 13 | * Comment: SequelizeModel, 14 | * User: SequelizeModel, 15 | * } 16 | */ 17 | export interface IIncludeModels { 18 | [modelName: string]: SequelizeModel; 19 | } 20 | 21 | /** 22 | * model fields to be selected 23 | * 24 | * Example 25 | * ```js 26 | * ['id', 'firstname', 'lastname'], 27 | */ 28 | export type SelectedAttributes = string[]; 29 | 30 | /** 31 | * Array of include options 32 | * for all models to be included 33 | * 34 | * Example 35 | * ```js 36 | * [ 37 | * { 38 | * model: User, 39 | * as: 'owner', 40 | * attributes: ['firstname', 'lastname'], 41 | * required: false, 42 | * }, 43 | * { 44 | * model: Comment, 45 | * as: 'comments', 46 | * attributes: ['id', 'content'], 47 | * required: false, 48 | * } 49 | * ] 50 | */ 51 | export type SelectedIncludes = IncludeOptions[]; 52 | 53 | /** 54 | * Object containing the selected fields and the 55 | * where constraints applied on them 56 | * 57 | * Example 58 | * ```js 59 | * { id: { [Symbol(gt)]: '5' } } 60 | */ 61 | export interface IWhereConstraints { 62 | [fieldName: string]: { 63 | [OperatorSymbol: string]: string 64 | } 65 | } 66 | 67 | export interface ISequelizeOperators { 68 | [operatorName: string]: symbol 69 | } 70 | 71 | export interface IQueryLoader { 72 | 73 | init: (object: { 74 | includeModels: IIncludeModels, 75 | }) => void; 76 | 77 | includeModels: IIncludeModels; 78 | 79 | getFindOptions: (object: { 80 | model: SequelizeModel, 81 | info: GraphQLResolveInfo 82 | }) => FindOptions; 83 | 84 | getSelectedAttributes: (object: { 85 | model: SequelizeModel | any, 86 | selections: ReadonlyArray | undefined 87 | }) => SelectedAttributes; 88 | 89 | getSelectedIncludes: (object: { 90 | model: SequelizeModel, 91 | selections: ReadonlyArray | undefined 92 | }) => SelectedIncludes; 93 | 94 | prepareIncludes: (object: { 95 | model: SequelizeModel, 96 | selections: ReadonlyArray | undefined 97 | }) => SelectedIncludes; 98 | 99 | getIncludeModel: (fieldName: string) => SequelizeModel 100 | 101 | turnArgsToWhere: (fieldArguments: ReadonlyArray) => IWhereConstraints | {} 102 | 103 | getWhereConstraints: (fieldArguments: ReadonlyArray) => IWhereConstraints | {} 104 | 105 | getValidScopeString: (fieldConditionString: string) => string[] 106 | } 107 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true, 8 | "watch": true, 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "**/__tests__/*", "**/__mocks__/*"] 12 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"] 3 | } --------------------------------------------------------------------------------