├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── examples ├── README.md ├── apollo │ ├── Dockerfile │ ├── docker-compose.yml │ ├── package.json │ ├── src │ │ ├── SolutionA │ │ │ ├── directives.ts │ │ │ └── makeExecutableSchema.ts │ │ ├── SolutionB │ │ │ ├── directives.ts │ │ │ └── makeExecutableSchema.ts │ │ ├── db.ts │ │ ├── index.ts │ │ └── schema.ts │ ├── tsconfig.json │ └── yarn.lock ├── express │ ├── Dockerfile │ ├── docker-compose.yml │ ├── package.json │ ├── src │ │ ├── db.ts │ │ ├── index.ts │ │ ├── queryFieldUtil.ts │ │ └── schema.ts │ ├── tsconfig.json │ └── yarn.lock └── graphql-tools │ ├── Dockerfile │ ├── docker-compose.yml │ ├── package.json │ ├── src │ ├── SolutionA │ │ ├── directives.ts │ │ └── makeExecutableSchema.ts │ ├── SolutionB │ │ ├── directives.ts │ │ └── makeExecutableSchema.ts │ ├── db.ts │ ├── index.ts │ └── schema.ts │ ├── tsconfig.json │ └── yarn.lock ├── index.ts ├── package.json ├── src ├── common.ts ├── graphQLFilterType.ts ├── graphQLInsertType.ts ├── graphQLPaginationType.ts ├── graphQLSortType.ts ├── graphQLUpdateType.ts ├── logger.ts ├── mongoDbFilter.ts ├── mongoDbProjection.ts ├── mongoDbSort.ts ├── mongoDbUpdate.ts ├── mongoDbUpdateValidation.ts ├── queryResolver.ts └── updateResolver.ts ├── tests ├── specs │ ├── graphQLFilterType.spec.ts │ ├── graphQLSortType.spec.ts │ ├── graphQLUpdateType.spec.ts │ ├── mongoDbFilter.spec.ts │ ├── mongoDbProjection.spec.ts │ ├── mongoDbSort.spec.ts │ ├── mongoDbUpdate.spec.ts │ └── mongoDbUpdateValidation.spec.ts └── utils │ ├── fieldResolve.ts │ ├── printInputType.ts │ └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | lib/ 4 | .history 5 | .idea 6 | .vscode/ 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | index.ts 3 | .gitignore 4 | .travis.yml 5 | CHANGELOG.md 6 | tsconfig.json 7 | tests/ 8 | examples/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | install: 5 | - npm install 6 | before_script: 7 | - npm install -g typescript 8 | script: 9 | - npm run build 10 | - npm run test 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ##### 1.6.5 3 | - Allow override of the is-resolved-field logic in projection and update validation 4 | ##### 1.6.4 5 | - `$all` 6 | ##### 1.6.3 7 | - GraphQLInterface support for filter type, including fragment projection 8 | - Correct non-null update validation to ignore _id in root 9 | - More tests 10 | ##### 1.6.2 11 | - `$nor` 12 | - `$not` 13 | - Refactor 14 | - More tests 15 | --- 16 | ##### 1.6.1 17 | - Expose the types cache 18 | --- 19 | ### 1.6.0 20 | - Add overwrite flag for nested objects in update args under `set`. Allows for overwriting of entire object instead of just the specified fields. Update validation adjusted to accomadate new flag. 21 | - More tests 22 | --- 23 | ##### 1.5.1 24 | - Bug fix, non-null validation should only apply on upserts or list items 25 | --- 26 | ### 1.5.0 27 | - Add server-side validation for update args instead of preserving non-nullability from the origin type. 28 | If a field is non-nullable it must be set in either the update operators (e.g. `setOnInsert`, `set`, `inc`, etc...) 29 | - Tests! (Limited coverage) 30 | --- 31 | ##### 1.4.4 32 | - Bug fix 33 | --- 34 | ##### 1.4.3 35 | - Fix yet another bug in resolve dependencies projection 36 | --- 37 | ##### 1.4.2 38 | - Fix a bug in array mutation 39 | --- 40 | ##### 1.4.1 41 | - Fix a bug in resolve dependencies projection 42 | --- 43 | ### 1.4.0 44 | - Add Support for FragmentSpread in projection 45 | --- 46 | ##### 1.3.5 47 | - Fix a bug in parsing a scalar array filter 48 | --- 49 | ##### 1.3.4 50 | - Fix a bug where a malformed sort param is produced when sorting by nested fields 51 | --- 52 | ##### 1.3.3 53 | - Add regex operator 54 | --- 55 | ##### 1.3.2 56 | - Can get different output type from resolver and omit projection 57 | --- 58 | ##### 1.3.1 59 | - Fix type declarations in package 60 | --- 61 | ### 1.3.0 62 | - Renamed package and repository 63 | - TypeScript! 64 | - Log warn and error callbacks 65 | --- 66 | ##### 1.2.3 67 | - Fix projection bug 68 | --- 69 | ##### 1.2.2 70 | - Projection now supports multiple of same field without alias 71 | --- 72 | ##### 1.2.1 73 | - Array items have full filtering range through the `$elemMatch` operator 74 | - Change log added 75 | --- 76 | ### 1.2.0 77 | - Oprerator selection changed to a shorter format: `name: { EQ: "John" }` 78 | - Old format deprecated 79 | --- 80 | ##### 1.1.1 81 | - Fix omition of zeros in update `$set` fields 82 | --- 83 | ### 1.1.0 84 | - Changes non-null field of name `_id` to nullable in insert type 85 | - Resolver dependencies now extracted from the GraphQL type definition 86 | - Query and update callbacks combined in `getUpdateResolver` 87 | --- 88 | ##### 1.0.9 89 | - Add Enum support 90 | - getUpdateResolver: make query callBack redaundent 91 | --- 92 | ##### 1.0.8 93 | - Fix of bugs in 1.0.7 94 | --- 95 | ##### 1.0.7 96 | - Common code file created 97 | - Arguments validators added 98 | - Non numberic type filtered from update `$inc` 99 | - If there are no valid fields, a fictive fieldis added 100 | - Nested objects added to sort 101 | - If there are no valid fields, a fictive field is added 102 | --- 103 | ##### 1.0.6 104 | - Package license set to MIT 105 | - Base fields of `getGraphQLUpdateArgs` are now non-null 106 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Graphql-To-Mongodb 2 | 3 | Welcome, and thank you for your interest in contributing to GraphQL to MongoDB! 4 | 5 | There are many ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level overview of how you can get involved. 6 | 7 | ## Asking Questions 8 | 9 | Have a question? Rather than opening an issue, please ask away on [Stack Overflow](https://stackoverflow.com/) using the a related tag 10 | 11 | The active community will be eager to assist you. Your well-worded question will serve as a resource to others searching for help. 12 | 13 | ## Providing Feedback 14 | 15 | Your comments and feedback are welcome, and the development team is available via a handful of different channels: 16 | 17 | - GitHub issues 18 | - Any related slack channels or stackoverflow 19 | 20 | ## Reporting Issues 21 | 22 | Have you identified a reproducible problem in GraphQL to MongoDB? Have a feature request? We want to hear about it! Here's how you can make reporting your issue as effective as possible. 23 | 24 | ### Look For an Existing Issue 25 | 26 | Before you create a new issue, please do a search in [open issues](https://github.com/Soluto/graphql-to-mongodb/issues) to see if the issue or feature request has already been filed. 27 | 28 | If you cannot find an existing issue that describes your bug or feature, create a new issue using the guidelines below. 29 | 30 | ### Writing Good Bug Reports and Feature Requests 31 | 32 | File a single issue per problem and feature request. Do not enumerate multiple bugs or feature requests in the same issue. 33 | 34 | Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes. 35 | 36 | The more information you can provide, the more likely someone will be successful at reproducing the issue and finding a fix. 37 | 38 | Please include the following with each issue: 39 | 40 | - Version of Graphql-To-Mongodb 41 | - Your operating system 42 | - Reproducible steps (1... 2... 3...) that cause the issue 43 | - What you expected to see, versus what you actually saw 44 | - Images, animations, or a link to a video showing the issue occurring 45 | - A code snippet that demonstrates the issue or a link to a code repository the developers can easily pull down to recreate the issue locally 46 | - **Note:** Because the developers need to copy and paste the code snippet, including a code snippet as a media file (i.e. .gif) is not sufficient. 47 | 48 | ### Final Checklist 49 | 50 | Please remember to do the following: 51 | 52 | - [ ] Search the issue repository to ensure your report is a new issue 53 | - [ ] Simplify your code around the issue to better isolate the problem 54 | 55 | Don't feel bad if the developers can't reproduce the issue right away. They will simply ask for more information! 56 | 57 | ## Contributing Fixes 58 | 59 | There are many ways to contribute to the GraphQL to MongoDB project: logging bugs, submitting pull requests, reporting issues, and creating suggestions. 60 | 61 | After cloning and building the repo, check out the issues list. Issues labeled `help wanted` are good issues to submit a PR for. Issues labeled `good first issue` are great candidates to pick up if you are in the code for the first time. If you are contributing significant changes, or if the issue is already assigned to a specific month milestone, please discuss with the issue assignee first before starting to work on the issue 62 | 63 | # Thank You! 64 | 65 | Your contributions to open source, large or small, make great projects like this possible. Thank you for taking the time to contribute. 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-to-mongodb 2 | [![Build Status](https://travis-ci.org/Soluto/graphql-to-mongodb.svg?branch=master)](https://travis-ci.org/Soluto/graphql-to-mongodb) 3 | 4 | If you want to grant your NodeJS GraphQL service a whole lot of the power of the MongoDB database standing behind it with very little hassle, you've come to the right place! 5 | 6 | ### [Examples](./examples) 7 | ### [Change Log](./CHANGELOG.md) 8 | ### [Blog Post](https://blog.solutotlv.com/graphql-to-mongodb-or-how-i-learned-to-stop-worrying-and-love-generated-query-apis/?utm_source=README) 9 | 10 | ### Let's take a look at the most common use case, ```getMongoDbQueryResolver``` and ```getGraphQLQueryArgs```: 11 | 12 | **Given a simple GraphQL type:** 13 | ```js 14 | new GraphQLObjectType({ 15 | name: 'PersonType', 16 | fields: () => ({ 17 | age: { type: GraphQLInt }, 18 | name: { type: new GraphQLObjectType({ 19 | name: 'NameType', 20 | fields: () => ({ 21 | first: { type: GraphQLString }, 22 | last: { type: GraphQLString } 23 | }) 24 | }), 25 | fullName: { 26 | type: GraphQLString, 27 | resolve: (obj, args, { db }) => `${obj.name.first} ${obj.name.last}` 28 | } 29 | }) 30 | }) 31 | ``` 32 | #### An example GraphQL query supported by the package: 33 | 34 | Queries the first 50 people, oldest first, over the age of 18, and whose first name is John. 35 | 36 | ``` 37 | { 38 | people ( 39 | filter: { 40 | age: { GT: 18 }, 41 | name: { 42 | first: { EQ: "John" } 43 | } 44 | }, 45 | sort: { age: DESC }, 46 | pagination: { limit: 50 } 47 | ) { 48 | fullName 49 | age 50 | } 51 | } 52 | ``` 53 | 54 | **To implement, we'll define the people query field in our GraphQL scheme like so:** 55 | 56 | 57 | ```js 58 | people: { 59 | type: new GraphQLList(PersonType), 60 | args: getGraphQLQueryArgs(PersonType), 61 | resolve: getMongoDbQueryResolver(PersonType, 62 | async (filter, projection, options, obj, args, context) => { 63 | return await context.db.collection('people').find(filter, projection, options).toArray(); 64 | }) 65 | } 66 | ``` 67 | You'll notice that integrating the package takes little more than adding some fancy middleware over the resolve function. The `filter, projection, options` added as the first parameters of the callback, can be sent directly to the MongoDB find function as shown. The rest of the parameter are the standard received from the GraphQL api. 68 | 69 | * Additionally, resolve fields' dependencies should be defined in the GraphQL type like so: 70 | ```js 71 | fullName: { 72 | type: GraphQLString, 73 | resolve: (obj, args, { db }) => `${obj.name.first} ${obj.name.last}`, 74 | dependencies: ['name'] // or ['name.first', 'name.Last'], whatever tickles your fancy 75 | } 76 | ``` 77 | This is needed to ensure that the projection does not omit any necessary fields. Alternatively, if throughput is of no concern, the projection can be replaced with an empty object. 78 | * As of `mongodb` package version 3.0, you should implement the resolve callback as: 79 | ```js 80 | return await context.db.collection('people').find(filter, options).toArray(); 81 | ``` 82 | 83 | ### That's it! 84 | 85 | **The following field is added to the schema (copied from graphiQl):** 86 | ``` 87 | people( 88 | filter: PersonFilterType 89 | sort: PersonSortType 90 | pagination: GraphQLPaginationType 91 | ): [PersonType] 92 | ``` 93 | 94 | **PersonFilterType:** 95 | ``` 96 | age: IntFilter 97 | name: NameObjectFilterType 98 | OR: [PersonFilterType] 99 | AND: [PersonFilterType] 100 | NOR: [PersonFilterType] 101 | ``` 102 | \* Filtering is possible over every none resolve field! 103 | 104 | **NameObjectFilterType:** 105 | ``` 106 | first: StringFilter 107 | last: StringFilter 108 | opr: OprExists 109 | ``` 110 | `OprExists` enum type can be `EXISTS` or `NOT_EXISTS`, and can be found in nested objects and arrays 111 | 112 | **StringFilter:** 113 | ``` 114 | EQ: String 115 | GT: String 116 | GTE: String 117 | IN: [String] 118 | LT: String 119 | LTE: String 120 | NEQ: String 121 | NIN: [String] 122 | NOT: [StringFNotilter] 123 | ``` 124 | 125 | **PersonSortType:** 126 | ``` 127 | age: SortType 128 | ``` 129 | `SortType` enum can be either `ASC` or `DESC` 130 | 131 | **GraphQLPaginationType:** 132 | ``` 133 | limit: Int 134 | skip: Int 135 | ``` 136 | 137 | ### Functionality galore! Also permits update, insert, and extensiable custom fields. 138 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | * **express:** Vanilla `graphql` using `express`. 4 | * **graphql-tools:** Using `graphql-tools` to define the schema through the schema definition language. 5 | * **apollo:** `apollo-server` implementation. 6 | 7 | Run with `docker-compose up` to start with a paired MongoDb image, or enter a new connection string in `db.ts` and run `yarn start`. 8 | 9 | **Note:** There are currently two solutions to integrate the package with the sdl, neither is entirely elegant. Suggestions for improvement are welcome. 10 | -------------------------------------------------------------------------------- /examples/apollo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine as dependencies 2 | 3 | WORKDIR /service 4 | COPY package.json ./ 5 | RUN yarn 6 | 7 | COPY ./src ./src 8 | 9 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /examples/apollo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mongo: 5 | image: mongo:3.6 6 | ports: 7 | - "27017:27017" 8 | 9 | apollo: 10 | build: . 11 | ports: 12 | - "3000:3000" 13 | depends_on: 14 | - mongo 15 | -------------------------------------------------------------------------------- /examples/apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "ts-node src/index.ts" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "apollo-server-express": "^2.2.0", 12 | "express": "^4.16.4", 13 | "graphql": "^14.2.1", 14 | "graphql-to-mongodb": "1.6.5", 15 | "mongodb": "^3.1.10" 16 | }, 17 | "devDependencies": { 18 | "@types/graphql": "^14.0.3", 19 | "@types/mongodb": "^3.1.15", 20 | "ts-node": "^7.0.1", 21 | "typescript": "^3.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/apollo/src/SolutionA/directives.ts: -------------------------------------------------------------------------------- 1 | import { SchemaDirectiveVisitor, gql } from "apollo-server-express" 2 | import { GraphQLField, GraphQLObjectType, GraphQLArgument, GraphQLInputType, GraphQLOutputType, isListType, isNonNullType, isObjectType, GraphQLNamedType } from "graphql"; 3 | import { getGraphQLQueryArgs, getMongoDbQueryResolver, QueryOptions, getGraphQLUpdateArgs, getMongoDbUpdateResolver, UpdateOptions, getGraphQLInsertType, getGraphQLFilterType, getMongoDbFilter } from "graphql-to-mongodb"; 4 | 5 | export const types = gql` 6 | input QueryOptions { 7 | differentOutputType: Boolean 8 | } 9 | 10 | input UpdateOptions { 11 | differentOutputType: Boolean 12 | validateUpdateArgs: Boolean 13 | overwrite: Boolean 14 | } 15 | 16 | directive @mongoDependencies(paths: [String!]!) on FIELD_DEFINITION 17 | directive @mongoQueryArgs(type: String!) on FIELD_DEFINITION 18 | directive @mongoQueryResolver(type: String!, queryOptions: QueryOptions) on FIELD_DEFINITION 19 | directive @mongoUpdateArgs(type: String!) on FIELD_DEFINITION 20 | directive @mongoUpdateResolver(type: String!, updateOptions: UpdateOptions) on FIELD_DEFINITION 21 | directive @mongoInsertArgs(type: String!, key: String!) on FIELD_DEFINITION 22 | directive @mongoFilterArgs(type: String!, key: String!) on FIELD_DEFINITION 23 | directive @mongoFilterResolver(type: String!, key: String!) on FIELD_DEFINITION 24 | `; 25 | 26 | export class MongoDirectivesContext { 27 | public static stage: "First" | "Second"; 28 | } 29 | 30 | export class MongoDependenciesVisitor extends SchemaDirectiveVisitor { 31 | public visitFieldDefinition(field: GraphQLField) { 32 | const { paths } = this.args as { paths: string[] } 33 | field["dependencies"] = paths; 34 | } 35 | } 36 | 37 | export class MongoQueryArgsVisitor extends SchemaDirectiveVisitor { 38 | public visitFieldDefinition(field: GraphQLField) { 39 | const { type } = this.args as { type: string } 40 | const graphqlType = this.schema.getType(type); 41 | 42 | if (!(graphqlType instanceof GraphQLObjectType)) { 43 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 44 | } 45 | 46 | if (MongoDirectivesContext.stage === "First") { 47 | getGraphQLQueryArgs(graphqlType); 48 | } 49 | 50 | if (MongoDirectivesContext.stage === "Second") { 51 | let queryArgs = getGraphQLQueryArgs(graphqlType); 52 | const args: GraphQLArgument[] = Object.keys(queryArgs).map(key => ({ 53 | name: key, 54 | type: this.schema.getType(queryArgs[key].type.name) as GraphQLInputType, 55 | description: undefined, 56 | defaultValue: undefined, 57 | extensions: undefined, 58 | astNode: undefined 59 | })); 60 | 61 | field.args = [...field.args, ...args]; 62 | } 63 | } 64 | } 65 | 66 | export class MongoQueryResolverVisitor extends SchemaDirectiveVisitor { 67 | public visitFieldDefinition(field: GraphQLField) { 68 | const { type, queryOptions: queryOptionsArg } = this.args as { type: string, queryOptions: QueryOptions } 69 | const graphqlType = this.schema.getType(type); 70 | 71 | if (!(graphqlType instanceof GraphQLObjectType)) { 72 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 73 | } 74 | 75 | if (MongoDirectivesContext.stage === "Second") { 76 | const queryOptions = { 77 | ...queryOptionsArg, 78 | isResolvedField: field => !field[resolverless], 79 | excludedFields: [] 80 | }; 81 | markResolverless(graphqlType); 82 | field.resolve = getMongoDbQueryResolver(graphqlType, field.resolve, queryOptions); 83 | } 84 | } 85 | } 86 | 87 | export class MongoUpdateArgsVisitor extends SchemaDirectiveVisitor { 88 | public visitFieldDefinition(field: GraphQLField) { 89 | const { type } = this.args as { type: string } 90 | const graphqlType = this.schema.getType(type); 91 | 92 | if (!(graphqlType instanceof GraphQLObjectType)) { 93 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 94 | } 95 | 96 | if (MongoDirectivesContext.stage === "First") { 97 | getGraphQLUpdateArgs(graphqlType); 98 | } 99 | 100 | if (MongoDirectivesContext.stage === "Second") { 101 | let updateArgs = getGraphQLUpdateArgs(graphqlType); 102 | const args: GraphQLArgument[] = Object.keys(updateArgs).map(key => ({ 103 | name: key, 104 | type: this.schema.getType(updateArgs[key].type.ofType.name) as GraphQLInputType, 105 | description: undefined, 106 | defaultValue: undefined, 107 | extensions: undefined, 108 | astNode: undefined 109 | })); 110 | 111 | field.args = [...field.args, ...args]; 112 | } 113 | } 114 | } 115 | 116 | export class MongoUpdateResolverVisitor extends SchemaDirectiveVisitor { 117 | public visitFieldDefinition(field: GraphQLField) { 118 | const { type, updateOptions: updateOptionsArg } = this.args as { type: string, updateOptions: UpdateOptions } 119 | const graphqlType = this.schema.getType(type); 120 | 121 | if (!(graphqlType instanceof GraphQLObjectType)) { 122 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 123 | } 124 | 125 | if (MongoDirectivesContext.stage === "Second") { 126 | const updateOptions = { 127 | ...updateOptionsArg, 128 | isResolvedField: field => !field[resolverless], 129 | excludedFields: [] 130 | }; 131 | markResolverless(graphqlType); 132 | 133 | field.resolve = getMongoDbUpdateResolver(graphqlType, field.resolve as any, updateOptions); 134 | } 135 | } 136 | } 137 | 138 | export class MongoInsertArgsVisitor extends SchemaDirectiveVisitor { 139 | public visitFieldDefinition(field: GraphQLField) { 140 | const { type, key } = this.args as { type: string, key: string } 141 | const graphqlType = this.schema.getType(type); 142 | 143 | if (!(graphqlType instanceof GraphQLObjectType)) { 144 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 145 | } 146 | 147 | if (MongoDirectivesContext.stage === "First") { 148 | getGraphQLInsertType(graphqlType); 149 | } 150 | 151 | if (MongoDirectivesContext.stage === "Second") { 152 | const insertType = getGraphQLInsertType(graphqlType); 153 | field.args = [...field.args, { 154 | name: key, type: this.schema.getType(insertType.name) as GraphQLInputType, 155 | description: undefined, 156 | defaultValue: undefined, 157 | extensions: undefined, 158 | astNode: undefined 159 | }]; 160 | } 161 | } 162 | } 163 | 164 | export class MongoFilterArgsVisitor extends SchemaDirectiveVisitor { 165 | public visitFieldDefinition(field: GraphQLField) { 166 | const { type, key } = this.args as { type: string, key: string } 167 | const graphqlType = this.schema.getType(type); 168 | 169 | if (!(graphqlType instanceof GraphQLObjectType)) { 170 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 171 | } 172 | 173 | if (MongoDirectivesContext.stage === "First") { 174 | getGraphQLFilterType(graphqlType); 175 | } 176 | 177 | if (MongoDirectivesContext.stage === "Second") { 178 | const filterType = getGraphQLFilterType(graphqlType); 179 | field.args = [...field.args, { 180 | name: key, type: this.schema.getType(filterType.name) as GraphQLInputType, 181 | description: undefined, 182 | defaultValue: undefined, 183 | extensions: undefined, 184 | astNode: undefined 185 | }]; 186 | }; 187 | } 188 | } 189 | 190 | export class MongoFilterResolverVisitor extends SchemaDirectiveVisitor { 191 | public visitFieldDefinition(field: GraphQLField) { 192 | const { type, key } = this.args as { type: string, key: string } 193 | const graphqlType = this.schema.getType(type); 194 | 195 | if (!(graphqlType instanceof GraphQLObjectType)) { 196 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 197 | } 198 | 199 | if (MongoDirectivesContext.stage === "Second") { 200 | const resolve = field.resolve; 201 | 202 | field.resolve = ((source, args, context, info) => { 203 | const filter = getMongoDbFilter(graphqlType, args[key]) 204 | return (resolve as any)(filter, source, args, context, info); 205 | }).bind(field); 206 | } 207 | } 208 | } 209 | 210 | export const visitors = { 211 | mongoDependencies: MongoDependenciesVisitor, 212 | mongoQueryArgs: MongoQueryArgsVisitor, 213 | mongoQueryResolver: MongoQueryResolverVisitor, 214 | mongoUpdateArgs: MongoUpdateArgsVisitor, 215 | mongoUpdateResolver: MongoUpdateResolverVisitor, 216 | mongoInsertArgs: MongoInsertArgsVisitor, 217 | mongoFilterArgs: MongoFilterArgsVisitor, 218 | mongoFilterResolver: MongoFilterResolverVisitor, 219 | }; 220 | 221 | const resolverless = Symbol("resolverless"); 222 | 223 | const markResolverless = (type: GraphQLObjectType) => { 224 | const innerType = (type: GraphQLOutputType): GraphQLOutputType & GraphQLNamedType => { 225 | if (isNonNullType(type) || isListType(type)) 226 | return innerType(type.ofType); 227 | return type; 228 | } 229 | 230 | const fields = type.getFields(); 231 | 232 | Object.keys(fields).map(key => fields[key]).forEach(field => { 233 | if (!!field.resolve) return; 234 | if (field[resolverless] === true) return; 235 | field[resolverless] = true; 236 | const fieldType = innerType(field.type); 237 | if (isObjectType(fieldType)) { 238 | markResolverless(fieldType); 239 | } 240 | }); 241 | } 242 | -------------------------------------------------------------------------------- /examples/apollo/src/SolutionA/makeExecutableSchema.ts: -------------------------------------------------------------------------------- 1 | import { gql, IExecutableSchemaDefinition, makeExecutableSchema } from "apollo-server-express"; 2 | import { 3 | GraphQLInputObjectType, 4 | GraphQLInputType, 5 | GraphQLNamedType, 6 | GraphQLSchema, 7 | isEnumType, 8 | isInputObjectType, 9 | isListType, 10 | isNonNullType, 11 | printType 12 | } from "graphql"; 13 | import { clearTypesCache, getTypesCache, GraphQLPaginationType, GraphQLSortType } from "graphql-to-mongodb"; 14 | import { MongoDirectivesContext, types as directiveTypes, visitors } from "./directives"; 15 | 16 | export default function (config: IExecutableSchemaDefinition): GraphQLSchema { 17 | clearTypesCache(); 18 | MongoDirectivesContext.stage = "First"; 19 | 20 | const configTypeDefs = Array.isArray(config.typeDefs) ? config.typeDefs : [config.typeDefs]; 21 | 22 | makeExecutableSchema({ 23 | ...config, 24 | typeDefs: [...configTypeDefs, directiveTypes], 25 | schemaDirectives: { ...config.schemaDirectives, ...visitors } 26 | }); 27 | 28 | let typesCache = getTypesCache(); 29 | resolveLazyFields(Object.keys(typesCache).map(_ => typesCache[_]).filter(isInputObjectType)); 30 | typesCache = getTypesCache(); 31 | typesCache[GraphQLPaginationType.name] = GraphQLPaginationType; 32 | typesCache[GraphQLSortType.name] = GraphQLSortType; 33 | const typesSdlRaw = Object 34 | .keys(typesCache) 35 | .map(key => printType(typesCache[key])) 36 | .join("\n"); 37 | 38 | const typesSdl = gql(typesSdlRaw); 39 | 40 | const enumResolvers = getEnumResolvers(typesCache); 41 | 42 | MongoDirectivesContext.stage = "Second"; 43 | 44 | const stageTwoSchema = makeExecutableSchema({ 45 | ...config, 46 | typeDefs: [...configTypeDefs, typesSdl, directiveTypes], 47 | schemaDirectives: { ...config.schemaDirectives, ...visitors }, 48 | resolvers: { ...enumResolvers, ...config.resolvers } 49 | }); 50 | 51 | return stageTwoSchema; 52 | } 53 | 54 | function resolveLazyFields(types: GraphQLInputObjectType[]) { 55 | types.forEach(type => { 56 | const typesCache = getTypesCache(); 57 | const fields = type.getFields(); 58 | resolveLazyFields(Object 59 | .keys(fields) 60 | .map(key => innerType(fields[key].type)) 61 | .filter(isInputObjectType) 62 | .filter(_ => !typesCache[_.name])); 63 | }); 64 | } 65 | 66 | function innerType(type: GraphQLInputType): GraphQLInputType & GraphQLNamedType { 67 | if (isNonNullType(type) || isListType(type)) { 68 | return innerType(type.ofType); 69 | } 70 | return type; 71 | } 72 | 73 | function getEnumResolvers(typesCache: { [key: string]: GraphQLNamedType; }) { 74 | return Object.keys(typesCache) 75 | .map(_ => typesCache[_]) 76 | .filter(isEnumType) 77 | .reduce((resolvers, enumType) => ({ 78 | ...resolvers, 79 | [enumType.name]: enumType.getValues().reduce((resolver, entry) => ({ 80 | ...resolver, [entry.name]: entry.value 81 | }), {}) 82 | }), {}); 83 | } 84 | -------------------------------------------------------------------------------- /examples/apollo/src/SolutionB/directives.ts: -------------------------------------------------------------------------------- 1 | import { SchemaDirectiveVisitor, gql } from "apollo-server-express" 2 | import { GraphQLField, GraphQLObjectType, GraphQLArgument, GraphQLNamedType, isNonNullType, isListType, GraphQLOutputType, isObjectType } from "graphql"; 3 | import { getGraphQLQueryArgs, getMongoDbQueryResolver, QueryOptions, getGraphQLUpdateArgs, getMongoDbUpdateResolver, UpdateOptions, getGraphQLInsertType, getGraphQLFilterType, getMongoDbFilter } from "graphql-to-mongodb"; 4 | 5 | export const types = gql` 6 | input QueryOptions { 7 | differentOutputType: Boolean 8 | } 9 | 10 | input UpdateOptions { 11 | differentOutputType: Boolean 12 | validateUpdateArgs: Boolean 13 | overwrite: Boolean 14 | } 15 | 16 | directive @mongoDependencies(paths: [String!]!) on FIELD_DEFINITION 17 | directive @mongoQueryArgs(type: String!) on FIELD_DEFINITION 18 | directive @mongoQueryResolver(type: String!, queryOptions: QueryOptions) on FIELD_DEFINITION 19 | directive @mongoUpdateArgs(type: String!) on FIELD_DEFINITION 20 | directive @mongoUpdateResolver(type: String!, updateOptions: UpdateOptions) on FIELD_DEFINITION 21 | directive @mongoInsertArgs(type: String!, key: String!) on FIELD_DEFINITION 22 | directive @mongoFilterArgs(type: String!, key: String!) on FIELD_DEFINITION 23 | directive @mongoFilterResolver(type: String!, key: String!) on FIELD_DEFINITION 24 | `; 25 | 26 | export class MongoDependenciesVisitor extends SchemaDirectiveVisitor { 27 | public visitFieldDefinition(field: GraphQLField) { 28 | const { paths } = this.args as { paths: string[] } 29 | field["dependencies"] = paths; 30 | } 31 | } 32 | 33 | export class MongoQueryArgsVisitor extends SchemaDirectiveVisitor { 34 | public visitFieldDefinition(field: GraphQLField) { 35 | const { type } = this.args as { type: string } 36 | const graphqlType = this.schema.getType(type); 37 | 38 | if (!(graphqlType instanceof GraphQLObjectType)) { 39 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 40 | } 41 | 42 | let queryArgs = getGraphQLQueryArgs(graphqlType); 43 | const args: GraphQLArgument[] = Object.keys(queryArgs).map(key => ({ 44 | name: key, 45 | type: queryArgs[key].type, 46 | description: undefined, 47 | defaultValue: undefined, 48 | extensions: undefined, 49 | astNode: undefined 50 | })); 51 | 52 | field.args = [...field.args, ...args]; 53 | } 54 | } 55 | 56 | export class MongoQueryResolverVisitor extends SchemaDirectiveVisitor { 57 | public visitFieldDefinition(field: GraphQLField) { 58 | const { type, queryOptions: queryOptionsArg } = this.args as { type: string, queryOptions: QueryOptions } 59 | const graphqlType = this.schema.getType(type); 60 | 61 | if (!(graphqlType instanceof GraphQLObjectType)) { 62 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 63 | } 64 | 65 | const queryOptions = { 66 | ...queryOptionsArg, 67 | isResolvedField: field => !field[resolverless], 68 | excludedFields: [] 69 | }; 70 | markResolverless(graphqlType); 71 | 72 | field.resolve = getMongoDbQueryResolver(graphqlType, field.resolve, queryOptions); 73 | } 74 | } 75 | 76 | export class MongoUpdateArgsVisitor extends SchemaDirectiveVisitor { 77 | public visitFieldDefinition(field: GraphQLField) { 78 | const { type } = this.args as { type: string } 79 | const graphqlType = this.schema.getType(type); 80 | 81 | if (!(graphqlType instanceof GraphQLObjectType)) { 82 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 83 | } 84 | 85 | let updateArgs = getGraphQLUpdateArgs(graphqlType); 86 | const args: GraphQLArgument[] = Object.keys(updateArgs).map(key => ({ 87 | name: key, 88 | type: updateArgs[key].type, 89 | description: undefined, 90 | defaultValue: undefined, 91 | extensions: undefined, 92 | astNode: undefined 93 | })); 94 | 95 | field.args = [...field.args, ...args]; 96 | } 97 | } 98 | 99 | export class MongoUpdateResolverVisitor extends SchemaDirectiveVisitor { 100 | public visitFieldDefinition(field: GraphQLField) { 101 | const { type, updateOptions: updateOptionsArg } = this.args as { type: string, updateOptions: UpdateOptions } 102 | const graphqlType = this.schema.getType(type); 103 | 104 | if (!(graphqlType instanceof GraphQLObjectType)) { 105 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 106 | } 107 | 108 | const updateOptions = { 109 | ...updateOptionsArg, 110 | isResolvedField: field => !field[resolverless], 111 | excludedFields: [] 112 | }; 113 | markResolverless(graphqlType); 114 | 115 | field.resolve = getMongoDbUpdateResolver(graphqlType, field.resolve as any, updateOptions); 116 | } 117 | } 118 | 119 | export class MongoInsertArgsVisitor extends SchemaDirectiveVisitor { 120 | public visitFieldDefinition(field: GraphQLField) { 121 | const { type, key } = this.args as { type: string, key: string } 122 | const graphqlType = this.schema.getType(type); 123 | 124 | if (!(graphqlType instanceof GraphQLObjectType)) { 125 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 126 | } 127 | 128 | const insertType = getGraphQLInsertType(graphqlType); 129 | field.args = [...field.args, { 130 | name: key, type: insertType, 131 | description: undefined, 132 | defaultValue: undefined, 133 | extensions: undefined, 134 | astNode: undefined 135 | }]; 136 | } 137 | } 138 | 139 | export class MongoFilterArgsVisitor extends SchemaDirectiveVisitor { 140 | public visitFieldDefinition(field: GraphQLField) { 141 | const { type, key } = this.args as { type: string, key: string } 142 | const graphqlType = this.schema.getType(type); 143 | 144 | if (!(graphqlType instanceof GraphQLObjectType)) { 145 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 146 | } 147 | 148 | const filterType = getGraphQLFilterType(graphqlType); 149 | field.args = [...field.args, { 150 | name: key, type: filterType, 151 | description: undefined, 152 | defaultValue: undefined, 153 | extensions: undefined, 154 | astNode: undefined 155 | }]; 156 | } 157 | } 158 | 159 | export class MongoFilterResolverVisitor extends SchemaDirectiveVisitor { 160 | public visitFieldDefinition(field: GraphQLField) { 161 | const { type, key } = this.args as { type: string, key: string } 162 | const graphqlType = this.schema.getType(type); 163 | 164 | if (!(graphqlType instanceof GraphQLObjectType)) { 165 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 166 | } 167 | 168 | const resolve = field.resolve; 169 | 170 | field.resolve = ((source, args, context, info) => { 171 | const filter = getMongoDbFilter(graphqlType, args[key]) 172 | return (resolve as any)(filter, source, args, context, info); 173 | }).bind(field); 174 | } 175 | } 176 | 177 | export const visitors = { 178 | mongoDependencies: MongoDependenciesVisitor, 179 | mongoQueryArgs: MongoQueryArgsVisitor, 180 | mongoQueryResolver: MongoQueryResolverVisitor, 181 | mongoUpdateArgs: MongoUpdateArgsVisitor, 182 | mongoUpdateResolver: MongoUpdateResolverVisitor, 183 | mongoInsertArgs: MongoInsertArgsVisitor, 184 | mongofilterArgs: MongoFilterArgsVisitor, 185 | mongoFilterResolver: MongoFilterResolverVisitor, 186 | }; 187 | 188 | const resolverless = Symbol("resolverless"); 189 | 190 | const markResolverless = (type: GraphQLObjectType) => { 191 | const innerType = (type: GraphQLOutputType): GraphQLOutputType & GraphQLNamedType => { 192 | if (isNonNullType(type) || isListType(type)) 193 | return innerType(type.ofType); 194 | return type; 195 | } 196 | 197 | const fields = type.getFields(); 198 | 199 | Object.keys(fields).map(key => fields[key]).forEach(field => { 200 | if (!!field.resolve) return; 201 | if (field[resolverless] === true) return; 202 | field[resolverless] = true; 203 | const fieldType = innerType(field.type); 204 | if (isObjectType(fieldType)) { 205 | markResolverless(fieldType); 206 | } 207 | }); 208 | } 209 | -------------------------------------------------------------------------------- /examples/apollo/src/SolutionB/makeExecutableSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLNamedType, GraphQLInputObjectType, GraphQLInputType, isInputObjectType, isNonNullType, isListType } from "graphql"; 2 | import { makeExecutableSchema, IExecutableSchemaDefinition } from "apollo-server-express"; 3 | import { visitors, types as directiveTypes } from "./directives"; 4 | import { getTypesCache, clearTypesCache, GraphQLPaginationType, GraphQLSortType } from "graphql-to-mongodb"; 5 | 6 | export default function (config: IExecutableSchemaDefinition): GraphQLSchema { 7 | clearTypesCache(); 8 | 9 | const configTypeDefs = Array.isArray(config.typeDefs) ? config.typeDefs : [config.typeDefs]; 10 | 11 | const schema = makeExecutableSchema({ 12 | ...config, 13 | typeDefs: [...configTypeDefs, directiveTypes], 14 | schemaDirectives: { ...config.schemaDirectives, ...visitors } 15 | }) 16 | 17 | let typesCache = getTypesCache(); 18 | resolveLazyFields(Object.keys(typesCache).map(_ => typesCache[_]).filter(isInputObjectType)) 19 | typesCache = getTypesCache(); 20 | typesCache[GraphQLPaginationType.name] = GraphQLPaginationType; 21 | typesCache[GraphQLSortType.name] = GraphQLSortType; 22 | 23 | const getTypeMap = schema.getTypeMap; 24 | schema.getTypeMap = (() => ({ ...getTypeMap.apply(schema), ...typesCache })).bind(schema); 25 | 26 | // OR (schema as any)._typeMap = { ...(schema as any)._typeMap, ...typesCache } 27 | 28 | return schema; 29 | } 30 | 31 | function resolveLazyFields(types: GraphQLInputObjectType[]) { 32 | types.forEach(type => { 33 | const typesCache = getTypesCache(); 34 | const fields = type.getFields(); 35 | resolveLazyFields(Object 36 | .keys(fields) 37 | .map(key => innerType(fields[key].type)) 38 | .filter(isInputObjectType) 39 | .filter(_ => !typesCache[_.name])) 40 | }); 41 | } 42 | 43 | function innerType(type: GraphQLInputType): GraphQLInputType & GraphQLNamedType { 44 | if (isNonNullType(type) || isListType(type)) 45 | return innerType(type.ofType); 46 | return type; 47 | } 48 | -------------------------------------------------------------------------------- /examples/apollo/src/db.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb' 2 | 3 | export default async function () { 4 | try { 5 | const mongoClient = await MongoClient.connect("mongodb://mongo:27017", { autoReconnect: true, useNewUrlParser: true }); 6 | const database = await mongoClient.db("PeopleDB"); 7 | return database; 8 | } catch(error) { 9 | return null; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /examples/apollo/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import createApolloSchema from './SolutionA/makeExecutableSchema'; 3 | import { resolvers, types } from './schema'; 4 | import connect from './db'; 5 | import { ApolloServer } from 'apollo-server-express'; 6 | 7 | console.log("GraphQL starting..."); 8 | 9 | connect().then(db => { 10 | const apolloServer = new ApolloServer({ 11 | schema: createApolloSchema({ typeDefs: types, resolvers: resolvers as any }), 12 | introspection: true, 13 | context: { db } 14 | }); 15 | 16 | const app = express(); 17 | 18 | apolloServer.applyMiddleware({ app, path: '/' }); 19 | 20 | app.listen(3000, () => { 21 | console.log('GraphQL listening on 3000') 22 | }); 23 | }) 24 | -------------------------------------------------------------------------------- /examples/apollo/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { Db } from "mongodb"; 2 | import { gql } from "apollo-server-core"; 3 | 4 | export const types = gql` 5 | type Name { 6 | first: String 7 | last: String 8 | } 9 | 10 | type Person { 11 | age: Int 12 | name: Name! 13 | fullName: String @mongoDependencies(paths: ["name"]) 14 | } 15 | 16 | type Query { 17 | people: [Person] @mongoQueryArgs(type: "Person") @mongoQueryResolver(type: "Person") 18 | } 19 | 20 | type Mutation { 21 | updatePeople: Int @mongoUpdateArgs(type: "Person") @mongoUpdateResolver(type: "Person", updateOptions: { differentOutputType: true, validateUpdateArgs: true }) 22 | insertPerson: String @mongoInsertArgs(type: "Person", key: "input") 23 | deletePeople: String @mongoFilterArgs(type: "Person", key: "filter") @mongoFilterResolver(type: "Person", key: "filter") 24 | clear: Int 25 | } 26 | ` 27 | 28 | export const resolvers = { 29 | Person: { 30 | fullName: (source) => { 31 | return [source.name.first, source.name.last].filter(_ => !!_).join(' '); 32 | } 33 | }, 34 | Query: { 35 | people: async (filter, projection, options, obj, args, { db }: { db: Db }) => 36 | await db.collection('people').find(filter, options).toArray() 37 | }, 38 | Mutation: { 39 | updatePeople: async (filter, update, options: {}, projection, obj, args, { db }: { db: Db }) => { 40 | const result = await db.collection('people').updateMany(filter, update, options); 41 | return result.modifiedCount; 42 | }, 43 | insertPerson: async (obj, args, { db }: { db: Db }) => { 44 | const result = await db.collection('people').insertOne(args.input); 45 | return JSON.stringify(result); 46 | }, 47 | deletePeople: async (filter, obj, args, { db }: { db: Db }) => { 48 | const result = await db.collection('people').deleteMany(filter) 49 | return result.deletedCount; 50 | }, 51 | clear: async (obj, args, { db }: { db: Db }) => { 52 | const result = await db.collection('people').deleteMany({}) 53 | return result.deletedCount; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/apollo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es7", 7 | "dom", 8 | "esnext.asynciterable" 9 | ], 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "noEmitOnError": true 13 | } 14 | } -------------------------------------------------------------------------------- /examples/express/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine as dependencies 2 | 3 | WORKDIR /service 4 | COPY package.json ./ 5 | RUN yarn 6 | 7 | COPY ./src ./src 8 | 9 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /examples/express/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mongo: 5 | image: mongo:3.6 6 | ports: 7 | - "27017:27017" 8 | 9 | express: 10 | build: . 11 | ports: 12 | - "3000:3000" 13 | depends_on: 14 | - mongo 15 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "ts-node src/index.ts" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "express": "^4.16.4", 12 | "express-graphql": "^0.7.1", 13 | "graphql": "^14.0.2", 14 | "graphql-to-mongodb": "1.6.5", 15 | "mongodb": "^3.1.10" 16 | }, 17 | "devDependencies": { 18 | "@types/express-graphql": "^0.6.2", 19 | "@types/graphql": "^14.0.3", 20 | "@types/mongodb": "^3.1.15", 21 | "typescript": "^3.2.2", 22 | "ts-node": "^7.0.1" 23 | } 24 | } -------------------------------------------------------------------------------- /examples/express/src/db.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb' 2 | 3 | export default async function () { 4 | try { 5 | const mongoClient = await MongoClient.connect("mongodb://mongo:27017", { autoReconnect: true, useNewUrlParser: true }); 6 | const database = await mongoClient.db("PeopleDB"); 7 | return database; 8 | } catch(error) { 9 | return null; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /examples/express/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as graphqlHTTP from 'express-graphql'; 3 | import schema from './schema'; 4 | import connect from './db'; 5 | 6 | console.log("GraphQL starting..."); 7 | 8 | const app = express(); 9 | 10 | connect().then(db => app 11 | .use('/', graphqlHTTP({ 12 | schema, 13 | context: { db }, 14 | graphiql: true 15 | })) 16 | .listen(3000, () => { 17 | console.log('GraphQL listening on 3000') 18 | })); 19 | -------------------------------------------------------------------------------- /examples/express/src/queryFieldUtil.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLList } from "graphql"; 2 | import { getGraphQLQueryArgs, getMongoDbQueryResolver } from "graphql-to-mongodb"; 3 | import { Collection, Db } from "mongodb"; 4 | 5 | export const getMongoDbQueryField1 = (type: GraphQLObjectType, collectionName: string) => ({ 6 | type: new GraphQLList(type), 7 | args: getGraphQLQueryArgs(type) as any, 8 | resolve: getMongoDbQueryResolver(type, 9 | async (filter, projection, options, obj, args, { db }: { db: Db }) => { 10 | return await db.collection(collectionName).find(filter, { ...options, projection }).toArray(); 11 | }) 12 | }) 13 | 14 | export const getMongoDbQueryField2 = (type: GraphQLObjectType, getCollection: (context: any) => Collection) => ({ 15 | type: new GraphQLList(type), 16 | args: getGraphQLQueryArgs(type) as any, 17 | resolve: getMongoDbQueryResolver(type, 18 | async (filter, projection, options, obj, args, context) => { 19 | return await getCollection(context).find(filter, { ...options, projection }).toArray(); 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/express/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLNonNull, GraphQLList, GraphQLSchema } from "graphql"; 2 | import { getGraphQLQueryArgs, getMongoDbQueryResolver, getGraphQLUpdateArgs, getMongoDbUpdateResolver, getGraphQLInsertType, getGraphQLFilterType, getMongoDbFilter } from "graphql-to-mongodb"; 3 | import { Db } from "mongodb"; 4 | import { getMongoDbQueryField1, getMongoDbQueryField2 } from "./queryFieldUtil"; 5 | 6 | const PersonType = new GraphQLObjectType({ 7 | name: 'PersonType', 8 | fields: () => ({ 9 | age: { type: GraphQLInt }, 10 | name: { 11 | type: new GraphQLNonNull(new GraphQLObjectType({ 12 | name: 'NameType', 13 | fields: () => ({ 14 | first: { type: GraphQLString }, 15 | last: { type: GraphQLString } 16 | }) 17 | })) 18 | }, 19 | fullName: { 20 | type: GraphQLString, 21 | resolve: (source) => [source.name.first, source.name.last].filter(_ => !!_).join(' '), 22 | dependencies: ['name'] 23 | } 24 | }) 25 | }) 26 | 27 | const QueryType = new GraphQLObjectType({ 28 | name: 'QueryType', 29 | fields: () => ({ 30 | people: { 31 | type: new GraphQLList(PersonType), 32 | args: getGraphQLQueryArgs(PersonType) as any, 33 | resolve: getMongoDbQueryResolver(PersonType, 34 | async (filter, projection, options, obj, args, { db }: { db: Db }) => { 35 | return await db.collection('people').find(filter, { ...options, projection }).toArray(); 36 | }) 37 | }, 38 | people1: getMongoDbQueryField1(PersonType, 'people'), 39 | people2: getMongoDbQueryField2(PersonType, ({ db }: { db: Db }) => db.collection('people')), 40 | }) 41 | }) 42 | 43 | 44 | 45 | const MutationType = new GraphQLObjectType({ 46 | name: 'MutationType', 47 | fields: () => ({ 48 | updatePeople: { 49 | type: GraphQLInt, 50 | args: getGraphQLUpdateArgs(PersonType) as any, 51 | resolve: getMongoDbUpdateResolver(PersonType, 52 | async (filter, update, options, projection, obj, args, { db }: { db: Db }) => { 53 | const result = await db.collection('people').updateMany(filter, update, options); 54 | return result.modifiedCount; 55 | }, { 56 | differentOutputType: true, 57 | validateUpdateArgs: true 58 | }) 59 | }, 60 | insertPerson: { 61 | type: GraphQLString, 62 | args: { input: { type: getGraphQLInsertType(PersonType) } }, 63 | resolve: async (obj, args, { db }: { db: Db }) => { 64 | const result = await db.collection('people').insertOne(args.input); 65 | return JSON.stringify(result); 66 | } 67 | }, 68 | deletePeople: { 69 | type: GraphQLInt, 70 | args: { filter: { type: new GraphQLNonNull(getGraphQLFilterType(PersonType)) } }, 71 | resolve: async (obj, args, { db }: { db: Db }) => { 72 | const filter = getMongoDbFilter(PersonType, args.filter); 73 | const result = await db.collection('people').deleteMany(filter) 74 | return result.deletedCount; 75 | } 76 | }, 77 | clear: { 78 | type: GraphQLInt, 79 | resolve: async (obj, args, { db }: { db: Db }) => { 80 | const result = await db.collection('people').deleteMany({}) 81 | return result.deletedCount; 82 | } 83 | } 84 | }) 85 | }) 86 | 87 | const Schema = new GraphQLSchema({ 88 | query: QueryType, 89 | mutation: MutationType 90 | }) 91 | 92 | export default Schema; 93 | -------------------------------------------------------------------------------- /examples/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es7", 7 | "dom", 8 | "esnext.asynciterable" 9 | ], 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "noEmitOnError": true 13 | } 14 | } -------------------------------------------------------------------------------- /examples/express/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/body-parser@*": 6 | version "1.17.0" 7 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" 8 | dependencies: 9 | "@types/connect" "*" 10 | "@types/node" "*" 11 | 12 | "@types/bson@*": 13 | version "1.0.11" 14 | resolved "https://registry.yarnpkg.com/@types/bson/-/bson-1.0.11.tgz#c95ad69bb0b3f5c33b4bb6cc86d86cafb273335c" 15 | dependencies: 16 | "@types/node" "*" 17 | 18 | "@types/connect@*": 19 | version "3.4.32" 20 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" 21 | dependencies: 22 | "@types/node" "*" 23 | 24 | "@types/events@*": 25 | version "1.2.0" 26 | resolved "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" 27 | 28 | "@types/express-graphql@^0.6.2": 29 | version "0.6.2" 30 | resolved "https://registry.yarnpkg.com/@types/express-graphql/-/express-graphql-0.6.2.tgz#2b0608e74f2aa63fae0948f3b58abf03b613867f" 31 | dependencies: 32 | "@types/express" "*" 33 | "@types/graphql" "*" 34 | 35 | "@types/express-serve-static-core@*": 36 | version "4.16.0" 37 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7" 38 | dependencies: 39 | "@types/events" "*" 40 | "@types/node" "*" 41 | "@types/range-parser" "*" 42 | 43 | "@types/express@*": 44 | version "4.16.0" 45 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19" 46 | dependencies: 47 | "@types/body-parser" "*" 48 | "@types/express-serve-static-core" "*" 49 | "@types/serve-static" "*" 50 | 51 | "@types/graphql@*", "@types/graphql@^14.0.3": 52 | version "14.0.3" 53 | resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.3.tgz#389e2e5b83ecdb376d9f98fae2094297bc112c1c" 54 | 55 | "@types/mime@*": 56 | version "2.0.0" 57 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" 58 | 59 | "@types/mongodb@^3.1.15": 60 | version "3.1.17" 61 | resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.1.17.tgz#11351b147b68e7674cff9055ea82072521bc6fe3" 62 | dependencies: 63 | "@types/bson" "*" 64 | "@types/node" "*" 65 | 66 | "@types/node@*": 67 | version "10.12.15" 68 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.15.tgz#20e85651b62fd86656e57c9c9bc771ab1570bc59" 69 | 70 | "@types/range-parser@*": 71 | version "1.2.3" 72 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" 73 | 74 | "@types/serve-static@*": 75 | version "1.13.2" 76 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48" 77 | dependencies: 78 | "@types/express-serve-static-core" "*" 79 | "@types/mime" "*" 80 | 81 | accepts@^1.3.5, accepts@~1.3.5: 82 | version "1.3.5" 83 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 84 | dependencies: 85 | mime-types "~2.1.18" 86 | negotiator "0.6.1" 87 | 88 | array-flatten@1.1.1: 89 | version "1.1.1" 90 | resolved "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 91 | 92 | arrify@^1.0.0: 93 | version "1.0.1" 94 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 95 | 96 | body-parser@1.18.3: 97 | version "1.18.3" 98 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" 99 | dependencies: 100 | bytes "3.0.0" 101 | content-type "~1.0.4" 102 | debug "2.6.9" 103 | depd "~1.1.2" 104 | http-errors "~1.6.3" 105 | iconv-lite "0.4.23" 106 | on-finished "~2.3.0" 107 | qs "6.5.2" 108 | raw-body "2.3.3" 109 | type-is "~1.6.16" 110 | 111 | bson@^1.1.0: 112 | version "1.1.0" 113 | resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.0.tgz#bee57d1fb6a87713471af4e32bcae36de814b5b0" 114 | 115 | buffer-from@^1.0.0, buffer-from@^1.1.0: 116 | version "1.1.1" 117 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 118 | 119 | bytes@3.0.0: 120 | version "3.0.0" 121 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 122 | 123 | content-disposition@0.5.2: 124 | version "0.5.2" 125 | resolved "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 126 | 127 | content-type@^1.0.4, content-type@~1.0.4: 128 | version "1.0.4" 129 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 130 | 131 | cookie-signature@1.0.6: 132 | version "1.0.6" 133 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 134 | 135 | cookie@0.3.1: 136 | version "0.3.1" 137 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 138 | 139 | debug@2.6.9: 140 | version "2.6.9" 141 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 142 | dependencies: 143 | ms "2.0.0" 144 | 145 | depd@~1.1.2: 146 | version "1.1.2" 147 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 148 | 149 | destroy@~1.0.4: 150 | version "1.0.4" 151 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 152 | 153 | diff@^3.1.0: 154 | version "3.5.0" 155 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" 156 | 157 | ee-first@1.1.1: 158 | version "1.1.1" 159 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 160 | 161 | encodeurl@~1.0.2: 162 | version "1.0.2" 163 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 164 | 165 | escape-html@~1.0.3: 166 | version "1.0.3" 167 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 168 | 169 | etag@~1.8.1: 170 | version "1.8.1" 171 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 172 | 173 | express-graphql@^0.7.1: 174 | version "0.7.1" 175 | resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.7.1.tgz#6c7712ee966c3aba1930e064ea4c8181e56fd3ef" 176 | dependencies: 177 | accepts "^1.3.5" 178 | content-type "^1.0.4" 179 | http-errors "^1.7.1" 180 | raw-body "^2.3.3" 181 | 182 | express@^4.16.4: 183 | version "4.16.4" 184 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" 185 | dependencies: 186 | accepts "~1.3.5" 187 | array-flatten "1.1.1" 188 | body-parser "1.18.3" 189 | content-disposition "0.5.2" 190 | content-type "~1.0.4" 191 | cookie "0.3.1" 192 | cookie-signature "1.0.6" 193 | debug "2.6.9" 194 | depd "~1.1.2" 195 | encodeurl "~1.0.2" 196 | escape-html "~1.0.3" 197 | etag "~1.8.1" 198 | finalhandler "1.1.1" 199 | fresh "0.5.2" 200 | merge-descriptors "1.0.1" 201 | methods "~1.1.2" 202 | on-finished "~2.3.0" 203 | parseurl "~1.3.2" 204 | path-to-regexp "0.1.7" 205 | proxy-addr "~2.0.4" 206 | qs "6.5.2" 207 | range-parser "~1.2.0" 208 | safe-buffer "5.1.2" 209 | send "0.16.2" 210 | serve-static "1.13.2" 211 | setprototypeof "1.1.0" 212 | statuses "~1.4.0" 213 | type-is "~1.6.16" 214 | utils-merge "1.0.1" 215 | vary "~1.1.2" 216 | 217 | finalhandler@1.1.1: 218 | version "1.1.1" 219 | resolved "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" 220 | dependencies: 221 | debug "2.6.9" 222 | encodeurl "~1.0.2" 223 | escape-html "~1.0.3" 224 | on-finished "~2.3.0" 225 | parseurl "~1.3.2" 226 | statuses "~1.4.0" 227 | unpipe "~1.0.0" 228 | 229 | forwarded@~0.1.2: 230 | version "0.1.2" 231 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 232 | 233 | fresh@0.5.2: 234 | version "0.5.2" 235 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 236 | 237 | graphql-to-mongodb@1.6.1: 238 | version "1.6.1" 239 | resolved "https://registry.yarnpkg.com/graphql-to-mongodb/-/graphql-to-mongodb-1.6.1.tgz#8a17317053e07d478c1b9a477f39588a3d4f12cf" 240 | 241 | graphql@^14.0.2: 242 | version "14.0.2" 243 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650" 244 | dependencies: 245 | iterall "^1.2.2" 246 | 247 | http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: 248 | version "1.6.3" 249 | resolved "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 250 | dependencies: 251 | depd "~1.1.2" 252 | inherits "2.0.3" 253 | setprototypeof "1.1.0" 254 | statuses ">= 1.4.0 < 2" 255 | 256 | http-errors@^1.7.1: 257 | version "1.7.1" 258 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027" 259 | dependencies: 260 | depd "~1.1.2" 261 | inherits "2.0.3" 262 | setprototypeof "1.1.0" 263 | statuses ">= 1.5.0 < 2" 264 | toidentifier "1.0.0" 265 | 266 | iconv-lite@0.4.23: 267 | version "0.4.23" 268 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" 269 | dependencies: 270 | safer-buffer ">= 2.1.2 < 3" 271 | 272 | inherits@2.0.3: 273 | version "2.0.3" 274 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 275 | 276 | ipaddr.js@1.8.0: 277 | version "1.8.0" 278 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" 279 | 280 | iterall@^1.2.2: 281 | version "1.2.2" 282 | resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" 283 | 284 | make-error@^1.1.1: 285 | version "1.3.5" 286 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" 287 | 288 | media-typer@0.3.0: 289 | version "0.3.0" 290 | resolved "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 291 | 292 | memory-pager@^1.0.2: 293 | version "1.3.1" 294 | resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.3.1.tgz#1153d2f5e157a407a436bf54ccf4139037515866" 295 | 296 | merge-descriptors@1.0.1: 297 | version "1.0.1" 298 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 299 | 300 | methods@~1.1.2: 301 | version "1.1.2" 302 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 303 | 304 | mime-db@~1.37.0: 305 | version "1.37.0" 306 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" 307 | 308 | mime-types@~2.1.18: 309 | version "2.1.21" 310 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" 311 | dependencies: 312 | mime-db "~1.37.0" 313 | 314 | mime@1.4.1: 315 | version "1.4.1" 316 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 317 | 318 | minimist@0.0.8: 319 | version "0.0.8" 320 | resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 321 | 322 | minimist@^1.2.0: 323 | version "1.2.0" 324 | resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 325 | 326 | mkdirp@^0.5.1: 327 | version "0.5.1" 328 | resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 329 | dependencies: 330 | minimist "0.0.8" 331 | 332 | mongodb-core@3.1.9: 333 | version "3.1.9" 334 | resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.1.9.tgz#c31ee407bf932b0149eaed775c17ee09974e4ca3" 335 | dependencies: 336 | bson "^1.1.0" 337 | require_optional "^1.0.1" 338 | safe-buffer "^5.1.2" 339 | optionalDependencies: 340 | saslprep "^1.0.0" 341 | 342 | mongodb@^3.1.10: 343 | version "3.1.10" 344 | resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.1.10.tgz#45ad9b74ea376f4122d0881b75e5489b9e504ed7" 345 | dependencies: 346 | mongodb-core "3.1.9" 347 | safe-buffer "^5.1.2" 348 | 349 | ms@2.0.0: 350 | version "2.0.0" 351 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 352 | 353 | negotiator@0.6.1: 354 | version "0.6.1" 355 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 356 | 357 | on-finished@~2.3.0: 358 | version "2.3.0" 359 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 360 | dependencies: 361 | ee-first "1.1.1" 362 | 363 | parseurl@~1.3.2: 364 | version "1.3.2" 365 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 366 | 367 | path-to-regexp@0.1.7: 368 | version "0.1.7" 369 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 370 | 371 | proxy-addr@~2.0.4: 372 | version "2.0.4" 373 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" 374 | dependencies: 375 | forwarded "~0.1.2" 376 | ipaddr.js "1.8.0" 377 | 378 | qs@6.5.2: 379 | version "6.5.2" 380 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 381 | 382 | range-parser@~1.2.0: 383 | version "1.2.0" 384 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 385 | 386 | raw-body@2.3.3, raw-body@^2.3.3: 387 | version "2.3.3" 388 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" 389 | dependencies: 390 | bytes "3.0.0" 391 | http-errors "1.6.3" 392 | iconv-lite "0.4.23" 393 | unpipe "1.0.0" 394 | 395 | require_optional@^1.0.1: 396 | version "1.0.1" 397 | resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" 398 | dependencies: 399 | resolve-from "^2.0.0" 400 | semver "^5.1.0" 401 | 402 | resolve-from@^2.0.0: 403 | version "2.0.0" 404 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" 405 | 406 | safe-buffer@5.1.2, safe-buffer@^5.1.2: 407 | version "5.1.2" 408 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 409 | 410 | "safer-buffer@>= 2.1.2 < 3": 411 | version "2.1.2" 412 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 413 | 414 | saslprep@^1.0.0: 415 | version "1.0.2" 416 | resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.2.tgz#da5ab936e6ea0bbae911ffec77534be370c9f52d" 417 | dependencies: 418 | sparse-bitfield "^3.0.3" 419 | 420 | semver@^5.1.0: 421 | version "5.6.0" 422 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" 423 | 424 | send@0.16.2: 425 | version "0.16.2" 426 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" 427 | dependencies: 428 | debug "2.6.9" 429 | depd "~1.1.2" 430 | destroy "~1.0.4" 431 | encodeurl "~1.0.2" 432 | escape-html "~1.0.3" 433 | etag "~1.8.1" 434 | fresh "0.5.2" 435 | http-errors "~1.6.2" 436 | mime "1.4.1" 437 | ms "2.0.0" 438 | on-finished "~2.3.0" 439 | range-parser "~1.2.0" 440 | statuses "~1.4.0" 441 | 442 | serve-static@1.13.2: 443 | version "1.13.2" 444 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" 445 | dependencies: 446 | encodeurl "~1.0.2" 447 | escape-html "~1.0.3" 448 | parseurl "~1.3.2" 449 | send "0.16.2" 450 | 451 | setprototypeof@1.1.0: 452 | version "1.1.0" 453 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 454 | 455 | source-map-support@^0.5.6: 456 | version "0.5.9" 457 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" 458 | dependencies: 459 | buffer-from "^1.0.0" 460 | source-map "^0.6.0" 461 | 462 | source-map@^0.6.0: 463 | version "0.6.1" 464 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 465 | 466 | sparse-bitfield@^3.0.3: 467 | version "3.0.3" 468 | resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" 469 | dependencies: 470 | memory-pager "^1.0.2" 471 | 472 | "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2": 473 | version "1.5.0" 474 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 475 | 476 | statuses@~1.4.0: 477 | version "1.4.0" 478 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" 479 | 480 | toidentifier@1.0.0: 481 | version "1.0.0" 482 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 483 | 484 | ts-node@^7.0.1: 485 | version "7.0.1" 486 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" 487 | dependencies: 488 | arrify "^1.0.0" 489 | buffer-from "^1.1.0" 490 | diff "^3.1.0" 491 | make-error "^1.1.1" 492 | minimist "^1.2.0" 493 | mkdirp "^0.5.1" 494 | source-map-support "^0.5.6" 495 | yn "^2.0.0" 496 | 497 | type-is@~1.6.16: 498 | version "1.6.16" 499 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 500 | dependencies: 501 | media-typer "0.3.0" 502 | mime-types "~2.1.18" 503 | 504 | typescript@^3.2.2: 505 | version "3.2.2" 506 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5" 507 | 508 | unpipe@1.0.0, unpipe@~1.0.0: 509 | version "1.0.0" 510 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 511 | 512 | utils-merge@1.0.1: 513 | version "1.0.1" 514 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 515 | 516 | vary@~1.1.2: 517 | version "1.1.2" 518 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 519 | 520 | yn@^2.0.0: 521 | version "2.0.0" 522 | resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" 523 | -------------------------------------------------------------------------------- /examples/graphql-tools/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine as dependencies 2 | 3 | WORKDIR /service 4 | COPY package.json ./ 5 | RUN yarn 6 | 7 | COPY ./src ./src 8 | 9 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /examples/graphql-tools/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mongo: 5 | image: mongo:3.6 6 | ports: 7 | - "27017:27017" 8 | 9 | graphql-tools: 10 | build: . 11 | ports: 12 | - "3000:3000" 13 | depends_on: 14 | - mongo 15 | -------------------------------------------------------------------------------- /examples/graphql-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "ts-node src/index.ts" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "express": "^4.16.4", 12 | "express-graphql": "^0.7.1", 13 | "graphql": "^14.2.1", 14 | "graphql-to-mongodb": "1.6.5", 15 | "graphql-tools": "^4.0.3", 16 | "mongodb": "^3.1.10" 17 | }, 18 | "devDependencies": { 19 | "@types/express-graphql": "^0.6.2", 20 | "@types/mongodb": "^3.1.15", 21 | "ts-node": "^7.0.1", 22 | "typescript": "^3.2.2" 23 | } 24 | } -------------------------------------------------------------------------------- /examples/graphql-tools/src/SolutionA/directives.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLObjectType, GraphQLArgument, GraphQLInputType } from "graphql"; 2 | import { getGraphQLQueryArgs, getMongoDbQueryResolver, QueryOptions, getGraphQLUpdateArgs, getMongoDbUpdateResolver, UpdateOptions, getGraphQLInsertType, getGraphQLFilterType, getMongoDbFilter } from "graphql-to-mongodb"; 3 | import { SchemaDirectiveVisitor } from "graphql-tools" 4 | 5 | export const types = ` 6 | input QueryOptions { 7 | differentOutputType: Boolean 8 | } 9 | 10 | input UpdateOptions { 11 | differentOutputType: Boolean 12 | validateUpdateArgs: Boolean 13 | overwrite: Boolean 14 | } 15 | 16 | directive @mongoDependencies(paths: [String!]!) on FIELD_DEFINITION 17 | directive @mongoQueryArgs(type: String!) on FIELD_DEFINITION 18 | directive @mongoQueryResolver(type: String!, queryOptions: QueryOptions) on FIELD_DEFINITION 19 | directive @mongoUpdateArgs(type: String!) on FIELD_DEFINITION 20 | directive @mongoUpdateResolver(type: String!, updateOptions: UpdateOptions) on FIELD_DEFINITION 21 | directive @mongoInsertArgs(type: String!, key: String!) on FIELD_DEFINITION 22 | directive @mongoFilterArgs(type: String!, key: String!) on FIELD_DEFINITION 23 | directive @mongoFilterResolver(type: String!, key: String!) on FIELD_DEFINITION 24 | `; 25 | 26 | export class MongoDirectivesContext { 27 | public static stage: "First" | "Second"; 28 | } 29 | 30 | export class MongoDependenciesVisitor extends SchemaDirectiveVisitor { 31 | public visitFieldDefinition(field: GraphQLField) { 32 | const { paths } = this.args as { paths: string[] } 33 | field["dependencies"] = paths; 34 | } 35 | } 36 | 37 | export class MongoQueryArgsVisitor extends SchemaDirectiveVisitor { 38 | public visitFieldDefinition(field: GraphQLField) { 39 | const { type } = this.args as { type: string } 40 | const graphqlType = this.schema.getType(type); 41 | 42 | if (!(graphqlType instanceof GraphQLObjectType)) { 43 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 44 | } 45 | 46 | if (MongoDirectivesContext.stage === "First") { 47 | getGraphQLQueryArgs(graphqlType); 48 | } 49 | 50 | if (MongoDirectivesContext.stage === "Second") { 51 | let queryArgs = getGraphQLQueryArgs(graphqlType); 52 | const args: GraphQLArgument[] = Object.keys(queryArgs).map(key => ({ 53 | name: key, 54 | type: this.schema.getType(queryArgs[key].type.name) as GraphQLInputType, 55 | description: undefined, 56 | defaultValue: undefined, 57 | extensions: undefined, 58 | astNode: undefined 59 | })); 60 | 61 | field.args = [...field.args, ...args]; 62 | } 63 | } 64 | } 65 | 66 | export class MongoQueryResolverVisitor extends SchemaDirectiveVisitor { 67 | public visitFieldDefinition(field: GraphQLField) { 68 | const { type, queryOptions } = this.args as { type: string, queryOptions: QueryOptions } 69 | const graphqlType = this.schema.getType(type); 70 | 71 | if (!(graphqlType instanceof GraphQLObjectType)) { 72 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 73 | } 74 | 75 | if (MongoDirectivesContext.stage === "Second") { 76 | field.resolve = getMongoDbQueryResolver(graphqlType, field.resolve, queryOptions); 77 | } 78 | } 79 | } 80 | 81 | export class MongoUpdateArgsVisitor extends SchemaDirectiveVisitor { 82 | public visitFieldDefinition(field: GraphQLField) { 83 | const { type } = this.args as { type: string } 84 | const graphqlType = this.schema.getType(type); 85 | 86 | if (!(graphqlType instanceof GraphQLObjectType)) { 87 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 88 | } 89 | 90 | if (MongoDirectivesContext.stage === "First") { 91 | getGraphQLUpdateArgs(graphqlType); 92 | } 93 | 94 | if (MongoDirectivesContext.stage === "Second") { 95 | let updateArgs = getGraphQLUpdateArgs(graphqlType); 96 | const args: GraphQLArgument[] = Object.keys(updateArgs).map(key => ({ 97 | name: key, 98 | type: this.schema.getType(updateArgs[key].type.ofType.name) as GraphQLInputType, 99 | description: undefined, 100 | defaultValue: undefined, 101 | extensions: undefined, 102 | astNode: undefined 103 | })); 104 | 105 | field.args = [...field.args, ...args]; 106 | } 107 | } 108 | } 109 | 110 | export class MongoUpdateResolverVisitor extends SchemaDirectiveVisitor { 111 | public visitFieldDefinition(field: GraphQLField) { 112 | const { type, updateOptions } = this.args as { type: string, updateOptions: UpdateOptions } 113 | const graphqlType = this.schema.getType(type); 114 | 115 | if (!(graphqlType instanceof GraphQLObjectType)) { 116 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 117 | } 118 | 119 | if (MongoDirectivesContext.stage === "Second") { 120 | field.resolve = getMongoDbUpdateResolver(graphqlType, field.resolve as any, updateOptions); 121 | } 122 | } 123 | } 124 | 125 | export class MongoInsertArgsVisitor extends SchemaDirectiveVisitor { 126 | public visitFieldDefinition(field: GraphQLField) { 127 | const { type, key } = this.args as { type: string, key: string } 128 | const graphqlType = this.schema.getType(type); 129 | 130 | if (!(graphqlType instanceof GraphQLObjectType)) { 131 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 132 | } 133 | 134 | if (MongoDirectivesContext.stage === "First") { 135 | getGraphQLInsertType(graphqlType); 136 | } 137 | 138 | if (MongoDirectivesContext.stage === "Second") { 139 | const insertType = getGraphQLInsertType(graphqlType); 140 | field.args = [...field.args, { 141 | name: key, type: this.schema.getType(insertType.name) as GraphQLInputType, 142 | description: undefined, 143 | defaultValue: undefined, 144 | extensions: undefined, 145 | astNode: undefined 146 | }]; 147 | } 148 | } 149 | } 150 | 151 | export class MongoFilterArgsVisitor extends SchemaDirectiveVisitor { 152 | public visitFieldDefinition(field: GraphQLField) { 153 | const { type, key } = this.args as { type: string, key: string } 154 | const graphqlType = this.schema.getType(type); 155 | 156 | if (!(graphqlType instanceof GraphQLObjectType)) { 157 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 158 | } 159 | 160 | if (MongoDirectivesContext.stage === "First") { 161 | getGraphQLFilterType(graphqlType); 162 | } 163 | 164 | if (MongoDirectivesContext.stage === "Second") { 165 | const filterType = getGraphQLFilterType(graphqlType); 166 | field.args = [...field.args, { 167 | name: key, type: this.schema.getType(filterType.name) as GraphQLInputType, 168 | description: undefined, 169 | defaultValue: undefined, 170 | extensions: undefined, 171 | astNode: undefined 172 | }]; 173 | }; 174 | } 175 | } 176 | 177 | export class MongoFilterResolverVisitor extends SchemaDirectiveVisitor { 178 | public visitFieldDefinition(field: GraphQLField) { 179 | const { type, key } = this.args as { type: string, key: string } 180 | const graphqlType = this.schema.getType(type); 181 | 182 | if (!(graphqlType instanceof GraphQLObjectType)) { 183 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 184 | } 185 | 186 | if (MongoDirectivesContext.stage === "Second") { 187 | const resolve = field.resolve; 188 | 189 | field.resolve = ((source, args, context, info) => { 190 | const filter = getMongoDbFilter(graphqlType, args[key]) 191 | return (resolve as any)(filter, source, args, context, info); 192 | }).bind(field); 193 | } 194 | } 195 | } 196 | 197 | export const visitors = { 198 | mongoDependencies: MongoDependenciesVisitor, 199 | mongoQueryArgs: MongoQueryArgsVisitor, 200 | mongoQueryResolver: MongoQueryResolverVisitor, 201 | mongoUpdateArgs: MongoUpdateArgsVisitor, 202 | mongoUpdateResolver: MongoUpdateResolverVisitor, 203 | mongoInsertArgs: MongoInsertArgsVisitor, 204 | mongoFilterArgs: MongoFilterArgsVisitor, 205 | mongoFilterResolver: MongoFilterResolverVisitor, 206 | }; 207 | -------------------------------------------------------------------------------- /examples/graphql-tools/src/SolutionA/makeExecutableSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLNamedType, GraphQLInputObjectType, GraphQLInputType, isInputObjectType, isNonNullType, isListType, isEnumType } from "graphql"; 2 | import { visitors, types as directiveTypes, MongoDirectivesContext } from "./directives"; 3 | import { getTypesCache, clearTypesCache, GraphQLPaginationType, GraphQLSortType } from "graphql-to-mongodb"; 4 | import { printType } from "graphql"; 5 | import { makeExecutableSchema, IExecutableSchemaDefinition } from "graphql-tools" 6 | 7 | export default function (config: IExecutableSchemaDefinition): GraphQLSchema { 8 | clearTypesCache(); 9 | MongoDirectivesContext.stage = "First"; 10 | 11 | const configTypeDefs = Array.isArray(config.typeDefs) ? config.typeDefs : [config.typeDefs]; 12 | 13 | makeExecutableSchema({ 14 | ...config, 15 | typeDefs: [...configTypeDefs, directiveTypes], 16 | schemaDirectives: { ...config.schemaDirectives, ...visitors } 17 | }) 18 | 19 | let typesCache = getTypesCache(); 20 | resolveLazyFields(Object.keys(typesCache).map(_ => typesCache[_]).filter(isInputObjectType)) 21 | typesCache = getTypesCache(); 22 | typesCache[GraphQLPaginationType.name] = GraphQLPaginationType; 23 | typesCache[GraphQLSortType.name] = GraphQLSortType; 24 | const typesSdlRaw = Object 25 | .keys(typesCache) 26 | .map(key => printType(typesCache[key])) 27 | .join("\n"); 28 | 29 | const enumResolvers = getEnumResolvers(typesCache); 30 | 31 | MongoDirectivesContext.stage = "Second"; 32 | const stageTwoSchema = makeExecutableSchema({ 33 | ...config, 34 | typeDefs: [...configTypeDefs, typesSdlRaw, directiveTypes], 35 | schemaDirectives: { ...config.schemaDirectives, ...visitors }, 36 | resolvers: { ...enumResolvers, ...config.resolvers } 37 | }) 38 | 39 | return stageTwoSchema; 40 | } 41 | 42 | function resolveLazyFields(types: GraphQLInputObjectType[]) { 43 | types.forEach(type => { 44 | const typesCache = getTypesCache(); 45 | const fields = type.getFields(); 46 | resolveLazyFields(Object 47 | .keys(fields) 48 | .map(key => innerType(fields[key].type)) 49 | .filter(isInputObjectType) 50 | .filter(_ => !typesCache[_.name])) 51 | }); 52 | } 53 | 54 | function innerType(type: GraphQLInputType): GraphQLInputType & GraphQLNamedType { 55 | if (isNonNullType(type) || isListType(type)) 56 | return innerType(type.ofType); 57 | return type; 58 | } 59 | 60 | function getEnumResolvers(typesCache: { [key: string]: GraphQLNamedType; }) { 61 | return Object.keys(typesCache) 62 | .map(_ => typesCache[_]) 63 | .filter(isEnumType) 64 | .reduce((resolvers, enumType) => ({ 65 | ...resolvers, 66 | [enumType.name]: enumType.getValues().reduce((resolver, entry) => ({ 67 | ...resolver, [entry.name]: entry.value 68 | }), {}) 69 | }), {}); 70 | } 71 | -------------------------------------------------------------------------------- /examples/graphql-tools/src/SolutionB/directives.ts: -------------------------------------------------------------------------------- 1 | import { SchemaDirectiveVisitor } from "graphql-tools" 2 | import { GraphQLField, GraphQLObjectType, GraphQLArgument } from "graphql"; 3 | import { getGraphQLQueryArgs, getMongoDbQueryResolver, QueryOptions, getGraphQLUpdateArgs, getMongoDbUpdateResolver, UpdateOptions, getGraphQLInsertType, getGraphQLFilterType, getMongoDbFilter } from "graphql-to-mongodb"; 4 | 5 | export const types = ` 6 | input QueryOptions { 7 | differentOutputType: Boolean 8 | } 9 | 10 | input UpdateOptions { 11 | differentOutputType: Boolean 12 | validateUpdateArgs: Boolean 13 | overwrite: Boolean 14 | } 15 | 16 | directive @mongoDependencies(paths: [String!]!) on FIELD_DEFINITION 17 | directive @mongoQueryArgs(type: String!) on FIELD_DEFINITION 18 | directive @mongoQueryResolver(type: String!, queryOptions: QueryOptions) on FIELD_DEFINITION 19 | directive @mongoUpdateArgs(type: String!) on FIELD_DEFINITION 20 | directive @mongoUpdateResolver(type: String!, updateOptions: UpdateOptions) on FIELD_DEFINITION 21 | directive @mongoInsertArgs(type: String!, key: String!) on FIELD_DEFINITION 22 | directive @mongoFilterArgs(type: String!, key: String!) on FIELD_DEFINITION 23 | directive @mongoFilterResolver(type: String!, key: String!) on FIELD_DEFINITION 24 | `; 25 | 26 | export class MongoDependenciesVisitor extends SchemaDirectiveVisitor { 27 | public visitFieldDefinition(field: GraphQLField) { 28 | const { paths } = this.args as { paths: string[] } 29 | field["dependencies"] = paths; 30 | } 31 | } 32 | 33 | export class MongoQueryArgsVisitor extends SchemaDirectiveVisitor { 34 | public visitFieldDefinition(field: GraphQLField) { 35 | const { type } = this.args as { type: string } 36 | const graphqlType = this.schema.getType(type); 37 | 38 | if (!(graphqlType instanceof GraphQLObjectType)) { 39 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 40 | } 41 | 42 | let queryArgs = getGraphQLQueryArgs(graphqlType); 43 | const args: GraphQLArgument[] = Object.keys(queryArgs).map(key => ({ 44 | name: key, 45 | type: queryArgs[key].type, 46 | description: undefined, 47 | defaultValue: undefined, 48 | extensions: undefined, 49 | astNode: undefined 50 | })); 51 | 52 | field.args = [...field.args, ...args]; 53 | } 54 | } 55 | 56 | export class MongoQueryResolverVisitor extends SchemaDirectiveVisitor { 57 | public visitFieldDefinition(field: GraphQLField) { 58 | const { type, queryOptions } = this.args as { type: string, queryOptions: QueryOptions } 59 | const graphqlType = this.schema.getType(type); 60 | 61 | if (!(graphqlType instanceof GraphQLObjectType)) { 62 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 63 | } 64 | 65 | field.resolve = getMongoDbQueryResolver(graphqlType, field.resolve, queryOptions); 66 | } 67 | } 68 | 69 | export class MongoUpdateArgsVisitor extends SchemaDirectiveVisitor { 70 | public visitFieldDefinition(field: GraphQLField) { 71 | const { type } = this.args as { type: string } 72 | const graphqlType = this.schema.getType(type); 73 | 74 | if (!(graphqlType instanceof GraphQLObjectType)) { 75 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 76 | } 77 | 78 | let updateArgs = getGraphQLUpdateArgs(graphqlType); 79 | const args: GraphQLArgument[] = Object.keys(updateArgs).map(key => ({ 80 | name: key, 81 | type: updateArgs[key].type, 82 | description: undefined, 83 | defaultValue: undefined, 84 | extensions: undefined, 85 | astNode: undefined 86 | })); 87 | 88 | field.args = [...field.args, ...args]; 89 | } 90 | } 91 | 92 | export class MongoUpdateResolverVisitor extends SchemaDirectiveVisitor { 93 | public visitFieldDefinition(field: GraphQLField) { 94 | const { type, updateOptions } = this.args as { type: string, updateOptions: UpdateOptions } 95 | const graphqlType = this.schema.getType(type); 96 | 97 | if (!(graphqlType instanceof GraphQLObjectType)) { 98 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 99 | } 100 | 101 | field.resolve = getMongoDbUpdateResolver(graphqlType, field.resolve as any, updateOptions); 102 | } 103 | } 104 | 105 | export class MongoInsertArgsVisitor extends SchemaDirectiveVisitor { 106 | public visitFieldDefinition(field: GraphQLField) { 107 | const { type, key } = this.args as { type: string, key: string } 108 | const graphqlType = this.schema.getType(type); 109 | 110 | if (!(graphqlType instanceof GraphQLObjectType)) { 111 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 112 | } 113 | 114 | const insertType = getGraphQLInsertType(graphqlType); 115 | field.args = [...field.args, { 116 | name: key, type: insertType, 117 | description: undefined, 118 | defaultValue: undefined, 119 | extensions: undefined, 120 | astNode: undefined 121 | }]; 122 | } 123 | } 124 | 125 | export class MongoFilterArgsVisitor extends SchemaDirectiveVisitor { 126 | public visitFieldDefinition(field: GraphQLField) { 127 | const { type, key } = this.args as { type: string, key: string } 128 | const graphqlType = this.schema.getType(type); 129 | 130 | if (!(graphqlType instanceof GraphQLObjectType)) { 131 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 132 | } 133 | 134 | const filterType = getGraphQLFilterType(graphqlType); 135 | field.args = [...field.args, { 136 | name: key, type: filterType, 137 | description: undefined, 138 | defaultValue: undefined, 139 | extensions: undefined, 140 | astNode: undefined 141 | }]; 142 | } 143 | } 144 | 145 | export class MongoFilterResolverVisitor extends SchemaDirectiveVisitor { 146 | public visitFieldDefinition(field: GraphQLField) { 147 | const { type, key } = this.args as { type: string, key: string } 148 | const graphqlType = this.schema.getType(type); 149 | 150 | if (!(graphqlType instanceof GraphQLObjectType)) { 151 | throw `${this.name} directive requires type arg to be GraphQLObjectType`; 152 | } 153 | 154 | const resolve = field.resolve; 155 | 156 | field.resolve = ((source, args, context, info) => { 157 | const filter = getMongoDbFilter(graphqlType, args[key]) 158 | return (resolve as any)(filter, source, args, context, info); 159 | }).bind(field); 160 | } 161 | } 162 | 163 | export const visitors = { 164 | mongoDependencies: MongoDependenciesVisitor, 165 | mongoQueryArgs: MongoQueryArgsVisitor, 166 | mongoQueryResolver: MongoQueryResolverVisitor, 167 | mongoUpdateArgs: MongoUpdateArgsVisitor, 168 | mongoUpdateResolver: MongoUpdateResolverVisitor, 169 | mongoInsertArgs: MongoInsertArgsVisitor, 170 | mongoFilterArgs: MongoFilterArgsVisitor, 171 | mongoFilterResolver: MongoFilterResolverVisitor, 172 | }; 173 | -------------------------------------------------------------------------------- /examples/graphql-tools/src/SolutionB/makeExecutableSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLNamedType, GraphQLInputObjectType, GraphQLInputType, isInputObjectType, isNonNullType, isListType } from "graphql"; 2 | import { makeExecutableSchema, IExecutableSchemaDefinition } from "graphql-tools" 3 | import { visitors, types as directiveTypes } from "./directives"; 4 | import { getTypesCache, clearTypesCache, GraphQLPaginationType, GraphQLSortType } from "graphql-to-mongodb"; 5 | 6 | export default function (config: IExecutableSchemaDefinition): GraphQLSchema { 7 | clearTypesCache(); 8 | 9 | const configTypeDefs = Array.isArray(config.typeDefs) ? config.typeDefs : [config.typeDefs]; 10 | 11 | const schema = makeExecutableSchema({ 12 | ...config, 13 | typeDefs: [...configTypeDefs, directiveTypes], 14 | schemaDirectives: { ...config.schemaDirectives, ...visitors } 15 | }) 16 | 17 | let typesCache = getTypesCache(); 18 | resolveLazyFields(Object.keys(typesCache).map(_ => typesCache[_]).filter(isInputObjectType)) 19 | typesCache = getTypesCache(); 20 | typesCache[GraphQLPaginationType.name] = GraphQLPaginationType; 21 | typesCache[GraphQLSortType.name] = GraphQLSortType; 22 | 23 | const getTypeMap = schema.getTypeMap; 24 | schema.getTypeMap = (() => ({ ...getTypeMap.apply(schema), ...typesCache })).bind(schema); 25 | 26 | // OR (schema as any)._typeMap = { ...(schema as any)._typeMap, ...typesCache } 27 | 28 | return schema; 29 | } 30 | 31 | function resolveLazyFields(types: GraphQLInputObjectType[]) { 32 | types.forEach(type => { 33 | const typesCache = getTypesCache(); 34 | const fields = type.getFields(); 35 | resolveLazyFields(Object 36 | .keys(fields) 37 | .map(key => innerType(fields[key].type)) 38 | .filter(isInputObjectType) 39 | .filter(_ => !typesCache[_.name])) 40 | }); 41 | } 42 | 43 | function innerType(type: GraphQLInputType): GraphQLInputType & GraphQLNamedType { 44 | if (isNonNullType(type) || isListType(type)) 45 | return innerType(type.ofType); 46 | return type; 47 | } 48 | -------------------------------------------------------------------------------- /examples/graphql-tools/src/db.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb' 2 | 3 | export default async function () { 4 | try { 5 | const mongoClient = await MongoClient.connect("mongodb://mongo:27017", { autoReconnect: true, useNewUrlParser: true }); 6 | const database = await mongoClient.db("PeopleDB"); 7 | return database; 8 | } catch(error) { 9 | return null; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /examples/graphql-tools/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import makeExecutableSchema from './SolutionA/makeExecutableSchema'; 3 | import { resolvers, types } from './schema'; 4 | import connect from './db'; 5 | import * as graphqlHTTP from 'express-graphql'; 6 | 7 | console.log("GraphQL starting..."); 8 | 9 | const app = express(); 10 | 11 | connect().then(db => app 12 | .use('/', graphqlHTTP({ 13 | schema: makeExecutableSchema({ typeDefs: types, resolvers: resolvers as any }), 14 | context: { db }, 15 | graphiql: true 16 | })) 17 | .listen(3000, () => { 18 | console.log('GraphQL listening on 3000') 19 | })); 20 | -------------------------------------------------------------------------------- /examples/graphql-tools/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { Db } from "mongodb"; 2 | 3 | export const types = ` 4 | type Name { 5 | first: String 6 | last: String 7 | } 8 | 9 | type Person { 10 | age: Int 11 | name: Name! 12 | fullName: String @mongoDependencies(paths: ["name"]) 13 | } 14 | 15 | type Query { 16 | people: [Person] @mongoQueryArgs(type: "Person") @mongoQueryResolver(type: "Person") 17 | } 18 | 19 | type Mutation { 20 | updatePeople: Int @mongoUpdateArgs(type: "Person") @mongoUpdateResolver(type: "Person", updateOptions: { differentOutputType: true, validateUpdateArgs: true }) 21 | insertPerson: String @mongoInsertArgs(type: "Person", key: "input") 22 | deletePeople: String @mongoFilterArgs(type: "Person", key: "filter") @mongoFilterResolver(type: "Person", key: "filter") 23 | clear: Int 24 | } 25 | ` 26 | 27 | export const resolvers = { 28 | Person: { 29 | fullName: (source) => { 30 | return [source.name.first, source.name.last].filter(_ => !!_).join(' '); 31 | } 32 | }, 33 | Query: { 34 | people: async (filter, projection, options, obj, args, { db }: { db: Db }) => 35 | await db.collection('people').find(filter, { ...options, projection }).toArray() 36 | }, 37 | Mutation: { 38 | updatePeople: async (filter, update, options: {}, projection, obj, args, { db }: { db: Db }) => { 39 | const result = await db.collection('people').updateMany(filter, update, options); 40 | return result.modifiedCount; 41 | }, 42 | insertPerson: async (obj, args, { db }: { db: Db }) => { 43 | const result = await db.collection('people').insertOne(args.input); 44 | return JSON.stringify(result); 45 | }, 46 | deletePeople: async (filter, obj, args, { db }: { db: Db }) => { 47 | const result = await db.collection('people').deleteMany(filter) 48 | return result.deletedCount; 49 | }, 50 | clear: async (obj, args, { db }: { db: Db }) => { 51 | const result = await db.collection('people').deleteMany({}) 52 | return result.deletedCount; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/graphql-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es7", 7 | "dom", 8 | "esnext.asynciterable" 9 | ], 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "noEmitOnError": true 13 | } 14 | } -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { getGraphQLFilterType } from './src/graphQLFilterType'; 2 | export { getMongoDbFilter, MongoDbFilter } from './src/mongoDbFilter'; 3 | export { getGraphQLUpdateType } from './src/graphQLUpdateType'; 4 | export { getGraphQLInsertType } from './src/graphQLInsertType'; 5 | export { getMongoDbUpdate, UpdateObj } from './src/mongoDbUpdate'; 6 | export { validateUpdateArgs } from "./src/mongoDbUpdateValidation"; 7 | export { default as GraphQLPaginationType } from './src/graphQLPaginationType'; 8 | export { getGraphQLSortType, GraphQLSortType } from './src/graphQLSortType'; 9 | export { default as getMongoDbSort, MongoDbSort } from './src/mongoDbSort'; 10 | export { getMongoDbProjection, MongoDbProjection, GetMongoDbProjectionOptions } from './src/mongoDbProjection'; 11 | export { getMongoDbQueryResolver, getGraphQLQueryArgs, QueryOptions, MongoDbOptions } from './src/queryResolver'; 12 | export { getMongoDbUpdateResolver, getGraphQLUpdateArgs, UpdateOptions } from './src/updateResolver'; 13 | export { setLogger, getLogger } from './src/logger'; 14 | export { getTypesCache, clearTypesCache } from './src/common'; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-to-mongodb", 3 | "version": "1.6.5", 4 | "description": "Generic run-time generation of input filter types for existing graphql types, and parsing of said input types into MongoDB queries", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc -p .", 9 | "publish-now": "npm run build && npm publish", 10 | "test": "ts-mocha tests/**/*.spec.ts" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Soluto/graphql-to-mongodb.git" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/Soluto/graphql-to-mongodb/issues" 20 | }, 21 | "homepage": "https://github.com/Soluto/graphql-to-mongodb#readme", 22 | "devDependencies": { 23 | "@types/chai": "^4.1.4", 24 | "@types/graphql": "^14.2.0", 25 | "@types/mocha": "^5.2.5", 26 | "chai": "^4.1.2", 27 | "graphql": "^0.13.2", 28 | "mocha": "^5.2.0", 29 | "ts-mocha": "^2.0.0", 30 | "typescript": "^3.0.3" 31 | }, 32 | "peerDependencies": { 33 | "graphql": "^14.2.1" 34 | }, 35 | "keywords": [ 36 | "graphql", 37 | "mongodb", 38 | "generate", 39 | "backend", 40 | "binding" 41 | ] 42 | } -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLType, GraphQLObjectType, GraphQLNonNull, GraphQLArgument, GraphQLFieldResolver, FieldDefinitionNode, GraphQLNamedType, GraphQLInterfaceType } from 'graphql' 2 | 3 | export interface cacheCallback { 4 | (key): T 5 | } 6 | 7 | export const typesCache: { [key: string]: GraphQLNamedType } = {}; 8 | export const getTypesCache: () => { [key: string]: GraphQLNamedType } = () => ({ ...typesCache }); 9 | 10 | export function clearTypesCache() { 11 | Object.keys(typesCache).forEach(_ => delete typesCache[_]); 12 | } 13 | 14 | export function cache(cacheObj: object, key: any, callback: cacheCallback): T { 15 | let item: T = cacheObj[key]; 16 | 17 | if (item === undefined) { 18 | item = callback(key); 19 | cacheObj[key] = item; 20 | } 21 | 22 | return item; 23 | } 24 | 25 | export function setSuffix(text: string, locate: string, replaceWith: string): string { 26 | const regex = new RegExp(`${locate}$`); 27 | return regex.test(text) 28 | ? text.replace(regex, replaceWith) 29 | : `${text}${replaceWith}`; 30 | } 31 | 32 | export interface FieldFilter { 33 | (name: string, field: { resolve?: Function, dependencies?: string[] }): Boolean 34 | } 35 | 36 | export interface TypeResolver { 37 | (graphQLType: GraphQLType): T 38 | } 39 | 40 | export interface FieldMap { 41 | [key: string]: Field & { type: T } 42 | } 43 | 44 | export interface Field { 49 | name?: string; 50 | description?: string; 51 | type: TType; 52 | args?: GraphQLArgument[]; 53 | resolve?: GraphQLFieldResolver; 54 | subscribe?: GraphQLFieldResolver; 55 | isDeprecated?: boolean; 56 | deprecationReason?: string; 57 | astNode?: FieldDefinitionNode; 58 | dependencies?: string[] 59 | } 60 | 61 | export type GraphQLFieldsType = GraphQLObjectType | GraphQLInterfaceType; 62 | 63 | export function getTypeFields( 64 | graphQLType: GraphQLFieldsType, 65 | filter: FieldFilter = null, 66 | typeResolver: TypeResolver = (type: T) => type, 67 | ...excludedFields: string[]) 68 | : () => FieldMap { 69 | return () => { 70 | const typeFields = graphQLType.getFields(); 71 | 72 | const generatedFields: FieldMap = {}; 73 | 74 | Object.keys(typeFields) 75 | .filter(key => !excludedFields.includes(key)) 76 | .filter(key => !filter || filter(key, typeFields[key])) 77 | .forEach(key => { 78 | const field = typeFields[key]; 79 | const type = typeResolver(field.type); 80 | if (type) generatedFields[key] = { ...field, type: type } 81 | }); //, ...excludedFields 82 | 83 | return generatedFields; 84 | }; 85 | } 86 | 87 | export function getUnresolvedFieldsTypes(graphQLType: GraphQLFieldsType, typeResolver: TypeResolver = null, ...excludedFields: string[]) 88 | : () => FieldMap { 89 | return () => { 90 | const fields = getTypeFields(graphQLType, (key, field) => !field.resolve, typeResolver, ...excludedFields)(); 91 | const fieldsTypes = {}; 92 | Object.keys(fields).forEach(key => fieldsTypes[key] = { type: fields[key].type }); 93 | return fieldsTypes; 94 | }; 95 | } 96 | 97 | export function getInnerType(graphQLType: GraphQLType): GraphQLType { 98 | let innerType = graphQLType; 99 | 100 | while (innerType instanceof GraphQLList 101 | || innerType instanceof GraphQLNonNull) { 102 | innerType = innerType.ofType; 103 | } 104 | 105 | return innerType; 106 | } 107 | 108 | export function isListField(graphQLType: GraphQLType): boolean { 109 | let innerType = graphQLType; 110 | 111 | while (innerType instanceof GraphQLList 112 | || innerType instanceof GraphQLNonNull) { 113 | if (innerType instanceof GraphQLList) return true; 114 | innerType = innerType.ofType; 115 | } 116 | 117 | return false; 118 | } 119 | 120 | export function isNonNullField(graphQLType: GraphQLType): boolean { 121 | let innerType = graphQLType; 122 | 123 | while (innerType instanceof GraphQLList 124 | || innerType instanceof GraphQLNonNull) { 125 | if (innerType instanceof GraphQLNonNull) return true; 126 | innerType = innerType.ofType; 127 | } 128 | 129 | return false; 130 | } 131 | 132 | export function flatten(nestedArray: T[][]): T[] { 133 | return nestedArray.reduce((agg, b) => agg.concat(b), []); 134 | } 135 | 136 | export function addPrefixToProperties(obj: T, prefix: string): T { 137 | return Object.keys(obj).reduce((agg, key) => ({ ...agg, [`${prefix}${key}`]: obj[key] }), {}) as T; 138 | } 139 | 140 | export function isPrimitive(value: any): boolean { 141 | const type = typeof value; 142 | return (type === "boolean" 143 | || type === "number" 144 | || type === "string" 145 | || type === "undefined" 146 | || (type === "object" && (value === null || isValidDate(value)))); 147 | } 148 | 149 | function isValidDate(date): boolean { 150 | return date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date); 151 | } 152 | -------------------------------------------------------------------------------- /src/graphQLFilterType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLList, GraphQLEnumType, GraphQLNonNull, GraphQLScalarType, GraphQLObjectType, GraphQLInputFieldConfigMap, GraphQLInputType, GraphQLString, isLeafType, GraphQLLeafType, GraphQLInterfaceType } from 'graphql'; 2 | import { cache, setSuffix, getUnresolvedFieldsTypes, getTypeFields, FieldMap, typesCache, GraphQLFieldsType } from './common'; 3 | import { warn } from './logger'; 4 | 5 | const warnedIndependentResolvers = {}; 6 | 7 | ////////////// DEPRECATED /////////////////////////////////////////// 8 | const GetOprType = () => cache(typesCache, "Opr", () => new GraphQLEnumType({ 9 | name: 'Opr', 10 | values: { 11 | EQL: { value: "$eq" }, 12 | GT: { value: "$gt" }, 13 | GTE: { value: "$gte" }, 14 | IN: { value: "$in" }, 15 | ALL: { value: "$all" }, 16 | LT: { value: "$lt" }, 17 | LTE: { value: "$lte" }, 18 | NE: { value: "$ne" }, 19 | NIN: { value: "$nin" } 20 | } 21 | })); 22 | ///////////////////////////////////////////////////////////////////// 23 | 24 | const GetOprExistsType = () => cache(typesCache, "OprExists", () => new GraphQLEnumType({ 25 | name: 'OprExists', 26 | values: { 27 | EXISTS: { value: "exists" }, 28 | NOT_EXISTS: { value: "not_exists" }, 29 | } 30 | })); 31 | 32 | export function getGraphQLFilterType(type: GraphQLFieldsType, ...excludedFields: string[]): GraphQLInputObjectType { 33 | const filterTypeName = setSuffix(type.name, 'Type', 'FilterType'); 34 | 35 | return cache(typesCache, filterTypeName, () => new GraphQLInputObjectType({ 36 | name: filterTypeName, 37 | fields: getOrAndFields(type, ...excludedFields) 38 | })); 39 | } 40 | 41 | function getOrAndFields(type: GraphQLFieldsType, ...excludedFields: string[]): () => FieldMap { 42 | return () => { 43 | const generatedFields = getUnresolvedFieldsTypes(type, getGraphQLObjectFilterType, ...excludedFields)(); 44 | 45 | warnOfIndependentResolveFields(type); 46 | 47 | generatedFields['OR'] = { type: new GraphQLList(getGraphQLFilterType(type, ...excludedFields)) }; 48 | generatedFields['AND'] = { type: new GraphQLList(getGraphQLFilterType(type, ...excludedFields)) }; 49 | generatedFields['NOR'] = { type: new GraphQLList(getGraphQLFilterType(type, ...excludedFields)) }; 50 | 51 | return generatedFields; 52 | }; 53 | } 54 | 55 | function getGraphQLObjectFilterType( 56 | type: GraphQLScalarType | GraphQLEnumType | GraphQLNonNull | GraphQLInterfaceType | GraphQLObjectType | GraphQLList, 57 | ...excludedFields: string[]): GraphQLInputType { 58 | if (isLeafType(type)) { 59 | return getGraphQLLeafFilterType(type); 60 | } 61 | 62 | if (type instanceof GraphQLNonNull) { 63 | return getGraphQLObjectFilterType(type.ofType); 64 | } 65 | 66 | if (type instanceof GraphQLList) { 67 | return getGraphQLObjectFilterType(type.ofType); 68 | } 69 | 70 | const typeName = setSuffix(type.name, 'Type', 'ObjectFilterType'); 71 | return cache(typesCache, typeName, () => new GraphQLInputObjectType({ 72 | name: typeName, 73 | fields: getInputObjectTypeFields(type, ...excludedFields) 74 | })); 75 | } 76 | 77 | function getInputObjectTypeFields(type: GraphQLFieldsType, ...excludedFields: string[]): () => FieldMap { 78 | return () => { 79 | const generatedFields = getUnresolvedFieldsTypes(type, getGraphQLObjectFilterType, ...excludedFields)(); 80 | 81 | warnOfIndependentResolveFields(type); 82 | 83 | generatedFields['opr'] = { type: GetOprExistsType() }; 84 | 85 | return generatedFields; 86 | }; 87 | } 88 | 89 | function getGraphQLLeafFilterType(leafType: GraphQLLeafType, not: boolean = false): GraphQLInputObjectType { 90 | const typeName = leafType.toString() + (not ? `Not` : '') + `Filter`; 91 | 92 | return cache(typesCache, typeName, () => new GraphQLInputObjectType({ 93 | name: typeName, 94 | description: `Filter type for ${(not ? `$not of ` : '')}${leafType} scalar`, 95 | fields: getGraphQLScalarFilterTypeFields(leafType, not) 96 | })); 97 | } 98 | 99 | function getGraphQLScalarFilterTypeFields(scalarType: GraphQLLeafType, not: boolean): GraphQLInputFieldConfigMap { 100 | const fields = { 101 | EQ: { type: scalarType, description: '$eq' }, 102 | GT: { type: scalarType, description: '$gt' }, 103 | GTE: { type: scalarType, description: '$gte' }, 104 | IN: { type: new GraphQLList(scalarType), description: '$in' }, 105 | ALL: { type: new GraphQLList(scalarType), description: '$all' }, 106 | LT: { type: scalarType, description: '$lt' }, 107 | LTE: { type: scalarType, description: '$lte' }, 108 | NE: { type: scalarType, description: '$ne' }, 109 | NIN: { type: new GraphQLList(scalarType), description: '$nin' } 110 | }; 111 | 112 | if (scalarType.name === 'String') enhanceWithRegexFields(fields); 113 | 114 | if (!not) { 115 | enhanceWithNotField(fields, scalarType); 116 | 117 | fields['opr'] = { type: GetOprType(), description: 'DEPRECATED: Switched to the more intuitive operator fields' }; 118 | fields['value'] = { type: scalarType, description: 'DEPRECATED: Switched to the more intuitive operator fields' }; 119 | fields['values'] = { type: new GraphQLList(scalarType), description: 'DEPRECATED: Switched to the more intuitive operator fields' }; 120 | fields['NEQ'] = { type: scalarType, description: 'DEPRECATED: use NE' }; 121 | } 122 | 123 | return fields; 124 | } 125 | 126 | function enhanceWithRegexFields(fields: GraphQLInputFieldConfigMap): void { 127 | fields.REGEX = { type: GraphQLString, description: '$regex' }; 128 | fields.OPTIONS = { type: GraphQLString, description: '$options. Modifiers for the $regex expression. Field is ignored on its own' }; 129 | } 130 | 131 | function enhanceWithNotField(fields: GraphQLInputFieldConfigMap, scalarType: GraphQLScalarType | GraphQLEnumType): void { 132 | fields.NOT = { type: getGraphQLLeafFilterType(scalarType, true), description: '$not' }; 133 | } 134 | 135 | function warnOfIndependentResolveFields(type: GraphQLFieldsType): void { 136 | cache(warnedIndependentResolvers, type.toString(), () => { 137 | const fields = 138 | getTypeFields(type, (key, field) => 139 | field.resolve && !Array.isArray(field.dependencies))(); 140 | 141 | Object.keys(fields).forEach(key => 142 | warn(`Field ${key} of type ${type} has a resolve function and no dependencies`)); 143 | 144 | return 1; 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /src/graphQLInsertType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLList, GraphQLEnumType, GraphQLNonNull, GraphQLScalarType, GraphQLObjectType, GraphQLInputType } from 'graphql'; 2 | import { cache, setSuffix, getUnresolvedFieldsTypes, typesCache, FieldMap } from './common'; 3 | 4 | 5 | export function getGraphQLInsertType(graphQLType: GraphQLObjectType, ...excludedFields: string[]) : GraphQLInputObjectType { 6 | const inputTypeName = setSuffix(graphQLType.name, 'Type', 'InsertType'); 7 | 8 | return cache(typesCache, inputTypeName, () => new GraphQLInputObjectType({ 9 | name: inputTypeName, 10 | fields: getGraphQLInsertTypeFields(graphQLType, ...excludedFields) 11 | })); 12 | } 13 | 14 | function getGraphQLInsertTypeFields(graphQLType: GraphQLObjectType, ...excludedFields: string[]) : () => FieldMap { 15 | return () => { 16 | const fields = getUnresolvedFieldsTypes(graphQLType, getGraphQLInsertTypeNested, ...excludedFields)(); 17 | 18 | const idField = fields['_id']; 19 | 20 | if (idField && idField.type instanceof GraphQLNonNull) { 21 | idField.type = idField.type.ofType; 22 | } 23 | 24 | return fields; 25 | }; 26 | } 27 | 28 | function getGraphQLInsertTypeNested( 29 | type: GraphQLScalarType | GraphQLEnumType | GraphQLNonNull | GraphQLObjectType | GraphQLList, 30 | ...excludedFields: string[]) : GraphQLInputType { 31 | 32 | if (type instanceof GraphQLScalarType || 33 | type instanceof GraphQLEnumType) { 34 | return type; 35 | } 36 | 37 | if (type instanceof GraphQLNonNull) { 38 | return new GraphQLNonNull(getGraphQLInsertTypeNested(type.ofType)); 39 | } 40 | 41 | if (type instanceof GraphQLList) { 42 | return new GraphQLList(getGraphQLInsertTypeNested(type.ofType)); 43 | } 44 | 45 | const inputTypeName = setSuffix(type.name, 'Type', 'InsertType'); 46 | 47 | return cache(typesCache, inputTypeName, () => new GraphQLInputObjectType({ 48 | name: inputTypeName, 49 | fields: getUnresolvedFieldsTypes(type, getGraphQLInsertTypeNested, ...excludedFields) 50 | })); 51 | } 52 | -------------------------------------------------------------------------------- /src/graphQLPaginationType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLInt } from "graphql"; 2 | 3 | export default new GraphQLInputObjectType({ 4 | name: "PaginationType", 5 | fields: { 6 | limit: { type: GraphQLInt }, 7 | skip: { type: GraphQLInt } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/graphQLSortType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLEnumType, GraphQLNonNull, GraphQLObjectType, GraphQLInputType, GraphQLType, isLeafType, GraphQLInterfaceType } from 'graphql'; 2 | import { cache, setSuffix, getUnresolvedFieldsTypes, typesCache, FieldMap, GraphQLFieldsType } from './common'; 3 | 4 | export const FICTIVE_SORT = "_FICTIVE_SORT"; 5 | export const FICTIVE_SORT_DESCRIPTION = "IGNORE. Due to limitations of the package, objects with no sortable fields are not ommited. GraphQL input object types must have at least one field"; 6 | 7 | function getGraphQLSortTypeObject(type: GraphQLType, ...excludedFields): GraphQLInputType { 8 | if (isLeafType(type)) { 9 | return GraphQLSortType; 10 | } 11 | 12 | if (type instanceof GraphQLNonNull) { 13 | return getGraphQLSortTypeObject(type.ofType); 14 | } 15 | 16 | if (type instanceof GraphQLObjectType || 17 | type instanceof GraphQLInterfaceType) { 18 | return getGraphQLSortType(type, ...excludedFields); 19 | } 20 | 21 | return undefined; 22 | } 23 | 24 | export function getGraphQLSortType(type: GraphQLFieldsType, ...excludedFields: string[]): GraphQLInputObjectType { 25 | const sortTypeName = setSuffix(type.name, 'Type', 'SortType'); 26 | 27 | return cache(typesCache, sortTypeName, () => new GraphQLInputObjectType({ 28 | name: sortTypeName, 29 | fields: getGraphQLSortTypeFields(type, ...excludedFields) 30 | })); 31 | } 32 | 33 | function getGraphQLSortTypeFields(type: GraphQLFieldsType, ...excludedFields: string[]): () => FieldMap { 34 | return () => { 35 | const fields = getUnresolvedFieldsTypes(type, getGraphQLSortTypeObject, ...excludedFields)(); 36 | 37 | if (Object.keys(fields).length > 0) { 38 | return fields; 39 | } 40 | 41 | return { [FICTIVE_SORT]: { type: GraphQLSortType, isDeprecated: true, description: FICTIVE_SORT_DESCRIPTION } } 42 | } 43 | } 44 | 45 | export const GraphQLSortType = new GraphQLEnumType({ 46 | name: 'SortType', 47 | values: { 48 | ASC: { value: 1 }, 49 | DESC: { value: -1 } 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /src/graphQLUpdateType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLList, GraphQLEnumType, GraphQLNonNull, GraphQLScalarType, GraphQLInt, GraphQLObjectType, GraphQLInputFieldConfigMap, GraphQLInputType, Thunk, GraphQLBoolean } from 'graphql'; 2 | import { cache, setSuffix, getUnresolvedFieldsTypes, typesCache } from './common'; 3 | 4 | export const OVERWRITE = "_OVERWRITE"; 5 | export const OVERWRITE_DESCRIPTION = "If set to true, the object would be overwriten entirely, including fields that are not specified. Non-null validation rules will apply. Once set to true, any child object will overwriten invariably of the value set to this field."; 6 | export const FICTIVE_INC = "_FICTIVE_INC"; 7 | export const FICTIVE_INC_DESCRIPTION = "IGNORE. Due to limitations of the package, objects with no incrementable fields cannot be ommited. All input object types must have at least one field"; 8 | 9 | export function getGraphQLUpdateType(type: GraphQLObjectType, ...excludedFields: string[]): GraphQLInputObjectType { 10 | const updateTypeName = setSuffix(type.name, 'Type', 'UpdateType'); 11 | 12 | return cache(typesCache, updateTypeName, () => new GraphQLInputObjectType({ 13 | name: updateTypeName, 14 | fields: getUpdateFields(type, ...excludedFields) 15 | })); 16 | } 17 | 18 | function getUpdateFields(graphQLType: GraphQLObjectType, ...excludedFields: string[]): () => GraphQLInputFieldConfigMap { 19 | return () => ({ 20 | setOnInsert: { type: getGraphQLSetOnInsertType(graphQLType, ...excludedFields) }, 21 | set: { type: getGraphQLSetType(graphQLType, ...excludedFields) }, 22 | inc: { type: getGraphQLIncType(graphQLType, ...excludedFields) } 23 | }); 24 | } 25 | 26 | export function getGraphQLSetOnInsertType( 27 | type: GraphQLScalarType | GraphQLEnumType | GraphQLNonNull | GraphQLObjectType | GraphQLList, 28 | ...excludedFields: string[]): GraphQLInputType { 29 | 30 | if (type instanceof GraphQLScalarType || 31 | type instanceof GraphQLEnumType) { 32 | return type; 33 | } 34 | 35 | if (type instanceof GraphQLNonNull) { 36 | return getGraphQLSetOnInsertType(type.ofType); 37 | } 38 | 39 | if (type instanceof GraphQLList) { 40 | return new GraphQLList(getGraphQLSetOnInsertType(type.ofType)); 41 | } 42 | 43 | const inputTypeName = setSuffix(type.name, 'Type', 'SetOnInsertType'); 44 | 45 | return cache(typesCache, inputTypeName, () => new GraphQLInputObjectType({ 46 | name: inputTypeName, 47 | fields: getUnresolvedFieldsTypes(type, getGraphQLSetOnInsertType, ...excludedFields) 48 | })); 49 | } 50 | 51 | export function getGraphQLSetType(type: GraphQLObjectType, ...excludedFields: string[]): GraphQLInputObjectType { 52 | const inputTypeName = setSuffix(type.name, 'Type', 'SetType'); 53 | 54 | return cache(typesCache, inputTypeName, () => new GraphQLInputObjectType({ 55 | name: inputTypeName, 56 | fields: getUnresolvedFieldsTypes(type, (_, ...excluded) => getGraphQLSetObjectType(_ as any, false, ...excluded), ...excludedFields) 57 | })); 58 | } 59 | 60 | function getGraphQLSetObjectType( 61 | type: GraphQLScalarType | GraphQLEnumType | GraphQLNonNull | GraphQLObjectType | GraphQLList, 62 | isInList: boolean, 63 | ...excludedFields: string[]): GraphQLInputType { 64 | 65 | 66 | if (type instanceof GraphQLScalarType || 67 | type instanceof GraphQLEnumType) { 68 | return type; 69 | } 70 | 71 | if (type instanceof GraphQLNonNull) { 72 | return getGraphQLSetObjectType(type.ofType, isInList); 73 | } 74 | 75 | if (type instanceof GraphQLList) { 76 | return new GraphQLList(getGraphQLSetObjectType(type.ofType, true)); 77 | } 78 | 79 | const inputTypeName = setSuffix(type.name, 'Type', isInList ? 'SetListObjectType' : 'SetObjectType'); 80 | 81 | return cache(typesCache, inputTypeName, () => new GraphQLInputObjectType({ 82 | name: inputTypeName, 83 | fields: getGraphQLSetObjectTypeFields(type, isInList, ...excludedFields) 84 | })); 85 | } 86 | 87 | function getGraphQLSetObjectTypeFields(type: GraphQLObjectType, isInList: boolean, ...excludedFields: string[]): Thunk { 88 | return () => { 89 | const fields = getUnresolvedFieldsTypes(type, (_, ...excluded) => getGraphQLSetObjectType(_ as any, isInList, ...excluded), ...excludedFields)(); 90 | 91 | if (!isInList) { 92 | fields[OVERWRITE] = { type: GraphQLBoolean, description: OVERWRITE_DESCRIPTION } 93 | } 94 | 95 | return fields; 96 | }; 97 | } 98 | 99 | export function getGraphQLIncType( 100 | type: GraphQLScalarType | GraphQLEnumType | GraphQLNonNull | GraphQLObjectType | GraphQLList, 101 | ...excludedFields: string[]): GraphQLInputType { 102 | 103 | if (type instanceof GraphQLScalarType || 104 | type instanceof GraphQLEnumType) { 105 | if (["Int", "Float"].includes(type.name)) { 106 | return type; 107 | } 108 | 109 | return undefined; 110 | } 111 | 112 | if (type instanceof GraphQLNonNull) { 113 | return getGraphQLIncType(type.ofType); 114 | } 115 | 116 | if (type instanceof GraphQLList) { 117 | return undefined; 118 | } 119 | 120 | const inputTypeName = setSuffix(type.name, 'Type', 'IncType'); 121 | 122 | return cache(typesCache, inputTypeName, () => new GraphQLInputObjectType({ 123 | name: inputTypeName, 124 | fields: getGraphQLIncTypeFields(type, ...excludedFields) 125 | })); 126 | } 127 | 128 | function getGraphQLIncTypeFields(type: GraphQLObjectType, ...excludedFields: string[]): () => GraphQLInputFieldConfigMap { 129 | return () => { 130 | const fields = getUnresolvedFieldsTypes(type, getGraphQLIncType, ...excludedFields)(); 131 | 132 | if (Object.keys(fields).length > 0) { 133 | return fields; 134 | } 135 | 136 | return { [FICTIVE_INC]: { type: GraphQLInt, description: FICTIVE_INC_DESCRIPTION } } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | export interface Log { 2 | (message?: any, ...optionalParams: any[]): void; 3 | } 4 | 5 | export interface Logger { 6 | error?: Log; 7 | warn?: Log; 8 | } 9 | 10 | let logger: Logger = { 11 | warn: (...args) => console.warn('\x1b[33m', 'graphql-to-mongodb warning:', '\x1b[0m', ...args), 12 | //error: (...args) => console.warn('\x1b[31m', 'graphql-to-mongodb error:', '\x1b[0m', ...args), 13 | }; 14 | 15 | export function warn(message?: any, ...optionalParams: any[]): void { 16 | if (logger.warn) { 17 | logger.warn(message, ...optionalParams); 18 | } 19 | } 20 | 21 | function error(message?: any, ...optionalParams: any[]): void { 22 | if (logger.error) { 23 | logger.error(message, ...optionalParams); 24 | } 25 | } 26 | 27 | export function setLogger(loggerObject: Logger): void { 28 | logger = loggerObject || {}; 29 | } 30 | export function getLogger(): Logger { 31 | return { ...logger }; 32 | } 33 | 34 | export function logOnError any>(func: T): T { 35 | const wrappedFunction = (...args) => { 36 | try { 37 | return func(...args); 38 | } catch (exception) { 39 | error(exception); 40 | throw exception; 41 | } 42 | }; 43 | 44 | return wrappedFunction as T; 45 | } 46 | -------------------------------------------------------------------------------- /src/mongoDbFilter.ts: -------------------------------------------------------------------------------- 1 | import { isType, GraphQLObjectType, isLeafType, GraphQLLeafType } from 'graphql'; 2 | import { getTypeFields, getInnerType, isListField, addPrefixToProperties, GraphQLFieldsType } from './common'; 3 | import { warn, logOnError } from './logger'; 4 | 5 | type GraphQLLeafOperators = 'EQ' | 'GT' | 'GTE' | 'IN' | 'ALL' | 'LT' | 'LTE' | 'NE' | 'NEQ' | 'NEQ' | 'NIN' | 'REGEX' | 'OPTIONS' | 'NOT'; 6 | type MongoDbLeafOperators = '$eq' | '$gt' | '$gte' | '$in' | '$all' | '$lt' | '$lte' | '$ne' | '$neq' | '$nin' | '$regex' | '$options' | '$not'; 7 | type GraphQLRootOperators = 'OR' | 'AND' | 'NOR'; 8 | export type MongoDbRootOperators = '$or' | '$and' | '$nor'; 9 | type GraphQLOperators = GraphQLLeafOperators | GraphQLRootOperators; 10 | type MongoDbOperators = MongoDbLeafOperators | MongoDbRootOperators; 11 | 12 | export type GraphQLFilter = { 13 | [key: string]: GraphQLFilter[] | GraphQLObjectFilter | GraphQLLeafFilter; 14 | OR?: GraphQLFilter[]; 15 | AND?: GraphQLFilter[]; 16 | NOR?: GraphQLFilter[]; 17 | }; 18 | 19 | type GraphQLObjectFilter = { 20 | [key: string]: GraphQLObjectFilter | GraphQLLeafFilter | ('exists' | 'not_exists'); 21 | opr?: 'exists' | 'not_exists'; 22 | }; 23 | 24 | type GraphQLLeafFilterInner = { 25 | [key in GraphQLLeafOperators]?: any | any[]; 26 | }; 27 | 28 | type GraphQLLeafFilter = GraphQLLeafFilterInner & { 29 | NOT?: GraphQLLeafFilterInner; 30 | opr?: MongoDbLeafOperators; 31 | value?: any; 32 | values?: any[]; 33 | }; 34 | 35 | function isLeafTypeTypeGuard(fieldFilter: GraphQLObjectFilter | GraphQLLeafFilter, fieldType: GraphQLObjectType | GraphQLLeafType): fieldFilter is GraphQLLeafFilter { 36 | return isLeafType(fieldType); 37 | } 38 | 39 | export type MongoDbFilter = { 40 | [key: string]: MongoDbLeafFilter | { $elemMatch: MongoDbObjectFilter } | { $exists?: boolean } | MongoDbFilter[]; 41 | $or?: MongoDbFilter[]; 42 | $and?: MongoDbFilter[]; 43 | $nor?: MongoDbFilter[]; 44 | }; 45 | 46 | type MongoDbObjectFilter = { 47 | [key: string]: MongoDbLeafFilter | { $elemMatch: MongoDbObjectFilter } | { $exists?: boolean } | boolean; 48 | }; 49 | 50 | type MongoDbLeafFilter = { 51 | [key in MongoDbLeafOperators]?: any | any[]; 52 | } & { 53 | $not?: MongoDbLeafFilter | RegExp 54 | }; 55 | 56 | const operatorsMongoDbKeys: { [key in GraphQLOperators]: MongoDbOperators } = { 57 | EQ: '$eq', 58 | GT: '$gt', 59 | GTE: '$gte', 60 | IN: '$in', 61 | ALL: '$all', 62 | LT: '$lt', 63 | LTE: '$lte', 64 | NE: '$ne', 65 | NEQ: '$ne', // DEPRECATED 66 | NIN: '$nin', 67 | REGEX: '$regex', 68 | OPTIONS: '$options', 69 | NOT: '$not', 70 | OR: '$or', 71 | AND: '$and', 72 | NOR: '$nor', 73 | }; 74 | 75 | const rootOperators: GraphQLRootOperators[] = ['OR', 'AND', 'NOR'] 76 | 77 | 78 | export const getMongoDbFilter = logOnError((graphQLType: GraphQLFieldsType, graphQLFilter: GraphQLFilter = {}): MongoDbFilter => { 79 | if (!isType(graphQLType)) throw 'First arg of getMongoDbFilter must be the base graphqlType to be parsed' 80 | 81 | const filter = parseMongoDbFilter(graphQLType, graphQLFilter as GraphQLObjectFilter, ...rootOperators) as MongoDbFilter; 82 | 83 | rootOperators 84 | .map(key => ({ key, args: graphQLFilter[key] as GraphQLFilter[] })) 85 | .filter(({ args }) => !!args && args.length > 0) 86 | .forEach(({ key, args }) => filter[operatorsMongoDbKeys[key] as MongoDbRootOperators] = 87 | args.map(_ => getMongoDbFilter(graphQLType, _))); 88 | 89 | return filter; 90 | }); 91 | 92 | function parseMongoDbFilter(type: GraphQLFieldsType, graphQLFilter: GraphQLObjectFilter, ...excludedFields: string[]): MongoDbObjectFilter { 93 | const typeFields = getTypeFields(type)(); 94 | 95 | return Object.keys(graphQLFilter) 96 | .filter(key => !excludedFields.includes(key)) 97 | .reduce((agg: MongoDbObjectFilter, key) => { 98 | if (key === 'opr') { 99 | return { ...agg, ...parseMongoExistsFilter(graphQLFilter[key]) } as MongoDbObjectFilter 100 | } 101 | 102 | const fieldFilter = graphQLFilter[key] as GraphQLObjectFilter | GraphQLLeafFilter; 103 | const fieldType = getInnerType(typeFields[key].type) as GraphQLLeafType | GraphQLObjectType; 104 | 105 | if (isLeafTypeTypeGuard(fieldFilter, fieldType)) { 106 | const leafFilter = parseMongoDbLeafFilter(fieldFilter); 107 | 108 | if (Object.keys(leafFilter).length > 0) { 109 | return { ...agg, [key]: leafFilter }; 110 | } 111 | } else { 112 | const nestedFilter = parseMongoDbFilter(fieldType, fieldFilter, ...excludedFields); 113 | 114 | if (Object.keys(nestedFilter).length > 0) { 115 | if (isListField(typeFields[key].type)) { 116 | return { ...agg, [key]: { '$elemMatch': nestedFilter } }; 117 | } else { 118 | const { $exists, ...nestedObjectFilter } = nestedFilter; 119 | 120 | const exists = typeof $exists === 'boolean' ? { [key]: { $exists } } : { }; 121 | 122 | return { ...agg, ...addPrefixToProperties(nestedObjectFilter, `${key}.`), ...exists}; 123 | } 124 | } 125 | } 126 | 127 | return agg; 128 | }, {} as MongoDbObjectFilter); 129 | } 130 | 131 | function parseMongoExistsFilter(exists: 'exists' | 'not_exists'): { $exists: boolean } { 132 | return { $exists: exists === 'exists' ? true : false }; 133 | } 134 | 135 | function parseMongoDbLeafFilter(graphQLLeafFilter: GraphQLLeafFilter, not: boolean = false): MongoDbLeafFilter { 136 | const mongoDbScalarFilter: MongoDbLeafFilter = {}; 137 | 138 | Object.keys(graphQLLeafFilter) 139 | .filter((key: keyof GraphQLLeafFilter) => key !== 'value' && key !== 'values' && key !== 'OPTIONS') 140 | .forEach((key: keyof GraphQLLeafFilter) => { 141 | ////////////// DEPRECATED ///////////////////////////////////////// 142 | if (key === 'opr') { 143 | Object.assign(mongoDbScalarFilter, parseMongoDbScalarFilterOpr(graphQLLeafFilter[key], graphQLLeafFilter)); 144 | return; 145 | } 146 | /////////////////////////////////////////////////////////////////// 147 | if (key === 'NOT') { 148 | mongoDbScalarFilter[operatorsMongoDbKeys[key]] = parseMongoDbLeafNoteFilter(graphQLLeafFilter[key]); 149 | return; 150 | } 151 | 152 | const element = graphQLLeafFilter[key]; 153 | 154 | mongoDbScalarFilter[operatorsMongoDbKeys[key]] = element; 155 | 156 | if (key === 'REGEX') { 157 | const options = graphQLLeafFilter['OPTIONS']; 158 | if (not) { 159 | mongoDbScalarFilter[operatorsMongoDbKeys[key]] = new RegExp(element, `g${options || ''}`); 160 | } else if (!!options) { 161 | mongoDbScalarFilter[operatorsMongoDbKeys['OPTIONS']] = graphQLLeafFilter['OPTIONS']; 162 | } 163 | } 164 | 165 | }); 166 | 167 | return mongoDbScalarFilter; 168 | } 169 | 170 | function parseMongoDbLeafNoteFilter(graphQLLeafFilterInner: GraphQLLeafFilterInner): MongoDbLeafFilter | RegExp { 171 | if (!graphQLLeafFilterInner.REGEX) { 172 | return parseMongoDbLeafFilter(graphQLLeafFilterInner, true); 173 | } 174 | 175 | if (Object.keys(graphQLLeafFilterInner).length > (!!graphQLLeafFilterInner.OPTIONS ? 2 : 1)) { 176 | throw "NOT operator can contain either REGEX [and OPTIONS], or every other operator." 177 | } 178 | 179 | return new RegExp(graphQLLeafFilterInner.REGEX, `g${graphQLLeafFilterInner.OPTIONS || ''}`); 180 | } 181 | 182 | ////////////// DEPRECATED /////////////////////////////////////////// 183 | let dperecatedMessageSent = false; 184 | 185 | function parseMongoDbScalarFilterOpr(opr: MongoDbLeafOperators, graphQLFilter: GraphQLLeafFilter): {} { 186 | if (!dperecatedMessageSent) { 187 | warn('scalar filter "opr" field is deprecated, please switch to the operator fields'); 188 | dperecatedMessageSent = true; 189 | } 190 | 191 | if (["$in", "$nin", "$all"].includes(opr)) { 192 | if (graphQLFilter['values']) { 193 | return { [opr]: graphQLFilter['values'] }; 194 | } 195 | } 196 | else if (graphQLFilter['value'] !== undefined) { 197 | return { [opr]: graphQLFilter['value'] }; 198 | } 199 | 200 | return {}; 201 | } 202 | ///////////////////////////////////////////////////////////////////// 203 | -------------------------------------------------------------------------------- /src/mongoDbProjection.ts: -------------------------------------------------------------------------------- 1 | import { getInnerType, flatten, addPrefixToProperties, GraphQLFieldsType } from './common'; 2 | import { isType, GraphQLResolveInfo, SelectionNode, FragmentSpreadNode, GraphQLField } from 'graphql'; 3 | import { logOnError } from './logger'; 4 | 5 | export interface MongoDbProjection { 6 | [key: string]: 1 7 | }; 8 | 9 | export interface GetMongoDbProjectionOptions { 10 | isResolvedField: (field: GraphQLField) => boolean; 11 | excludedFields: string[]; 12 | } 13 | 14 | const defaultOptions: GetMongoDbProjectionOptions = { 15 | isResolvedField: ((field: GraphQLField) => !!field.resolve), 16 | excludedFields: [], 17 | } 18 | 19 | export const getMongoDbProjection = logOnError((info: GraphQLResolveInfo, graphQLFieldsType: GraphQLFieldsType, options: GetMongoDbProjectionOptions = defaultOptions): MongoDbProjection => { 20 | if (!Object.keys(info).includes('fieldNodes')) throw 'First argument of "getMongoDbProjection" must be a GraphQLResolveInfo'; 21 | if (!isType(graphQLFieldsType)) throw 'Second argument of "getMongoDbProjection" must be a GraphQLType'; 22 | 23 | const nodes = flatten(info.fieldNodes.map(_ => [..._.selectionSet.selections])); 24 | 25 | const projection = getSelectedProjection(nodes, graphQLFieldsType, { info, fragments: {} }, { 26 | ...options, 27 | isResolvedField: options.isResolvedField || ((field: GraphQLField) => !!field.resolve) 28 | }); 29 | 30 | return omitRedundantProjection(projection); 31 | }); 32 | 33 | function getSelectedProjection( 34 | selectionNodes: SelectionNode[], 35 | graphQLFieldsType: GraphQLFieldsType, 36 | extra: { info: GraphQLResolveInfo, fragments: { [key: string]: MongoDbProjection } }, 37 | options: GetMongoDbProjectionOptions = defaultOptions): MongoDbProjection { 38 | const fields = graphQLFieldsType.getFields() 39 | 40 | return selectionNodes.reduce((projection, node) => { 41 | if (node.kind === 'Field') { 42 | if (node.name.value === '__typename' || options.excludedFields.includes(node.name.value)) return projection; 43 | 44 | const field = fields[node.name.value]; 45 | 46 | if (options.isResolvedField(field)) { 47 | const dependencies: string[] = field["dependencies"] || []; 48 | const dependenciesProjection = dependencies.reduce((agg, dependency) => ({ ...agg, [dependency]: 1 }), {}); 49 | return { 50 | ...projection, 51 | ...dependenciesProjection 52 | }; 53 | } 54 | 55 | if (!node.selectionSet) return { 56 | ...projection, 57 | [node.name.value]: 1 58 | }; 59 | 60 | const nested = getSelectedProjection([...node.selectionSet.selections], getInnerType(field.type) as GraphQLFieldsType, extra, options); 61 | 62 | return { 63 | ...projection, 64 | ...addPrefixToProperties(nested, `${node.name.value}.`) 65 | }; 66 | } else if (node.kind === 'InlineFragment') { 67 | const type = extra.info.schema.getType(node.typeCondition.name.value); 68 | return { 69 | ...projection, 70 | ...getSelectedProjection([...node.selectionSet.selections], type as GraphQLFieldsType, extra, options) 71 | }; 72 | } else if (node.kind === 'FragmentSpread') { 73 | return { 74 | ...projection, 75 | ...getFragmentProjection(node, graphQLFieldsType, extra, options) 76 | }; 77 | } 78 | }, {}); 79 | } 80 | 81 | function getFragmentProjection( 82 | fragmentSpreadNode: FragmentSpreadNode, 83 | graphQLFieldsType: GraphQLFieldsType, 84 | extra: { info: GraphQLResolveInfo, fragments: { [key: string]: MongoDbProjection } }, 85 | options: GetMongoDbProjectionOptions = defaultOptions): MongoDbProjection { 86 | const fragmentName = fragmentSpreadNode.name.value; 87 | if (extra.fragments[fragmentName]) return extra.fragments[fragmentName]; 88 | const fragmentNode = extra.info.fragments[fragmentName]; 89 | extra.fragments[fragmentName] = getSelectedProjection([...fragmentNode.selectionSet.selections], graphQLFieldsType, extra, options); 90 | return extra.fragments[fragmentName]; 91 | } 92 | 93 | function omitRedundantProjection(projection: MongoDbProjection) { 94 | return Object.keys(projection).reduce((proj, key) => { 95 | if (Object.keys(projection).some(otherKey => 96 | otherKey !== key && new RegExp(`^${otherKey}[.]`).test(key))) { 97 | return proj; 98 | } 99 | return { 100 | ...proj, 101 | [key]: 1 102 | }; 103 | }, {}); 104 | } -------------------------------------------------------------------------------- /src/mongoDbSort.ts: -------------------------------------------------------------------------------- 1 | import { addPrefixToProperties } from "./common"; 2 | import { FICTIVE_SORT } from "./graphQLSortType"; 3 | 4 | export type SortDirection = 1 | -1 5 | 6 | export interface SortArg { 7 | [key: string]: SortArg | SortDirection; 8 | }; 9 | 10 | export interface MongoDbSort { 11 | [key: string]: SortDirection; 12 | }; 13 | 14 | function getMongoDbSort(sort: SortArg): MongoDbSort { 15 | return Object.keys(sort) 16 | .filter(key => key != FICTIVE_SORT) 17 | .reduce((agg, key) => { 18 | const value = sort[key]; 19 | 20 | if (typeof value === 'number') { 21 | return { ...agg, [key]: value } 22 | } 23 | 24 | const nested = getMongoDbSort(value as SortArg); 25 | 26 | return { ...agg, ...addPrefixToProperties(nested, `${key}.`) } 27 | }, {}); 28 | } 29 | 30 | export default getMongoDbSort; 31 | -------------------------------------------------------------------------------- /src/mongoDbUpdate.ts: -------------------------------------------------------------------------------- 1 | import { addPrefixToProperties, isPrimitive } from './common'; 2 | import { logOnError } from './logger'; 3 | import { OVERWRITE, FICTIVE_INC } from './graphQLUpdateType'; 4 | 5 | export enum SetOverwrite { 6 | DefaultTrueRoot, 7 | True, 8 | False 9 | } 10 | 11 | export interface UpdateArgs { 12 | setOnInsert?: object, 13 | set?: object, 14 | inc?: object, 15 | } 16 | 17 | export interface UpdateObj { 18 | $setOnInsert?: SetOnInsertObj 19 | $set?: SetObj 20 | $inc?: IncObj 21 | } 22 | 23 | export interface SetOnInsertObj { 24 | [key: string]: SetOnInsertObj | any 25 | } 26 | 27 | export interface SetObj { 28 | [key: string]: SetObj | any 29 | } 30 | 31 | export interface IncObj { 32 | [key: string]: number 33 | } 34 | 35 | export interface updateParams { 36 | update: UpdateObj, 37 | options?: { upsert?: boolean } 38 | } 39 | 40 | export const getMongoDbUpdate = logOnError((update: UpdateArgs, overwrite: boolean = false): updateParams => { 41 | const updateParams: updateParams = { 42 | update: {} 43 | }; 44 | 45 | if (update.setOnInsert) { 46 | updateParams.update.$setOnInsert = update.setOnInsert; 47 | updateParams.options = { upsert: true }; 48 | } 49 | 50 | if (update.set) { 51 | updateParams.update.$set = getMongoDbSet(update.set, overwrite ? SetOverwrite.DefaultTrueRoot : SetOverwrite.False); 52 | } 53 | 54 | if (update.inc) { 55 | updateParams.update.$inc = getMongoDbInc(update.inc); 56 | } 57 | 58 | return updateParams; 59 | }); 60 | 61 | export function getMongoDbSet(set: object, setOverwrite: SetOverwrite): SetObj { 62 | return Object.keys(set).filter(_ => _ !== OVERWRITE).reduce((agg, key) => { 63 | const value = set[key]; 64 | 65 | if (isPrimitive(value)) { 66 | if (value === undefined) return agg; 67 | return { ...agg, [key]: value }; 68 | } 69 | 70 | if (Array.isArray(value)) { 71 | return { ...agg, [key]: value }; 72 | } 73 | 74 | 75 | const childOverwrite = getOverwrite(setOverwrite, value[OVERWRITE]); 76 | const child = getMongoDbSet(value, childOverwrite); 77 | 78 | if (childOverwrite === SetOverwrite.False) { 79 | return { ...agg, ...addPrefixToProperties(child, `${key}.`) }; 80 | } 81 | 82 | return { ...agg, [key]: child }; 83 | }, {}); 84 | } 85 | 86 | export function getOverwrite(current: SetOverwrite, input?: boolean): SetOverwrite { 87 | if (current === SetOverwrite.True) { 88 | return SetOverwrite.True; 89 | } 90 | 91 | if (typeof input !== "undefined") { 92 | return input ? SetOverwrite.True : SetOverwrite.False; 93 | } 94 | 95 | if (current === SetOverwrite.DefaultTrueRoot) { 96 | return SetOverwrite.True; 97 | } 98 | 99 | return current; 100 | } 101 | 102 | export function getMongoDbInc(inc: object): IncObj { 103 | return Object.keys(inc).filter(_ => _ !== FICTIVE_INC).reduce((agg, key) => { 104 | const value = inc[key]; 105 | 106 | if (typeof value === "number") { 107 | return { ...agg, [key]: value } 108 | } 109 | 110 | const child = getMongoDbInc(value); 111 | 112 | if (Object.keys(child).length === 0) { 113 | return agg; 114 | } 115 | 116 | return { ...agg, ...addPrefixToProperties(child, `${key}.`) }; 117 | }, {}); 118 | } 119 | -------------------------------------------------------------------------------- /src/mongoDbUpdateValidation.ts: -------------------------------------------------------------------------------- 1 | import { UpdateArgs } from "./mongoDbUpdate"; 2 | import { GraphQLObjectType, GraphQLType, GraphQLNonNull, GraphQLList, GraphQLFieldMap, GraphQLField, GraphQLError } from "graphql"; 3 | import { isNonNullField, getInnerType, flatten, isListField } from "./common"; 4 | import { OVERWRITE } from "./graphQLUpdateType"; 5 | 6 | export interface UpdateField { 7 | [key: string]: UpdateField | UpdateField[] | 1 8 | } 9 | 10 | export enum ShouldAssert { 11 | DefaultTrueRoot, 12 | True, 13 | False 14 | } 15 | 16 | export interface ValidateUpdateArgsOptions { 17 | overwrite: boolean; 18 | isResolvedField?: (field: GraphQLField) => boolean; 19 | } 20 | 21 | const defaultOptions: ValidateUpdateArgsOptions = { 22 | overwrite: false, 23 | }; 24 | 25 | export function validateUpdateArgs(updateArgs: UpdateArgs, graphQLType: GraphQLObjectType, options: ValidateUpdateArgsOptions = defaultOptions): void { 26 | let errors: string[] = []; 27 | 28 | errors = errors.concat(validateNonNullableFieldsOuter(updateArgs, graphQLType, options)); 29 | 30 | if (errors.length > 0) { 31 | throw new GraphQLError(errors.join("\n")); 32 | } 33 | } 34 | 35 | function validateNonNullableFieldsOuter( 36 | updateArgs: UpdateArgs, 37 | graphQLType: GraphQLObjectType, 38 | { overwrite, isResolvedField }: ValidateUpdateArgsOptions): string[] { 39 | const shouldAssert: ShouldAssert = !!updateArgs.setOnInsert 40 | ? ShouldAssert.True 41 | : overwrite 42 | ? ShouldAssert.DefaultTrueRoot 43 | : ShouldAssert.False; 44 | 45 | return validateNonNullableFields(Object.keys(updateArgs).map(_ => updateArgs[_]), graphQLType, shouldAssert, isResolvedField); 46 | } 47 | 48 | export function validateNonNullableFields( 49 | objects: object[], 50 | graphQLType: GraphQLObjectType, 51 | shouldAssert: ShouldAssert, 52 | isResolvedField: ((field: GraphQLField) => boolean) = (field: GraphQLField) => !!field.resolve, 53 | path: string[] = []): string[] { 54 | const typeFields = graphQLType.getFields(); 55 | 56 | const errors: string[] = shouldAssert === ShouldAssert.True ? validateNonNullableFieldsAssert(objects, typeFields, path) : []; 57 | 58 | const overwrite = objects.map(_ => _[OVERWRITE]).filter(_ => _)[0]; 59 | shouldAssert = getShouldAssert(shouldAssert, overwrite); 60 | 61 | return [...errors, ...validateNonNullableFieldsTraverse(objects, typeFields, shouldAssert, isResolvedField, path)]; 62 | } 63 | 64 | export function validateNonNullableFieldsAssert(objects: object[], typeFields: GraphQLFieldMap, path: string[] = []): string[] { 65 | return Object 66 | .keys(typeFields) 67 | .map(key => ({ key, type: typeFields[key].type })) 68 | .filter(field => isNonNullField(field.type) && (field.key !== '_id' || path.length > 0)) 69 | .reduce((agg, field) => { 70 | let fieldPath = [...path, field.key].join("."); 71 | const fieldValues = objects.map(_ => _[field.key]).filter(_ => _ !== undefined); 72 | if (field.type instanceof GraphQLNonNull) { 73 | if (fieldValues.some(_ => _ === null)) 74 | return [...agg, `Non-nullable field "${fieldPath}" is set to null`]; 75 | if (fieldValues.length === 0) 76 | return [...agg, `Missing non-nullable field "${fieldPath}"`]; 77 | } 78 | if (isListField(field.type) && !validateNonNullListField(fieldValues, field.type)) { 79 | return [...agg, `Non-nullable element of array "${fieldPath}" is set to null`]; 80 | } 81 | 82 | return agg; 83 | }, []); 84 | } 85 | 86 | export function validateNonNullListField(fieldValues: object[], type: GraphQLType): boolean { 87 | if (type instanceof GraphQLNonNull) { 88 | if (fieldValues.some(_ => _ === null)) { 89 | return false; 90 | } 91 | 92 | return validateNonNullListField(fieldValues, type.ofType); 93 | } 94 | 95 | if (type instanceof GraphQLList) { 96 | return validateNonNullListField(flatten(fieldValues.filter(_ => _) as object[][]), type.ofType); 97 | } 98 | 99 | return true; 100 | } 101 | 102 | export function getShouldAssert(current: ShouldAssert, input?: boolean): ShouldAssert { 103 | if (current === ShouldAssert.True) { 104 | return ShouldAssert.True; 105 | } 106 | 107 | if (typeof input !== "undefined") { 108 | return input ? ShouldAssert.True : ShouldAssert.False; 109 | } 110 | 111 | if (current === ShouldAssert.DefaultTrueRoot) { 112 | return ShouldAssert.True; 113 | } 114 | 115 | return current; 116 | } 117 | 118 | export function validateNonNullableFieldsTraverse( 119 | objects: object[], 120 | typeFields: GraphQLFieldMap, 121 | shouldAssert: ShouldAssert, 122 | isResolvedField: (field: GraphQLField) => boolean = (field: GraphQLField) => !!field.resolve, 123 | path: string[] = []): string[] { 124 | let keys: string[] = Array.from(new Set(flatten(objects.map(_ => Object.keys(_))))); 125 | 126 | return keys.reduce((agg, key) => { 127 | const field = typeFields[key]; 128 | const type = field.type; 129 | const innerType = getInnerType(type); 130 | 131 | if (!(innerType instanceof GraphQLObjectType) || isResolvedField(field)) { 132 | return agg; 133 | } 134 | 135 | const newPath = [...path, key]; 136 | const values = objects.map(_ => _[key]).filter(_ => _); 137 | 138 | if (isListField(type)) { 139 | return [...agg, ...flatten(flattenListField(values, type).map(_ => validateNonNullableFields([_], innerType, ShouldAssert.True, isResolvedField, newPath)))]; 140 | } else { 141 | return [...agg, ...validateNonNullableFields(values, innerType, shouldAssert, isResolvedField, newPath)]; 142 | } 143 | }, []); 144 | } 145 | 146 | export function flattenListField(objects: object[], type: GraphQLType): object[] { 147 | if (type instanceof GraphQLNonNull) { 148 | return flattenListField(objects, type.ofType); 149 | } 150 | 151 | if (type instanceof GraphQLList) { 152 | return flattenListField(flatten(objects as object[][]).filter(_ => _), type.ofType); 153 | } 154 | 155 | return objects; 156 | } 157 | -------------------------------------------------------------------------------- /src/queryResolver.ts: -------------------------------------------------------------------------------- 1 | import { getMongoDbFilter, MongoDbFilter } from './mongoDbFilter'; 2 | import { getMongoDbProjection, MongoDbProjection, GetMongoDbProjectionOptions } from './mongoDbProjection'; 3 | import { getGraphQLFilterType } from './graphQLFilterType'; 4 | import { getGraphQLSortType } from './graphQLSortType'; 5 | import GraphQLPaginationType from './graphQLPaginationType'; 6 | import getMongoDbSort, { MongoDbSort } from "./mongoDbSort"; 7 | import { isType, GraphQLResolveInfo, GraphQLFieldResolver, GraphQLInputObjectType, GraphQLField } from 'graphql'; 8 | import { GraphQLFieldsType } from './common'; 9 | 10 | export interface QueryCallback { 11 | ( 12 | filter: MongoDbFilter, 13 | projection: MongoDbProjection, 14 | options: MongoDbOptions, 15 | source: TSource, 16 | args: { [argName: string]: any }, 17 | context: TContext, 18 | info: GraphQLResolveInfo 19 | ): Promise 20 | }; 21 | 22 | export interface MongoDbOptions { 23 | sort?: MongoDbSort; 24 | limit?: number; 25 | skip?: number; 26 | projection?: MongoDbProjection; 27 | } 28 | 29 | export type QueryOptions = { 30 | differentOutputType?: boolean; 31 | } & Partial; 32 | 33 | const defaultOptions: Required = { 34 | differentOutputType: false, 35 | excludedFields: [], 36 | isResolvedField: (field: GraphQLField) => !!field.resolve 37 | }; 38 | 39 | export function getMongoDbQueryResolver( 40 | graphQLType: GraphQLFieldsType, 41 | queryCallback: QueryCallback, 42 | queryOptions?: QueryOptions): GraphQLFieldResolver { 43 | if (!isType(graphQLType)) throw 'getMongoDbQueryResolver must recieve a graphql type'; 44 | if (typeof queryCallback !== 'function') throw 'getMongoDbQueryResolver must recieve a queryCallback function'; 45 | const requiredQueryOptions = { ...defaultOptions, ...queryOptions }; 46 | 47 | return async (source: TSource, args: { [argName: string]: any }, context: TContext, info: GraphQLResolveInfo): Promise => { 48 | const filter = getMongoDbFilter(graphQLType, args.filter); 49 | const projection = requiredQueryOptions.differentOutputType ? undefined : getMongoDbProjection(info, graphQLType, requiredQueryOptions); 50 | const options: MongoDbOptions = { projection }; 51 | if (args.sort) options.sort = getMongoDbSort(args.sort); 52 | if (args.pagination && args.pagination.limit) options.limit = args.pagination.limit; 53 | if (args.pagination && args.pagination.skip) options.skip = args.pagination.skip; 54 | 55 | return await queryCallback(filter, projection, options, source, args, context, info); 56 | } 57 | } 58 | 59 | export function getGraphQLQueryArgs(graphQLType: GraphQLFieldsType): { [key: string]: { type: GraphQLInputObjectType } } & { 60 | filter: { type: GraphQLInputObjectType }, 61 | sort: { type: GraphQLInputObjectType }, 62 | pagination: { type: GraphQLInputObjectType } 63 | } { 64 | return { 65 | filter: { type: getGraphQLFilterType(graphQLType) }, 66 | sort: { type: getGraphQLSortType(graphQLType) }, 67 | pagination: { type: GraphQLPaginationType } 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/updateResolver.ts: -------------------------------------------------------------------------------- 1 | import { getGraphQLFilterType } from './graphQLFilterType'; 2 | import { getGraphQLUpdateType } from './graphQLUpdateType'; 3 | import { getMongoDbFilter, MongoDbFilter } from './mongoDbFilter'; 4 | import { getMongoDbUpdate, UpdateObj } from './mongoDbUpdate'; 5 | import { validateUpdateArgs } from './mongoDbUpdateValidation'; 6 | import { GraphQLNonNull, isType, GraphQLResolveInfo, GraphQLFieldResolver, GraphQLObjectType, GraphQLInputObjectType } from 'graphql'; 7 | import { getMongoDbProjection, MongoDbProjection, GetMongoDbProjectionOptions } from './mongoDbProjection'; 8 | 9 | export interface UpdateCallback { 10 | ( 11 | filter: MongoDbFilter, 12 | update: UpdateObj, 13 | options: { upsert?: boolean } | undefined, 14 | projection: MongoDbProjection | undefined, 15 | source: TSource, 16 | args: { [argName: string]: any }, 17 | context: TContext, 18 | info: GraphQLResolveInfo 19 | ): Promise 20 | }; 21 | 22 | 23 | export type UpdateOptions = { 24 | differentOutputType?: boolean; 25 | validateUpdateArgs?: boolean; 26 | overwrite?: boolean; 27 | } & Partial; 28 | 29 | const defaultOptions: Required = { 30 | differentOutputType: false, 31 | validateUpdateArgs: false, 32 | overwrite: false, 33 | excludedFields: [], 34 | isResolvedField: undefined 35 | }; 36 | 37 | export function getMongoDbUpdateResolver( 38 | graphQLType: GraphQLObjectType, 39 | updateCallback: UpdateCallback, 40 | updateOptions?: UpdateOptions): GraphQLFieldResolver { 41 | if (!isType(graphQLType)) throw 'getMongoDbUpdateResolver must recieve a graphql type'; 42 | if (typeof updateCallback !== 'function') throw 'getMongoDbUpdateResolver must recieve an updateCallback'; 43 | const requiredUpdateOptions = { ...defaultOptions, ...updateOptions }; 44 | 45 | return async (source: TSource, args: { [argName: string]: any }, context: TContext, info: GraphQLResolveInfo): Promise => { 46 | const filter = getMongoDbFilter(graphQLType, args.filter); 47 | if (requiredUpdateOptions.validateUpdateArgs) validateUpdateArgs(args.update, graphQLType, requiredUpdateOptions); 48 | const mongoUpdate = getMongoDbUpdate(args.update, requiredUpdateOptions.overwrite); 49 | const projection = requiredUpdateOptions.differentOutputType ? undefined : getMongoDbProjection(info, graphQLType, requiredUpdateOptions); 50 | return await updateCallback(filter, mongoUpdate.update, mongoUpdate.options, projection, source, args, context, info); 51 | }; 52 | } 53 | 54 | export function getGraphQLUpdateArgs(graphQLType: GraphQLObjectType): { [key: string]: { type: GraphQLNonNull } } & { 55 | filter: { type: GraphQLNonNull }, 56 | update: { type: GraphQLNonNull } 57 | } { 58 | return { 59 | filter: { type: new GraphQLNonNull(getGraphQLFilterType(graphQLType)) as GraphQLNonNull }, 60 | update: { type: new GraphQLNonNull(getGraphQLUpdateType(graphQLType)) as GraphQLNonNull } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /tests/specs/graphQLSortType.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { clearTypesCache } from "../../src/common"; 3 | import printInputType from "../utils/printInputType"; 4 | import { ObjectType, InterfaceType } from "../utils/types"; 5 | import { GraphQLInputObjectType, GraphQLString, GraphQLList, printType, GraphQLEnumType, GraphQLObjectType } from "graphql"; 6 | import { getGraphQLSortType, FICTIVE_SORT_DESCRIPTION, FICTIVE_SORT } from "../../src/graphQLSortType"; 7 | 8 | describe("graphQLSortType", () => { 9 | describe("getGraphQLSortType", () => { 10 | beforeEach(clearTypesCache); 11 | 12 | it("Should get a sort type from object", () => { 13 | // Arrange 14 | const sortTypeEnum = new GraphQLEnumType({ 15 | name: 'SortType', 16 | values: { 17 | ASC: { value: 1 }, 18 | DESC: { value: -1 } 19 | } 20 | }); 21 | 22 | const nestedSortType = new GraphQLInputObjectType({ 23 | name: "NestedSortType", 24 | fields: () => ({ 25 | stringScalar: { type: sortTypeEnum }, 26 | intScalar: { type: sortTypeEnum }, 27 | floatScalar: { type: sortTypeEnum }, 28 | enumScalar: { type: sortTypeEnum }, 29 | 30 | nonNullScalar: { type: sortTypeEnum }, 31 | 32 | recursive: { type: nestedSortType }, 33 | 34 | typeSpecificScalar: { type: sortTypeEnum }, 35 | }) 36 | }); 37 | 38 | const nestedInterfaceSortType = new GraphQLInputObjectType({ 39 | name: "NestedInterfaceSortType", 40 | fields: () => ({ 41 | stringScalar: { type: sortTypeEnum }, 42 | intScalar: { type: sortTypeEnum }, 43 | floatScalar: { type: sortTypeEnum }, 44 | enumScalar: { type: sortTypeEnum }, 45 | 46 | nonNullScalar: { type: sortTypeEnum }, 47 | 48 | recursive: { type: nestedSortType }, 49 | }) 50 | }); 51 | 52 | const expectedType = new GraphQLInputObjectType({ 53 | name: "ObjectSortType", 54 | fields: () => ({ 55 | _id: { type: sortTypeEnum }, 56 | 57 | stringScalar: { type: sortTypeEnum }, 58 | intScalar: { type: sortTypeEnum }, 59 | floatScalar: { type: sortTypeEnum }, 60 | enumScalar: { type: sortTypeEnum }, 61 | 62 | nested: { type: nestedSortType }, 63 | nestedInterface: { type: nestedInterfaceSortType }, 64 | 65 | nonNullScalar: { type: sortTypeEnum }, 66 | }) 67 | }) 68 | 69 | // Act 70 | const sortType = getGraphQLSortType(ObjectType); 71 | 72 | // Assert 73 | expect(printType(sortType)).to.eql(printType(expectedType), "Base type should be correct"); 74 | expect(printInputType(sortType)).to.eql(printInputType(expectedType), "Type schema should be correct"); 75 | }) 76 | 77 | it("Should get a sort type from interface", () => { 78 | // Arrange 79 | const sortTypeEnum = new GraphQLEnumType({ 80 | name: 'SortType', 81 | values: { 82 | ASC: { value: 1 }, 83 | DESC: { value: -1 } 84 | } 85 | }); 86 | 87 | const nestedSortType = new GraphQLInputObjectType({ 88 | name: "NestedSortType", 89 | fields: () => ({ 90 | stringScalar: { type: sortTypeEnum }, 91 | intScalar: { type: sortTypeEnum }, 92 | floatScalar: { type: sortTypeEnum }, 93 | enumScalar: { type: sortTypeEnum }, 94 | 95 | nonNullScalar: { type: sortTypeEnum }, 96 | 97 | recursive: { type: nestedSortType }, 98 | 99 | typeSpecificScalar: { type: sortTypeEnum }, 100 | }) 101 | }); 102 | 103 | const nestedInterfaceSortType = new GraphQLInputObjectType({ 104 | name: "NestedInterfaceSortType", 105 | fields: () => ({ 106 | stringScalar: { type: sortTypeEnum }, 107 | intScalar: { type: sortTypeEnum }, 108 | floatScalar: { type: sortTypeEnum }, 109 | enumScalar: { type: sortTypeEnum }, 110 | 111 | nonNullScalar: { type: sortTypeEnum }, 112 | 113 | recursive: { type: nestedSortType }, 114 | }) 115 | }); 116 | 117 | const expectedType = new GraphQLInputObjectType({ 118 | name: "InterfaceSortType", 119 | fields: () => ({ 120 | stringScalar: { type: sortTypeEnum }, 121 | intScalar: { type: sortTypeEnum }, 122 | floatScalar: { type: sortTypeEnum }, 123 | enumScalar: { type: sortTypeEnum }, 124 | 125 | nested: { type: nestedInterfaceSortType }, 126 | 127 | nonNullScalar: { type: sortTypeEnum }, 128 | }) 129 | }) 130 | 131 | // Act 132 | const sortType = getGraphQLSortType(InterfaceType); 133 | 134 | // Assert 135 | expect(printType(sortType)).to.eql(printType(expectedType), "Base type should be correct"); 136 | expect(printInputType(sortType)).to.eql(printInputType(expectedType), "Type schema should be correct"); 137 | }) 138 | 139 | it("Should get fictive sort type", () => { 140 | // Arrange 141 | const type = new GraphQLObjectType({ 142 | name: "SomeType", 143 | fields: { 144 | stringList: { type: new GraphQLList(GraphQLString) } 145 | } 146 | }); 147 | 148 | const sortTypeEnum = new GraphQLEnumType({ 149 | name: 'SortType', 150 | values: { 151 | ASC: { value: 1 }, 152 | DESC: { value: -1 } 153 | } 154 | }); 155 | 156 | const expectedType = new GraphQLInputObjectType({ 157 | name: "SomeSortType", 158 | fields: () => ({ 159 | [FICTIVE_SORT]: { type: sortTypeEnum, description: FICTIVE_SORT_DESCRIPTION }, 160 | }) 161 | }) 162 | 163 | // Act 164 | const sortType = getGraphQLSortType(type); 165 | 166 | // Assert 167 | expect(printType(sortType)).to.eql(printType(expectedType), "Base type should be correct"); 168 | expect(printInputType(sortType)).to.eql(printInputType(expectedType), "Type schema should be correct"); 169 | }); 170 | 171 | it("Should get nested fictive sort type", () => { 172 | // Arrange 173 | const type = new GraphQLObjectType({ 174 | name: "SomeType", 175 | fields: { 176 | nested: { 177 | type: new GraphQLObjectType({ 178 | name: "NestedType", 179 | fields: { 180 | stringList: { type: new GraphQLList(GraphQLString) } 181 | } 182 | }) 183 | } 184 | } 185 | }); 186 | 187 | const sortTypeEnum = new GraphQLEnumType({ 188 | name: 'SortType', 189 | values: { 190 | ASC: { value: 1 }, 191 | DESC: { value: -1 } 192 | } 193 | }); 194 | 195 | const expectedType = new GraphQLInputObjectType({ 196 | name: "SomeSortType", 197 | fields: () => ({ 198 | nested: { 199 | type: new GraphQLInputObjectType({ 200 | name: "NestedSortType", 201 | fields: { 202 | [FICTIVE_SORT]: { type: sortTypeEnum, description: FICTIVE_SORT_DESCRIPTION } 203 | } 204 | }) 205 | } 206 | }) 207 | }) 208 | 209 | // Act 210 | const sortType = getGraphQLSortType(type); 211 | 212 | // Assert 213 | expect(printType(sortType)).to.eql(printType(expectedType), "Base type should be correct"); 214 | expect(printInputType(sortType)).to.eql(printInputType(expectedType), "Type schema should be correct"); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /tests/specs/mongoDbFilter.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { getMongoDbFilter, MongoDbFilter, GraphQLFilter } from "../../src/mongoDbFilter"; 3 | import { ObjectType } from "../utils/types"; 4 | import { setLogger, getLogger } from "../../src/logger"; 5 | 6 | describe("mongoDbFilter", () => { 7 | describe("getMongoDbFilter", () => { 8 | let logger; 9 | before(() => { logger = getLogger(); setLogger({}) }) 10 | after(() => { setLogger(logger) }) 11 | 12 | const tests: { name: string, graphQLFilter: GraphQLFilter, expectedMongoDbFilter: MongoDbFilter }[] = [{ 13 | name: "Should parse deprecated scalar filter", 14 | graphQLFilter: { 15 | stringScalar: { 16 | opr: "$eq", 17 | value: "some-eql", 18 | NEQ: "some-neq" 19 | }, 20 | intScalar: { 21 | opr: "$gt", 22 | value: "some-gt" 23 | }, 24 | floatScalar: { 25 | opr: "$gte", 26 | value: "some-gte" 27 | }, 28 | enumScalar: { 29 | opr: "$in", 30 | values: ["some-in", "another-in"] 31 | }, 32 | stringList: { 33 | opr: "$lt", 34 | value: "some-lt" 35 | }, 36 | intList: { 37 | opr: "$lte", 38 | value: "some-lte" 39 | }, 40 | floatList: { 41 | opr: "$ne", 42 | value: "some-ne" 43 | }, 44 | enumList: { 45 | opr: "$nin", 46 | values: ["some-nin", "another-nin"] 47 | }, 48 | }, 49 | expectedMongoDbFilter: { 50 | stringScalar: { $eq: "some-eql", $ne: "some-neq" }, 51 | intScalar: { $gt: "some-gt" }, 52 | floatScalar: { $gte: "some-gte" }, 53 | enumScalar: { $in: ["some-in", "another-in"] }, 54 | stringList: { $lt: "some-lt" }, 55 | intList: { $lte: "some-lte" }, 56 | floatList: { $ne: "some-ne" }, 57 | enumList: { $nin: ["some-nin", "another-nin"] } 58 | } 59 | }, { 60 | name: "Should parse scalar filter", 61 | graphQLFilter: { 62 | stringScalar: { 63 | EQ: "some-eql" 64 | }, 65 | intScalar: { 66 | GT: "some-gt" 67 | }, 68 | floatScalar: { 69 | GTE: "some-gte" 70 | }, 71 | enumScalar: { 72 | IN: ["some-in", "another-in"] 73 | }, 74 | stringList: { 75 | LT: "some-lt" 76 | }, 77 | intList: { 78 | LTE: "some-lte" 79 | }, 80 | floatList: { 81 | NE: "some-ne" 82 | }, 83 | enumList: { 84 | NIN: ["some-nin", "another-nin"] 85 | }, 86 | }, 87 | expectedMongoDbFilter: { 88 | stringScalar: { $eq: "some-eql" }, 89 | intScalar: { $gt: "some-gt" }, 90 | floatScalar: { $gte: "some-gte" }, 91 | enumScalar: { $in: ["some-in", "another-in"] }, 92 | stringList: { $lt: "some-lt" }, 93 | intList: { $lte: "some-lte" }, 94 | floatList: { $ne: "some-ne" }, 95 | enumList: { $nin: ["some-nin", "another-nin"] } 96 | } 97 | }, { 98 | name: "Should parse scalar not filter", 99 | graphQLFilter: { 100 | stringScalar: { 101 | NOT: { EQ: "some-eql" } 102 | }, 103 | intScalar: { 104 | NOT: { GT: "some-gt" } 105 | }, 106 | floatScalar: { 107 | NOT: { GTE: "some-gte" } 108 | }, 109 | enumScalar: { 110 | NOT: { IN: ["some-in", "another-in"] } 111 | }, 112 | stringList: { 113 | NOT: { LT: "some-lt" } 114 | }, 115 | intList: { 116 | NOT: { LTE: "some-lte" } 117 | }, 118 | floatList: { 119 | NOT: { NE: "some-ne" } 120 | }, 121 | enumList: { 122 | NOT: { NIN: ["some-nin", "another-nin"] } 123 | }, 124 | }, 125 | expectedMongoDbFilter: { 126 | stringScalar: { $not: { $eq: "some-eql" } }, 127 | intScalar: { $not: { $gt: "some-gt" } }, 128 | floatScalar: { $not: { $gte: "some-gte" } }, 129 | enumScalar: { $not: { $in: ["some-in", "another-in"] } }, 130 | stringList: { $not: { $lt: "some-lt" } }, 131 | intList: { $not: { $lte: "some-lte" } }, 132 | floatList: { $not: { $ne: "some-ne" } }, 133 | enumList: { $not: { $nin: ["some-nin", "another-nin"] } } 134 | } 135 | }, { 136 | name: "Should parse regex", 137 | graphQLFilter: { 138 | stringScalar: { 139 | REGEX: "someValue" 140 | }, 141 | stringList: { 142 | REGEX: "someValueWithOptions", 143 | OPTIONS: "im" 144 | }, 145 | nested: { 146 | stringScalar: { 147 | NOT: { REGEX: "someValue" } 148 | }, 149 | stringList: { 150 | NOT: { REGEX: "someValueWithOptions", OPTIONS: "im" } 151 | } 152 | } 153 | }, 154 | expectedMongoDbFilter: { 155 | stringScalar: { $regex: "someValue" }, 156 | stringList: { $regex: "someValueWithOptions", $options: "im" }, 157 | "nested.stringScalar": { $not: /someValue/g }, 158 | "nested.stringList": { $not: /someValueWithOptions/gim }, 159 | } 160 | }, { 161 | name: "Should parse scalar list", 162 | graphQLFilter: { 163 | stringList: { 164 | EQ: "someValue" 165 | }, 166 | nested: { 167 | floatList: { 168 | NE: "someNestedValue" 169 | } 170 | } 171 | }, 172 | expectedMongoDbFilter: { 173 | stringList: { $eq: "someValue" }, 174 | "nested.floatList": { $ne: "someNestedValue" }, 175 | } 176 | }, { 177 | name: "Should parse nested objects", 178 | graphQLFilter: { 179 | stringScalar: { 180 | EQ: "someValue" 181 | }, 182 | nested: { 183 | stringScalar: { 184 | NE: "someNestedValue" 185 | }, 186 | intScalar: { 187 | NE: "anotherNestedValue" 188 | } 189 | } 190 | }, 191 | expectedMongoDbFilter: { 192 | stringScalar: { $eq: "someValue" }, 193 | "nested.stringScalar": { $ne: "someNestedValue" }, 194 | "nested.intScalar": { $ne: "anotherNestedValue" }, 195 | } 196 | }, { 197 | name: "Should parse exists operator in nested objects", 198 | graphQLFilter: { 199 | nested: { 200 | opr: "exists", 201 | stringScalar: { EQ: "someValue" }, 202 | recursive: { 203 | opr: "not_exists", 204 | recursive: { 205 | recursive: { 206 | stringScalar: { EQ: "deep" }, 207 | } 208 | } 209 | } 210 | } 211 | }, 212 | expectedMongoDbFilter: { 213 | nested: { $exists: true }, 214 | "nested.stringScalar": { $eq: "someValue" }, 215 | "nested.recursive": { $exists: false }, 216 | "nested.recursive.recursive.recursive.stringScalar": { $eq: "deep" }, 217 | } 218 | }, { 219 | name: "Should parse OR and AND operators at root", 220 | graphQLFilter: { 221 | OR: [ 222 | { 223 | stringScalar: { 224 | EQ: "someValue" 225 | } 226 | }, { 227 | stringScalar: { 228 | NE: "someOtherValue" 229 | } 230 | }, { 231 | AND: [ 232 | { 233 | stringScalar: { 234 | GT: "thirdValue" 235 | } 236 | }, { 237 | stringScalar: { 238 | LTE: "fourthValue" 239 | } 240 | }] 241 | }], 242 | AND: [ 243 | { 244 | stringScalar: { 245 | EQ: "someValue" 246 | } 247 | }, { 248 | stringScalar: { 249 | NE: "someOtherValue" 250 | } 251 | }, { 252 | OR: [ 253 | { 254 | stringScalar: { 255 | GT: "thirdValue" 256 | } 257 | }, { 258 | stringScalar: { 259 | LTE: "fourthValue" 260 | } 261 | }] 262 | }] 263 | }, 264 | expectedMongoDbFilter: { 265 | $or: [ 266 | { stringScalar: { $eq: "someValue" } }, 267 | { stringScalar: { $ne: "someOtherValue" } }, 268 | { 269 | $and: [ 270 | { stringScalar: { $gt: "thirdValue" } }, 271 | { stringScalar: { $lte: "fourthValue" } }, 272 | ] 273 | } 274 | ], 275 | $and: [ 276 | { stringScalar: { $eq: "someValue" } }, 277 | { stringScalar: { $ne: "someOtherValue" } }, 278 | { 279 | $or: [ 280 | { stringScalar: { $gt: "thirdValue" } }, 281 | { stringScalar: { $lte: "fourthValue" } }, 282 | ] 283 | } 284 | ] 285 | } 286 | }, { 287 | name: "Should parse object list", 288 | graphQLFilter: { 289 | nestedList: { 290 | opr: "exists", 291 | stringScalar: { EQ: "someValue" } 292 | } 293 | }, 294 | expectedMongoDbFilter: { 295 | nestedList: { 296 | $elemMatch: { 297 | $exists: true, 298 | stringScalar: { $eq: "someValue" } 299 | } 300 | } 301 | } 302 | }, { 303 | name: "Should parse everything", 304 | graphQLFilter: { 305 | intScalar: { GT: "some-gt", LTE: "some-lte" }, 306 | intList: { IN: ["some-in", "another-in"] }, 307 | stringScalar: { REGEX: "some-regex" }, 308 | nested: { 309 | floatScalar: { LT: "some-lt" }, 310 | floatList: { NIN: ["some-nin", "another-nin"] }, 311 | stringScalar: { NOT: { REGEX: "some-not-regex" } }, 312 | recursive: { 313 | opr: "exists" 314 | } 315 | }, 316 | nestedList: { 317 | opr: "not_exists", 318 | enumScalar: { 319 | EQ: "some-eq" 320 | }, 321 | recursive: { 322 | stringList: { NOT: { NE: "some-not-ne" } } 323 | } 324 | } 325 | }, 326 | expectedMongoDbFilter: { 327 | intScalar: { $gt: "some-gt", $lte: "some-lte" }, 328 | intList: { $in: ["some-in", "another-in"] }, 329 | stringScalar: { $regex: "some-regex" }, 330 | "nested.floatScalar": { $lt: "some-lt" }, 331 | "nested.floatList": { $nin: ["some-nin", "another-nin"] }, 332 | "nested.stringScalar": { $not: /some-not-regex/g }, 333 | "nested.recursive": { $exists: true }, 334 | nestedList: { 335 | $elemMatch: { 336 | $exists: false, 337 | enumScalar: { $eq: "some-eq" }, 338 | "recursive.stringList": { $not: { $ne: "some-not-ne" } } 339 | } 340 | } 341 | } 342 | }]; 343 | 344 | tests.forEach(test => it(test.name, () => { 345 | // Act 346 | const actualMongoDbFilter = getMongoDbFilter(ObjectType, test.graphQLFilter); 347 | 348 | // Assert 349 | expect(actualMongoDbFilter).to.deep.equal(test.expectedMongoDbFilter, "Actual mongodb filter object does not match expected"); 350 | })); 351 | 352 | it("Should throw exception for a non-solitary REGEX opeartor in a NOT operator", () => { 353 | // Arrange 354 | const graphQLFilter = { 355 | stringScalar: { 356 | NOT: { 357 | REGEX: "SomeRegex", 358 | EQ: "SomeValue" 359 | } 360 | } 361 | } 362 | 363 | // Act 364 | const action = () => getMongoDbFilter(ObjectType, graphQLFilter); 365 | 366 | // Assert 367 | expect(action).to.throw(); 368 | }) 369 | }); 370 | }); 371 | -------------------------------------------------------------------------------- /tests/specs/mongoDbProjection.spec.ts: -------------------------------------------------------------------------------- 1 | import { getMongoDbProjection } from "../../src/mongoDbProjection"; 2 | import { ObjectType } from "../utils/types"; 3 | import fieldResolve from "../utils/fieldResolve"; 4 | import { expect } from "chai"; 5 | import { GraphQLField } from "graphql"; 6 | 7 | describe("mongoDbProjection", () => { 8 | describe("getMongoDbProjection", () => { 9 | const tests: { description: string, query: string, expectedProjection: any, exckudedField?: string[], isResolvedField?: (field: GraphQLField) => boolean }[] = [ 10 | { 11 | description: "Should create projection for a simple request", 12 | query: ` 13 | query { 14 | test { 15 | stringScalar 16 | intScalar 17 | floatScalar 18 | enumScalar 19 | stringList 20 | intList 21 | floatList 22 | enumList 23 | } 24 | }`, 25 | expectedProjection: { 26 | "stringScalar": 1, 27 | "intScalar": 1, 28 | "floatScalar": 1, 29 | "enumScalar": 1, 30 | "stringList": 1, 31 | "intList": 1, 32 | "floatList": 1, 33 | "enumList": 1, 34 | } 35 | }, { 36 | description: "Should create projection for a nested request", 37 | query: ` 38 | query { 39 | test { 40 | nested { 41 | stringScalar 42 | intScalar 43 | floatScalar 44 | enumScalar 45 | stringList 46 | intList 47 | floatList 48 | enumList 49 | } 50 | } 51 | }`, 52 | expectedProjection: { 53 | "nested.stringScalar": 1, 54 | "nested.intScalar": 1, 55 | "nested.floatScalar": 1, 56 | "nested.enumScalar": 1, 57 | "nested.stringList": 1, 58 | "nested.intList": 1, 59 | "nested.floatList": 1, 60 | "nested.enumList": 1, 61 | } 62 | }, { 63 | description: "Should create projection for a request with fragments", 64 | query: ` 65 | query { 66 | test { 67 | ...fieldsA 68 | ...fieldsB 69 | } 70 | } 71 | 72 | fragment fieldsA on Object { 73 | stringScalar 74 | intScalar 75 | floatScalar 76 | enumScalar 77 | } 78 | 79 | fragment fieldsB on Object { 80 | stringScalar 81 | intList 82 | floatList 83 | enumList 84 | nested { 85 | stringScalar 86 | intScalar 87 | floatScalar 88 | enumScalar 89 | } 90 | }`, 91 | expectedProjection: { 92 | "stringScalar": 1, 93 | "intScalar": 1, 94 | "floatScalar": 1, 95 | "enumScalar": 1, 96 | "intList": 1, 97 | "floatList": 1, 98 | "enumList": 1, 99 | "nested.stringScalar": 1, 100 | "nested.intScalar": 1, 101 | "nested.floatScalar": 1, 102 | "nested.enumScalar": 1, 103 | } 104 | }, { 105 | description: "Should create projection for a request with dependent resolvers", 106 | query: ` 107 | query { 108 | test { 109 | resolveSpecificDependencies 110 | } 111 | }`, 112 | expectedProjection: { 113 | "nested.stringScalar": 1, 114 | "nested.intScalar": 1 115 | } 116 | }, { 117 | description: "Should create projection for a request with inline fragments", 118 | query: ` 119 | query { 120 | test { 121 | nestedInterface { 122 | stringScalar 123 | ... on Nested { 124 | typeSpecificScalar 125 | } 126 | } 127 | } 128 | }`, 129 | expectedProjection: { 130 | "nestedInterface.stringScalar": 1, 131 | "nestedInterface.typeSpecificScalar": 1 132 | } 133 | }, { 134 | description: "Should not project a supposedly resolved field", 135 | query: ` 136 | query { 137 | test { 138 | stringScalar 139 | intScalar 140 | } 141 | }`, 142 | expectedProjection: { 143 | "intScalar": 1 144 | }, 145 | isResolvedField: field => !!field.resolve || field.name === "stringScalar" 146 | }, { 147 | description: "Should create projection for a request", 148 | query: ` 149 | query { 150 | test { 151 | stringScalar 152 | intScalar 153 | nested { 154 | floatScalar 155 | enumScalar 156 | } 157 | nestedList { 158 | enumList 159 | recursive { 160 | stringList 161 | recursive { 162 | intList 163 | } 164 | } 165 | } 166 | nestedInterface { 167 | ... on Nested { 168 | typeSpecificScalar 169 | } 170 | } 171 | resolveCommonDependencies 172 | ...fieldsA 173 | } 174 | } 175 | 176 | fragment fieldsA on Object { 177 | nestedList { 178 | floatScalar 179 | enumScalar 180 | } 181 | }`, 182 | expectedProjection: { 183 | "stringScalar": 1, 184 | "intScalar": 1, 185 | "nested": 1, 186 | "nestedList.floatScalar": 1, 187 | "nestedList.enumScalar": 1, 188 | "nestedList.enumList": 1, 189 | "nestedList.recursive.stringList": 1, 190 | "nestedList.recursive.recursive.intList": 1, 191 | "nestedInterface.typeSpecificScalar": 1 192 | } 193 | } 194 | ]; 195 | 196 | tests.forEach(test => it(test.description, () => { 197 | // Arrange 198 | const query = test.query; 199 | const { info } = fieldResolve(ObjectType, query); 200 | 201 | // Act 202 | const projection = getMongoDbProjection(info, ObjectType, { isResolvedField: test.isResolvedField, excludedFields: test.exckudedField || [] }); 203 | 204 | // Assert 205 | expect(projection).to.deep.equal(test.expectedProjection, "should produce a correct projection") 206 | })); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /tests/specs/mongoDbSort.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import getMongoDbSort, { SortArg, MongoDbSort } from "../../src/mongoDbSort"; 3 | 4 | describe("mongoDbSort", () => { 5 | describe("getMongoDbSort", () => { 6 | const tests: { name: string, sortArg: SortArg, expectedMongoDbSort: MongoDbSort }[] = [{ 7 | name: "Should parse scalar sort", 8 | sortArg: { 9 | stringScalar: 1 10 | }, 11 | expectedMongoDbSort: { 12 | stringScalar: 1 13 | } 14 | }, { 15 | name: "Should parse nested sort", 16 | sortArg: { 17 | nested: { 18 | stringScalar: 1 19 | } 20 | }, 21 | expectedMongoDbSort: { 22 | "nested.stringScalar": 1 23 | } 24 | }, { 25 | name: "Should parse sort arg", 26 | sortArg: { 27 | nested: { 28 | stringScalar: 1 29 | }, 30 | stringScalar: -1, 31 | nestedAlso: { 32 | floatScalar: -1 33 | } 34 | }, 35 | expectedMongoDbSort: { 36 | "nested.stringScalar": 1, 37 | "stringScalar": -1, 38 | "nestedAlso.floatScalar": -1 39 | } 40 | }]; 41 | 42 | tests.forEach(test => it(test.name, () => { 43 | // Act 44 | const actualMongoDbSort = getMongoDbSort(test.sortArg); 45 | 46 | // Assert 47 | expect(actualMongoDbSort).to.deep.equal(test.expectedMongoDbSort, "Actual mongodb sort object does not match expected"); 48 | })); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/specs/mongoDbUpdate.spec.ts: -------------------------------------------------------------------------------- 1 | import { getMongoDbUpdate, getMongoDbSet, getOverwrite, getMongoDbInc, SetOverwrite, IncObj, UpdateArgs, updateParams } from "../../src/mongoDbUpdate"; 2 | import { OVERWRITE, FICTIVE_INC } from "../../src/graphQLUpdateType"; 3 | import { expect } from "chai"; 4 | 5 | describe("mongoDbUpdate", () => { 6 | describe("getMongoDbUpdate", () => { 7 | const tests: { description: string, update: UpdateArgs, overwrite?: boolean, expected: updateParams }[] = [{ 8 | description: "Should set upsert true when setOnInsert exists", 9 | update: { 10 | setOnInsert: {} 11 | }, 12 | expected: { 13 | options: { 14 | upsert: true 15 | }, 16 | update: { 17 | $setOnInsert: {} 18 | } 19 | } 20 | }, { 21 | description: "Should not set upsert true when setOnInsert doesn't exist", 22 | update: { 23 | set: {} 24 | }, 25 | expected: { 26 | update: { 27 | $set: {} 28 | } 29 | } 30 | }, { 31 | description: "Should get moongo db update params", 32 | update: { 33 | setOnInsert: { 34 | object: { 35 | scalar: 1 36 | } 37 | }, 38 | set: { 39 | scalar: 1, 40 | list: [2], 41 | nested: { 42 | scalar: 3, 43 | nestedOverwrite: { 44 | scalar: 4, 45 | [OVERWRITE]: true, 46 | } 47 | }, 48 | nestedOverwrite: { 49 | scalar: 5, 50 | [OVERWRITE]: true, 51 | nestedNested: { 52 | scalar: 6, 53 | [OVERWRITE]: false, 54 | } 55 | } 56 | }, 57 | inc: { 58 | intScalar: 2, 59 | [FICTIVE_INC]: 3, 60 | nested: { 61 | intScalar: 4, 62 | [FICTIVE_INC]: 5 63 | }, 64 | } 65 | }, 66 | expected: { 67 | update: { 68 | $setOnInsert: { 69 | object: { 70 | scalar: 1 71 | } 72 | }, 73 | $set: { 74 | scalar: 1, 75 | list: [2], 76 | "nested.scalar": 3, 77 | "nested.nestedOverwrite": { 78 | scalar: 4 79 | }, 80 | nestedOverwrite: { 81 | scalar: 5, 82 | nestedNested: { 83 | scalar: 6 84 | } 85 | } 86 | }, 87 | $inc: { 88 | intScalar: 2, 89 | "nested.intScalar": 4 90 | } 91 | }, 92 | options: { 93 | upsert: true 94 | } 95 | } 96 | }]; 97 | 98 | tests.forEach(test => it(test.description, () => { 99 | // Act 100 | const mongoDbSet = getMongoDbUpdate(test.update, test.overwrite); 101 | 102 | // Assert 103 | expect(mongoDbSet).to.deep.equal(test.expected, "Should return expected update object"); 104 | })); 105 | }); 106 | 107 | describe("getMongoDbSet", () => { 108 | const now = Date(); 109 | 110 | const tests: { description: string, set: object, setOverwrite: SetOverwrite, expected: object }[] = [{ 111 | description: "Should get primitive fields", 112 | set: { 113 | intScalar: 1, 114 | stringScalar: "string", 115 | nullable: null, 116 | date: now, 117 | Boolean: false, 118 | undefined: undefined, 119 | [OVERWRITE]: false 120 | }, 121 | setOverwrite: SetOverwrite.False, 122 | expected: { 123 | intScalar: 1, 124 | stringScalar: "string", 125 | nullable: null, 126 | date: now, 127 | Boolean: false 128 | } 129 | }, { 130 | description: "Should get list fields", 131 | set: { 132 | emptyList: [], 133 | intList: [1], 134 | stringListList: [["asd"], []], 135 | emptyListList: [[], []] 136 | }, 137 | setOverwrite: SetOverwrite.False, 138 | expected: { 139 | emptyList: [], 140 | intList: [1], 141 | stringListList: [["asd"], []], 142 | emptyListList: [[], []] 143 | } 144 | }, { 145 | description: "Should get nested fields", 146 | set: { 147 | nested: { 148 | intScalar: 1, 149 | stringScalar: "string", 150 | nullable: null, 151 | date: now, 152 | Boolean: false, 153 | undefined: undefined, 154 | intList: [1], 155 | stringListList: [["asd"], []] 156 | } 157 | }, 158 | setOverwrite: SetOverwrite.False, 159 | expected: { 160 | "nested.intScalar": 1, 161 | "nested.stringScalar": "string", 162 | "nested.nullable": null, 163 | "nested.date": now, 164 | "nested.Boolean": false, 165 | "nested.intList": [1], 166 | "nested.stringListList": [["asd"], []] 167 | } 168 | }, { 169 | description: "Should get nested list fields", 170 | set: { 171 | nested: [{ 172 | intScalar: 1, 173 | stringScalar: "string", 174 | nullable: null, 175 | date: now, 176 | Boolean: false, 177 | undefined: undefined, 178 | intList: [1], 179 | stringListList: [["asd"], []] 180 | }] 181 | }, 182 | setOverwrite: SetOverwrite.False, 183 | expected: { 184 | nested: [{ 185 | intScalar: 1, 186 | stringScalar: "string", 187 | nullable: null, 188 | date: now, 189 | Boolean: false, 190 | undefined: undefined, 191 | intList: [1], 192 | stringListList: [["asd"], []] 193 | }] 194 | } 195 | }, { 196 | description: "Should get overwrite nested item", 197 | set: { 198 | nested: { 199 | intScalar: 1, 200 | stringScalar: "string", 201 | nullable: null, 202 | date: now, 203 | Boolean: false, 204 | undefined: undefined, 205 | [OVERWRITE]: true, 206 | intList: [1], 207 | stringListList: [["asd"], []] 208 | } 209 | }, 210 | setOverwrite: SetOverwrite.False, 211 | expected: { 212 | nested: { 213 | intScalar: 1, 214 | stringScalar: "string", 215 | nullable: null, 216 | date: now, 217 | Boolean: false, 218 | intList: [1], 219 | stringListList: [["asd"], []] 220 | } 221 | } 222 | }, { 223 | description: "Should get overwrite nested nested item", 224 | set: { 225 | root: { 226 | nested: { 227 | intScalar: 1, 228 | stringScalar: "string", 229 | nullable: null, 230 | date: now, 231 | Boolean: false, 232 | undefined: undefined, 233 | [OVERWRITE]: true, 234 | intList: [1], 235 | stringListList: [["asd"], []] 236 | } 237 | } 238 | }, 239 | setOverwrite: SetOverwrite.False, 240 | expected: { 241 | "root.nested": { 242 | intScalar: 1, 243 | stringScalar: "string", 244 | nullable: null, 245 | date: now, 246 | Boolean: false, 247 | intList: [1], 248 | stringListList: [["asd"], []] 249 | } 250 | } 251 | }, { 252 | description: "Should not overwrite nested when default true and set to false", 253 | set: { 254 | nested: { 255 | intScalar: 1, 256 | stringScalar: "string", 257 | nullable: null, 258 | date: now, 259 | Boolean: false, 260 | undefined: undefined, 261 | [OVERWRITE]: false, 262 | intList: [1], 263 | stringListList: [["asd"], []] 264 | }, 265 | otherNested: { 266 | intScalar: 1, 267 | } 268 | }, 269 | setOverwrite: SetOverwrite.DefaultTrueRoot, 270 | expected: { 271 | "nested.intScalar": 1, 272 | "nested.stringScalar": "string", 273 | "nested.nullable": null, 274 | "nested.date": now, 275 | "nested.Boolean": false, 276 | "nested.intList": [1], 277 | "nested.stringListList": [["asd"], []], 278 | otherNested: { 279 | intScalar: 1, 280 | } 281 | } 282 | }, { 283 | description: "Should overwrite nested nested when default true and set to false", 284 | set: { 285 | nested: { 286 | nestedNested: { 287 | intScalar: 1, 288 | stringScalar: "string", 289 | nullable: null, 290 | date: now, 291 | Boolean: false, 292 | undefined: undefined, 293 | [OVERWRITE]: false, 294 | intList: [1], 295 | stringListList: [["asd"], []] 296 | } 297 | } 298 | }, 299 | setOverwrite: SetOverwrite.DefaultTrueRoot, 300 | expected: { 301 | nested: { 302 | nestedNested: { 303 | intScalar: 1, 304 | stringScalar: "string", 305 | nullable: null, 306 | date: now, 307 | Boolean: false, 308 | intList: [1], 309 | stringListList: [["asd"], []] 310 | } 311 | } 312 | } 313 | }, { 314 | description: "Should get mongo set object", 315 | set: { 316 | scalar: 1, 317 | list: [2], 318 | nested: { 319 | scalar: 3, 320 | nestedOverwrite: { 321 | scalar: 4, 322 | [OVERWRITE]: true, 323 | } 324 | }, 325 | nestedOverwrite: { 326 | scalar: 5, 327 | [OVERWRITE]: true, 328 | nestedNested: { 329 | scalar: 6, 330 | [OVERWRITE]: false, 331 | } 332 | } 333 | }, 334 | setOverwrite: SetOverwrite.False, 335 | expected: { 336 | scalar: 1, 337 | list: [2], 338 | "nested.scalar": 3, 339 | "nested.nestedOverwrite": { 340 | scalar: 4 341 | }, 342 | nestedOverwrite: { 343 | scalar: 5, 344 | nestedNested: { 345 | scalar: 6 346 | } 347 | } 348 | } 349 | }]; 350 | 351 | tests.forEach(test => it(test.description, () => { 352 | // Act 353 | const mongoDbSet = getMongoDbSet(test.set, test.setOverwrite); 354 | 355 | // Assert 356 | expect(mongoDbSet).to.deep.equal(test.expected, "Should return expected set object"); 357 | })); 358 | }); 359 | 360 | describe("getOverwrite", () => { 361 | const tests: { setOverwrite: SetOverwrite, input: boolean | undefined, expectedOverwrite: SetOverwrite }[] = [ 362 | { setOverwrite: SetOverwrite.DefaultTrueRoot, input: undefined, expectedOverwrite: SetOverwrite.True }, 363 | { setOverwrite: SetOverwrite.DefaultTrueRoot, input: false, expectedOverwrite: SetOverwrite.False }, 364 | { setOverwrite: SetOverwrite.DefaultTrueRoot, input: true, expectedOverwrite: SetOverwrite.True }, 365 | { setOverwrite: SetOverwrite.True, input: undefined, expectedOverwrite: SetOverwrite.True }, 366 | { setOverwrite: SetOverwrite.True, input: false, expectedOverwrite: SetOverwrite.True }, 367 | { setOverwrite: SetOverwrite.True, input: true, expectedOverwrite: SetOverwrite.True }, 368 | { setOverwrite: SetOverwrite.False, input: undefined, expectedOverwrite: SetOverwrite.False }, 369 | { setOverwrite: SetOverwrite.False, input: false, expectedOverwrite: SetOverwrite.False }, 370 | { setOverwrite: SetOverwrite.False, input: true, expectedOverwrite: SetOverwrite.True }, 371 | ]; 372 | 373 | const description = (test: { setOverwrite: SetOverwrite, input: boolean | undefined, expectedOverwrite: SetOverwrite }) => 374 | `For SetOverwrite: "${SetOverwrite[test.setOverwrite]}" and input: "${test.input}" should return "${SetOverwrite[test.expectedOverwrite]}"`; 375 | 376 | tests.forEach(test => it(description(test), () => { 377 | // Act 378 | const overwrite = getOverwrite(test.setOverwrite, test.input); 379 | 380 | // Assert 381 | expect(SetOverwrite[overwrite]).to.eql(SetOverwrite[test.expectedOverwrite], "Should return expected overwrite"); 382 | })); 383 | }); 384 | 385 | describe("getMongoDbInc", () => { 386 | const tests: { description: string, incArg: object, expected: IncObj }[] = [{ 387 | description: "Should handle empty object", 388 | incArg: {}, 389 | expected: {} 390 | }, { 391 | description: "Should ignore FICTIVE_INC", 392 | incArg: { [FICTIVE_INC]: "asd" }, 393 | expected: {} 394 | }, { 395 | description: "Should get root fields", 396 | incArg: { intScalar: 2 }, 397 | expected: { intScalar: 2 } 398 | }, { 399 | description: "Should get nested fields", 400 | incArg: { nested: { intScalar: 2 } }, 401 | expected: { "nested.intScalar": 2 } 402 | }, { 403 | description: "Should get mongo inc object", 404 | incArg: { 405 | intScalar: 2, 406 | [FICTIVE_INC]: 3, 407 | nested: { 408 | intScalar: 4, 409 | [FICTIVE_INC]: 5 410 | }, 411 | }, 412 | expected: { intScalar: 2, "nested.intScalar": 4 } 413 | }]; 414 | 415 | tests.forEach(test => it(test.description, () => { 416 | // Act 417 | const incObj = getMongoDbInc(test.incArg); 418 | 419 | // Assert 420 | expect(incObj).to.deep.equal(test.expected, "Should return expected inc object"); 421 | })); 422 | }); 423 | }); 424 | -------------------------------------------------------------------------------- /tests/specs/mongoDbUpdateValidation.spec.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType } from "../utils/types"; 2 | import { validateUpdateArgs, validateNonNullableFields, validateNonNullableFieldsAssert, validateNonNullListField, validateNonNullableFieldsTraverse, flattenListField, ShouldAssert } from "../../src/mongoDbUpdateValidation"; 3 | import { expect } from "chai"; 4 | import { GraphQLObjectType, GraphQLType, GraphQLList, GraphQLNonNull, GraphQLString, GraphQLField } from "graphql"; 5 | import { UpdateArgs } from "../../src/mongoDbUpdate"; 6 | 7 | describe("mongoDbUpdateValidation", () => { 8 | describe("validateUpdateArgs", () => { 9 | const tests: { description: string, type: GraphQLObjectType, updateArgs: UpdateArgs, expectedErrors: string[] }[] = [{ 10 | description: "Should invalidate non-null on upsert", 11 | type: ObjectType, 12 | updateArgs: { 13 | setOnInsert: { 14 | stringScalar: "x", 15 | }, 16 | set: { 17 | nonNullScalar: null 18 | } 19 | }, 20 | expectedErrors: ["Missing non-nullable field \"nonNullList\"", "Non-nullable field \"nonNullScalar\" is set to null"] 21 | }, { 22 | description: "Should ignore non-null on update", 23 | type: ObjectType, 24 | updateArgs: { 25 | set: { 26 | nonNullScalar: null 27 | } 28 | }, 29 | expectedErrors: [] 30 | }, { 31 | description: "Should invalidate non-null on update list item", 32 | type: ObjectType, 33 | updateArgs: { 34 | set: { 35 | nonNullScalar: null, 36 | nestedList: [{ 37 | }] 38 | } 39 | }, 40 | expectedErrors: ["Missing non-nullable field \"nestedList.nonNullList\"", "Missing non-nullable field \"nestedList.nonNullScalar\""] 41 | }, { 42 | description: "Should validate update correct non-null", 43 | type: ObjectType, 44 | updateArgs: { 45 | setOnInsert: { 46 | stringScalar: "x", 47 | }, 48 | set: { 49 | nonNullScalar: "x", 50 | nonNullList: [] 51 | } 52 | }, 53 | expectedErrors: [] 54 | }]; 55 | 56 | tests.forEach(test => it(test.description, () => { 57 | // Arrange 58 | let error; 59 | 60 | // Act 61 | try { 62 | validateUpdateArgs(test.updateArgs, test.type, { overwrite: false }); 63 | } catch (err) { 64 | error = err; 65 | } 66 | 67 | // Assert 68 | if (test.expectedErrors.length > 0) { 69 | expect(error, "error object expected").to.not.be.undefined; 70 | 71 | const errorString: string = typeof error == "string" ? error : (error as Error).message; 72 | const errors = errorString.split("\n"); 73 | expect(errors).to.have.members(test.expectedErrors, "Should detect correct errors"); 74 | } else { 75 | if (error) throw error 76 | } 77 | })); 78 | }); 79 | 80 | describe("validateNonNullableFields", () => { 81 | const tests: { description: string, type: GraphQLObjectType, updateArgs: UpdateArgs, expectedErrors: string[] }[] = [{ 82 | description: "Should invalidate root fields", 83 | type: ObjectType, 84 | updateArgs: { 85 | setOnInsert: { 86 | stringScalar: "x", 87 | }, 88 | set: { 89 | nonNullScalar: null 90 | } 91 | }, 92 | expectedErrors: ["Missing non-nullable field \"nonNullList\"", "Non-nullable field \"nonNullScalar\" is set to null"] 93 | }, { 94 | description: "Should invalidate nested fields", 95 | type: ObjectType, 96 | updateArgs: { 97 | setOnInsert: { 98 | stringScalar: "x", 99 | nested: { 100 | stringScalar: "x", 101 | } 102 | }, 103 | set: { 104 | nonNullScalar: "x", 105 | nonNullList: [], 106 | nested: { 107 | nonNullList: [] 108 | } 109 | } 110 | }, 111 | expectedErrors: ["Missing non-nullable field \"nested.nonNullScalar\""] 112 | }, { 113 | description: "Should invalidate full hierarchy", 114 | type: ObjectType, 115 | updateArgs: { 116 | setOnInsert: { 117 | stringScalar: "x", 118 | nested: { 119 | stringScalar: "x", 120 | }, 121 | listOfNonNulls: ["x", null], 122 | nestedList: [{ 123 | nonNullScalar: "x" 124 | }] 125 | }, 126 | set: { 127 | nonNullList: null, 128 | nested: { 129 | nonNullList: [] 130 | } 131 | } 132 | }, 133 | expectedErrors: [ 134 | "Missing non-nullable field \"nonNullScalar\"", 135 | "Non-nullable field \"nonNullList\" is set to null", 136 | "Non-nullable element of array \"listOfNonNulls\" is set to null", 137 | "Missing non-nullable field \"nested.nonNullScalar\"", 138 | "Missing non-nullable field \"nestedList.nonNullList\"" 139 | ] 140 | }, { 141 | description: "Should validate valid update args", 142 | type: ObjectType, 143 | updateArgs: { 144 | setOnInsert: { 145 | nested: { 146 | nonNullScalar: "x", 147 | }, 148 | nonNullList: [], 149 | listOfNonNulls: ["x"] 150 | }, 151 | set: { 152 | nonNullScalar: "x", 153 | nested: { 154 | nonNullList: [] 155 | } 156 | } 157 | }, 158 | expectedErrors: [] 159 | }]; 160 | 161 | tests.forEach(test => it(test.description, () => { 162 | // Arrange 163 | const objects = Object.keys(test.updateArgs).map(_ => test.updateArgs[_]); 164 | 165 | // Act 166 | const errors = validateNonNullableFields(objects, test.type, ShouldAssert.True); 167 | 168 | // Assert 169 | expect(errors).to.have.members(test.expectedErrors, "Should detect correct errors"); 170 | })); 171 | }); 172 | 173 | describe("validateNonNullableFieldsAssert", () => { 174 | const tests: { description: string, objects: object[], type: GraphQLObjectType, path?: string[], expectedErrors: string[] }[] = [{ 175 | description: "Should invalidate a missing non-nullable field", 176 | type: ObjectType, 177 | objects: [{ stringScalar: "x" }, { nonNullList: [] }], 178 | expectedErrors: ["Missing non-nullable field \"nonNullScalar\""] 179 | }, { 180 | description: "Should invalidate a nulled non-nullable field", 181 | type: ObjectType, 182 | objects: [{ stringScalar: "x", nonNullScalar: null }, { nonNullList: [] }], 183 | expectedErrors: ["Non-nullable field \"nonNullScalar\" is set to null"] 184 | }, { 185 | description: "Should invalidate a null element of a list of non-nullables", 186 | type: ObjectType, 187 | objects: [{ stringScalar: "x", nonNullScalar: 'x' }, { nonNullList: [], listOfNonNulls: [null] }], 188 | expectedErrors: ["Non-nullable element of array \"listOfNonNulls\" is set to null"] 189 | }, { 190 | description: "Should add perfix path to error", 191 | type: ObjectType, 192 | objects: [{ stringScalar: "x" }, { nonNullList: [] }], 193 | path: ["nested"], 194 | expectedErrors: ["Missing non-nullable field \"nested._id\"", "Missing non-nullable field \"nested.nonNullScalar\""] 195 | }, { 196 | description: "Should concat multiple errors", 197 | type: ObjectType, 198 | objects: [{ stringScalar: "x" }, { nonNullList: null }], 199 | expectedErrors: ["Missing non-nullable field \"nonNullScalar\"", "Non-nullable field \"nonNullList\" is set to null"] 200 | }, { 201 | description: "Should validate valid update args, and ignore non-null root _id", 202 | type: ObjectType, 203 | objects: [{ stringScalar: "x", nonNullScalar: 'x' }, { nonNullList: [] }], 204 | expectedErrors: [] 205 | }]; 206 | 207 | tests.forEach(test => it(test.description, () => { 208 | // Act 209 | const errors = validateNonNullableFieldsAssert(test.objects, test.type.getFields(), test.path); 210 | 211 | // Assert 212 | expect(errors).to.have.members(test.expectedErrors, "Should detect correct errors"); 213 | })); 214 | }); 215 | 216 | describe("validateNonNullListField", () => { 217 | const tests: { description: string, values: object[], type: GraphQLType, expectedResult: boolean }[] = [{ 218 | description: "Should invalidate a nulled non-nullable list", 219 | type: new GraphQLNonNull(new GraphQLList(GraphQLString)), 220 | values: [[], null, ["X"]], 221 | expectedResult: false 222 | }, { 223 | description: "Should invalidate a list of non-nullables with a null element", 224 | type: new GraphQLList(new GraphQLNonNull(GraphQLString)), 225 | values: [["x", null], ["X"]], 226 | expectedResult: false 227 | }, { 228 | description: "Should invalidate a list of a list of non-nullables with a null element", 229 | type: new GraphQLList(new GraphQLList(new GraphQLNonNull(GraphQLString))), 230 | values: [[["x", null]], []], 231 | expectedResult: false 232 | }, { 233 | description: "Should validate a non-nullable list", 234 | type: new GraphQLNonNull(new GraphQLList(GraphQLString)), 235 | values: [[null], []], 236 | expectedResult: true 237 | }, { 238 | description: "Should validate a list of non-nullables", 239 | type: new GraphQLList(new GraphQLNonNull(GraphQLString)), 240 | values: [null, ["x"], null], 241 | expectedResult: true 242 | }, { 243 | description: "Should validate a list of a list of non-nullables", 244 | type: new GraphQLList(new GraphQLList(new GraphQLNonNull(GraphQLString))), 245 | values: [null, [null, ["x"]]], 246 | expectedResult: true 247 | }]; 248 | 249 | tests.forEach(test => it(test.description, () => { 250 | // Act 251 | const result = validateNonNullListField(test.values, test.type); 252 | 253 | // Assert 254 | expect(test.expectedResult).to.equal(result); 255 | })); 256 | }); 257 | 258 | describe("validateNonNullableFieldsTraverse", () => { 259 | const tests: { description: string, objects: object[], type: GraphQLObjectType, path?: string[], isResolvedField?: (field: GraphQLField) => boolean, expectedErrors: string[] }[] = [{ 260 | description: 'Should invalidate field in nested object', 261 | type: ObjectType, 262 | objects: [{ nested: { nonNullList: [] } }, { nested: { stringScalar: "x" } }], 263 | expectedErrors: ["Missing non-nullable field \"nested.nonNullScalar\""] 264 | }, { 265 | description: 'Should invalidate field in a nested list', 266 | type: ObjectType, 267 | objects: [{ nestedList: [null, { nonNullList: [] }] }], 268 | expectedErrors: ["Missing non-nullable field \"nestedList.nonNullScalar\""] 269 | }, { 270 | description: 'Should invalidate fields in multiple objects', 271 | type: ObjectType, 272 | objects: [{ nestedList: [{ nonNullList: [] }], nested: { stringScalar: 'x' } }, { nested: { nonNullScalar: null } }], 273 | expectedErrors: [ 274 | "Missing non-nullable field \"nestedList.nonNullScalar\"", 275 | "Non-nullable field \"nested.nonNullScalar\" is set to null", 276 | "Missing non-nullable field \"nested.nonNullList\"" 277 | ] 278 | }, { 279 | description: 'Should validate a valid update', 280 | type: ObjectType, 281 | objects: [{ nested: { nonNullList: [] } }, { nested: { nonNullScalar: "x" } }], 282 | expectedErrors: [] 283 | }, { 284 | description: 'Should validate at traverse end', 285 | type: ObjectType, 286 | objects: [{ stringScalar: 'x' }], 287 | expectedErrors: [] 288 | }]; 289 | 290 | tests.forEach(test => it(test.description, () => { 291 | // act 292 | const errors = validateNonNullableFieldsTraverse(test.objects, test.type.getFields(), ShouldAssert.True, test.isResolvedField, test.path); 293 | 294 | // Assert 295 | expect(errors).to.have.members(test.expectedErrors, "Should detect correct errors"); 296 | })); 297 | }); 298 | 299 | describe("flattenListField", () => { 300 | const tests: { description: string, type: GraphQLType, objects: object[], expectedResult: object[] }[] = [{ 301 | description: "Should flatten list", 302 | type: new GraphQLList(ObjectType), 303 | objects: [[{ stringScalar: "a" }, { stringScalar: "b" }], [{ stringScalar: "c" }]], 304 | expectedResult: [{ stringScalar: "a" }, { stringScalar: "b" }, { stringScalar: "c" }] 305 | }, { 306 | description: "Should flatten nested list", 307 | type: new GraphQLList(new GraphQLList(ObjectType)), 308 | objects: [[[{ stringScalar: "a" }, { stringScalar: "b" }], [{ stringScalar: "c" }]], [[{ stringScalar: "d" }]]], 309 | expectedResult: [{ stringScalar: "a" }, { stringScalar: "b" }, { stringScalar: "c" }, { stringScalar: "d" }] 310 | }, { 311 | description: "Should filter nulls", 312 | type: new GraphQLList(new GraphQLList(ObjectType)), 313 | objects: [[[null, { stringScalar: "a" }, null, { stringScalar: "b" }, null], null, [{ stringScalar: "c" }]], [[{ stringScalar: "d" }], null]], 314 | expectedResult: [{ stringScalar: "a" }, { stringScalar: "b" }, { stringScalar: "c" }, { stringScalar: "d" }] 315 | }]; 316 | 317 | tests.forEach(test => it(test.description, () => { 318 | // Act 319 | const result = flattenListField(test.objects, test.type); 320 | 321 | // Assert 322 | expect(result).to.deep.equal(test.expectedResult, "Should flatten list field correctly"); 323 | })); 324 | }); 325 | }); 326 | -------------------------------------------------------------------------------- /tests/utils/fieldResolve.ts: -------------------------------------------------------------------------------- 1 | import { parse, execute, GraphQLSchema, GraphQLObjectType, GraphQLFieldConfigArgumentMap, GraphQLOutputType, GraphQLResolveInfo, Source } from "graphql" 2 | 3 | export interface ResolveArgs { 4 | args: any 5 | info: GraphQLResolveInfo 6 | }; 7 | 8 | export default (type: GraphQLOutputType, query: string, args?: GraphQLFieldConfigArgumentMap, fieldName: string = "test"): ResolveArgs => { 9 | let resolveArgs: ResolveArgs; 10 | 11 | const schema = new GraphQLSchema({ 12 | query: new GraphQLObjectType({ 13 | name: "RootQuery", 14 | fields: () => ({ 15 | [fieldName]: { 16 | type, 17 | args, 18 | resolve: (obj, args, context, info) => { 19 | resolveArgs = { args, info }; 20 | } 21 | } 22 | }) 23 | }) 24 | }); 25 | 26 | const source = new Source(query, 'GraphQL request'); 27 | const document = parse(source); 28 | 29 | execute(schema, document); 30 | 31 | return resolveArgs; 32 | }; 33 | -------------------------------------------------------------------------------- /tests/utils/printInputType.ts: -------------------------------------------------------------------------------- 1 | import { printSchema, GraphQLInputType, GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; 2 | 3 | const FICTIVE_FIELD = "FICTIVE_FIELD"; 4 | const FICTIVE_QUERY = "FICTIVE_QUERY"; 5 | 6 | function buildFictiveSchema(inputType: GraphQLInputType): GraphQLSchema { 7 | return new GraphQLSchema({ 8 | query: new GraphQLObjectType({ 9 | name: FICTIVE_QUERY, 10 | fields: () => ({ 11 | [FICTIVE_FIELD]: { 12 | type: GraphQLString, 13 | args: { input: { type: inputType } } 14 | } 15 | }) 16 | }) 17 | }) 18 | } 19 | 20 | function trimFictiveParts(schema: string, inputType: GraphQLInputType): string { 21 | return schema 22 | .replace(new RegExp(`schema[\\n\\s]*\\{[\\n\\s]*query:[\\n\\s]*${FICTIVE_QUERY}[\\n\\s]*\\}`), "") 23 | .replace(new RegExp(`type[\\n\\s]*${FICTIVE_QUERY}[\\n\\s]*\\{[\\n\\s]*${FICTIVE_FIELD}\\(input:[\\n\\s]*${inputType.toString()}\\)[\\n\\s]*:[\\n\\s]*String[\\n\\s]*\\}`), "") 24 | } 25 | 26 | export default function printInputType(inputType: GraphQLInputType): string { 27 | return trimFictiveParts( 28 | printSchema( 29 | buildFictiveSchema(inputType), { commentDescriptions: true } 30 | ), 31 | inputType 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /tests/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLInt, GraphQLString, GraphQLList, GraphQLEnumType, GraphQLFloat, GraphQLNonNull, GraphQLInterfaceType, GraphQLID } from "graphql"; 2 | 3 | export const CharactersEnum = new GraphQLEnumType({ 4 | name: "Characters", 5 | values: { 6 | A: { value: "A" }, 7 | B: { value: "B" }, 8 | C: { value: "C" }, 9 | } 10 | }); 11 | 12 | 13 | 14 | export const NestedInterfaceType = new GraphQLInterfaceType({ 15 | name: "NestedInterface", 16 | fields: () => ({ 17 | stringScalar: { type: GraphQLString }, 18 | intScalar: { type: GraphQLInt }, 19 | floatScalar: { type: GraphQLFloat }, 20 | enumScalar: { type: CharactersEnum }, 21 | 22 | stringList: { type: new GraphQLList(GraphQLString) }, 23 | intList: { type: new GraphQLList(GraphQLInt) }, 24 | floatList: { type: new GraphQLList(GraphQLFloat) }, 25 | enumList: { type: new GraphQLList(CharactersEnum) }, 26 | 27 | nonNullScalar: { type: new GraphQLNonNull(GraphQLString) }, 28 | nonNullList: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, 29 | listOfNonNulls: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) }, 30 | 31 | recursive: { type: NestedType }, 32 | 33 | resolveScalar: { 34 | type: GraphQLString, 35 | resolve: obj => obj.stringScalar, 36 | dependencies: ["stringScalar"] 37 | }, 38 | resolveObject: { 39 | type: NestedType, 40 | resolve: obj => ({ stringScalar: obj.stringScalar }), 41 | dependencies: ["stringScalar"] 42 | }, 43 | }) 44 | }); 45 | 46 | export const NestedType = new GraphQLObjectType({ 47 | name: "Nested", 48 | fields: () => ({ 49 | stringScalar: { type: GraphQLString }, 50 | intScalar: { type: GraphQLInt }, 51 | floatScalar: { type: GraphQLFloat }, 52 | enumScalar: { type: CharactersEnum }, 53 | 54 | stringList: { type: new GraphQLList(GraphQLString) }, 55 | intList: { type: new GraphQLList(GraphQLInt) }, 56 | floatList: { type: new GraphQLList(GraphQLFloat) }, 57 | enumList: { type: new GraphQLList(CharactersEnum) }, 58 | 59 | nonNullScalar: { type: new GraphQLNonNull(GraphQLString) }, 60 | nonNullList: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, 61 | listOfNonNulls: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) }, 62 | 63 | recursive: { type: NestedType }, 64 | 65 | resolveScalar: { 66 | type: GraphQLString, 67 | resolve: obj => obj.stringScalar, 68 | dependencies: ["stringScalar"] 69 | }, 70 | resolveObject: { 71 | type: NestedType, 72 | resolve: obj => ({ stringScalar: obj.stringScalar }), 73 | dependencies: ["stringScalar"] 74 | }, 75 | 76 | typeSpecificScalar: { type: GraphQLString }, 77 | }), 78 | interfaces: [NestedInterfaceType] 79 | }); 80 | 81 | export const ObjectType = new GraphQLObjectType({ 82 | name: "Object", 83 | fields: () => ({ 84 | _id: { type: new GraphQLNonNull(GraphQLID) }, 85 | 86 | stringScalar: { type: GraphQLString }, 87 | intScalar: { type: GraphQLInt }, 88 | floatScalar: { type: GraphQLFloat }, 89 | enumScalar: { type: CharactersEnum }, 90 | 91 | stringList: { type: new GraphQLList(GraphQLString) }, 92 | intList: { type: new GraphQLList(GraphQLInt) }, 93 | floatList: { type: new GraphQLList(GraphQLFloat) }, 94 | enumList: { type: new GraphQLList(CharactersEnum) }, 95 | 96 | nested: { type: NestedType }, 97 | nestedList: { type: new GraphQLList(NestedType) }, 98 | nestedInterface: { type: NestedInterfaceType }, 99 | 100 | nonNullScalar: { type: new GraphQLNonNull(GraphQLString) }, 101 | nonNullList: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, 102 | listOfNonNulls: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) }, 103 | 104 | resolveSpecificDependencies: { 105 | type: GraphQLString, 106 | resolve: obj => `${obj.nested.stringScalar} ${obj.nested.intScalar}`, 107 | dependencies: ["nested.stringScalar", "nested.intScalar"] 108 | }, 109 | resolveCommonDependencies: { 110 | type: GraphQLString, 111 | resolve: obj => `${obj.nested.stringScalar} ${obj.nested.intScalar}`, 112 | dependencies: ["nested"] 113 | }, 114 | resolveObject: { 115 | type: NestedType, 116 | resolve: obj => ({ stringScalar: obj.stringScalar }), 117 | dependencies: ["stringScalar"] 118 | }, 119 | }) 120 | }); 121 | 122 | export const InterfaceType = new GraphQLInterfaceType({ 123 | name: "Interface", 124 | fields: () => ({ 125 | stringScalar: { type: GraphQLString }, 126 | intScalar: { type: GraphQLInt }, 127 | floatScalar: { type: GraphQLFloat }, 128 | enumScalar: { type: CharactersEnum }, 129 | 130 | stringList: { type: new GraphQLList(GraphQLString) }, 131 | intList: { type: new GraphQLList(GraphQLInt) }, 132 | floatList: { type: new GraphQLList(GraphQLFloat) }, 133 | enumList: { type: new GraphQLList(CharactersEnum) }, 134 | 135 | nested: { type: NestedInterfaceType }, 136 | nestedList: { type: new GraphQLList(NestedInterfaceType) }, 137 | 138 | nonNullScalar: { type: new GraphQLNonNull(GraphQLString) }, 139 | nonNullList: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, 140 | listOfNonNulls: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) }, 141 | 142 | resolveSpecificDependencies: { 143 | type: GraphQLString, 144 | resolve: obj => `${obj.nested.stringScalar} ${obj.nested.intScalar}`, 145 | dependencies: ["nested.stringScalar", "nested.intScalar"] 146 | }, 147 | resolveCommonDependencies: { 148 | type: GraphQLString, 149 | resolve: obj => `${obj.nested.stringScalar} ${obj.nested.intScalar}`, 150 | dependencies: ["nested"] 151 | }, 152 | resolveObject: { 153 | type: NestedInterfaceType, 154 | resolve: obj => ({ stringScalar: obj.stringScalar }), 155 | dependencies: ["stringScalar"] 156 | }, 157 | }) 158 | }); 159 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "target": "es5", 5 | "module": "commonjs", 6 | "lib": [ 7 | "es7", 8 | "dom", 9 | "esnext.asynciterable" 10 | ], 11 | "allowSyntheticDefaultImports": true, 12 | "declaration": true, 13 | "moduleResolution": "node", 14 | "noEmitOnError": true 15 | }, 16 | "exclude": [ 17 | "tests", 18 | "lib", 19 | "examples" 20 | ], 21 | "compileOnSave": true 22 | } 23 | --------------------------------------------------------------------------------