├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .markdownlint.json ├── .npmignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs └── releases │ └── 9.0.0.md ├── jest.config.js ├── package.json ├── src ├── __mocks__ │ ├── contactsSchema.ts │ ├── enumEmployment.ts │ ├── languageSchema.ts │ ├── mongooseCommon.ts │ ├── postModel.ts │ └── userModel.ts ├── __tests__ │ ├── __snapshots__ │ │ └── integration-test.ts.snap │ ├── composeWithMongoose-test.ts │ ├── composeWithMongooseDiscriminators-test.ts │ ├── fieldConverter-test.ts │ ├── github_issues │ │ ├── 117-test.ts │ │ ├── 120-test.ts │ │ ├── 128-test.ts │ │ ├── 135-test.ts │ │ ├── 136-test.ts │ │ ├── 141-test.ts │ │ ├── 157-test.ts │ │ ├── 181-test.ts │ │ ├── 184-test.ts │ │ ├── 194-test.ts │ │ ├── 219-test.ts │ │ ├── 248-test.ts │ │ ├── 250-test.ts │ │ ├── 253-test.ts │ │ ├── 260-test.ts │ │ ├── 261-test.ts │ │ ├── 263-test.ts │ │ ├── 268-test.ts │ │ ├── 271-test.ts │ │ ├── 286-test.ts │ │ ├── 289-test.ts │ │ ├── 304-test.ts │ │ ├── 315-test.ts │ │ ├── 358-test.ts │ │ ├── 370-test.ts │ │ ├── 376-test.ts │ │ ├── 377-test.ts │ │ ├── 384-test.ts │ │ ├── 78-test.ts │ │ ├── 92-test.ts │ │ ├── 93-test.ts │ │ └── gc282-test.ts │ ├── integration-discriminators-test.ts │ └── integration-test.ts ├── composeMongoose.ts ├── composeWithMongoose.ts ├── composeWithMongooseDiscriminators.ts ├── discriminators │ ├── DiscriminatorTypeComposer.ts │ ├── __mocks__ │ │ ├── characterModels.ts │ │ ├── droidSchema.ts │ │ ├── movieModel.ts │ │ └── personSchema.ts │ ├── __tests__ │ │ ├── DiscriminatorTypeComposer-test.ts │ │ ├── composeChildTC-test.ts │ │ ├── prepareBaseResolvers-test.ts │ │ └── prepareChildResolvers-test.ts │ ├── composeChildTC.ts │ ├── index.ts │ ├── prepareBaseResolvers.ts │ ├── prepareChildResolvers.ts │ └── utils │ │ ├── __tests__ │ │ ├── mergeCustomizationOptions-test.ts │ │ └── mergeTypeConverterResolverOpts-test.ts │ │ ├── mergeCustomizationOptions.ts │ │ ├── mergeTypeConverterResolversOpts.ts │ │ └── reorderFields.ts ├── errors │ ├── MongoError.ts │ ├── RuntimeError.ts │ ├── ValidationError.ts │ └── index.ts ├── fieldsConverter.ts ├── index.ts ├── resolvers │ ├── __tests__ │ │ ├── connection-test.ts │ │ ├── count-test.ts │ │ ├── createMany-test.ts │ │ ├── createOne-test.ts │ │ ├── dataLoader-test.ts │ │ ├── dataLoaderMany-test.ts │ │ ├── findById-test.ts │ │ ├── findByIds-test.ts │ │ ├── findMany-test.ts │ │ ├── findOne-test.ts │ │ ├── pagination-test.ts │ │ ├── removeById-test.ts │ │ ├── removeMany-test.ts │ │ ├── removeOne-test.ts │ │ ├── updateById-test.ts │ │ ├── updateMany-test.ts │ │ └── updateOne-test.ts │ ├── connection.ts │ ├── count.ts │ ├── createMany.ts │ ├── createOne.ts │ ├── dataLoader.ts │ ├── dataLoaderMany.ts │ ├── findById.ts │ ├── findByIds.ts │ ├── findMany.ts │ ├── findOne.ts │ ├── helpers │ │ ├── __tests__ │ │ │ ├── aliases-test.ts │ │ │ ├── beforeQueryHelper-test.ts │ │ │ ├── filter-test.ts │ │ │ ├── filterOperators-test.ts │ │ │ ├── limit-test.ts │ │ │ ├── projection-test.ts │ │ │ ├── record-test.ts │ │ │ ├── skip-test.ts │ │ │ └── sort-test.ts │ │ ├── aliases.ts │ │ ├── beforeQueryHelper.ts │ │ ├── dataLoaderHelper.ts │ │ ├── errorCatcher.ts │ │ ├── filter.ts │ │ ├── filterOperators.ts │ │ ├── index.ts │ │ ├── limit.ts │ │ ├── payloadRecordId.ts │ │ ├── projection.ts │ │ ├── record.ts │ │ ├── skip.ts │ │ ├── sort.ts │ │ └── validate.ts │ ├── index.ts │ ├── pagination.ts │ ├── removeById.ts │ ├── removeMany.ts │ ├── removeOne.ts │ ├── updateById.ts │ ├── updateMany.ts │ └── updateOne.ts ├── types │ ├── BSONDecimal.ts │ ├── MongoID.ts │ ├── RegExpAsString.ts │ └── __tests__ │ │ ├── BSONDecimal-test.ts │ │ ├── MongoID-test.ts │ │ └── RegExpAsString-test.ts └── utils │ ├── __tests__ │ ├── getIndexesFromModel-test.ts │ └── toMongoDottedObject-test.ts │ ├── getIndexesFromModel.ts │ ├── index.ts │ ├── makeFieldsRecursiveNullable.ts │ ├── testHelpers.ts │ └── toMongoDottedObject.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | max_line_length = 120 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [{Makefile, makefile}] 13 | indent_style = tab 14 | 15 | [*.go] 16 | indent_style = tab 17 | 18 | [*.proto] 19 | indent_size = 2 20 | 21 | [*.swift] 22 | indent_size = 4 23 | 24 | [*.tmpl] 25 | indent_size = 2 26 | 27 | [{*.js, *.ts, *.jsx, *.tsx}] 28 | indent_size = 2 29 | 30 | [*.html] 31 | indent_size = 2 32 | 33 | [*.bat] 34 | end_of_line = crlf 35 | 36 | [*.{json, yml, yaml}] 37 | indent_size = 2 38 | 39 | [.{babelrc, eslintrc}] 40 | indent_size = 2 41 | 42 | [{Fastfile, .buckconfig, BUCK}] 43 | indent_size = 2 44 | 45 | [*.diff] 46 | indent_size = 1 47 | 48 | [*.m] 49 | indent_size = 1 50 | indent_style = space 51 | 52 | [*.java] 53 | indent_size = 4 54 | indent_style = space 55 | 56 | [*.md] 57 | trim_trailing_whitespace = false 58 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | es/* 3 | mjs/* 4 | node8/* 5 | jest.config.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint', 'prettier'], 6 | extends: ['plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended'], 7 | parserOptions: { 8 | sourceType: 'module', 9 | useJSXTextNode: true, 10 | project: [path.resolve(__dirname, 'tsconfig.json')], 11 | }, 12 | rules: { 13 | 'no-underscore-dangle': 0, 14 | 'arrow-body-style': 0, 15 | 'no-unused-expressions': 0, 16 | 'no-plusplus': 0, 17 | 'no-console': 0, 18 | 'func-names': 0, 19 | 'comma-dangle': [ 20 | 'error', 21 | { 22 | arrays: 'always-multiline', 23 | objects: 'always-multiline', 24 | imports: 'always-multiline', 25 | exports: 'always-multiline', 26 | functions: 'ignore', 27 | }, 28 | ], 29 | 'no-prototype-builtins': 0, 30 | 'prefer-destructuring': 0, 31 | 'no-else-return': 0, 32 | 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 33 | '@typescript-eslint/explicit-member-accessibility': 0, 34 | '@typescript-eslint/no-explicit-any': 0, 35 | '@typescript-eslint/no-inferrable-types': 0, 36 | '@typescript-eslint/explicit-function-return-type': 0, 37 | '@typescript-eslint/no-use-before-define': 0, 38 | '@typescript-eslint/no-empty-function': 0, 39 | '@typescript-eslint/camelcase': 0, 40 | '@typescript-eslint/ban-ts-comment': 0, 41 | }, 42 | env: { 43 | jasmine: true, 44 | jest: true, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [nodkz] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: graphql-compose 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | packages: write 13 | contents: write 14 | strategy: 15 | matrix: 16 | node-version: [16.x, 18.x, 20.x] 17 | steps: 18 | - run: echo "🎉 The job was triggered by a ${{ github.event_name }} event." 19 | - uses: styfle/cancel-workflow-action@0.12.0 20 | with: 21 | workflow_id: nodejs.yml 22 | access_token: ${{ secrets.GITHUB_TOKEN }} 23 | - uses: FranzDiebold/github-env-vars-action@v2 24 | - uses: actions/checkout@v4 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - name: Install node_modules 30 | run: yarn 31 | - name: Test & lint 32 | run: yarn test 33 | env: 34 | CI: true 35 | - name: Testing with previous mongoose versions 7 36 | run: yarn test-prev-vers-7 37 | env: 38 | CI: true 39 | - name: Testing with previous mongoose versions 6 40 | run: yarn test-prev-vers-6 41 | env: 42 | CI: true 43 | - name: Send codecov.io stats 44 | if: matrix.node-version == '18.x' 45 | run: bash <(curl -s https://codecov.io/bash) || echo '' 46 | 47 | publish: 48 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta' 49 | needs: [tests] 50 | runs-on: ubuntu-latest 51 | permissions: 52 | packages: write 53 | contents: write 54 | pull-requests: write 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Use Node.js 18 58 | uses: actions/setup-node@v4 59 | with: 60 | node-version: 18.x 61 | - name: Install node_modules 62 | run: yarn install 63 | - name: Build 64 | run: yarn build 65 | - name: Semantic Release (publish to npm) 66 | run: npx semantic-release@19 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 70 | 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | 15 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 16 | .grunt 17 | 18 | # node-waf configuration 19 | .lock-wscript 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # IntelliJ Files 25 | *.iml 26 | *.ipr 27 | *.iws 28 | /out/ 29 | .idea/ 30 | .idea_modules/ 31 | 32 | # Dependency directory 33 | node_modules 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Transpiled code 42 | /es 43 | /lib 44 | /node8 45 | /mjs 46 | 47 | coverage 48 | .nyc_output 49 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-trailing-punctuation": { 4 | "punctuation": ",;" 5 | }, 6 | "no-inline-html": false, 7 | "ol-prefix": false, 8 | "first-line-h1": false, 9 | "first-heading-h1": false, 10 | "no-bare-urls": false, 11 | "blanks-around-lists": false 12 | } 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "printWidth": 100, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Jest", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand", "--watch"], 13 | "cwd": "${workspaceFolder}", 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen", 16 | "disableOptimisticBPs": true 17 | }, 18 | { 19 | "name": "Jest Current File", 20 | "type": "node", 21 | "request": "launch", 22 | "program": "${workspaceFolder}/node_modules/.bin/jest", 23 | "args": [ 24 | "${fileBasenameNoExtension}", 25 | "--config", 26 | "jest.config.js" 27 | ], 28 | "console": "integratedTerminal", 29 | "internalConsoleOptions": "neverOpen", 30 | "disableOptimisticBPs": true 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "editor.formatOnSave": false, 5 | "javascript.format.enable": false, 6 | "typescript.format.enable": false, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.0-semantically-released 2 | 3 | This package publishing automated by [semantic-release](https://github.com/semantic-release/semantic-release). Changelog is generated automatically and can be found here: https://github.com/graphql-compose/graphql-compose-mongoose/releases 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present Pavel Chertorogov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | moduleFileExtensions: ['ts', 'js'], 5 | transform: { 6 | '^.+\\.ts$': [ 7 | 'ts-jest', 8 | { 9 | tsconfig: '/tsconfig.json', 10 | isolatedModules: true, 11 | diagnostics: false, 12 | }, 13 | ], 14 | '^.+\\.js$': 'babel-jest', 15 | }, 16 | roots: ['/src'], 17 | testPathIgnorePatterns: ['/node_modules/', '/lib/'], 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-compose-mongoose", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Plugin for `graphql-compose` which derive a graphql types from a mongoose model.", 5 | "license": "MIT", 6 | "files": [ 7 | "lib" 8 | ], 9 | "main": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/graphql-compose/graphql-compose-mongoose.git" 14 | }, 15 | "keywords": [ 16 | "graphql", 17 | "compose", 18 | "graphql-compose", 19 | "mongoose", 20 | "mongodb" 21 | ], 22 | "bugs": { 23 | "url": "https://github.com/graphql-compose/graphql-compose-mongoose/issues" 24 | }, 25 | "homepage": "https://github.com/graphql-compose/graphql-compose-mongoose", 26 | "dependencies": { 27 | "dataloader": "^2.2.2", 28 | "graphql-compose-connection": "8.2.1", 29 | "graphql-compose-pagination": "8.3.0" 30 | }, 31 | "peerDependencies": { 32 | "graphql-compose": "^7.21.4 || ^8.0.0 || ^9.0.0", 33 | "mongoose": "^8.0.0 || ^7.0.0 || ^6.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "29.5.8", 37 | "@typescript-eslint/eslint-plugin": "6.11.0", 38 | "@typescript-eslint/parser": "6.11.0", 39 | "eslint": "8.53.0", 40 | "eslint-config-airbnb-base": "15.0.0", 41 | "eslint-config-prettier": "9.0.0", 42 | "eslint-plugin-import": "2.29.0", 43 | "eslint-plugin-prettier": "5.0.1", 44 | "graphql": "16.8.1", 45 | "graphql-compose": "9.0.10", 46 | "jest": "29.7.0", 47 | "mongodb-memory-server": "9.0.1", 48 | "mongoose": "8.0.0", 49 | "prettier": "3.1.0", 50 | "request": "2.88.2", 51 | "rimraf": "5.0.5", 52 | "ts-jest": "29.1.1", 53 | "typescript": "5.2.2" 54 | }, 55 | "scripts": { 56 | "prepare": "tsc -p ./tsconfig.build.json", 57 | "build": "rimraf lib && tsc -p ./tsconfig.build.json", 58 | "watch": "jest --watch", 59 | "coverage": "jest --coverage --maxWorkers 4", 60 | "lint": "yarn eslint && yarn tscheck", 61 | "eslint": "eslint --ext .ts ./src", 62 | "tscheck": "tsc --noEmit", 63 | "test": "yarn coverage && yarn lint", 64 | "link": "yarn build && yarn link graphql-compose && yarn link graphql-compose-connection && yarn link graphql-compose-pagination && yarn link mongoose && yarn link", 65 | "unlink": "rimraf node_modules && yarn install", 66 | "semantic-release": "semantic-release", 67 | "test-prev-vers-7": "yarn add mongoose@7.6.4 --dev --ignore-scripts && yarn coverage && git checkout HEAD -- package.json yarn.lock", 68 | "test-prev-vers-6": "yarn add mongoose@6.1.2 --dev --ignore-scripts && yarn coverage && git checkout HEAD -- package.json yarn.lock" 69 | }, 70 | "engines": { 71 | "node": ">=16.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/__mocks__/contactsSchema.ts: -------------------------------------------------------------------------------- 1 | import type { Schema as SchemaType } from 'mongoose'; 2 | import { Schema } from './mongooseCommon'; 3 | 4 | const ContactsSchema: SchemaType = new Schema({ 5 | phones: [String], 6 | email: { 7 | type: String, 8 | required: true, 9 | }, 10 | skype: String, 11 | locationId: Schema.Types.ObjectId, 12 | }); 13 | 14 | export default ContactsSchema; 15 | -------------------------------------------------------------------------------- /src/__mocks__/enumEmployment.ts: -------------------------------------------------------------------------------- 1 | const enumEmployment = { 2 | full: { description: 'Full time' }, 3 | partial: { description: 'Partial time' }, 4 | remote: { description: 'Remote work' }, 5 | }; 6 | 7 | export default enumEmployment; 8 | -------------------------------------------------------------------------------- /src/__mocks__/languageSchema.ts: -------------------------------------------------------------------------------- 1 | import type { Schema as SchemaType } from 'mongoose'; 2 | import { schemaComposer } from 'graphql-compose'; 3 | import { Schema } from './mongooseCommon'; 4 | import { convertSchemaToGraphQL } from '../fieldsConverter'; 5 | 6 | // name: 'EnumLanguageName', 7 | // description: 'Language names (https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)', 8 | const enumLanguageName = { 9 | en: { description: 'English' }, 10 | ru: { description: 'Russian' }, 11 | zh: { description: 'Chinese' }, 12 | }; 13 | 14 | const enumLanguageSkill = { 15 | basic: { description: 'can read' }, 16 | fluent: { description: 'can talk' }, 17 | native: { description: 'since birth' }, 18 | }; 19 | 20 | const LanguageSchema: SchemaType = new Schema({ 21 | ln: { 22 | type: String, 23 | description: 'Language names (https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)', 24 | enum: Object.keys(enumLanguageName), 25 | }, 26 | sk: { 27 | type: String, 28 | description: 'Language skills', 29 | enum: Object.keys(enumLanguageSkill), 30 | }, 31 | }); 32 | 33 | export default LanguageSchema; 34 | 35 | // Such way we can set Type name for Schema which is used in another schema. 36 | // Otherwise by default it will have name `${ParentSchemaName}${ParentSchemaFieldName}` 37 | convertSchemaToGraphQL(LanguageSchema, 'Language', schemaComposer); 38 | -------------------------------------------------------------------------------- /src/__mocks__/mongooseCommon.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, no-console */ 2 | 3 | import mongoose from 'mongoose'; 4 | import MongoMemoryServer from 'mongodb-memory-server-core'; 5 | import net, { AddressInfo } from 'net'; 6 | 7 | const { Schema, Types } = mongoose; 8 | 9 | mongoose.Promise = Promise; 10 | 11 | export async function getPortFree() { 12 | return new Promise((res) => { 13 | const srv = net.createServer(); 14 | srv.listen(0, () => { 15 | const port = (srv.address() as AddressInfo).port; 16 | srv.close(() => res(port)); 17 | }); 18 | }); 19 | } 20 | 21 | const originalConnect = mongoose.connect; 22 | mongoose.createConnection = (async () => { 23 | const mongoServer = await MongoMemoryServer.create({ 24 | instance: { 25 | port: await getPortFree(), 26 | }, 27 | }); 28 | const mongoUri = mongoServer.getUri(); 29 | 30 | originalConnect.bind(mongoose)(mongoUri, {}); 31 | 32 | mongoose.connection.on('error', (e) => { 33 | if (e.message.code === 'ETIMEDOUT') { 34 | console.error(e); 35 | } else { 36 | throw e; 37 | } 38 | }); 39 | 40 | mongoose.connection.once('open', () => { 41 | // console.log(`MongoDB successfully connected to ${mongoUri}`); 42 | }); 43 | 44 | mongoose.connection.once('disconnected', () => { 45 | // console.log('MongoDB disconnected!'); 46 | mongoServer.stop(); 47 | }); 48 | }) as any; 49 | 50 | mongoose.connect = mongoose.createConnection as any; 51 | 52 | export { mongoose, Schema, Types }; 53 | -------------------------------------------------------------------------------- /src/__mocks__/postModel.ts: -------------------------------------------------------------------------------- 1 | import type { Schema as SchemaType, Document } from 'mongoose'; 2 | import { mongoose, Schema } from './mongooseCommon'; 3 | 4 | const PostSchema: SchemaType = new Schema({ 5 | _id: { 6 | type: Number, 7 | }, 8 | title: { 9 | type: String, 10 | description: 'Post title', 11 | }, 12 | 13 | // createdAt, created via option `timastamp: true` (see bottom) 14 | // updatedAt, created via option `timastamp: true` (see bottom) 15 | }); 16 | 17 | export interface IPost extends Document { 18 | title: string; 19 | } 20 | 21 | const PostModel = mongoose.model('Post', PostSchema); 22 | 23 | export { PostSchema, PostModel }; 24 | -------------------------------------------------------------------------------- /src/__mocks__/userModel.ts: -------------------------------------------------------------------------------- 1 | import { mongoose, Schema } from './mongooseCommon'; 2 | import ContactsSchema from './contactsSchema'; 3 | import enumEmployment from './enumEmployment'; 4 | import LanguageSchema from './languageSchema'; 5 | import { Document } from 'mongoose'; 6 | 7 | const UserSchema = new Schema( 8 | { 9 | subDoc: { 10 | field1: String, 11 | field2: { 12 | field21: String, 13 | field22: String, 14 | }, 15 | }, 16 | 17 | user: { 18 | type: Schema.Types.ObjectId, 19 | ref: 'User', 20 | }, 21 | 22 | users: { 23 | type: [Schema.Types.ObjectId], 24 | ref: 'User', 25 | }, 26 | 27 | n: { 28 | type: String, 29 | required: true, 30 | description: 'Person name', 31 | alias: 'name', 32 | }, 33 | 34 | age: { 35 | type: Number, 36 | description: 'Full years', 37 | required() { 38 | // in graphql this field should be Nullable 39 | return (this as any).name === 'Something special'; 40 | }, 41 | }, 42 | 43 | gender: { 44 | type: String, 45 | enum: ['male', 'female', 'ladyboy'], 46 | }, 47 | 48 | skills: { 49 | type: [String], 50 | default: [], 51 | description: 'List of skills', 52 | }, 53 | 54 | employment: { 55 | type: [ 56 | { 57 | type: String, 58 | enum: Object.keys(enumEmployment), 59 | }, 60 | ], 61 | description: 'List of desired employment types', 62 | index: true, 63 | }, 64 | 65 | relocation: { 66 | type: Boolean, 67 | description: 'Does candidate relocate to another region', 68 | }, 69 | 70 | contacts: { 71 | type: ContactsSchema, 72 | default: {}, 73 | description: 'Contacts of person (phone, skype, mail and etc)', 74 | }, 75 | 76 | languages: { 77 | type: [LanguageSchema], 78 | default: [], 79 | description: 'Knowledge of languages', 80 | }, 81 | 82 | __secretField: { 83 | type: String, 84 | }, 85 | 86 | someDynamic: { 87 | type: Schema.Types.Mixed, 88 | description: "Some mixed value, that served with @taion's `graphql-type-json`", 89 | }, 90 | 91 | periods: [{ from: Number, to: Number }], 92 | 93 | someDeep: { 94 | periods: [{ from: Number, to: Number }], 95 | }, 96 | 97 | salary: { 98 | type: Schema.Types.Decimal128, 99 | }, 100 | 101 | mapField: { 102 | type: Map, 103 | of: String, 104 | }, 105 | 106 | mapFieldDeep: { 107 | subField: { 108 | type: Map, 109 | of: String, 110 | }, 111 | }, 112 | 113 | billingAddress: { 114 | street: { type: String, index: true }, 115 | state: { type: String, index: true, enum: ['FL', 'MA', 'NY'] }, 116 | country: { type: String, index: true }, 117 | }, 118 | 119 | // for error payloads tests 120 | valid: { 121 | type: String, 122 | required: false, 123 | validate: [ 124 | () => { 125 | return false; 126 | }, 127 | 'this is a validate message', 128 | ], 129 | }, 130 | 131 | // createdAt, created via option `timestamp: true` (see bottom) 132 | // updatedAt, created via option `timestamp: true` (see bottom) 133 | }, 134 | { 135 | timestamps: true, // add createdAt, updatedAt fields 136 | toJSON: { getters: true }, 137 | toObject: { virtuals: true }, 138 | } 139 | ); 140 | 141 | UserSchema.set('autoIndex', false); 142 | UserSchema.index({ n: 1, age: -1 }); 143 | 144 | // eslint-disable-next-line 145 | UserSchema.virtual('nameVirtual').get(function (this: any) { 146 | return `VirtualFieldValue${this._id}`; 147 | }); 148 | 149 | export interface IUser extends Document { 150 | _id: any; 151 | name?: string; 152 | age?: number; 153 | gender?: string; 154 | someDynamic?: any; 155 | skills?: string[]; 156 | relocation?: boolean; 157 | contacts: { 158 | email: string; 159 | skype?: string; 160 | }; 161 | } 162 | 163 | const UserModel = mongoose.model('User', UserSchema); 164 | 165 | export { UserSchema, UserModel }; 166 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/integration-test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`integration tests projection should request all fields to rawData field: projection from all fields 1`] = ` 4 | [ 5 | "__v", 6 | "_id", 7 | "age", 8 | "contacts", 9 | "createdAt", 10 | "employment", 11 | "gender", 12 | "id", 13 | "languages", 14 | "n", 15 | "name", 16 | "nameVirtual", 17 | "periods", 18 | "relocation", 19 | "skills", 20 | "someDeep", 21 | "updatedAt", 22 | "users", 23 | ] 24 | `; 25 | 26 | exports[`integration tests projection should request only fields from query: projection from query fields 1`] = ` 27 | { 28 | "data": { 29 | "user": { 30 | "name": "Name", 31 | }, 32 | }, 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /src/__tests__/composeWithMongooseDiscriminators-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, ObjectTypeComposer } from 'graphql-compose'; 2 | import { getCharacterModels } from '../discriminators/__mocks__/characterModels'; 3 | import { MovieModel } from '../discriminators/__mocks__/movieModel'; 4 | import { composeWithMongooseDiscriminators } from '../composeWithMongooseDiscriminators'; 5 | import { DiscriminatorTypeComposer } from '../discriminators'; 6 | 7 | beforeAll(() => MovieModel.base.createConnection()); 8 | afterAll(() => MovieModel.base.disconnect()); 9 | 10 | const { CharacterModel, PersonModel } = getCharacterModels('type'); 11 | 12 | describe('composeWithMongooseDiscriminators ->', () => { 13 | beforeEach(() => { 14 | schemaComposer.clear(); 15 | }); 16 | 17 | describe('basics', () => { 18 | it('should create and return a DiscriminatorTypeComposer', () => { 19 | expect(composeWithMongooseDiscriminators(CharacterModel)).toBeInstanceOf( 20 | DiscriminatorTypeComposer 21 | ); 22 | }); 23 | 24 | it('should return a ObjectTypeComposer as childTC on discriminator() call', () => { 25 | expect( 26 | composeWithMongooseDiscriminators(CharacterModel).discriminator(PersonModel) 27 | ).toBeInstanceOf(ObjectTypeComposer); 28 | }); 29 | }); 30 | 31 | describe('composeWithMongoose customizationOptions', () => { 32 | it('required input fields, should be passed down to resolvers', () => { 33 | const typeComposer = composeWithMongooseDiscriminators(CharacterModel, { 34 | inputType: { 35 | fields: { 36 | required: ['kind'], 37 | }, 38 | }, 39 | }); 40 | const ac: any = typeComposer.getResolver('createOne').getArgTC('record'); 41 | expect(ac.isFieldNonNull('kind')).toBe(true); 42 | }); 43 | 44 | it('should proceed customizationOptions.inputType.fields.required', () => { 45 | const itc = composeWithMongooseDiscriminators(CharacterModel, { 46 | inputType: { 47 | fields: { 48 | required: ['name', 'friends'], 49 | }, 50 | }, 51 | }).getInputTypeComposer(); 52 | 53 | expect(itc.isFieldNonNull('name')).toBe(true); 54 | expect(itc.isFieldNonNull('friends')).toBe(true); 55 | }); 56 | 57 | it('should be passed down record opts to resolvers', () => { 58 | const typeComposer = composeWithMongooseDiscriminators(CharacterModel, { 59 | resolvers: { 60 | createOne: { 61 | record: { 62 | removeFields: ['friends'], 63 | requiredFields: ['name'], 64 | }, 65 | }, 66 | }, 67 | }); 68 | const createOneRecordArgTC = typeComposer.getResolver('createOne').getArgITC('record'); 69 | expect(createOneRecordArgTC.isFieldNonNull('name')).toBe(true); 70 | expect(createOneRecordArgTC.hasField('friends')).toBe(false); 71 | }); 72 | 73 | it('should pass down records opts to createMany resolver', () => { 74 | const typeComposer = composeWithMongooseDiscriminators(CharacterModel, { 75 | resolvers: { 76 | createMany: { 77 | records: { 78 | removeFields: ['friends'], 79 | requiredFields: ['name'], 80 | }, 81 | }, 82 | }, 83 | }); 84 | const createManyRecordsArgTC = typeComposer.getResolver('createMany').getArgITC('records'); 85 | expect(createManyRecordsArgTC.isFieldNonNull('name')).toBe(true); 86 | expect(createManyRecordsArgTC.hasField('friends')).toBe(false); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/117-test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | import { composeWithMongoose } from '../../index'; 4 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 5 | 6 | let mongoServer: MongoMemoryServer; 7 | beforeAll(async () => { 8 | mongoServer = await MongoMemoryServer.create({ 9 | instance: { 10 | port: await getPortFree(), 11 | }, 12 | }); 13 | const mongoUri = mongoServer.getUri(); 14 | await mongoose.connect( 15 | mongoUri, 16 | { 17 | useNewUrlParser: true, 18 | useUnifiedTopology: true, 19 | } as any /* for tests compatibility with mongoose v5 & v6 */ 20 | ); 21 | }); 22 | 23 | afterAll(() => { 24 | mongoose.disconnect(); 25 | mongoServer.stop(); 26 | }); 27 | 28 | describe('issue #117', () => { 29 | it('`populate()` method for arrays is broken', async () => { 30 | const PlayerSchema = new mongoose.Schema({ 31 | name: { 32 | type: String, 33 | required: true, 34 | }, 35 | surname: { 36 | type: String, 37 | required: true, 38 | }, 39 | sex: { 40 | type: String, 41 | required: true, 42 | enum: ['m', 'f'], 43 | }, 44 | }); 45 | 46 | const GameSchema = new mongoose.Schema({ 47 | players: { 48 | required: true, 49 | type: [ 50 | { 51 | type: mongoose.Schema.Types.ObjectId, 52 | ref: 'PlayerModel', 53 | }, 54 | ], 55 | }, 56 | }); 57 | 58 | const GameModel = mongoose.model('GameModel', GameSchema); 59 | const PlayerModel = mongoose.model('PlayerModel', PlayerSchema); 60 | 61 | const player1 = await PlayerModel.create({ name: '1', surname: '1', sex: 'm' }); 62 | const player2 = await PlayerModel.create({ name: '2', surname: '2', sex: 'f' }); 63 | const game = await GameModel.create({ players: [player1, player2] }); 64 | 65 | const id = game._id; 66 | const g1 = await GameModel.findOne({ _id: id }).populate('players'); 67 | expect(g1?.toJSON()).toEqual({ 68 | __v: 0, 69 | _id: expect.anything(), 70 | players: [ 71 | { __v: 0, _id: expect.anything(), name: '1', sex: 'm', surname: '1' }, 72 | { __v: 0, _id: expect.anything(), name: '2', sex: 'f', surname: '2' }, 73 | ], 74 | }); 75 | 76 | composeWithMongoose(GameModel); 77 | const g2 = await GameModel.findOne({ _id: id }).populate('players'); 78 | 79 | // WAS SUCH ERROR 80 | // expect(g2.toJSON()).toEqual({ __v: 0, _id: expect.anything(), players: [] }); 81 | 82 | // EXPECTED BEHAVIOR 83 | expect(g2?.toJSON()).toEqual({ 84 | __v: 0, 85 | _id: expect.anything(), 86 | players: [ 87 | { __v: 0, _id: expect.anything(), name: '1', sex: 'm', surname: '1' }, 88 | { __v: 0, _id: expect.anything(), name: '2', sex: 'f', surname: '2' }, 89 | ], 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/120-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 3 | import mongoose from 'mongoose'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | import { composeWithMongoose } from '../../index'; 6 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 7 | 8 | let mongoServer: MongoMemoryServer; 9 | beforeAll(async () => { 10 | mongoServer = await MongoMemoryServer.create({ 11 | instance: { 12 | port: await getPortFree(), 13 | }, 14 | }); 15 | const mongoUri = mongoServer.getUri(); 16 | await mongoose.connect( 17 | mongoUri, 18 | { 19 | useNewUrlParser: true, 20 | useUnifiedTopology: true, 21 | } as any /* for tests compatibility with mongoose v5 & v6 */ 22 | ); 23 | // mongoose.set('debug', true); 24 | }); 25 | 26 | afterAll(() => { 27 | mongoose.disconnect(); 28 | mongoServer.stop(); 29 | }); 30 | 31 | describe('issue #120 - check `connection` resolver with last/before', () => { 32 | const RecordSchema = new mongoose.Schema({ id: String, title: String }); 33 | const Record = mongoose.model('Record', RecordSchema); 34 | const RecordTC = composeWithMongoose(Record); 35 | const resolver = RecordTC.getResolver('connection'); 36 | 37 | beforeAll(async () => { 38 | for (let i = 1; i <= 9; i++) { 39 | await Record.create({ _id: `10000000000000000000000${i}`, title: `${i}` }); 40 | } 41 | }); 42 | 43 | it('check last/before with sorting', async () => { 44 | const res1 = await resolver.resolve({ args: { last: 2, before: '', sort: { _id: 1 } } }); 45 | expect( 46 | res1.edges.map(({ cursor, node }: any) => ({ cursor, _id: node._id.toString() })) 47 | ).toEqual([ 48 | { 49 | cursor: 'eyJfaWQiOiIxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgifQ==', 50 | _id: '100000000000000000000008', 51 | }, 52 | { 53 | cursor: 'eyJfaWQiOiIxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDkifQ==', 54 | _id: '100000000000000000000009', 55 | }, 56 | ]); 57 | 58 | const res2 = await resolver.resolve({ 59 | args: { 60 | last: 2, 61 | before: 'eyJfaWQiOiIxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgifQ==', 62 | sort: { _id: 1 }, 63 | }, 64 | }); 65 | expect( 66 | res2.edges.map(({ cursor, node }: any) => ({ cursor, _id: node._id.toString() })) 67 | ).toEqual([ 68 | { 69 | cursor: 'eyJfaWQiOiIxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYifQ==', 70 | _id: '100000000000000000000006', 71 | }, 72 | { 73 | cursor: 'eyJfaWQiOiIxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDcifQ==', 74 | _id: '100000000000000000000007', 75 | }, 76 | ]); 77 | }); 78 | 79 | it('check last/before without sorting', async () => { 80 | const res1 = await resolver.resolve({ args: { last: 2, before: '' } }); 81 | expect( 82 | res1.edges.map(({ cursor, node }: any) => ({ cursor, _id: node._id.toString() })) 83 | ).toEqual([ 84 | { cursor: 'Nw==', _id: '100000000000000000000008' }, 85 | { cursor: 'OA==', _id: '100000000000000000000009' }, 86 | ]); 87 | 88 | const res2 = await resolver.resolve({ args: { last: 2, before: 'Nw==' } }); 89 | expect( 90 | res2.edges.map(({ cursor, node }: any) => ({ cursor, _id: node._id.toString() })) 91 | ).toEqual([ 92 | { cursor: 'NQ==', _id: '100000000000000000000006' }, 93 | { cursor: 'Ng==', _id: '100000000000000000000007' }, 94 | ]); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/128-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 3 | import mongoose from 'mongoose'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | import { composeWithMongoose } from '../../index'; 6 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 7 | 8 | let mongoServer: MongoMemoryServer; 9 | beforeAll(async () => { 10 | mongoServer = await MongoMemoryServer.create({ 11 | instance: { 12 | port: await getPortFree(), 13 | }, 14 | }); 15 | const mongoUri = mongoServer.getUri(); 16 | await mongoose.connect( 17 | mongoUri, 18 | { 19 | useNewUrlParser: true, 20 | useUnifiedTopology: true, 21 | } as any /* for tests compatibility with mongoose v5 & v6 */ 22 | ); 23 | // mongoose.set('debug', true); 24 | }); 25 | 26 | afterAll(() => { 27 | mongoose.disconnect(); 28 | mongoServer.stop(); 29 | }); 30 | 31 | describe('issue #128 - OR/AND filter args not working with some other filter args', () => { 32 | const RecordSchema = new mongoose.Schema({ 33 | id: String, 34 | name: String, 35 | pets: [String], 36 | friends: [String], 37 | }); 38 | const Record = mongoose.model('Record', RecordSchema); 39 | const RecordTC = composeWithMongoose(Record); 40 | const resolver = RecordTC.getResolver('findMany'); 41 | 42 | beforeAll(async () => { 43 | for (let i = 1; i <= 9; i++) { 44 | await Record.create({ 45 | _id: `10000000000000000000000${i}`, 46 | name: `Name ${i}`, 47 | pets: [`Pet ${i}`], 48 | friends: [`Friend ${i}`], 49 | }); 50 | } 51 | }); 52 | 53 | it('check with OR filter arg', async () => { 54 | const res1 = await resolver.resolve({ 55 | args: { 56 | filter: { 57 | OR: [ 58 | { _operators: { pets: { in: ['Pet 2'] } } }, 59 | { _operators: { friends: { in: ['Friend 4'] } } }, 60 | ], 61 | }, 62 | }, 63 | }); 64 | 65 | expect( 66 | res1.map(({ pets, friends }: any) => ({ pets: [...pets], friends: [...friends] })) 67 | ).toEqual([ 68 | { 69 | pets: ['Pet 2'], 70 | friends: ['Friend 2'], 71 | }, 72 | { 73 | pets: ['Pet 4'], 74 | friends: ['Friend 4'], 75 | }, 76 | ]); 77 | }); 78 | 79 | it('check with AND filter arg', async () => { 80 | const res1 = await resolver.resolve({ 81 | args: { 82 | filter: { 83 | OR: [{ _operators: { pets: { in: ['Pet 2'] } } }, { name: 'Name 4' }], 84 | }, 85 | }, 86 | }); 87 | 88 | expect( 89 | res1.map(({ pets, friends }: any) => ({ pets: [...pets], friends: [...friends] })) 90 | ).toEqual([ 91 | { 92 | pets: ['Pet 2'], 93 | friends: ['Friend 2'], 94 | }, 95 | { 96 | pets: ['Pet 4'], 97 | friends: ['Friend 4'], 98 | }, 99 | ]); 100 | }); 101 | 102 | it('check without OR filter arg', async () => { 103 | const res1 = await resolver.resolve({ 104 | args: { 105 | filter: { 106 | _operators: { pets: { in: ['Pet 2'] } }, 107 | }, 108 | }, 109 | }); 110 | 111 | expect(res1.map((res: any) => ({ pets: [...res.pets], friends: [...res.friends] }))).toEqual([ 112 | { 113 | pets: ['Pet 2'], 114 | friends: ['Friend 2'], 115 | }, 116 | ]); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/135-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 3 | import mongoose from 'mongoose'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | import { schemaComposer, graphql } from 'graphql-compose'; 6 | import { composeWithMongoose } from '../../index'; 7 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 8 | 9 | let mongoServer: MongoMemoryServer; 10 | beforeAll(async () => { 11 | mongoServer = await MongoMemoryServer.create({ 12 | instance: { 13 | port: await getPortFree(), 14 | }, 15 | }); 16 | const mongoUri = mongoServer.getUri(); 17 | await mongoose.connect( 18 | mongoUri, 19 | { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | } as any /* for tests compatibility with mongoose v5 & v6 */ 23 | ); 24 | // mongoose.set('debug', true); 25 | }); 26 | 27 | afterAll(() => { 28 | mongoose.disconnect(); 29 | mongoServer.stop(); 30 | }); 31 | 32 | describe('issue #135 - Mongoose virtuals', () => { 33 | const RecordSchema = new mongoose.Schema({ id: String, title: String }); 34 | 35 | // ADD VIRTUAL FIELDS VIA loadClass METHOD 36 | // see https://mongoosejs.com/docs/api.html#schema_Schema-loadClass 37 | class RecordDoc { 38 | get virtualField123() { 39 | return `Improved ${(this as any).title}`; 40 | } 41 | } 42 | RecordSchema.loadClass(RecordDoc); 43 | 44 | // ADD MOCK DATA TO DB 45 | const Record = mongoose.model('Record', RecordSchema); 46 | beforeAll(async () => { 47 | for (let i = 1; i <= 3; i++) { 48 | await Record.create({ _id: `10000000000000000000000${i}`, title: `Title ${i}` }); 49 | } 50 | }); 51 | 52 | // ADD VIRTUAL FIELD DEFINITION <------------------- JUST ADD FIELD DEFINITION 🛑🛑🛑 53 | // no need to define resolve method explicitly 54 | const RecordTC = composeWithMongoose(Record); 55 | RecordTC.addFields({ 56 | virtualField123: { 57 | type: 'String', 58 | }, 59 | }); 60 | 61 | // INIT GRAPHQL SCHEMA 62 | schemaComposer.Query.addFields({ 63 | findMany: RecordTC.getResolver('findMany'), 64 | }); 65 | 66 | const schema = schemaComposer.buildSchema(); 67 | 68 | it('check that virtual field works', async () => { 69 | const res = await graphql.graphql({ 70 | schema, 71 | source: 'query { findMany { id title virtualField123 } }', 72 | }); 73 | expect(res).toEqual({ 74 | data: { 75 | findMany: [ 76 | { id: null, title: 'Title 1', virtualField123: 'Improved Title 1' }, 77 | { id: null, title: 'Title 2', virtualField123: 'Improved Title 2' }, 78 | { id: null, title: 'Title 3', virtualField123: 'Improved Title 3' }, 79 | ], 80 | }, 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/136-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 3 | import mongoose from 'mongoose'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | import { schemaComposer, graphql } from 'graphql-compose'; 6 | import { composeWithMongoose } from '../../index'; 7 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 8 | 9 | let mongoServer: MongoMemoryServer; 10 | beforeAll(async () => { 11 | mongoServer = await MongoMemoryServer.create({ 12 | instance: { 13 | port: await getPortFree(), 14 | }, 15 | }); 16 | const mongoUri = mongoServer.getUri(); 17 | await mongoose.connect( 18 | mongoUri, 19 | { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | } as any /* for tests compatibility with mongoose v5 & v6 */ 23 | ); 24 | // mongoose.set('debug', true); 25 | }); 26 | 27 | afterAll(() => { 28 | mongoose.disconnect(); 29 | mongoServer.stop(); 30 | }); 31 | 32 | describe('issue #136 - Mongoose virtuals', () => { 33 | const CommentSchema = new mongoose.Schema({ 34 | author: { 35 | type: mongoose.Schema.Types.ObjectId, 36 | rel: 'Autor', 37 | }, 38 | links: [String], 39 | }); 40 | 41 | const Comment = mongoose.model('Comment', CommentSchema); 42 | const CommentTC = composeWithMongoose(Comment); 43 | 44 | CommentTC.wrapResolverAs('createManyFiltered', 'createMany', (updateManyFiltered) => { 45 | const recordsTC = CommentTC.getResolver('createMany').getArgITC('records'); 46 | const clonedRecordTC = recordsTC.clone('createManyFilteredInput'); 47 | clonedRecordTC.removeField('links').addFields({ hi: 'String' }); 48 | updateManyFiltered.extendArg('records', { type: clonedRecordTC.List }); 49 | 50 | return updateManyFiltered 51 | .wrapResolve((next) => async (rp) => { 52 | console.log(rp.args); // eslint-disable-line 53 | return next(rp); 54 | }) 55 | .debug(); 56 | }); 57 | 58 | it('check that virtual field works', async () => { 59 | // INIT GRAPHQL SCHEMA 60 | schemaComposer.Query.addFields({ noop: 'String' }); 61 | schemaComposer.Mutation.addFields({ 62 | createCommentsFiltered: CommentTC.getResolver('createManyFiltered'), 63 | createManyComments: CommentTC.getResolver('createMany'), 64 | }); 65 | const schema = schemaComposer.buildSchema(); 66 | 67 | const res = await graphql.graphql({ 68 | schema, 69 | source: 'mutation { createManyComments(records: [{ links: ["a"] }]) { createdCount } }', 70 | }); 71 | 72 | expect(res).toEqual({ data: { createManyComments: { createdCount: 1 } } }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/157-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 3 | import mongoose from 'mongoose'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | import { schemaComposer, graphql } from 'graphql-compose'; 6 | import { composeWithMongoose } from '../../index'; 7 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 8 | 9 | let mongoServer: MongoMemoryServer; 10 | beforeAll(async () => { 11 | mongoServer = await MongoMemoryServer.create({ 12 | instance: { 13 | port: await getPortFree(), 14 | }, 15 | }); 16 | const mongoUri = mongoServer.getUri(); 17 | await mongoose.connect( 18 | mongoUri, 19 | { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | } as any /* for tests compatibility with mongoose v5 & v6 */ 23 | ); 24 | // mongoose.set('debug', true); 25 | }); 26 | 27 | afterAll(() => { 28 | mongoose.disconnect(); 29 | mongoServer.stop(); 30 | }); 31 | 32 | describe('issue #157 - Optional enum error', () => { 33 | const Visit = mongoose.model( 34 | 'visit', 35 | new mongoose.Schema({ 36 | url: { type: String, required: true }, 37 | referredBy: { type: String, enum: ['WEBSITE', 'NEWSPAPER'] }, 38 | }) 39 | ); 40 | 41 | it('check ', async () => { 42 | const VisitTC = composeWithMongoose(Visit); 43 | 44 | const referredBy: any = VisitTC.getFieldType('referredBy'); 45 | expect(referredBy).toBeInstanceOf(graphql.GraphQLEnumType); 46 | const etc = schemaComposer.createEnumTC(referredBy); 47 | expect(etc.getFieldNames()).toEqual(['WEBSITE', 'NEWSPAPER']); 48 | 49 | etc.addFields({ 50 | EMPTY_STRING: { value: '' }, 51 | NULL: { value: null }, 52 | }); 53 | expect(etc.getFieldNames()).toEqual(['WEBSITE', 'NEWSPAPER', 'EMPTY_STRING', 'NULL']); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/181-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer } from 'graphql-compose'; 2 | import { 3 | processFilterOperators, 4 | OPERATORS_FIELDNAME, 5 | } from '../../resolvers/helpers/filterOperators'; 6 | 7 | beforeEach(() => { 8 | schemaComposer.clear(); 9 | schemaComposer.createInputTC({ 10 | name: 'UserFilterInput', 11 | fields: { 12 | _id: 'String', 13 | employment: 'String', 14 | name: 'String', 15 | age: 'Int', 16 | skills: ['String'], 17 | }, 18 | }); 19 | }); 20 | 21 | describe(`issue #181 - Cannot read property '_operators' of null`, () => { 22 | it('should call query.find if operator value is null', () => { 23 | const filter = { 24 | [OPERATORS_FIELDNAME]: { age: { ne: null } }, 25 | }; 26 | expect(processFilterOperators(filter)).toEqual({ age: { $ne: null } }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/184-test.ts: -------------------------------------------------------------------------------- 1 | import { toMongoDottedObject } from '../../utils/toMongoDottedObject'; 2 | 3 | describe('toMongoDottedObject()', () => { 4 | it('should handle operators using date object values when nested', () => { 5 | expect(toMongoDottedObject({ a: { dateField: { $gte: new Date(100) } } })).toEqual({ 6 | 'a.dateField': { $gte: new Date(100) }, 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/194-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 3 | import mongoose from 'mongoose'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | import { schemaComposer, graphql } from 'graphql-compose'; 6 | import { composeWithMongoose } from '../../index'; 7 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 8 | 9 | let mongoServer: MongoMemoryServer; 10 | beforeAll(async () => { 11 | mongoServer = await MongoMemoryServer.create({ 12 | instance: { 13 | port: await getPortFree(), 14 | }, 15 | }); 16 | const mongoUri = mongoServer.getUri(); 17 | await mongoose.connect( 18 | mongoUri, 19 | { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | } as any /* for tests compatibility with mongoose v5 & v6 */ 23 | ); 24 | // mongoose.set('debug', true); 25 | }); 26 | 27 | afterAll(() => { 28 | mongoose.disconnect(); 29 | mongoServer.stop(); 30 | }); 31 | 32 | describe('issue #194 - useAlias', () => { 33 | const UserSchema = new mongoose.Schema({ 34 | e: { 35 | type: String, 36 | alias: 'emailAddress', 37 | }, 38 | }); 39 | 40 | const User = mongoose.model('User', UserSchema); 41 | const UserTC = composeWithMongoose(User); 42 | 43 | it('check that virtual field works', async () => { 44 | // INIT GRAPHQL SCHEMA 45 | schemaComposer.Query.addFields({ findMany: UserTC.getResolver('findMany') }); 46 | schemaComposer.Mutation.addFields({ 47 | createOne: UserTC.getResolver('createOne'), 48 | }); 49 | const schema = schemaComposer.buildSchema(); 50 | 51 | const res = await graphql.graphql({ 52 | schema, 53 | source: 54 | 'mutation { createOne(record: { emailAddress: "a@a.com" }) { record { emailAddress } } }', 55 | }); 56 | expect(res).toEqual({ data: { createOne: { record: { emailAddress: 'a@a.com' } } } }); 57 | 58 | const res2 = await graphql.graphql({ 59 | schema, 60 | source: 'query { findMany { emailAddress } }', 61 | }); 62 | expect(res2).toEqual({ data: { findMany: [{ emailAddress: 'a@a.com' }] } }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/219-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { schemaComposer, graphql } from 'graphql-compose'; 4 | import { composeWithMongoose } from '../../index'; 5 | import { UserModel } from '../../__mocks__/userModel'; 6 | 7 | beforeAll(async () => { 8 | await UserModel.base.createConnection(); 9 | await UserModel.create({ 10 | name: 'AAAAA', 11 | age: 10, 12 | contacts: { email: '1@1.com' }, 13 | }); 14 | await UserModel.create({ 15 | name: 'BBBBB', 16 | age: 20, 17 | contacts: { email: '2@2.com' }, 18 | }); 19 | }); 20 | afterAll(() => UserModel.base.disconnect()); 21 | 22 | const UserTC = composeWithMongoose(UserModel); 23 | 24 | describe('issue #219 - Authorization using wrapResolve', () => { 25 | it('correct request', async () => { 26 | UserTC.wrapResolverResolve('findOne', (next) => (rp) => { 27 | rp.beforeQuery = async (query: any) => { 28 | // Choose any case or mix of them 29 | // 1) check rp.context 30 | // 2) make another query await Permission.find(); 31 | // 3) modify query = query.where({ perm: 'ALLOWED', userId: context?.req?.user?.id }) 32 | query = query.where({ age: { $gt: 19 } }); 33 | 34 | // 4) return cached data return UserCachedData[rp.args.id]; 35 | if (rp?.args?.filter?.name === 'CACHED') { 36 | return { name: 'CACHED', age: 99 }; 37 | } 38 | 39 | // 5) just check arg value 40 | if (rp?.args?.filter?.name === 'ERROR') { 41 | throw new Error('Wrong arg!'); 42 | } 43 | 44 | return query.exec(); 45 | }; 46 | return next(rp); 47 | }); 48 | schemaComposer.Query.addFields({ 49 | findUser: UserTC.getResolver('findOne'), 50 | }); 51 | const schema = schemaComposer.buildSchema(); 52 | expect( 53 | await graphql.graphql({ 54 | schema, 55 | source: `query { 56 | findUser(filter: { name: "AAAAA" }) { 57 | name 58 | age 59 | } 60 | }`, 61 | }) 62 | ).toEqual({ data: { findUser: null } }); 63 | 64 | expect( 65 | await graphql.graphql({ 66 | schema, 67 | source: `query { 68 | findUser(filter: { name: "BBBBB" }) { 69 | name 70 | age 71 | } 72 | }`, 73 | }) 74 | ).toEqual({ data: { findUser: { age: 20, name: 'BBBBB' } } }); 75 | 76 | expect( 77 | await graphql.graphql({ 78 | schema, 79 | source: `query { 80 | findUser(filter: { name: "CACHED" }) { 81 | name 82 | age 83 | } 84 | }`, 85 | }) 86 | ).toEqual({ data: { findUser: { age: 99, name: 'CACHED' } } }); 87 | 88 | expect( 89 | await graphql.graphql({ 90 | schema, 91 | source: `query { 92 | findUser(filter: { name: "ERROR" }) { 93 | name 94 | age 95 | } 96 | }`, 97 | }) 98 | ).toEqual({ data: { findUser: null }, errors: [new Error('Wrong arg!')] }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/250-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { schemaComposer, graphql } from 'graphql-compose'; 4 | import { composeWithMongoose } from '../../index'; 5 | import { UserModel } from '../../__mocks__/userModel'; 6 | 7 | beforeAll(async () => { 8 | await UserModel.base.createConnection(); 9 | await UserModel.create({ 10 | name: 'AAAAA', 11 | age: 10, 12 | contacts: { email: '1@1.com' }, 13 | }); 14 | await UserModel.create({ 15 | name: 'BBBBB', 16 | age: 20, 17 | gender: 'male', 18 | contacts: { email: '2@2.com', skype: 'aaa' }, 19 | }); 20 | await UserModel.create({ 21 | name: 'CCCCC', 22 | age: 30, 23 | contacts: { email: '3@3.com' }, 24 | }); 25 | }); 26 | afterAll(() => UserModel.base.disconnect()); 27 | 28 | const UserTC = composeWithMongoose(UserModel, { 29 | resolvers: { 30 | findMany: { 31 | filter: { 32 | operators: true, 33 | }, 34 | }, 35 | }, 36 | }); 37 | // console.log(UserTC.getResolver('findOne').getArgITC('filter').toSDL({ deep: true })); 38 | schemaComposer.Query.addFields({ 39 | findMany: UserTC.getResolver('findMany'), 40 | }); 41 | const schema = schemaComposer.buildSchema(); 42 | 43 | describe('issue #250 - Adds support for nested `_operators`, add `exists`, `regex` operators', () => { 44 | it('check `exist` operator', async () => { 45 | expect( 46 | await graphql.graphql({ 47 | schema, 48 | source: `query { 49 | findMany(filter: { _operators: { gender: { exists: true } } }) { 50 | name 51 | gender 52 | } 53 | }`, 54 | }) 55 | ).toEqual({ data: { findMany: [{ gender: 'male', name: 'BBBBB' }] } }); 56 | }); 57 | 58 | it('check nested `exist` operator', async () => { 59 | expect( 60 | await graphql.graphql({ 61 | schema, 62 | source: `query { 63 | findMany(filter: { _operators: { contacts: { skype: { exists: true } } } }) { 64 | name 65 | } 66 | }`, 67 | }) 68 | ).toEqual({ data: { findMany: [{ name: 'BBBBB' }] } }); 69 | }); 70 | 71 | it('check `regex` operator', async () => { 72 | expect( 73 | await graphql.graphql({ 74 | schema, 75 | source: `query { 76 | findMany(filter: { _operators: { name: { regex: "^AA|CC.*" } } }) { 77 | name 78 | } 79 | }`, 80 | }) 81 | ).toEqual({ data: { findMany: [{ name: 'AAAAA' }, { name: 'CCCCC' }] } }); 82 | }); 83 | 84 | it('check nested `regex` operator', async () => { 85 | expect( 86 | await graphql.graphql({ 87 | schema, 88 | source: `query { 89 | findMany(filter: { _operators: { contacts: { email: { regex: "/3.COM/i" } } } }) { 90 | name 91 | } 92 | }`, 93 | }) 94 | ).toEqual({ data: { findMany: [{ name: 'CCCCC' }] } }); 95 | }); 96 | 97 | it('check combined nested `regex` operator', async () => { 98 | expect( 99 | await graphql.graphql({ 100 | schema, 101 | source: `query { 102 | findMany( 103 | filter: { OR: [ 104 | { _operators: { contacts: { email: { regex: "/3.COM/i" } } } }, 105 | { _operators: { contacts: { skype: { exists: true } } } } 106 | ]} 107 | ) { 108 | name 109 | } 110 | }`, 111 | }) 112 | ).toEqual({ data: { findMany: [{ name: 'BBBBB' }, { name: 'CCCCC' }] } }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/253-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer } from 'graphql-compose'; 2 | import { composeWithMongooseDiscriminators } from '../../index'; 3 | import { mongoose, Schema } from '../../__mocks__/mongooseCommon'; 4 | import { testFieldConfig } from '../../utils/testHelpers'; 5 | 6 | const SubCarSchema = new Schema({ s: { type: Number, required: true, alias: 'speed' } }); 7 | 8 | const CarSchema = new Schema( 9 | { s: { type: Number, required: true, alias: 'speed' }, aaa: SubCarSchema }, 10 | { discriminatorKey: 't' } 11 | ); 12 | const Car = mongoose.model('Car', CarSchema); 13 | 14 | const TimeMachineSchema = new Schema( 15 | { f: { type: Number, required: true, alias: 'fluxCompensatorVersion' } }, 16 | { discriminatorKey: 't' } 17 | ); 18 | const TimeMachine = Car.discriminator('TimeMachine', TimeMachineSchema); 19 | 20 | const CarDTC = composeWithMongooseDiscriminators(Car); 21 | 22 | schemaComposer.Query.addFields({ 23 | allCars: CarDTC.getResolver('findMany'), 24 | timeMachines: CarDTC.discriminator(TimeMachine).getResolver('findMany'), 25 | }); 26 | 27 | // console.log(schemaComposer.toSDL({ omitDescriptions: true })); 28 | 29 | beforeAll(async () => { 30 | await mongoose.createConnection(); 31 | await TimeMachine.create({ speed: 300, fluxCompensatorVersion: 5 }); 32 | }); 33 | afterAll(() => mongoose.disconnect()); 34 | 35 | describe('issue #253 - Consider aliases from discriminators during preparation', () => { 36 | it('check data in db', async () => { 37 | const data = await Car.find({}).lean(); 38 | expect(data).toEqual([{ __v: 0, _id: expect.anything(), f: 5, s: 300, t: 'TimeMachine' }]); 39 | }); 40 | 41 | it('check query', async () => { 42 | const res = await testFieldConfig({ 43 | field: CarDTC.getResolver('findMany'), 44 | selection: `{ 45 | __typename 46 | speed 47 | ... on TimeMachine { 48 | fluxCompensatorVersion 49 | } 50 | }`, 51 | schemaComposer, 52 | }); 53 | expect(res).toEqual([ 54 | { 55 | __typename: 'TimeMachine', 56 | fluxCompensatorVersion: 5, 57 | speed: 300, 58 | }, 59 | ]); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/260-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Document } from 'mongoose'; 5 | 6 | const UserSchema = new mongoose.Schema({ 7 | name: { type: String, required: true }, 8 | }); 9 | const PostSchema = new mongoose.Schema({ 10 | title: { type: String, required: true }, 11 | authorId: { type: mongoose.Types.ObjectId }, 12 | reviewerIds: { type: [mongoose.Types.ObjectId] }, 13 | }); 14 | 15 | interface IUser extends Document { 16 | name: string; 17 | } 18 | 19 | interface IPost extends Document { 20 | title: string; 21 | authorId?: mongoose.Types.ObjectId; 22 | reviewerIds?: [mongoose.Types.ObjectId]; 23 | } 24 | 25 | const UserModel = mongoose.model('User', UserSchema); 26 | const PostModel = mongoose.model('Post', PostSchema); 27 | 28 | const UserTC = composeMongoose(UserModel); 29 | const PostTC = composeMongoose(PostModel); 30 | 31 | PostTC.addRelation('author', { 32 | resolver: UserTC.mongooseResolvers.dataLoader({ 33 | lean: true, // <---- `Lean` loads record from DB without support of mongoose getters & virtuals 34 | }), 35 | prepareArgs: { 36 | _id: (s) => s.authorId, 37 | }, 38 | projection: { authorId: true }, 39 | }); 40 | 41 | PostTC.addRelation('reviewers', { 42 | resolver: UserTC.mongooseResolvers.dataLoaderMany({ 43 | lean: true, // <---- `Lean` loads records from DB without support of mongoose getters & virtuals 44 | }), 45 | prepareArgs: { 46 | _ids: (s) => s.reviewerIds, 47 | }, 48 | projection: { reviewerIds: true }, 49 | }); 50 | 51 | schemaComposer.Query.addFields({ 52 | posts: PostTC.mongooseResolvers.findMany(), 53 | }); 54 | const schema = schemaComposer.buildSchema(); 55 | 56 | // console.log(schemaComposer.toSDL()); 57 | 58 | beforeAll(async () => { 59 | await UserModel.base.createConnection(); 60 | const User1 = await UserModel.create({ name: 'User1' }); 61 | const User2 = await UserModel.create({ name: 'User2' }); 62 | const User3 = await UserModel.create({ name: 'User3' }); 63 | 64 | await PostModel.create({ title: 'Post1', authorId: User1._id }); 65 | await PostModel.create({ title: 'Post2', authorId: User1._id, reviewerIds: [] }); 66 | await PostModel.create({ title: 'Post3', authorId: User1._id, reviewerIds: [User2._id] }); 67 | await PostModel.create({ title: 'Post4', authorId: User2._id, reviewerIds: [User1._id] }); 68 | await PostModel.create({ title: 'Post5', authorId: User2._id, reviewerIds: [User1._id] }); 69 | await PostModel.create({ 70 | title: 'Post6', 71 | authorId: User3._id, 72 | reviewerIds: [User1._id, User2._id], 73 | }); 74 | }); 75 | afterAll(() => UserModel.base.disconnect()); 76 | 77 | describe('issue #260 - new resolvers which works via DataLoader', () => { 78 | it('check response', async () => { 79 | // 👀 uncomment next line if you want to see real mongoose queries 80 | // mongoose.set('debug', true); 81 | 82 | expect( 83 | await graphql.graphql({ 84 | schema, 85 | source: `query { 86 | posts { 87 | title 88 | author { name } 89 | reviewers { name } 90 | } 91 | }`, 92 | contextValue: {}, 93 | }) 94 | ).toEqual({ 95 | data: { 96 | posts: [ 97 | { title: 'Post1', author: { name: 'User1' }, reviewers: [] }, 98 | { title: 'Post2', author: { name: 'User1' }, reviewers: [] }, 99 | { title: 'Post3', author: { name: 'User1' }, reviewers: [{ name: 'User2' }] }, 100 | { title: 'Post4', author: { name: 'User2' }, reviewers: [{ name: 'User1' }] }, 101 | { title: 'Post5', author: { name: 'User2' }, reviewers: [{ name: 'User1' }] }, 102 | { 103 | title: 'Post6', 104 | author: { name: 'User3' }, 105 | reviewers: [{ name: 'User1' }, { name: 'User2' }], 106 | }, 107 | ], 108 | }, 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/261-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, dedent } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Document } from 'mongoose'; 5 | import { testFieldConfig } from '../../utils/testHelpers'; 6 | 7 | const schemaComposer = new SchemaComposer<{ req: any }>(); 8 | 9 | const UserSchema = new mongoose.Schema({ 10 | _id: { type: Number }, 11 | n: { 12 | type: String, 13 | alias: 'name', 14 | default: 'User', 15 | }, 16 | age: { type: Number }, 17 | isActive: { type: Boolean, default: false }, 18 | analytics: { 19 | isEnabled: { type: Boolean, default: false }, 20 | }, 21 | periods: [{ from: Number, to: Number, _id: false }], 22 | }); 23 | interface IUser extends Document { 24 | _id: number; 25 | name: string; 26 | age?: number; 27 | isActive: boolean; 28 | analytics: { 29 | isEnabled: boolean; 30 | }; 31 | periods: Array<{ from: number; to: number }>; 32 | } 33 | 34 | const UserModel = mongoose.model('User', UserSchema); 35 | const UserTC = composeMongoose(UserModel, { schemaComposer, defaultsAsNonNull: true }); 36 | 37 | schemaComposer.Query.addFields({ 38 | userById: UserTC.mongooseResolvers.findById(), 39 | }); 40 | 41 | // const schema = schemaComposer.buildSchema(); 42 | // console.log(schemaComposer.toSDL()); 43 | 44 | beforeAll(async () => { 45 | await UserModel.base.createConnection(); 46 | await UserModel.create({ _id: 1 } as any); 47 | }); 48 | afterAll(() => UserModel.base.disconnect()); 49 | 50 | describe('issue #261 - Non-nullability for mongoose fields that have a default value', () => { 51 | it('mongoose should hydrate doc with default values', async () => { 52 | const user1 = await UserModel.findById(1); 53 | expect(user1?.toObject({ virtuals: true })).toEqual( 54 | expect.objectContaining({ 55 | _id: 1, 56 | name: 'User', 57 | isActive: false, 58 | analytics: { isEnabled: false }, 59 | periods: [], 60 | }) 61 | ); 62 | }); 63 | 64 | it('UserTC should have non-null fields if default value is provided and option `defaultsAsNonNull`', () => { 65 | expect(UserTC.toSDL({ deep: true, omitScalars: true })).toBe(dedent` 66 | type User { 67 | _id: Int! 68 | name: String! 69 | age: Float 70 | isActive: Boolean! 71 | analytics: UserAnalytics! 72 | periods: [UserPeriods]! 73 | } 74 | 75 | type UserAnalytics { 76 | isEnabled: Boolean! 77 | } 78 | 79 | type UserPeriods { 80 | from: Float 81 | to: Float 82 | } 83 | `); 84 | }); 85 | 86 | it('UserTC should not have non-null fields which have default values', () => { 87 | const UserWithoutDefaultsTC = composeMongoose(UserModel, { 88 | schemaComposer: new SchemaComposer(), 89 | name: 'UserWithoutDefaults', 90 | }); 91 | expect(UserWithoutDefaultsTC.toSDL({ deep: true, omitScalars: true })).toBe(dedent` 92 | type UserWithoutDefaults { 93 | _id: Int! 94 | name: String 95 | age: Float 96 | isActive: Boolean 97 | analytics: UserWithoutDefaultsAnalytics 98 | periods: [UserWithoutDefaultsPeriods] 99 | } 100 | 101 | type UserWithoutDefaultsAnalytics { 102 | isEnabled: Boolean 103 | } 104 | 105 | type UserWithoutDefaultsPeriods { 106 | from: Float 107 | to: Float 108 | } 109 | `); 110 | }); 111 | 112 | it('check that graphql gets all default values', async () => { 113 | expect( 114 | await testFieldConfig({ 115 | field: UserTC.mongooseResolvers.findById(), 116 | args: { _id: 1 }, 117 | selection: `{ 118 | _id 119 | name 120 | age 121 | isActive 122 | analytics { 123 | isEnabled 124 | } 125 | periods { 126 | from 127 | to 128 | } 129 | }`, 130 | }) 131 | ).toEqual({ 132 | _id: 1, 133 | age: null, 134 | analytics: { isEnabled: false }, 135 | isActive: false, 136 | name: 'User', 137 | periods: expect.anything(), 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/263-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | 5 | const UserSchema = new mongoose.Schema({ 6 | name: { type: String, required: true }, 7 | }); 8 | 9 | interface IUser extends mongoose.Document { 10 | name: string; 11 | } 12 | 13 | const PostSchema = new mongoose.Schema({ 14 | title: { type: String, required: true }, 15 | authorId: { type: mongoose.Types.ObjectId }, 16 | reviewerIds: { type: [mongoose.Types.ObjectId] }, 17 | }); 18 | 19 | interface IPost extends mongoose.Document { 20 | title: string; 21 | authorId?: mongoose.Types.ObjectId; 22 | reviewerIds?: [mongoose.Types.ObjectId]; 23 | } 24 | 25 | const UserModel = mongoose.model('User', UserSchema); 26 | const PostModel = mongoose.model('Post', PostSchema); 27 | 28 | const UserTC = composeMongoose(UserModel); 29 | const PostTC = composeMongoose(PostModel); 30 | 31 | PostTC.addRelation('author', { 32 | resolver: UserTC.mongooseResolvers.dataLoader({ lean: true }), 33 | prepareArgs: { 34 | _id: (s) => s.authorId, 35 | }, 36 | projection: { authorId: true }, 37 | }); 38 | 39 | PostTC.addRelation('reviewers', { 40 | resolver: UserTC.mongooseResolvers.dataLoaderMany({ lean: true }), 41 | prepareArgs: { 42 | _ids: (s) => s.reviewerIds, 43 | }, 44 | projection: { reviewerIds: true }, 45 | }); 46 | 47 | schemaComposer.Query.addFields({ 48 | post: PostTC.mongooseResolvers.findById(), 49 | posts: PostTC.mongooseResolvers.findMany(), 50 | user: UserTC.mongooseResolvers.findById(), 51 | users: UserTC.mongooseResolvers.findMany({ sort: false }), 52 | }); 53 | const schema = schemaComposer.buildSchema(); 54 | 55 | // console.log(schemaComposer.toSDL()); 56 | 57 | beforeAll(async () => { 58 | await UserModel.base.createConnection(); 59 | const User1 = await UserModel.create({ name: 'User1' }); 60 | const User2 = await UserModel.create({ name: 'User2' }); 61 | const User3 = await UserModel.create({ name: 'User3' }); 62 | 63 | await PostModel.create({ title: 'Post1', authorId: User1._id }); 64 | await PostModel.create({ title: 'Post2', authorId: User1._id, reviewerIds: [] }); 65 | await PostModel.create({ title: 'Post3', authorId: User1._id, reviewerIds: [User2._id] }); 66 | await PostModel.create({ title: 'Post4', authorId: User2._id, reviewerIds: [User1._id] }); 67 | await PostModel.create({ title: 'Post5', authorId: User2._id, reviewerIds: [User1._id] }); 68 | await PostModel.create({ 69 | title: 'Post6', 70 | authorId: User3._id, 71 | reviewerIds: [User1._id, User2._id], 72 | }); 73 | }); 74 | afterAll(() => UserModel.base.disconnect()); 75 | 76 | describe('issue #263 - new resolvers which works via DataLoader', () => { 77 | it('check response', async () => { 78 | // 👀 uncomment next line if you want to see real mongoose queries 79 | // mongoose.set('debug', true); 80 | 81 | expect( 82 | await graphql.graphql({ 83 | schema, 84 | source: `query { 85 | posts { 86 | title 87 | author { name } 88 | reviewers { name } 89 | } 90 | }`, 91 | contextValue: {}, 92 | }) 93 | ).toEqual({ 94 | data: { 95 | posts: [ 96 | { title: 'Post1', author: { name: 'User1' }, reviewers: [] }, 97 | { title: 'Post2', author: { name: 'User1' }, reviewers: [] }, 98 | { title: 'Post3', author: { name: 'User1' }, reviewers: [{ name: 'User2' }] }, 99 | { title: 'Post4', author: { name: 'User2' }, reviewers: [{ name: 'User1' }] }, 100 | { title: 'Post5', author: { name: 'User2' }, reviewers: [{ name: 'User1' }] }, 101 | { 102 | title: 'Post6', 103 | author: { name: 'User3' }, 104 | reviewers: [{ name: 'User1' }, { name: 'User2' }], 105 | }, 106 | ], 107 | }, 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/289-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Document } from 'mongoose'; 5 | 6 | const schemaComposer = new SchemaComposer<{ req: any }>(); 7 | 8 | // mongoose.set('debug', true); 9 | 10 | const AuthorSchema = new mongoose.Schema({ 11 | name: { type: String }, 12 | age: { type: Number }, 13 | }); 14 | 15 | interface IAuthor extends Document { 16 | name: string; 17 | age: number; 18 | } 19 | 20 | const AuthorModel = mongoose.model('Author', AuthorSchema); 21 | const AuthorTC = composeMongoose(AuthorModel, { schemaComposer }); 22 | 23 | schemaComposer.Query.addFields({ 24 | authorMany: AuthorTC.mongooseResolvers.findMany().addFilterArg({ 25 | name: 'test', 26 | type: 'String', 27 | query: (query, value) => { 28 | query.name = new RegExp(value, 'i'); 29 | }, 30 | }), 31 | }); 32 | 33 | const schema = schemaComposer.buildSchema(); 34 | 35 | beforeAll(async () => { 36 | await AuthorModel.base.createConnection(); 37 | await AuthorModel.create({ 38 | name: 'Ayn Rand', 39 | age: 115, 40 | }); 41 | }); 42 | afterAll(() => { 43 | AuthorModel.base.disconnect(); 44 | }); 45 | 46 | describe('check addFilterArg - issue #289', () => { 47 | it('check SDL', async () => { 48 | expect(schemaComposer.getITC('FilterFindManyAuthorInput').toSDL({ omitDescriptions: true })) 49 | .toMatchInlineSnapshot(` 50 | "input FilterFindManyAuthorInput { 51 | name: String 52 | age: Float 53 | _id: MongoID 54 | _operators: FilterFindManyAuthorOperatorsInput 55 | OR: [FilterFindManyAuthorInput!] 56 | AND: [FilterFindManyAuthorInput!] 57 | test: String 58 | }" 59 | `); 60 | }); 61 | 62 | it('check runtime', async () => { 63 | const result = await graphql.graphql({ 64 | schema, 65 | source: `query { 66 | authorMany(filter: { test: "ayn" }) { 67 | name 68 | age 69 | } 70 | }`, 71 | contextValue: {}, 72 | }); 73 | expect(result).toEqual({ data: { authorMany: [{ age: 115, name: 'Ayn Rand' }] } }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/304-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Document } from 'mongoose'; 5 | 6 | const schemaComposer = new SchemaComposer<{ req: any }>(); 7 | 8 | // mongoose.set('debug', true); 9 | 10 | const OrderSchema = new mongoose.Schema({ 11 | orderStatus: String, 12 | inbound: { 13 | timeStamp: { 14 | type: Date, 15 | index: true, 16 | }, 17 | }, 18 | }); 19 | 20 | interface IOrder extends Document { 21 | orderStatus?: string; 22 | inbound?: { 23 | timeStamp?: Date | null; 24 | }; 25 | } 26 | 27 | const OrderModel = mongoose.model('Order', OrderSchema); 28 | const OrderTC = composeMongoose(OrderModel, { schemaComposer }); 29 | 30 | schemaComposer.Query.addFields({ 31 | orderMany: OrderTC.mongooseResolvers.findMany({ 32 | suffix: 'Extended', 33 | filter: { 34 | operators: true, 35 | }, 36 | }), 37 | }); 38 | 39 | const schema = schemaComposer.buildSchema(); 40 | 41 | beforeAll(async () => { 42 | await OrderModel.base.createConnection(); 43 | await OrderModel.create([ 44 | { orderStatus: 'PAID', inbound: { timeStamp: null } }, 45 | { orderStatus: 'PAID', inbound: { timeStamp: 123 } }, 46 | { orderStatus: 'UNPAID', inbound: { timeStamp: null } }, 47 | { orderStatus: 'UNPAID', inbound: { timeStamp: 456 } }, 48 | ]); 49 | }); 50 | afterAll(() => { 51 | OrderModel.base.disconnect(); 52 | }); 53 | 54 | describe('issue #304 - Filter _operator for nested fields not apply', () => { 55 | it('check runtime', async () => { 56 | const result = await graphql.graphql({ 57 | schema, 58 | source: `query { 59 | orderMany(filter: { 60 | _operators: { 61 | inbound: { timeStamp: { ne: null } } 62 | orderStatus: { ne: "PAID" } 63 | } 64 | }) { 65 | orderStatus 66 | inbound { 67 | timeStamp 68 | } 69 | } 70 | }`, 71 | }); 72 | expect(result).toEqual({ 73 | data: { 74 | orderMany: [{ inbound: { timeStamp: '1970-01-01T00:00:00.456Z' }, orderStatus: 'UNPAID' }], 75 | }, 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/315-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | 5 | const schemaComposer = new SchemaComposer<{ req: any }>(); 6 | 7 | // mongoose.set('debug', true); 8 | 9 | const BookSchema = new mongoose.Schema({ 10 | _id: { type: Number }, 11 | title: { type: String }, 12 | date: { type: Date }, 13 | }); 14 | 15 | const BookModel = mongoose.model('Book', BookSchema); 16 | 17 | const BookTC = composeMongoose(BookModel, { schemaComposer }); 18 | const booksFindMany = BookTC.mongooseResolvers.findMany().addFilterArg({ 19 | name: 'from', 20 | type: 'Date', 21 | description: 'Appointment date should be after the provided date.', 22 | query: (rawQuery: any, value: Date) => { 23 | rawQuery.date = { 24 | $gte: value, 25 | ...(rawQuery.date && typeof rawQuery.date != 'object' 26 | ? { $eq: rawQuery.date } 27 | : rawQuery.date ?? {}), 28 | }; 29 | }, 30 | }); 31 | 32 | schemaComposer.Query.addFields({ 33 | booksMany: booksFindMany, 34 | }); 35 | 36 | const schema = schemaComposer.buildSchema(); 37 | 38 | beforeAll(async () => { 39 | await BookModel.base.createConnection(); 40 | await BookModel.create({ 41 | _id: 1, 42 | title: 'Atlas Shrugged', 43 | date: new Date('2020-01-01'), 44 | }); 45 | await BookModel.create({ 46 | _id: 2, 47 | title: 'Atlas Shrugged vol 2', 48 | date: new Date('2021-03-30'), 49 | }); 50 | }); 51 | afterAll(() => { 52 | mongoose.set('debug', false); 53 | BookModel.base.disconnect(); 54 | }); 55 | 56 | describe('Custom filters breaks mongo queries with 9.0.1 - issue #315', () => { 57 | it('check custom filter', async () => { 58 | const result = await graphql.graphql({ 59 | schema, 60 | source: `query { 61 | booksMany(filter: { from: "2021-01-01T00:00:00" }) { 62 | title 63 | } 64 | }`, 65 | }); 66 | expect(result).toEqual({ 67 | data: { 68 | booksMany: [{ title: 'Atlas Shrugged vol 2' }], 69 | }, 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/358-test.ts: -------------------------------------------------------------------------------- 1 | import { composeMongoose } from '../../index'; 2 | import { mongoose } from '../../__mocks__/mongooseCommon'; 3 | 4 | // mongoose.set('debug', true); 5 | 6 | const WithSubSchema = new mongoose.Schema({ 7 | subDocument: new mongoose.Schema({ 8 | field: { type: String, default: 'Hey' }, 9 | }), 10 | }); 11 | 12 | const WithoutSubSchema = new mongoose.Schema({ 13 | subDocument: { 14 | field: { type: String, default: 'Hey' }, 15 | }, 16 | }); 17 | 18 | const WithSubModel = mongoose.model('WithSubModel', WithSubSchema); 19 | const WithoutSubModel = mongoose.model('WithoutSubModel', WithoutSubSchema); 20 | 21 | describe('defaultsAsNonNull falsely reports non-nullability for subdocuments that have a Schema - issue #358', () => { 22 | it('with sub schema', async () => { 23 | const WithSubTC = composeMongoose(WithSubModel, { defaultsAsNonNull: true }); 24 | 25 | // sub-Schema breaks the "recursive default value assignation" behavior 26 | const data = new WithSubModel().subDocument; 27 | expect(data).toEqual(undefined); 28 | 29 | // so field should not be non-null 30 | expect(WithSubTC.getFieldTypeName('subDocument')).toBe('WithSubModelSubDocument'); 31 | }); 32 | 33 | it('as nested fields', async () => { 34 | const WithoutSubTC = composeMongoose(WithoutSubModel, { defaultsAsNonNull: true }); 35 | 36 | const data = new WithoutSubModel().subDocument; 37 | expect(data).toEqual({ field: 'Hey' }); 38 | 39 | // should be non-null! 40 | expect(WithoutSubTC.getFieldTypeName('subDocument')).toBe('WithoutSubModelSubDocument!'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/370-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Schema } from 'mongoose'; 5 | 6 | const schemaComposer = new SchemaComposer<{ req: any }>(); 7 | 8 | // mongoose.set('debug', true); 9 | 10 | const UserSchema = new mongoose.Schema({ 11 | firstName: { 12 | type: String, 13 | required: true, 14 | }, 15 | _organizationIds: [ 16 | { 17 | type: Schema.Types.ObjectId, 18 | ref: 'Organization', 19 | index: true, 20 | }, 21 | ], 22 | }); 23 | const UserModel = mongoose.model('User', UserSchema); 24 | const UserTC = composeMongoose(UserModel, { schemaComposer }); 25 | 26 | const OrganizationSchema = new mongoose.Schema({ 27 | title: { 28 | type: String, 29 | required: true, 30 | }, 31 | }); 32 | const OrganizationModel = mongoose.model('Organization', OrganizationSchema); 33 | const OrganizationTC = composeMongoose(OrganizationModel, { schemaComposer }); 34 | 35 | UserTC.addRelation('organizations', { 36 | resolver: () => OrganizationTC.mongooseResolvers.findByIds(), 37 | prepareArgs: { 38 | _ids: (source: any) => source._organizationIds, 39 | skip: null, 40 | sort: null, 41 | }, 42 | projection: { _organizationIds: 1 }, 43 | }); 44 | 45 | schemaComposer.Query.addFields({ 46 | users: UserTC.mongooseResolvers.findMany(), 47 | }); 48 | 49 | const schema = schemaComposer.buildSchema(); 50 | 51 | beforeAll(async () => { 52 | await OrganizationModel.base.createConnection(); 53 | const orgs = await OrganizationModel.create([ 54 | { title: 'Org1' }, 55 | { title: 'Org2' }, 56 | { title: 'Org3' }, 57 | ]); 58 | await UserModel.create([ 59 | { firstName: 'User1', _organizationIds: [orgs[0]._id, orgs[1]._id] }, 60 | { firstName: 'User2', _organizationIds: [orgs[2]._id] }, 61 | ]); 62 | }); 63 | afterAll(() => { 64 | OrganizationModel.base.disconnect(); 65 | }); 66 | 67 | describe('issue #370 - addRelation: projection not working as expected ', () => { 68 | it('check', async () => { 69 | const result = await graphql.graphql({ 70 | schema, 71 | source: `query { 72 | users(sort: _ID_ASC) { 73 | firstName 74 | organizations { 75 | title 76 | } 77 | } 78 | }`, 79 | }); 80 | expect(result).toEqual({ 81 | data: { 82 | users: [ 83 | { firstName: 'User1', organizations: [{ title: 'Org1' }, { title: 'Org2' }] }, 84 | { firstName: 'User2', organizations: [{ title: 'Org3' }] }, 85 | ], 86 | }, 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/376-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Schema } from 'mongoose'; 5 | 6 | const schemaComposer = new SchemaComposer<{ req: any }>(); 7 | 8 | // mongoose.set('debug', true); 9 | 10 | const UserSchema = new Schema( 11 | { 12 | name: { 13 | type: String, 14 | required: true, 15 | trim: true, 16 | }, 17 | orgId: { 18 | type: mongoose.Schema.Types.ObjectId, 19 | ref: 'Org', 20 | required: true, 21 | set: function (this: any, newOrgId: any) { 22 | // temporarily store previous org so 23 | // assignment to new org will work. 24 | this._prevOrg = this.orgId; 25 | return newOrgId; 26 | }, 27 | }, 28 | }, 29 | { 30 | collection: 'users', 31 | timestamps: { 32 | createdAt: 'created', 33 | updatedAt: 'modified', 34 | }, 35 | } 36 | ); 37 | const UserModel = mongoose.model('User', UserSchema); 38 | const UserTC = composeMongoose(UserModel, { schemaComposer }); 39 | 40 | const OrgSchema = new Schema( 41 | { 42 | name: { 43 | type: String, 44 | required: true, 45 | trim: true, 46 | unique: true, 47 | }, 48 | Users: [ 49 | { 50 | type: mongoose.Schema.Types.ObjectId, 51 | ref: 'User', 52 | }, 53 | ], 54 | }, 55 | { 56 | collection: 'orgs', 57 | timestamps: { 58 | createdAt: 'created', 59 | updatedAt: 'modified', 60 | }, 61 | } 62 | ); 63 | const OrgModel = mongoose.model('Org', OrgSchema); 64 | const OrgTC = composeMongoose(OrgModel, { schemaComposer }); 65 | 66 | UserTC.addRelation('org', { 67 | resolver: () => OrgTC.mongooseResolvers.findById(), 68 | prepareArgs: { 69 | // Define the args passed to the resolver (eg what the _id value should be) 70 | // Source is the filter passed to the user query 71 | _id: (source) => { 72 | // console.log(source); 73 | return source.orgId; 74 | }, 75 | }, 76 | projection: { orgId: true }, // Additional fields from UserSchema we need to pass to the Org resolver 77 | }); 78 | 79 | schemaComposer.Query.addFields({ 80 | users: UserTC.mongooseResolvers.findMany(), 81 | }); 82 | 83 | const schema = schemaComposer.buildSchema(); 84 | 85 | beforeAll(async () => { 86 | await OrgModel.base.createConnection(); 87 | const orgs = await OrgModel.create([ 88 | { name: 'Organization1' }, 89 | { name: 'Organization2' }, 90 | { name: 'Organization3' }, 91 | ]); 92 | await UserModel.create([ 93 | { name: 'User1', orgId: orgs[1]._id }, 94 | { name: 'User2', orgId: orgs[2]._id }, 95 | ]); 96 | }); 97 | afterAll(() => { 98 | OrgModel.base.disconnect(); 99 | }); 100 | 101 | describe('issue #376 - Projection not being added to query in relation', () => { 102 | it('check', async () => { 103 | const result = await graphql.graphql({ 104 | schema, 105 | source: `query { 106 | users(sort: _ID_ASC) { 107 | name 108 | org { 109 | name 110 | } 111 | } 112 | }`, 113 | }); 114 | expect(result).toEqual({ 115 | data: { 116 | users: [ 117 | { name: 'User1', org: { name: 'Organization2' } }, 118 | { name: 'User2', org: { name: 'Organization3' } }, 119 | ], 120 | }, 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/377-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, graphql } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Schema } from 'mongoose'; 5 | 6 | const schemaComposer = new SchemaComposer<{ req: any }>(); 7 | 8 | // mongoose.set('debug', true); 9 | 10 | const PrevisitSchema = new Schema( 11 | { 12 | name: { 13 | type: String, 14 | required: true, 15 | trim: true, 16 | }, 17 | project_id: { type: Schema.Types.ObjectId, required: true }, 18 | }, 19 | { 20 | collection: 'previsits', 21 | } 22 | ); 23 | const PrevisitModel = mongoose.model('Previsit', PrevisitSchema); 24 | const PrevisitTC = composeMongoose(PrevisitModel, { schemaComposer }); 25 | 26 | const ProjectSchema = new Schema( 27 | { 28 | name: { 29 | type: String, 30 | required: true, 31 | trim: true, 32 | unique: true, 33 | }, 34 | }, 35 | { 36 | collection: 'projects', 37 | } 38 | ); 39 | const ProjectModel = mongoose.model('Project', ProjectSchema); 40 | const ProjectTC = composeMongoose(ProjectModel, { schemaComposer }); 41 | 42 | PrevisitTC.addRelation('project', { 43 | resolver: () => ProjectTC.mongooseResolvers.dataLoader({ lean: true }), 44 | prepareArgs: { 45 | _id: (source) => source.project_id || null, 46 | }, 47 | projection: { project_id: 1 }, 48 | }); 49 | 50 | schemaComposer.Query.addFields({ 51 | previsitMany: PrevisitTC.mongooseResolvers.findMany(), 52 | }); 53 | 54 | const schema = schemaComposer.buildSchema(); 55 | 56 | beforeAll(async () => { 57 | await ProjectModel.base.createConnection(); 58 | const projects = await ProjectModel.create([ 59 | { name: 'Project1' }, 60 | { name: 'Project2' }, 61 | { name: 'Project3' }, 62 | ]); 63 | await PrevisitModel.create([ 64 | { name: 'Previsit1', project_id: projects[0] }, 65 | { name: 'Previsit2', project_id: projects[1] }, 66 | { name: 'Previsit3', project_id: projects[2] }, 67 | ]); 68 | }); 69 | afterAll(() => { 70 | ProjectModel.base.disconnect(); 71 | }); 72 | 73 | describe('issue #377 - Missing fields from projection in addRelation', () => { 74 | it('check', async () => { 75 | const result = await graphql.graphql({ 76 | schema, 77 | contextValue: {}, 78 | source: ` 79 | { 80 | previsitMany(sort: _ID_ASC) { 81 | name 82 | project { 83 | name 84 | } 85 | } 86 | } 87 | `, 88 | }); 89 | expect(result).toEqual({ 90 | data: { 91 | previsitMany: [ 92 | { name: 'Previsit1', project: { name: 'Project1' } }, 93 | { name: 'Previsit2', project: { name: 'Project2' } }, 94 | { name: 'Previsit3', project: { name: 'Project3' } }, 95 | ], 96 | }, 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/384-test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, graphql, EnumTypeComposer } from 'graphql-compose'; 2 | import { composeMongoose } from '../../index'; 3 | import { mongoose } from '../../__mocks__/mongooseCommon'; 4 | import { Schema } from 'mongoose'; 5 | 6 | const schemaComposer = new SchemaComposer<{ req: any }>(); 7 | 8 | // mongoose.set('debug', true); 9 | 10 | const ArticleSchema = new Schema( 11 | { 12 | name: { 13 | type: String, 14 | index: true, 15 | }, 16 | label: { 17 | type: String, 18 | enum: ['', null, 'val1'], 19 | }, 20 | }, 21 | { 22 | collection: 'article', 23 | } 24 | ); 25 | const ArticleModel = mongoose.model('Article', ArticleSchema); 26 | const ArticleTC = composeMongoose(ArticleModel, { schemaComposer }); 27 | 28 | let lastResolverInputArg = [] as any; 29 | const enumTC = ArticleTC.getFieldTC('label') as EnumTypeComposer; 30 | schemaComposer.Query.addFields({ 31 | articles: ArticleTC.mongooseResolvers.findMany(), 32 | test: { 33 | type: enumTC.List, 34 | args: { input: enumTC.List }, 35 | resolve: (_, { input }) => { 36 | lastResolverInputArg = [...input]; 37 | return input; 38 | }, 39 | }, 40 | }); 41 | 42 | const schema = schemaComposer.buildSchema(); 43 | 44 | beforeAll(async () => { 45 | await ArticleModel.base.createConnection(); 46 | await ArticleModel.create([ 47 | { name: 'A1', label: null }, 48 | { name: 'A2', label: '' }, 49 | { name: 'A3', label: 'val1' }, 50 | ]); 51 | }); 52 | afterAll(() => { 53 | ArticleModel.base.disconnect(); 54 | }); 55 | 56 | describe('issue #384 - New feature Request: To allow null, string in Enum', () => { 57 | it('check SDL', async () => { 58 | expect(ArticleTC.toSDL({ omitDescriptions: true, deep: true, omitScalars: true })) 59 | .toMatchInlineSnapshot(` 60 | "type Article { 61 | name: String 62 | label: EnumArticleLabel 63 | _id: MongoID! 64 | } 65 | 66 | enum EnumArticleLabel { 67 | EMPTY_STRING 68 | NULL 69 | val1 70 | }" 71 | `); 72 | }); 73 | 74 | it('check runtime output', async () => { 75 | const result = await graphql.graphql({ 76 | schema, 77 | contextValue: {}, 78 | source: ` 79 | { 80 | articles(sort: NAME_ASC) { 81 | name 82 | label 83 | } 84 | } 85 | `, 86 | }); 87 | expect(result).toEqual({ 88 | data: { 89 | articles: [ 90 | { label: null, name: 'A1' }, // <-- special `null` case. It cannot be converted to NULL string 91 | { label: 'EMPTY_STRING', name: 'A2' }, // <-- has correct ENUM key 92 | { label: 'val1', name: 'A3' }, 93 | ], 94 | }, 95 | }); 96 | }); 97 | 98 | it('check runtime input', async () => { 99 | const result = await graphql.graphql({ 100 | schema, 101 | contextValue: {}, 102 | source: ` 103 | { 104 | test(input: [val1, NULL, EMPTY_STRING]) 105 | } 106 | `, 107 | }); 108 | 109 | // inside resolvers should be provided real values for null & string 110 | expect(lastResolverInputArg).toEqual(['val1', null, '']); 111 | 112 | // in output JSON should be provided keys 113 | // BUT be aware `null` does not converted back to `NULL` string 114 | expect(result).toEqual({ data: { test: ['val1', null, 'EMPTY_STRING'] } }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/78-test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | import { schemaComposer, graphql } from 'graphql-compose'; 4 | import { composeWithMongoose } from '../../index'; 5 | import { getPortFree } from '../../__mocks__/mongooseCommon'; 6 | 7 | let mongoServer: MongoMemoryServer; 8 | beforeAll(async () => { 9 | mongoServer = await MongoMemoryServer.create({ 10 | instance: { 11 | port: await getPortFree(), 12 | }, 13 | }); 14 | const mongoUri = mongoServer.getUri(); 15 | await mongoose.connect( 16 | mongoUri, 17 | { 18 | useNewUrlParser: true, 19 | useUnifiedTopology: true, 20 | } as any /* for tests compatibility with mongoose v5 & v6 */ 21 | ); 22 | }); 23 | 24 | afterAll(() => { 25 | mongoose.disconnect(); 26 | mongoServer.stop(); 27 | }); 28 | 29 | describe('issue #78 - Mongoose and Discriminators', () => { 30 | const options = { discriminatorKey: 'kind' }; 31 | 32 | const eventSchema = new mongoose.Schema({ refId: String }, options); 33 | const Event = mongoose.model('GenericEvent', eventSchema); 34 | 35 | const clickedLinkSchema = new mongoose.Schema({ url: String }, options); 36 | const ClickedLinkEvent = Event.discriminator('ClickedLinkEvent', clickedLinkSchema); 37 | 38 | const EventTC = composeWithMongoose(Event); 39 | const ClickedLinkEventTC = composeWithMongoose(ClickedLinkEvent); 40 | 41 | it('creating Types from models', () => { 42 | expect(EventTC.getFieldNames()).toEqual(['refId', '_id', 'kind']); 43 | expect(ClickedLinkEventTC.getFieldNames()).toEqual(['url', '_id', 'refId', 'kind']); 44 | }); 45 | 46 | it('manually override resolver output type for findMany', async () => { 47 | const EventDescriminatorType = new graphql.GraphQLUnionType({ 48 | name: 'EventDescriminator', 49 | types: [EventTC.getType(), ClickedLinkEventTC.getType()], 50 | resolveType: (value) => { 51 | if (value.kind === 'ClickedLinkEvent') { 52 | return ClickedLinkEventTC.getTypeName(); 53 | } 54 | return EventTC.getTypeName(); 55 | }, 56 | }); 57 | 58 | EventTC.getResolver('findMany').setType(new graphql.GraphQLList(EventDescriminatorType)); 59 | 60 | // let's check graphql response 61 | 62 | await Event.create({ refId: 'aaa' }); 63 | await Event.create({ refId: 'bbb' }); 64 | await ClickedLinkEvent.create({ refId: 'ccc', url: 'url1' }); 65 | await ClickedLinkEvent.create({ refId: 'ddd', url: 'url2' }); 66 | 67 | schemaComposer.Query.addFields({ 68 | eventFindMany: EventTC.getResolver('findMany'), 69 | }); 70 | const schema = schemaComposer.buildSchema(); 71 | 72 | const res = await graphql.graphql({ 73 | schema, 74 | source: `{ 75 | eventFindMany { 76 | __typename 77 | ... on GenericEvent { 78 | refId 79 | } 80 | ... on ClickedLinkEvent { 81 | refId 82 | url 83 | } 84 | } 85 | }`, 86 | }); 87 | 88 | expect(res).toEqual({ 89 | data: { 90 | eventFindMany: [ 91 | { __typename: 'GenericEvent', refId: 'aaa' }, 92 | { __typename: 'GenericEvent', refId: 'bbb' }, 93 | { __typename: 'ClickedLinkEvent', refId: 'ccc', url: 'url1' }, 94 | { __typename: 'ClickedLinkEvent', refId: 'ddd', url: 'url2' }, 95 | ], 96 | }, 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/92-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, graphql } from 'graphql-compose'; 2 | import { composeWithMongoose } from '../../index'; 3 | import { UserModel } from '../../__mocks__/userModel'; 4 | 5 | beforeAll(() => UserModel.base.createConnection()); 6 | afterAll(() => UserModel.base.disconnect()); 7 | 8 | const UserTC = composeWithMongoose(UserModel); 9 | schemaComposer.Query.addFields({ 10 | users: UserTC.getResolver('findMany'), 11 | }); 12 | 13 | describe('issue #92 - How to verify the fields?', () => { 14 | UserTC.wrapResolverResolve('createOne', (next) => (rp) => { 15 | if (rp.args.record.age < 21) throw new Error('You are too young'); 16 | if (rp.args.record.age > 60) throw new Error('You are too old'); 17 | return next(rp); 18 | }); 19 | 20 | schemaComposer.Mutation.addFields({ 21 | addUser: UserTC.getResolver('createOne'), 22 | }); 23 | const schema = schemaComposer.buildSchema(); 24 | 25 | it('correct request', async () => { 26 | const result: any = await graphql.graphql({ 27 | schema, 28 | source: ` 29 | mutation { 30 | addUser(record: { name: "User1", age: 30, contacts: { email: "1@1.com" } }) { 31 | record { 32 | name 33 | age 34 | } 35 | } 36 | } 37 | `, 38 | }); 39 | expect(result).toEqual({ data: { addUser: { record: { age: 30, name: 'User1' } } } }); 40 | }); 41 | 42 | it('wrong request', async () => { 43 | const result: any = await graphql.graphql({ 44 | schema, 45 | source: ` 46 | mutation { 47 | addUser(record: { name: "User1", age: 10, contacts: { email: "1@1.com" } }) { 48 | record { 49 | name 50 | age 51 | } 52 | } 53 | } 54 | `, 55 | }); 56 | expect(result).toEqual({ data: { addUser: null }, errors: expect.anything() }); 57 | expect(result.errors[0].message).toBe('You are too young'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/93-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer, graphql } from 'graphql-compose'; 2 | import { composeWithMongoose } from '../../index'; 3 | import { UserModel } from '../../__mocks__/userModel'; 4 | 5 | beforeAll(() => UserModel.base.createConnection()); 6 | afterAll(() => UserModel.base.disconnect()); 7 | 8 | const UserTC = composeWithMongoose(UserModel); 9 | schemaComposer.Query.addFields({ 10 | users: UserTC.getResolver('findMany'), 11 | }); 12 | 13 | describe('issue #93', () => { 14 | it('$or, $and operator for filtering', async () => { 15 | schemaComposer.Query.addFields({ 16 | users: UserTC.getResolver('findMany'), 17 | }); 18 | const schema = schemaComposer.buildSchema(); 19 | await UserModel.create({ 20 | _id: '100000000000000000000301', 21 | name: 'User301', 22 | age: 301, 23 | contacts: { email: '1@1.com' }, 24 | }); 25 | await UserModel.create({ 26 | _id: '100000000000000000000302', 27 | name: 'User302', 28 | age: 302, 29 | gender: 'male', 30 | contacts: { email: '2@2.com' }, 31 | }); 32 | await UserModel.create({ 33 | _id: '100000000000000000000303', 34 | name: 'User303', 35 | age: 302, 36 | gender: 'female', 37 | contacts: { email: '3@3.com' }, 38 | }); 39 | 40 | const res = await graphql.graphql({ 41 | schema, 42 | source: ` 43 | { 44 | users(filter: { OR: [{ age: 301 }, { AND: [{ gender: male }, { age: 302 }] }] }) { 45 | name 46 | } 47 | } 48 | `, 49 | }); 50 | expect(res).toEqual({ data: { users: [{ name: 'User301' }, { name: 'User302' }] } }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/__tests__/github_issues/gc282-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { schemaComposer, graphql } from 'graphql-compose'; 4 | import { composeWithMongoose } from '../../index'; 5 | import { mongoose } from '../../__mocks__/mongooseCommon'; 6 | 7 | const EventSchema = new mongoose.Schema( 8 | { 9 | timestamp: { type: Date, required: true }, 10 | level: { type: String, required: true }, 11 | message: { type: String, required: true }, 12 | meta: { 13 | topic: { type: String }, 14 | subjects: { type: [String] }, 15 | variables: { type: Map, of: String }, 16 | }, 17 | }, 18 | { collection: 'events' } 19 | ); 20 | 21 | const EventModel = mongoose.model('Event', EventSchema); 22 | const EventTC = composeWithMongoose(EventModel); 23 | EventTC.getResolvers().forEach((resolver) => { 24 | const newResolver = resolver.addFilterArg({ 25 | name: 'subjects', 26 | filterTypeNameFallback: 'FilterEventInput', 27 | type: '[String]', 28 | query: (query, value) => { 29 | query['meta.subjects'] = { $elemMatch: { $in: value } }; 30 | }, 31 | }); 32 | 33 | EventTC.setResolver(resolver.name, newResolver); 34 | }); 35 | schemaComposer.Query.addFields({ 36 | eventMany: EventTC.getResolver('findMany'), 37 | }); 38 | const schema = schemaComposer.buildSchema(); 39 | 40 | beforeAll(async () => { 41 | await EventModel.base.createConnection(); 42 | await EventModel.create({ 43 | timestamp: new Date(), 44 | level: 'status', 45 | message: 'event1', 46 | meta: { 47 | topic: 'topic', 48 | subjects: ['metaValue'], 49 | }, 50 | }); 51 | await EventModel.create({ 52 | timestamp: new Date(), 53 | level: 'status', 54 | message: 'event2', 55 | meta: { 56 | topic: 'topic', 57 | subjects: ['notMetaValue'], 58 | }, 59 | }); 60 | }); 61 | afterAll(() => EventModel.base.disconnect()); 62 | 63 | // Issue from graphql-compose repo 64 | // @see https://github.com/graphql-compose/graphql-compose/pull/282 65 | describe('graphql-compose/issue #282 - Filter nested array by string', () => { 66 | it('correct request', async () => { 67 | expect( 68 | await graphql.graphql({ 69 | schema, 70 | source: `query { 71 | eventMany(filter: { subjects: ["notMetaValue"] }) { 72 | message 73 | meta { 74 | subjects 75 | } 76 | } 77 | }`, 78 | }) 79 | ).toEqual({ 80 | data: { eventMany: [{ message: 'event2', meta: { subjects: ['notMetaValue'] } }] }, 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/composeWithMongoose.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define, no-param-reassign, global-require */ 2 | 3 | import type { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; 4 | import { schemaComposer as globalSchemaComposer } from 'graphql-compose'; 5 | import type { Model, Document } from 'mongoose'; 6 | import { convertModelToGraphQL } from './fieldsConverter'; 7 | import { resolverFactory, AllResolversOpts } from './resolvers'; 8 | import MongoID from './types/MongoID'; 9 | import { GraphQLResolveInfo } from 'graphql'; 10 | import { TypeConverterInputTypeOpts, prepareFields, createInputType } from './composeMongoose'; 11 | 12 | export type ComposeWithMongooseOpts = { 13 | schemaComposer?: SchemaComposer; 14 | name?: string; 15 | description?: string; 16 | fields?: { 17 | only?: string[]; 18 | // rename?: { [oldName: string]: string }, 19 | remove?: string[]; 20 | }; 21 | inputType?: TypeConverterInputTypeOpts; 22 | resolvers?: false | AllResolversOpts; 23 | /** You may customize document id */ 24 | transformRecordId?: TransformRecordIdFn; 25 | }; 26 | 27 | export type TransformRecordIdFn = ( 28 | source: Document, 29 | context: TContext, 30 | info: GraphQLResolveInfo 31 | ) => any; 32 | 33 | export function composeWithMongoose( 34 | model: Model, 35 | opts: ComposeWithMongooseOpts = {} 36 | ): ObjectTypeComposer { 37 | const m = model as Model; 38 | const name: string = (opts && opts.name) || m.modelName; 39 | 40 | const sc = opts.schemaComposer || globalSchemaComposer; 41 | sc.add(MongoID); 42 | 43 | if (sc.has(name)) { 44 | throw new Error( 45 | `You try to generate GraphQL Type with name ${name} from mongoose model but this type already exists in SchemaComposer. Please choose another type name "composeWithMongoose(model, { name: 'NewTypeName' })", or reuse existed type "schemaComposer.getOTC('TypeName')", or remove type from SchemaComposer before calling composeWithMongoose method "schemaComposer.delete('TypeName')".` 46 | ); 47 | } 48 | if (sc.has(m.schema)) { 49 | // looks like you want to generate new TypeComposer from model 50 | // so remove cached model (which is used for cross-reference types) 51 | sc.delete(m.schema); 52 | } 53 | 54 | const tc = convertModelToGraphQL(m, name, sc); 55 | 56 | if (opts.description) { 57 | tc.setDescription(opts.description); 58 | } 59 | 60 | prepareFields(tc, opts); 61 | 62 | createInputType(tc, opts.inputType); 63 | 64 | if (!{}.hasOwnProperty.call(opts, 'resolvers') || opts.resolvers !== false) { 65 | createResolvers(m, tc, opts.resolvers || {}); 66 | } 67 | 68 | tc.makeFieldNonNull('_id'); 69 | 70 | return tc; 71 | } 72 | 73 | export function createResolvers( 74 | model: Model, 75 | tc: ObjectTypeComposer, 76 | opts: AllResolversOpts 77 | ): void { 78 | (Object.keys(resolverFactory) as any).forEach((resolverName: keyof typeof resolverFactory) => { 79 | if (!opts.hasOwnProperty(resolverName) || opts[resolverName] !== false) { 80 | const createResolverFn = resolverFactory[resolverName] as any; 81 | if (typeof createResolverFn === 'function') { 82 | const resolver = createResolverFn(model, tc, opts[resolverName] || {}); 83 | if (resolver) { 84 | tc.setResolver(resolverName, resolver); 85 | } 86 | } 87 | } 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/composeWithMongooseDiscriminators.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer as globalSchemaComposer } from 'graphql-compose'; 2 | import type { Model } from 'mongoose'; 3 | import { ComposeWithMongooseDiscriminatorsOpts, DiscriminatorTypeComposer } from './discriminators'; 4 | 5 | export * from './discriminators'; 6 | 7 | export function composeWithMongooseDiscriminators( 8 | baseModel: Model, 9 | opts?: ComposeWithMongooseDiscriminatorsOpts 10 | ): DiscriminatorTypeComposer { 11 | const sc = opts?.schemaComposer || globalSchemaComposer; 12 | return DiscriminatorTypeComposer.createFromModel(baseModel, sc, opts); 13 | } 14 | -------------------------------------------------------------------------------- /src/discriminators/__mocks__/characterModels.ts: -------------------------------------------------------------------------------- 1 | import type { Model } from 'mongoose'; 2 | import { mongoose, Schema, Types } from '../../__mocks__/mongooseCommon'; 3 | import { DroidSchema } from './droidSchema'; 4 | import { PersonSchema } from './personSchema'; 5 | 6 | const enumCharacterType = { 7 | PERSON: 'Person', 8 | DROID: 'Droid', 9 | }; 10 | 11 | export const CharacterObject = { 12 | _id: { 13 | type: String, 14 | default: (): any => new Types.ObjectId(), 15 | }, 16 | name: String, 17 | 18 | type: { 19 | type: String, 20 | require: true, 21 | enum: Object.keys(enumCharacterType), 22 | }, 23 | kind: { 24 | type: String, 25 | require: true, 26 | enum: Object.keys(enumCharacterType), 27 | }, 28 | 29 | friends: [String], // another Character 30 | appearsIn: [String], // movie 31 | }; 32 | 33 | const CharacterSchema = new Schema(CharacterObject); 34 | const ACharacterSchema = new Schema({ ...CharacterObject }); 35 | 36 | export function getCharacterModels(DKey: string): { 37 | CharacterModel: Model; 38 | PersonModel: Model; 39 | DroidModel: Model; 40 | } { 41 | CharacterSchema.set('discriminatorKey', DKey); 42 | 43 | const CharacterModel: Model = mongoose.models.Character 44 | ? mongoose.models.Character 45 | : mongoose.model('Character', CharacterSchema); 46 | 47 | const PersonModel: Model = mongoose.models[enumCharacterType.PERSON] 48 | ? mongoose.models[enumCharacterType.PERSON] 49 | : CharacterModel.discriminator(enumCharacterType.PERSON, PersonSchema); 50 | 51 | const DroidModel: Model = mongoose.models[enumCharacterType.DROID] 52 | ? mongoose.models[enumCharacterType.DROID] 53 | : CharacterModel.discriminator(enumCharacterType.DROID, DroidSchema); 54 | 55 | return { CharacterModel, PersonModel, DroidModel }; 56 | } 57 | 58 | export function getCharacterModelClone(): { NoDKeyCharacterModel: Model } { 59 | const NoDKeyCharacterModel = mongoose.model('NoDKeyCharacter', ACharacterSchema); 60 | 61 | /* 62 | const APersonModel = ACharacterModel.discriminator('A' + enumCharacterType.PERSON, PersonSchema.clone()); 63 | 64 | const ADroidModel = ACharacterModel.discriminator('A' + enumCharacterType.DROID, DroidSchema.clone()); 65 | */ 66 | 67 | return { NoDKeyCharacterModel }; // APersonModel, ADroidModel }; 68 | } 69 | -------------------------------------------------------------------------------- /src/discriminators/__mocks__/droidSchema.ts: -------------------------------------------------------------------------------- 1 | import type { Schema as SchemaType } from 'mongoose'; 2 | import { Schema } from '../../__mocks__/mongooseCommon'; 3 | 4 | export const DroidSchema: SchemaType = new Schema({ 5 | makeDate: Date, 6 | modelNumber: Number, 7 | primaryFunction: [String], 8 | }); 9 | -------------------------------------------------------------------------------- /src/discriminators/__mocks__/movieModel.ts: -------------------------------------------------------------------------------- 1 | import { mongoose, Schema } from '../../__mocks__/mongooseCommon'; 2 | 3 | const MovieSchema = new Schema({ 4 | _id: String, 5 | 6 | characters: { 7 | type: [String], // redundant but i need it. 8 | description: 'A character in the Movie, Person or Droid.', 9 | }, 10 | 11 | director: { 12 | type: String, // id of director 13 | description: 'Directed the movie.', 14 | }, 15 | 16 | imdbRatings: String, 17 | releaseDate: String, 18 | }); 19 | 20 | export const MovieModel = mongoose.model('Movie', MovieSchema); 21 | -------------------------------------------------------------------------------- /src/discriminators/__mocks__/personSchema.ts: -------------------------------------------------------------------------------- 1 | import type { Schema as SchemaType } from 'mongoose'; 2 | import { Schema } from '../../__mocks__/mongooseCommon'; 3 | 4 | export const PersonSchema: SchemaType = new Schema({ 5 | dob: Number, 6 | starShips: [String], 7 | totalCredits: Number, 8 | }); 9 | -------------------------------------------------------------------------------- /src/discriminators/__tests__/composeChildTC-test.ts: -------------------------------------------------------------------------------- 1 | import { schemaComposer } from 'graphql-compose'; 2 | import { getCharacterModels } from '../__mocks__/characterModels'; 3 | import { composeWithMongooseDiscriminators } from '../../composeWithMongooseDiscriminators'; 4 | 5 | const { CharacterModel, PersonModel, DroidModel } = getCharacterModels('type'); 6 | 7 | beforeAll(() => schemaComposer.clear()); 8 | 9 | describe('composeChildTC ->', () => { 10 | const CharacterDTC = composeWithMongooseDiscriminators(CharacterModel); 11 | CharacterDTC.addRelation('friends', { 12 | resolver: () => CharacterDTC.getResolver('findById'), 13 | prepareArgs: { 14 | _id: (source) => source.friends, 15 | }, 16 | projection: { friends: 1 }, 17 | }); 18 | CharacterDTC.extendField('friends', { type: '[String!]' }); 19 | 20 | const PersonTC = CharacterDTC.discriminator(PersonModel); 21 | const DroidTC = CharacterDTC.discriminator(DroidModel); 22 | 23 | it('should set DInterface to childTC', () => { 24 | expect(DroidTC.hasInterface(CharacterDTC.getDInterface())).toBeTruthy(); 25 | expect(PersonTC.hasInterface(CharacterDTC.getDInterface())).toBeTruthy(); 26 | }); 27 | 28 | it('should copy all baseFields from BaseDTC to ChildTCs', () => { 29 | expect(DroidTC.getFieldNames()).toEqual(expect.arrayContaining(CharacterDTC.getFieldNames())); 30 | expect(PersonTC.getFieldNames()).toEqual(expect.arrayContaining(CharacterDTC.getFieldNames())); 31 | }); 32 | 33 | it('should copy all relations from BaseDTC to ChildTCs', () => { 34 | expect(DroidTC.getRelations()).toEqual(CharacterDTC.getRelations()); 35 | expect(PersonTC.getRelations()).toEqual(CharacterDTC.getRelations()); 36 | }); 37 | 38 | it('should copy the extended Field from BaseDTC to ChildTCs', () => { 39 | expect(DroidTC.getField('friends').type).toEqual(CharacterDTC.getField('friends').type); 40 | expect(PersonTC.getField('friends').type).toEqual(CharacterDTC.getField('friends').type); 41 | }); 42 | 43 | it('should make childTC have same fieldTypes as baseTC', () => { 44 | const characterFields = CharacterDTC.getFieldNames(); 45 | 46 | for (const field of characterFields) { 47 | expect(DroidTC.getFieldType(field)).toEqual(CharacterDTC.getFieldType(field)); 48 | expect(PersonTC.getFieldType(field)).toEqual(CharacterDTC.getFieldType(field)); 49 | } 50 | }); 51 | 52 | it('should operate normally like any other ObjectTypeComposer', () => { 53 | const fields = PersonTC.getFieldNames(); 54 | 55 | PersonTC.addFields({ 56 | field: { type: 'String' }, 57 | }); 58 | 59 | expect(PersonTC.getFieldNames()).toEqual(fields.concat(['field'])); 60 | 61 | PersonTC.removeField('field'); 62 | expect(PersonTC.getFieldNames()).toEqual(fields); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/discriminators/composeChildTC.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeComposer } from 'graphql-compose'; 2 | import type { 3 | DiscriminatorTypeComposer, 4 | ComposeWithMongooseDiscriminatorsOpts, 5 | } from './DiscriminatorTypeComposer'; 6 | import { prepareChildResolvers } from './prepareChildResolvers'; 7 | import { reorderFields } from './utils/reorderFields'; 8 | 9 | function copyBaseTcRelationsToChildTc( 10 | baseDTC: ObjectTypeComposer, 11 | childTC: ObjectTypeComposer 12 | ) { 13 | const relations = baseDTC.getRelations(); 14 | const childRelations = childTC.getRelations(); 15 | Object.keys(relations).forEach((name) => { 16 | if (childRelations[name]) { 17 | return; 18 | } 19 | childTC.addRelation(name, relations[name]); 20 | }); 21 | 22 | return childTC; 23 | } 24 | 25 | // copy all baseTypeComposer fields to childTC 26 | // these are the fields before calling discriminator 27 | function copyBaseTCFieldsToChildTC( 28 | baseDTC: ObjectTypeComposer, 29 | childTC: ObjectTypeComposer 30 | ) { 31 | const baseFields = baseDTC.getFieldNames(); 32 | const childFields = childTC.getFieldNames(); 33 | 34 | for (const field of baseFields) { 35 | const isFieldExists = childFields.find((fld) => fld === field); 36 | 37 | if (isFieldExists) { 38 | childTC.extendField(field, { 39 | type: baseDTC.getField(field).type, 40 | }); 41 | } else { 42 | childTC.setField(field, baseDTC.getField(field)); 43 | } 44 | } 45 | 46 | return childTC; 47 | } 48 | 49 | export function composeChildTC( 50 | baseDTC: DiscriminatorTypeComposer, 51 | childTC: ObjectTypeComposer, 52 | opts: ComposeWithMongooseDiscriminatorsOpts 53 | ): ObjectTypeComposer { 54 | let composedChildTC = copyBaseTcRelationsToChildTc(baseDTC, childTC); 55 | composedChildTC = copyBaseTCFieldsToChildTC(baseDTC, composedChildTC); 56 | 57 | composedChildTC.addInterface(baseDTC.getDInterface()); 58 | 59 | prepareChildResolvers(baseDTC, composedChildTC, opts); 60 | 61 | reorderFields(composedChildTC, opts.reorderFields, baseDTC.getDKey(), baseDTC.getFieldNames()); 62 | 63 | return composedChildTC; 64 | } 65 | -------------------------------------------------------------------------------- /src/discriminators/index.ts: -------------------------------------------------------------------------------- 1 | export { DiscriminatorTypeComposer } from './DiscriminatorTypeComposer'; 2 | export type { ComposeWithMongooseDiscriminatorsOpts } from './DiscriminatorTypeComposer'; 3 | 4 | export { mergeCustomizationOptions } from './utils/mergeCustomizationOptions'; 5 | -------------------------------------------------------------------------------- /src/discriminators/prepareBaseResolvers.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver } from 'graphql-compose'; 2 | import { resolverFactory } from '../resolvers'; 3 | import { DiscriminatorTypeComposer } from './DiscriminatorTypeComposer'; 4 | 5 | // change type on DKey generated by composeWithMongoose 6 | // set it to created enum ObjectTypeComposer for DKey DKeyETC 7 | // only sets on filter and record typeComposers, since they contain our DKey 8 | function setDKeyEnumOnITCArgs(resolver: Resolver, baseTC: DiscriminatorTypeComposer) { 9 | // setDKeyEnum for filter types, and on record types 10 | if (resolver) { 11 | const argNames = resolver.getArgNames(); 12 | 13 | for (const argName of argNames) { 14 | if (argName === 'filter' || argName === 'record' || argName === 'records') { 15 | const filterArgTC = resolver.getArgITC(argName); 16 | 17 | if (filterArgTC) { 18 | filterArgTC.extendField(baseTC.getDKey(), { 19 | type: baseTC.getDKeyETC(), 20 | }); 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | // recomposing sets up the DInterface as the return types for 28 | // Also sets up DKey enum as type for DKey field on composers with filter and/or record args 29 | // composeWithMongoose composers 30 | export function prepareBaseResolvers(baseTC: DiscriminatorTypeComposer): void { 31 | Object.keys(resolverFactory).forEach((resolverName) => { 32 | if (baseTC.hasResolver(resolverName)) { 33 | const resolver = baseTC.getResolver(resolverName); 34 | 35 | switch (resolverName) { 36 | case 'findMany': 37 | case 'findByIds': 38 | resolver.setType(baseTC.getDInterface().List); 39 | resolver.projection[baseTC.getDKey()] = 1; 40 | break; 41 | 42 | case 'findById': 43 | case 'findOne': 44 | resolver.setType(baseTC.getDInterface()); 45 | resolver.projection[baseTC.getDKey()] = 1; 46 | break; 47 | 48 | case 'createOne': 49 | case 'updateOne': 50 | case 'updateById': 51 | case 'removeOne': 52 | case 'removeById': 53 | resolver.getOTC().extendField('record', { 54 | type: baseTC.getDInterface(), 55 | projection: { 56 | [baseTC.getDKey()]: 1, 57 | }, 58 | }); 59 | break; 60 | 61 | case 'createMany': 62 | resolver.getOTC().extendField('records', { 63 | type: baseTC.getDInterface().List.NonNull, 64 | projection: { 65 | [baseTC.getDKey()]: 1, 66 | }, 67 | }); 68 | break; 69 | 70 | case 'pagination': 71 | resolver.getOTC().extendField('items', { 72 | type: baseTC.getDInterface().List, 73 | projection: { 74 | [baseTC.getDKey()]: 1, 75 | }, 76 | }); 77 | break; 78 | 79 | case 'connection': 80 | const edgesTC = resolver 81 | .getOTC() 82 | .getFieldOTC('edges') 83 | .clone(`${baseTC.getTypeName()}Edge`); 84 | 85 | edgesTC.extendField('node', { 86 | type: baseTC.getDInterface().NonNull, 87 | projection: { 88 | [baseTC.getDKey()]: 1, 89 | }, 90 | }); 91 | 92 | resolver.getOTC().setField('edges', edgesTC.NonNull.List.NonNull); 93 | break; 94 | 95 | default: 96 | } 97 | 98 | setDKeyEnumOnITCArgs(resolver, baseTC); 99 | 100 | // set DKey as required field to create from base 101 | // must be done after setting DKeyEnum 102 | if (resolverName === 'createOne' || resolverName === 'createMany') { 103 | const fieldName = resolverName === 'createMany' ? 'records' : 'record'; 104 | resolver.getArgITC(fieldName).extendField(baseTC.getDKey(), { 105 | type: baseTC.getDKeyETC().NonNull, 106 | }); 107 | } 108 | } 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /src/discriminators/utils/mergeCustomizationOptions.ts: -------------------------------------------------------------------------------- 1 | import type { ComposeWithMongooseOpts } from '../../composeWithMongoose'; 2 | import { mergeTypeConverterResolverOpts } from './mergeTypeConverterResolversOpts'; 3 | 4 | type FieldMap = { 5 | [fieldName: string]: string[] | typeof undefined; 6 | }; 7 | 8 | export function mergeStringAndStringArraysFields( 9 | baseField?: string[] | string, 10 | childField?: string[] | string, 11 | argOptsTypes?: string[] | string 12 | ): string[] | typeof undefined { 13 | if (Array.isArray(argOptsTypes)) { 14 | if (argOptsTypes.find((v) => v === 'string' || v === 'string[]')) { 15 | return mergeStringAndStringArraysFields(baseField, childField, 'string'); 16 | } 17 | } 18 | 19 | let merged = childField; 20 | 21 | if (argOptsTypes === 'string' || argOptsTypes === 'string[]') { 22 | if (!baseField) { 23 | if (childField) { 24 | return Array.isArray(childField) ? childField : [childField]; 25 | } 26 | return undefined; 27 | } 28 | 29 | if (!childField) { 30 | if (baseField) { 31 | return Array.isArray(baseField) ? baseField : [baseField]; 32 | } 33 | return undefined; 34 | } 35 | 36 | merged = Array.of( 37 | ...(Array.isArray(baseField) ? baseField : [baseField]), 38 | ...(Array.isArray(childField) ? childField : [childField]) 39 | ); 40 | 41 | let length = merged.length; 42 | 43 | for (let i = 0; i <= length; i++) { 44 | for (let j = i + 1; j < length; j++) { 45 | if (merged[i] === merged[j]) { 46 | merged.splice(j, 1); 47 | length--; 48 | } 49 | } 50 | } 51 | } 52 | 53 | return merged as any; 54 | } 55 | 56 | export function mergeFieldMaps( 57 | baseFieldMap?: FieldMap, 58 | childFieldMap?: FieldMap 59 | ): FieldMap | typeof undefined { 60 | if (!baseFieldMap) { 61 | return childFieldMap; 62 | } 63 | 64 | const mergedFieldMap = childFieldMap || {}; 65 | 66 | for (const key in baseFieldMap) { 67 | if (baseFieldMap.hasOwnProperty(key)) { 68 | mergedFieldMap[key] = mergeStringAndStringArraysFields( 69 | baseFieldMap[key], 70 | mergedFieldMap[key], 71 | 'string' 72 | ); 73 | } 74 | } 75 | 76 | return mergedFieldMap; 77 | } 78 | 79 | export function mergeCustomizationOptions( 80 | baseCOptions: ComposeWithMongooseOpts, 81 | childCOptions?: ComposeWithMongooseOpts 82 | ): ComposeWithMongooseOpts | undefined { 83 | if (!baseCOptions) { 84 | return childCOptions; 85 | } 86 | 87 | const mergedOptions: ComposeWithMongooseOpts = childCOptions || {}; 88 | 89 | if ( 90 | baseCOptions.schemaComposer !== mergedOptions.schemaComposer && 91 | mergedOptions.schemaComposer 92 | ) { 93 | throw new Error( 94 | '[Discriminators] ChildModels should have same schemaComposer as its BaseModel' 95 | ); 96 | } 97 | 98 | // use base schemaComposer 99 | mergedOptions.schemaComposer = baseCOptions.schemaComposer; 100 | 101 | // merge fields map 102 | if (baseCOptions.fields) { 103 | mergedOptions.fields = mergeFieldMaps(baseCOptions.fields, mergedOptions.fields); 104 | } 105 | 106 | // merge inputType fields map 107 | if (baseCOptions.inputType && baseCOptions.inputType.fields) { 108 | if (mergedOptions.inputType) { 109 | mergedOptions.inputType.fields = mergeFieldMaps( 110 | baseCOptions.inputType.fields, 111 | mergedOptions.inputType.fields 112 | ); 113 | } else { 114 | mergedOptions.inputType = { 115 | fields: mergeFieldMaps(baseCOptions.inputType.fields, undefined), 116 | }; 117 | } 118 | } 119 | 120 | mergedOptions.resolvers = mergeTypeConverterResolverOpts( 121 | baseCOptions.resolvers, 122 | mergedOptions.resolvers 123 | ) as any; 124 | 125 | return mergedOptions; 126 | } 127 | -------------------------------------------------------------------------------- /src/discriminators/utils/reorderFields.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeComposer } from 'graphql-compose'; 2 | import { DiscriminatorTypeComposer } from '../DiscriminatorTypeComposer'; 3 | 4 | export function reorderFields( 5 | modelTC: DiscriminatorTypeComposer | ObjectTypeComposer, 6 | order: string[] | boolean | undefined, 7 | DKey: string, 8 | commonFieldKeys?: string[] 9 | ): DiscriminatorTypeComposer | ObjectTypeComposer { 10 | if (order) { 11 | if (Array.isArray(order)) { 12 | modelTC.reorderFields(order); 13 | } else { 14 | const newOrder = []; 15 | 16 | // is child discriminator 17 | if (modelTC instanceof ObjectTypeComposer && commonFieldKeys) { 18 | newOrder.push(...commonFieldKeys); 19 | 20 | newOrder.filter((value) => value === '_id' || value === DKey); 21 | 22 | newOrder.unshift('_id', DKey); 23 | } else { 24 | if (modelTC.getField('_id')) { 25 | newOrder.push('_id'); 26 | } 27 | newOrder.push(DKey); 28 | } 29 | 30 | modelTC.reorderFields(newOrder); 31 | } 32 | } 33 | 34 | return modelTC; 35 | } 36 | -------------------------------------------------------------------------------- /src/errors/MongoError.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, ObjectTypeComposer } from 'graphql-compose'; 2 | 3 | export function getMongoErrorOTC(schemaComposer: SchemaComposer): ObjectTypeComposer { 4 | return schemaComposer.getOrCreateOTC('MongoError', (otc) => { 5 | otc.addFields({ 6 | message: { 7 | description: 'MongoDB error message', 8 | type: 'String', 9 | }, 10 | code: { 11 | description: 'MongoDB error code', 12 | type: 'Int', 13 | }, 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/errors/RuntimeError.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, ObjectTypeComposer } from 'graphql-compose'; 2 | 3 | export class RuntimeError extends Error { 4 | constructor(message: string) { 5 | super(message); 6 | (this as any).__proto__ = RuntimeError.prototype; 7 | } 8 | } 9 | 10 | export function getRuntimeErrorOTC(schemaComposer: SchemaComposer): ObjectTypeComposer { 11 | return schemaComposer.getOrCreateOTC('RuntimeError', (otc) => { 12 | otc.addFields({ 13 | message: { 14 | description: 'Runtime error message', 15 | type: 'String', 16 | }, 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import { SchemaComposer, ObjectTypeComposer } from 'graphql-compose'; 2 | import type { ValidationErrorData, ValidationsWithMessage } from '../resolvers/helpers/validate'; 3 | 4 | export class ValidationError extends Error { 5 | public errors: ValidationErrorData[]; 6 | 7 | constructor(validation: ValidationsWithMessage) { 8 | super(validation.message); 9 | this.errors = validation.errors; 10 | (this as any).__proto__ = ValidationError.prototype; 11 | } 12 | } 13 | 14 | export function getValidatorErrorOTC(schemaComposer: SchemaComposer): ObjectTypeComposer { 15 | return schemaComposer.getOrCreateOTC('ValidatorError', (otc) => { 16 | otc.addFields({ 17 | message: { 18 | description: 'Validation error message', 19 | type: 'String', 20 | }, 21 | path: { 22 | description: 'Source of the validation error from the model path', 23 | type: 'String', 24 | }, 25 | value: { 26 | description: 'Field value which occurs the validation error', 27 | type: 'JSON', 28 | }, 29 | idx: { 30 | description: 31 | 'Input record idx in array which occurs the validation error. This `idx` is useful for createMany operation. For singular operations it always be 0. For *Many operations `idx` represents record index in array received from user.', 32 | type: 'Int!', 33 | resolve: (s: any) => s.idx || 0, 34 | }, 35 | }); 36 | }); 37 | } 38 | 39 | export function getValidationErrorOTC(schemaComposer: SchemaComposer): ObjectTypeComposer { 40 | return schemaComposer.getOrCreateOTC('ValidationError', (otc) => { 41 | otc.addFields({ 42 | message: { 43 | description: 'Combined error message from all validators', 44 | type: 'String', 45 | }, 46 | errors: { 47 | description: 'List of validator errors', 48 | type: getValidatorErrorOTC(schemaComposer).NonNull.List, 49 | }, 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | import { graphqlVersion, InterfaceTypeComposer, SchemaComposer } from 'graphql-compose'; 2 | import { getMongoErrorOTC } from './MongoError'; 3 | import { ValidationError, getValidationErrorOTC } from './ValidationError'; 4 | import { RuntimeError, getRuntimeErrorOTC } from './RuntimeError'; 5 | 6 | export { ValidationError, RuntimeError }; 7 | 8 | export function getErrorInterface(schemaComposer: SchemaComposer): InterfaceTypeComposer { 9 | const ErrorInterface = schemaComposer.getOrCreateIFTC('ErrorInterface', (iftc) => { 10 | iftc.addFields({ 11 | message: { 12 | description: 'Generic error message', 13 | type: 'String', 14 | }, 15 | }); 16 | 17 | const ValidationErrorOTC = getValidationErrorOTC(schemaComposer); 18 | const MongoErrorOTC = getMongoErrorOTC(schemaComposer); 19 | const RuntimeErrorOTC = getRuntimeErrorOTC(schemaComposer); 20 | 21 | ValidationErrorOTC.addInterface(iftc); 22 | MongoErrorOTC.addInterface(iftc); 23 | RuntimeErrorOTC.addInterface(iftc); 24 | 25 | schemaComposer.addSchemaMustHaveType(ValidationErrorOTC); 26 | schemaComposer.addSchemaMustHaveType(MongoErrorOTC); 27 | schemaComposer.addSchemaMustHaveType(RuntimeErrorOTC); 28 | 29 | let ValidationErrorType: any; 30 | let MongoErrorType: any; 31 | let RuntimeErrorType: any; 32 | if (graphqlVersion >= 16) { 33 | ValidationErrorType = ValidationErrorOTC.getTypeName(); 34 | MongoErrorType = MongoErrorOTC.getTypeName(); 35 | RuntimeErrorType = RuntimeErrorOTC.getTypeName(); 36 | } else { 37 | ValidationErrorType = ValidationErrorOTC.getType(); 38 | MongoErrorType = MongoErrorOTC.getType(); 39 | RuntimeErrorType = RuntimeErrorOTC.getType(); 40 | } 41 | 42 | iftc.setResolveType((value) => { 43 | switch (value?.name) { 44 | case 'ValidationError': 45 | return ValidationErrorType; 46 | case 'MongoError': 47 | return MongoErrorType; 48 | default: 49 | return RuntimeErrorType; 50 | } 51 | }); 52 | }); 53 | 54 | return ErrorInterface; 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import GraphQLMongoID from './types/MongoID'; 2 | import GraphQLBSONDecimal from './types/BSONDecimal'; 3 | 4 | export * from './composeWithMongoose'; 5 | export * from './composeMongoose'; 6 | export * from './composeWithMongooseDiscriminators'; 7 | export * from './fieldsConverter'; 8 | export * from './resolvers'; 9 | export * from './errors'; 10 | export { GraphQLMongoID, GraphQLBSONDecimal }; 11 | -------------------------------------------------------------------------------- /src/resolvers/count.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 2 | import type { Model, Document } from 'mongoose'; 3 | import { 4 | filterHelper, 5 | filterHelperArgs, 6 | FilterHelperArgsOpts, 7 | prepareNestedAliases, 8 | } from './helpers'; 9 | import type { ExtendedResolveParams } from './index'; 10 | import { beforeQueryHelper } from './helpers/beforeQueryHelper'; 11 | 12 | export interface CountResolverOpts { 13 | /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */ 14 | suffix?: string; 15 | /** Customize input-type for `filter` argument. If `false` then arg will be removed. */ 16 | filter?: FilterHelperArgsOpts | false; 17 | } 18 | 19 | type TArgs = { 20 | filter?: any; 21 | }; 22 | 23 | export function count( 24 | model: Model, 25 | tc: ObjectTypeComposer | InterfaceTypeComposer, 26 | opts?: CountResolverOpts 27 | ): Resolver { 28 | if (!model || !model.modelName || !model.schema) { 29 | throw new Error('First arg for Resolver count() should be instance of Mongoose Model.'); 30 | } 31 | 32 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 33 | throw new Error('Second arg for Resolver count() should be instance of ObjectTypeComposer.'); 34 | } 35 | 36 | const aliases = prepareNestedAliases(model.schema); 37 | 38 | return tc.schemaComposer.createResolver({ 39 | type: 'Int', 40 | name: 'count', 41 | kind: 'query', 42 | args: { 43 | ...filterHelperArgs(tc, model, { 44 | prefix: 'FilterCount', 45 | suffix: `${opts?.suffix || ''}Input`, 46 | ...opts?.filter, 47 | }), 48 | }, 49 | resolve: ((resolveParams: ExtendedResolveParams) => { 50 | resolveParams.query = model.find(); 51 | resolveParams.model = model; 52 | filterHelper(resolveParams, aliases); 53 | if (resolveParams.query.countDocuments) { 54 | // mongoose 5.2.0 and above 55 | resolveParams.query.countDocuments(); 56 | return beforeQueryHelper(resolveParams); 57 | } else { 58 | // mongoose 5 and below 59 | resolveParams.query.count(); 60 | return beforeQueryHelper(resolveParams); 61 | } 62 | }) as any, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/resolvers/createOne.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 2 | import type { Model, Document } from 'mongoose'; 3 | import { recordHelperArgs, RecordHelperArgsOpts } from './helpers'; 4 | import type { ExtendedResolveParams } from './index'; 5 | import { addErrorCatcherField } from './helpers/errorCatcher'; 6 | import { validateAndThrow } from './helpers/validate'; 7 | import { PayloadRecordIdHelperOpts, payloadRecordId } from './helpers/payloadRecordId'; 8 | 9 | export interface CreateOneResolverOpts { 10 | /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */ 11 | suffix?: string; 12 | /** Customize input-type for `record` argument */ 13 | record?: RecordHelperArgsOpts; 14 | /** Customize payload.recordId field. If false, then this field will be removed. */ 15 | recordId?: PayloadRecordIdHelperOpts | false; 16 | /** Customize payload.error field. If true, then this field will be removed. */ 17 | disableErrorField?: boolean; 18 | } 19 | 20 | type TArgs = { 21 | record: Record; 22 | }; 23 | 24 | export function createOne( 25 | model: Model, 26 | tc: ObjectTypeComposer | InterfaceTypeComposer, 27 | opts?: CreateOneResolverOpts 28 | ): Resolver { 29 | if (!model || !model.modelName || !model.schema) { 30 | throw new Error('First arg for Resolver createOne() should be instance of Mongoose Model.'); 31 | } 32 | 33 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 34 | throw new Error( 35 | 'Second arg for Resolver createOne() should be instance of ObjectTypeComposer.' 36 | ); 37 | } 38 | 39 | const tree = model.schema.obj; 40 | const requiredFields = []; 41 | for (const field in tree) { 42 | if (tree.hasOwnProperty(field)) { 43 | const fieldOptions = tree[field] as any; 44 | if (fieldOptions.required && typeof fieldOptions.required !== 'function') { 45 | requiredFields.push(field); 46 | } 47 | } 48 | } 49 | 50 | const outputTypeName = `CreateOne${tc.getTypeName()}${opts?.suffix || ''}Payload`; 51 | const outputType = tc.schemaComposer.getOrCreateOTC(outputTypeName, (t) => { 52 | t.setFields({ 53 | ...payloadRecordId(tc, opts?.recordId), 54 | record: { 55 | type: tc, 56 | description: 'Created document', 57 | }, 58 | }); 59 | }); 60 | 61 | const resolver = tc.schemaComposer.createResolver({ 62 | name: `createOne${opts?.suffix || ''}`, 63 | kind: 'mutation', 64 | description: 'Create one document with mongoose defaults, setters, hooks and validation', 65 | type: outputType, 66 | args: { 67 | ...recordHelperArgs(tc, { 68 | prefix: 'CreateOne', 69 | suffix: `${opts?.suffix || ''}Input`, 70 | removeFields: ['id', '_id'], 71 | isRequired: true, 72 | requiredFields, 73 | ...opts?.record, 74 | }), 75 | }, 76 | resolve: (async (resolveParams: ExtendedResolveParams) => { 77 | const recordData = resolveParams?.args?.record; 78 | 79 | if (!(typeof recordData === 'object') || Object.keys(recordData).length === 0) { 80 | throw new Error( 81 | `${tc.getTypeName()}.createOne resolver requires at least one value in args.record` 82 | ); 83 | } 84 | 85 | let doc = new model(recordData); 86 | if (resolveParams.beforeRecordMutate) { 87 | doc = await resolveParams.beforeRecordMutate(doc, resolveParams); 88 | if (!doc) return null; 89 | } 90 | 91 | await validateAndThrow(doc); 92 | await doc.save({ validateBeforeSave: false }); 93 | 94 | return { 95 | record: doc, 96 | }; 97 | }) as any, 98 | }); 99 | 100 | if (!opts?.disableErrorField) { 101 | // Add `error` field to payload which can catch resolver Error 102 | // and return it in mutation payload 103 | addErrorCatcherField(resolver); 104 | } 105 | 106 | return resolver; 107 | } 108 | -------------------------------------------------------------------------------- /src/resolvers/dataLoader.ts: -------------------------------------------------------------------------------- 1 | import { toInputType } from 'graphql-compose'; 2 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 3 | import type { Model, Document } from 'mongoose'; 4 | import { 5 | projectionHelper, 6 | prepareNestedAliases, 7 | prepareAliasesReverse, 8 | replaceAliases, 9 | } from './helpers'; 10 | import type { ExtendedResolveParams } from './index'; 11 | import { beforeQueryHelper, beforeQueryHelperLean } from './helpers/beforeQueryHelper'; 12 | import { getDataLoader } from './helpers/dataLoaderHelper'; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 15 | export interface DataLoaderResolverOpts { 16 | /** 17 | * Enabling the lean option tells Mongoose to skip instantiating 18 | * a full Mongoose document and just give you the plain JavaScript objects. 19 | * Documents are much heavier than vanilla JavaScript objects, 20 | * because they have a lot of internal state for change tracking. 21 | * The downside of enabling lean is that lean docs don't have: 22 | * Default values 23 | * Getters and setters 24 | * Virtuals 25 | * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html 26 | */ 27 | lean?: boolean; 28 | } 29 | 30 | type TArgs = { 31 | _id: any; 32 | }; 33 | 34 | export function dataLoader( 35 | model: Model, 36 | tc: ObjectTypeComposer | InterfaceTypeComposer, 37 | opts?: DataLoaderResolverOpts 38 | ): Resolver { 39 | if (!model || !model.modelName || !model.schema) { 40 | throw new Error('First arg for Resolver dataLoader() should be instance of Mongoose Model.'); 41 | } 42 | 43 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 44 | throw new Error( 45 | 'Second arg for Resolver dataLoader() should be instance of ObjectTypeComposer.' 46 | ); 47 | } 48 | 49 | const aliases = prepareNestedAliases(model.schema); 50 | const aliasesReverse = prepareAliasesReverse(model.schema); 51 | 52 | return tc.schemaComposer.createResolver({ 53 | type: tc, 54 | name: 'dataLoader', 55 | kind: 'query', 56 | args: { 57 | _id: tc.hasField('_id') ? toInputType(tc.getFieldTC('_id')).NonNull : 'MongoID!', 58 | }, 59 | resolve: ((resolveParams: ExtendedResolveParams) => { 60 | const args = resolveParams.args || {}; 61 | 62 | if (!args._id) { 63 | return Promise.resolve(null); 64 | } 65 | 66 | if (!resolveParams.info) { 67 | throw new Error( 68 | `Cannot use ${tc.getTypeName()}.dataLoader resolver without 'info: GraphQLResolveInfo'` 69 | ); 70 | } 71 | 72 | const dl = getDataLoader(resolveParams.context, resolveParams.info, async (ids) => { 73 | resolveParams.query = model.find({ 74 | _id: { $in: ids }, 75 | } as any); 76 | resolveParams.model = model; 77 | projectionHelper(resolveParams, aliases); 78 | 79 | if (opts?.lean) { 80 | const result = (await beforeQueryHelperLean(resolveParams)) || []; 81 | return Array.isArray(result) && aliasesReverse 82 | ? result.map((r) => replaceAliases(r, aliasesReverse)) 83 | : result; 84 | } else { 85 | return beforeQueryHelper(resolveParams) || []; 86 | } 87 | }); 88 | 89 | return dl.load(args._id); 90 | }) as any, 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /src/resolvers/dataLoaderMany.ts: -------------------------------------------------------------------------------- 1 | import { toInputType } from 'graphql-compose'; 2 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 3 | import type { Model, Document } from 'mongoose'; 4 | import { 5 | projectionHelper, 6 | prepareNestedAliases, 7 | prepareAliasesReverse, 8 | replaceAliases, 9 | } from './helpers'; 10 | import type { ExtendedResolveParams } from './index'; 11 | import { beforeQueryHelper, beforeQueryHelperLean } from './helpers/beforeQueryHelper'; 12 | import { getDataLoader } from './helpers/dataLoaderHelper'; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 15 | export interface DataLoaderManyResolverOpts { 16 | /** 17 | * Enabling the lean option tells Mongoose to skip instantiating 18 | * a full Mongoose document and just give you the plain JavaScript objects. 19 | * Documents are much heavier than vanilla JavaScript objects, 20 | * because they have a lot of internal state for change tracking. 21 | * The downside of enabling lean is that lean docs don't have: 22 | * Default values 23 | * Getters and setters 24 | * Virtuals 25 | * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html 26 | */ 27 | lean?: boolean; 28 | } 29 | 30 | type TArgs = { 31 | _ids: any; 32 | }; 33 | 34 | export function dataLoaderMany( 35 | model: Model, 36 | tc: ObjectTypeComposer | InterfaceTypeComposer, 37 | opts?: DataLoaderManyResolverOpts 38 | ): Resolver { 39 | if (!model || !model.modelName || !model.schema) { 40 | throw new Error( 41 | 'First arg for Resolver dataLoaderMany() should be instance of Mongoose Model.' 42 | ); 43 | } 44 | 45 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 46 | throw new Error( 47 | 'Second arg for Resolver dataLoaderMany() should be instance of ObjectTypeComposer.' 48 | ); 49 | } 50 | 51 | const aliases = prepareNestedAliases(model.schema); 52 | const aliasesReverse = prepareAliasesReverse(model.schema); 53 | 54 | return tc.schemaComposer.createResolver({ 55 | type: tc.List.NonNull, 56 | name: 'dataLoaderMany', 57 | kind: 'query', 58 | args: { 59 | _ids: tc.hasField('_id') 60 | ? toInputType(tc.getFieldTC('_id')).NonNull.List.NonNull 61 | : '[MongoID!]!', 62 | }, 63 | resolve: ((resolveParams: ExtendedResolveParams) => { 64 | const args = resolveParams.args || {}; 65 | 66 | if (!Array.isArray(args._ids) || args._ids.length === 0) { 67 | return Promise.resolve([]); 68 | } 69 | 70 | if (!resolveParams.info) { 71 | throw new Error( 72 | `Cannot use ${tc.getTypeName()}.dataLoaderMany resolver without 'info: GraphQLResolveInfo'` 73 | ); 74 | } 75 | 76 | const dl = getDataLoader(resolveParams.context, resolveParams.info, async (ids) => { 77 | resolveParams.query = model.find({ 78 | _id: { $in: ids }, 79 | } as any); 80 | resolveParams.model = model; 81 | projectionHelper(resolveParams, aliases); 82 | 83 | if (opts?.lean) { 84 | const result = (await beforeQueryHelperLean(resolveParams)) || []; 85 | return Array.isArray(result) && aliasesReverse 86 | ? result.map((r) => replaceAliases(r, aliasesReverse)) 87 | : result; 88 | } else { 89 | return beforeQueryHelper(resolveParams) || []; 90 | } 91 | }); 92 | 93 | return dl.loadMany(args._ids); 94 | }) as any, 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /src/resolvers/findById.ts: -------------------------------------------------------------------------------- 1 | import { toInputType } from 'graphql-compose'; 2 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 3 | import type { Model, Document } from 'mongoose'; 4 | import { 5 | projectionHelper, 6 | prepareNestedAliases, 7 | prepareAliasesReverse, 8 | replaceAliases, 9 | } from './helpers'; 10 | import type { ExtendedResolveParams } from './index'; 11 | import { beforeQueryHelper, beforeQueryHelperLean } from './helpers/beforeQueryHelper'; 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 14 | export interface FindByIdResolverOpts { 15 | /** 16 | * Enabling the lean option tells Mongoose to skip instantiating 17 | * a full Mongoose document and just give you the plain JavaScript objects. 18 | * Documents are much heavier than vanilla JavaScript objects, 19 | * because they have a lot of internal state for change tracking. 20 | * The downside of enabling lean is that lean docs don't have: 21 | * Default values 22 | * Getters and setters 23 | * Virtuals 24 | * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html 25 | */ 26 | lean?: boolean; 27 | } 28 | 29 | type TArgs = { 30 | _id: any; 31 | }; 32 | 33 | export function findById( 34 | model: Model, 35 | tc: ObjectTypeComposer | InterfaceTypeComposer, 36 | opts?: FindByIdResolverOpts 37 | ): Resolver { 38 | if (!model || !model.modelName || !model.schema) { 39 | throw new Error('First arg for Resolver findById() should be instance of Mongoose Model.'); 40 | } 41 | 42 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 43 | throw new Error('Second arg for Resolver findById() should be instance of ObjectTypeComposer.'); 44 | } 45 | 46 | const aliases = prepareNestedAliases(model.schema); 47 | const aliasesReverse = prepareAliasesReverse(model.schema); 48 | 49 | return tc.schemaComposer.createResolver({ 50 | type: tc, 51 | name: 'findById', 52 | kind: 'query', 53 | args: { 54 | _id: tc.hasField('_id') ? toInputType(tc.getFieldTC('_id')).NonNull : 'MongoID', 55 | }, 56 | resolve: (async (resolveParams: ExtendedResolveParams) => { 57 | const args = resolveParams.args || {}; 58 | 59 | if (args._id) { 60 | resolveParams.query = model.findById(args._id); 61 | resolveParams.model = model; 62 | projectionHelper(resolveParams, aliases); 63 | if (opts?.lean) { 64 | const result = await beforeQueryHelperLean(resolveParams); 65 | return result && aliasesReverse ? replaceAliases(result, aliasesReverse) : result; 66 | } else { 67 | return beforeQueryHelper(resolveParams); 68 | } 69 | } 70 | return Promise.resolve(null); 71 | }) as any, 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/resolvers/findByIds.ts: -------------------------------------------------------------------------------- 1 | import { toInputType } from 'graphql-compose'; 2 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 3 | import type { Model, Document } from 'mongoose'; 4 | import { 5 | limitHelper, 6 | limitHelperArgs, 7 | sortHelper, 8 | sortHelperArgs, 9 | projectionHelper, 10 | prepareNestedAliases, 11 | prepareAliasesReverse, 12 | replaceAliases, 13 | LimitHelperArgsOpts, 14 | SortHelperArgsOpts, 15 | } from './helpers'; 16 | import type { ExtendedResolveParams } from './'; 17 | import { beforeQueryHelper, beforeQueryHelperLean } from './helpers/beforeQueryHelper'; 18 | 19 | export interface FindByIdsResolverOpts { 20 | /** 21 | * Enabling the lean option tells Mongoose to skip instantiating 22 | * a full Mongoose document and just give you the plain JavaScript objects. 23 | * Documents are much heavier than vanilla JavaScript objects, 24 | * because they have a lot of internal state for change tracking. 25 | * The downside of enabling lean is that lean docs don't have: 26 | * Default values 27 | * Getters and setters 28 | * Virtuals 29 | * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html 30 | */ 31 | lean?: boolean; 32 | limit?: LimitHelperArgsOpts | false; 33 | sort?: SortHelperArgsOpts | false; 34 | } 35 | 36 | type TArgs = { 37 | _ids: any; 38 | limit?: number; 39 | sort?: string | string[] | Record; 40 | }; 41 | 42 | export function findByIds( 43 | model: Model, 44 | tc: ObjectTypeComposer | InterfaceTypeComposer, 45 | opts?: FindByIdsResolverOpts 46 | ): Resolver { 47 | if (!model || !model.modelName || !model.schema) { 48 | throw new Error('First arg for Resolver findByIds() should be instance of Mongoose Model.'); 49 | } 50 | 51 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 52 | throw new Error( 53 | 'Second arg for Resolver findByIds() should be instance of ObjectTypeComposer.' 54 | ); 55 | } 56 | 57 | const aliases = prepareNestedAliases(model.schema); 58 | const aliasesReverse = prepareAliasesReverse(model.schema); 59 | 60 | return tc.schemaComposer.createResolver({ 61 | type: tc.NonNull.List.NonNull, 62 | name: 'findByIds', 63 | kind: 'query', 64 | args: { 65 | _ids: tc.hasField('_id') 66 | ? toInputType(tc.getFieldTC('_id')).NonNull.List.NonNull 67 | : '[MongoID!]!', 68 | ...limitHelperArgs({ 69 | ...opts?.limit, 70 | }), 71 | ...sortHelperArgs(tc, model, { 72 | sortTypeName: `SortFindByIds${tc.getTypeName()}Input`, 73 | ...opts?.sort, 74 | }), 75 | }, 76 | resolve: (async (resolveParams: ExtendedResolveParams) => { 77 | const args = resolveParams.args || {}; 78 | 79 | if (!Array.isArray(args._ids) || args._ids.length === 0) { 80 | return Promise.resolve([]); 81 | } 82 | 83 | resolveParams.query = model.find({ 84 | _id: { $in: args._ids }, 85 | } as any); 86 | resolveParams.model = model; 87 | projectionHelper(resolveParams, aliases); 88 | limitHelper(resolveParams); 89 | sortHelper(resolveParams); 90 | if (opts?.lean) { 91 | const result = (await beforeQueryHelperLean(resolveParams)) || []; 92 | return Array.isArray(result) && aliasesReverse 93 | ? result.map((r) => replaceAliases(r, aliasesReverse)) 94 | : result; 95 | } else { 96 | return beforeQueryHelper(resolveParams) || []; 97 | } 98 | }) as any, 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /src/resolvers/findMany.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 2 | import type { Model, Document } from 'mongoose'; 3 | import { 4 | limitHelper, 5 | limitHelperArgs, 6 | skipHelper, 7 | skipHelperArgs, 8 | filterHelper, 9 | filterHelperArgs, 10 | sortHelper, 11 | sortHelperArgs, 12 | projectionHelper, 13 | prepareNestedAliases, 14 | prepareAliasesReverse, 15 | replaceAliases, 16 | FilterHelperArgsOpts, 17 | SortHelperArgsOpts, 18 | LimitHelperArgsOpts, 19 | } from './helpers'; 20 | import type { ExtendedResolveParams } from './index'; 21 | import { beforeQueryHelper, beforeQueryHelperLean } from './helpers/beforeQueryHelper'; 22 | 23 | export interface FindManyResolverOpts { 24 | /** 25 | * Enabling the lean option tells Mongoose to skip instantiating 26 | * a full Mongoose document and just give you the plain JavaScript objects. 27 | * Documents are much heavier than vanilla JavaScript objects, 28 | * because they have a lot of internal state for change tracking. 29 | * The downside of enabling lean is that lean docs don't have: 30 | * Default values 31 | * Getters and setters 32 | * Virtuals 33 | * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html 34 | */ 35 | lean?: boolean; 36 | /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */ 37 | suffix?: string; 38 | /** Customize input-type for `filter` argument. If `false` then arg will be removed. */ 39 | filter?: FilterHelperArgsOpts | false; 40 | sort?: SortHelperArgsOpts | false; 41 | limit?: LimitHelperArgsOpts | false; 42 | skip?: false; 43 | } 44 | 45 | type TArgs = { 46 | filter?: any; 47 | limit?: number; 48 | skip?: number; 49 | sort?: string | string[] | Record; 50 | }; 51 | 52 | export function findMany( 53 | model: Model, 54 | tc: ObjectTypeComposer | InterfaceTypeComposer, 55 | opts?: FindManyResolverOpts 56 | ): Resolver { 57 | if (!model || !model.modelName || !model.schema) { 58 | throw new Error('First arg for Resolver findMany() should be instance of Mongoose Model.'); 59 | } 60 | 61 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 62 | throw new Error('Second arg for Resolver findMany() should be instance of ObjectTypeComposer.'); 63 | } 64 | 65 | const aliases = prepareNestedAliases(model.schema); 66 | const aliasesReverse = prepareAliasesReverse(model.schema); 67 | 68 | return tc.schemaComposer.createResolver({ 69 | type: tc.NonNull.List.NonNull, 70 | name: 'findMany', 71 | kind: 'query', 72 | args: { 73 | ...filterHelperArgs(tc, model, { 74 | prefix: 'FilterFindMany', 75 | suffix: `${opts?.suffix || ''}Input`, 76 | ...opts?.filter, 77 | }), 78 | ...skipHelperArgs(), 79 | ...limitHelperArgs({ 80 | ...opts?.limit, 81 | }), 82 | ...sortHelperArgs(tc, model, { 83 | sortTypeName: `SortFindMany${tc.getTypeName()}${opts?.suffix || ''}Input`, 84 | ...opts?.sort, 85 | }), 86 | }, 87 | resolve: (async (resolveParams: ExtendedResolveParams) => { 88 | resolveParams.query = model.find(); 89 | resolveParams.model = model; 90 | filterHelper(resolveParams, aliases); 91 | skipHelper(resolveParams); 92 | limitHelper(resolveParams); 93 | sortHelper(resolveParams); 94 | projectionHelper(resolveParams, aliases); 95 | 96 | if (opts?.lean) { 97 | const result = (await beforeQueryHelperLean(resolveParams)) || []; 98 | return Array.isArray(result) && aliasesReverse 99 | ? result.map((r) => replaceAliases(r, aliasesReverse)) 100 | : result; 101 | } else { 102 | return beforeQueryHelper(resolveParams) || []; 103 | } 104 | }) as any, 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /src/resolvers/findOne.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 2 | import type { Model, Document } from 'mongoose'; 3 | import { 4 | skipHelper, 5 | skipHelperArgs, 6 | filterHelper, 7 | filterHelperArgs, 8 | sortHelper, 9 | sortHelperArgs, 10 | projectionHelper, 11 | prepareNestedAliases, 12 | prepareAliasesReverse, 13 | replaceAliases, 14 | FilterHelperArgsOpts, 15 | SortHelperArgsOpts, 16 | } from './helpers'; 17 | import type { ExtendedResolveParams } from './index'; 18 | import { beforeQueryHelper, beforeQueryHelperLean } from './helpers/beforeQueryHelper'; 19 | 20 | export interface FindOneResolverOpts { 21 | /** 22 | * Enabling the lean option tells Mongoose to skip instantiating 23 | * a full Mongoose document and just give you the plain JavaScript objects. 24 | * Documents are much heavier than vanilla JavaScript objects, 25 | * because they have a lot of internal state for change tracking. 26 | * The downside of enabling lean is that lean docs don't have: 27 | * Default values 28 | * Getters and setters 29 | * Virtuals 30 | * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html 31 | */ 32 | lean?: boolean; 33 | /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */ 34 | suffix?: string; 35 | /** Customize input-type for `filter` argument. If `false` then arg will be removed. */ 36 | filter?: FilterHelperArgsOpts | false; 37 | sort?: SortHelperArgsOpts | false; 38 | skip?: false; 39 | } 40 | 41 | type TArgs = { 42 | filter?: any; 43 | sort?: string | string[] | Record; 44 | skip?: number; 45 | }; 46 | 47 | export function findOne( 48 | model: Model, 49 | tc: ObjectTypeComposer | InterfaceTypeComposer, 50 | opts?: FindOneResolverOpts 51 | ): Resolver { 52 | if (!model || !model.modelName || !model.schema) { 53 | throw new Error('First arg for Resolver findOne() should be instance of Mongoose Model.'); 54 | } 55 | 56 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 57 | throw new Error('Second arg for Resolver findOne() should be instance of ObjectTypeComposer.'); 58 | } 59 | 60 | const aliases = prepareNestedAliases(model.schema); 61 | const aliasesReverse = prepareAliasesReverse(model.schema); 62 | 63 | return tc.schemaComposer.createResolver({ 64 | type: tc, 65 | name: 'findOne', 66 | kind: 'query', 67 | args: { 68 | ...filterHelperArgs(tc, model, { 69 | prefix: 'FilterFindOne', 70 | suffix: `${opts?.suffix || ''}Input`, 71 | ...opts?.filter, 72 | }), 73 | ...skipHelperArgs(), 74 | ...sortHelperArgs(tc, model, { 75 | sortTypeName: `SortFindOne${tc.getTypeName()}${opts?.suffix || ''}Input`, 76 | ...opts?.sort, 77 | }), 78 | }, 79 | resolve: (async (resolveParams: ExtendedResolveParams) => { 80 | resolveParams.query = model.findOne({}); 81 | resolveParams.model = model; 82 | filterHelper(resolveParams, aliases); 83 | skipHelper(resolveParams); 84 | sortHelper(resolveParams); 85 | projectionHelper(resolveParams, aliases); 86 | 87 | if (opts?.lean) { 88 | const result = await beforeQueryHelperLean(resolveParams); 89 | return result && aliasesReverse ? replaceAliases(result, aliasesReverse) : result; 90 | } else { 91 | return beforeQueryHelper(resolveParams); 92 | } 93 | }) as any, 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /src/resolvers/helpers/__tests__/aliases-test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { prepareAliases } from '../aliases'; 3 | 4 | describe('Resolver helper `alises` ->', () => { 5 | describe('prepareAliases()', () => { 6 | it('should extract field aliases from Model', () => { 7 | const UserSchema = new mongoose.Schema({ 8 | e: { 9 | type: String, 10 | alias: 'emailAddress', 11 | }, 12 | }); 13 | const User = mongoose.model('User123', UserSchema); 14 | const aliases = prepareAliases(User); 15 | expect(aliases).toEqual({ emailAddress: 'e' }); 16 | }); 17 | 18 | it('should return undefined if field aliases do not exists in Model', () => { 19 | const UserSchema = new mongoose.Schema({ 20 | e: { 21 | type: String, 22 | }, 23 | }); 24 | const User = mongoose.model('User456', UserSchema); 25 | const aliases = prepareAliases(User); 26 | expect(aliases).toEqual(false); 27 | }); 28 | 29 | it('should extract field aliases from discriminator Models', () => { 30 | const UserSchema = new mongoose.Schema({ 31 | e: { 32 | type: String, 33 | alias: 'emailAddress', 34 | }, 35 | }); 36 | const User = mongoose.model('User111', UserSchema); 37 | const VIPUserSchema = new mongoose.Schema({ 38 | f: { 39 | type: Number, 40 | alias: 'freeDrinks', 41 | }, 42 | }); 43 | User.discriminator('VIPUser111', VIPUserSchema); 44 | const aliases = prepareAliases(User); 45 | expect(aliases).toEqual({ emailAddress: 'e', freeDrinks: 'f' }); 46 | }); 47 | 48 | it('should extract field aliases in discriminator Models inherited from base Model', () => { 49 | const UserSchema = new mongoose.Schema({ 50 | e: { 51 | type: String, 52 | alias: 'emailAddress', 53 | }, 54 | }); 55 | const User = mongoose.model('User789', UserSchema); 56 | const VIPUserSchema = new mongoose.Schema({ 57 | f: { 58 | type: Number, 59 | alias: 'freeDrinks', 60 | }, 61 | }); 62 | const VIPUser = User.discriminator('VIPUser789', VIPUserSchema); 63 | const aliases = prepareAliases(VIPUser); 64 | expect(aliases).toEqual({ emailAddress: 'e', freeDrinks: 'f' }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/resolvers/helpers/__tests__/beforeQueryHelper-test.ts: -------------------------------------------------------------------------------- 1 | import { beforeQueryHelper } from '../beforeQueryHelper'; 2 | import { UserModel } from '../../../__mocks__/userModel'; 3 | 4 | describe('Resolver helper `beforeQueryHelper` ->', () => { 5 | let spyExec; 6 | let spyWhere; 7 | let resolveParams: any; 8 | 9 | beforeEach(() => { 10 | spyWhere = jest.fn(); 11 | spyExec = jest.fn(() => Promise.resolve('EXEC_RETURN')); 12 | 13 | resolveParams = { 14 | query: { 15 | exec: spyExec, 16 | where: spyWhere, 17 | }, 18 | model: UserModel, 19 | }; 20 | }); 21 | 22 | it('should return query.exec() if `resolveParams.beforeQuery` is empty', async () => { 23 | const result = await beforeQueryHelper(resolveParams); 24 | expect(result).toBe('EXEC_RETURN'); 25 | }); 26 | 27 | it('should call the `exec` method of `beforeQuery` return', async () => { 28 | resolveParams.beforeQuery = function beforeQuery() { 29 | return { 30 | exec: () => Promise.resolve('changed'), 31 | }; 32 | }; 33 | 34 | const result = await beforeQueryHelper(resolveParams); 35 | expect(result).toBe('changed'); 36 | }); 37 | 38 | it('should return the complete payload if not a Query', async () => { 39 | resolveParams.beforeQuery = function beforeQuery() { 40 | return 'NOT_A_QUERY'; 41 | }; 42 | 43 | expect(await beforeQueryHelper(resolveParams)).toBe('NOT_A_QUERY'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/resolvers/helpers/__tests__/limit-test.ts: -------------------------------------------------------------------------------- 1 | import { limitHelperArgs, limitHelper } from '../limit'; 2 | 3 | describe('Resolver helper `limit` ->', () => { 4 | describe('limitHelperArgs()', () => { 5 | it('should return limit field', () => { 6 | const args: any = limitHelperArgs(); 7 | expect(args.limit.type).toBe('Int'); 8 | }); 9 | it('should process `opts.defaultValue` arg', () => { 10 | expect((limitHelperArgs() as any).limit.defaultValue).toBe(100); 11 | expect( 12 | ( 13 | limitHelperArgs({ 14 | defaultValue: 333, 15 | }) as any 16 | ).limit.defaultValue 17 | ).toBe(333); 18 | }); 19 | }); 20 | 21 | describe('limitHelper()', () => { 22 | let spyFn: jest.Mock; 23 | let resolveParams: any; 24 | 25 | beforeEach(() => { 26 | spyFn = jest.fn(); 27 | resolveParams = { 28 | query: { 29 | limit: spyFn, 30 | }, 31 | }; 32 | }); 33 | 34 | it('should not call query.limit if args.limit is empty', () => { 35 | limitHelper(resolveParams); 36 | expect(spyFn).not.toBeCalled(); 37 | }); 38 | it('should call query.limit if args.limit is provided', () => { 39 | resolveParams.args = { limit: 333 }; 40 | limitHelper(resolveParams); 41 | expect(spyFn).toBeCalledWith(333); 42 | }); 43 | it('should convert string to int in args.limit', () => { 44 | resolveParams.args = { limit: '444' }; 45 | limitHelper(resolveParams); 46 | expect(spyFn).toBeCalledWith(444); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/resolvers/helpers/__tests__/projection-test.ts: -------------------------------------------------------------------------------- 1 | import { projectionHelper } from '../projection'; 2 | 3 | describe('Resolver helper `projection` ->', () => { 4 | describe('projectionHelper()', () => { 5 | let spyFn: jest.Mock; 6 | let resolveParams: any; 7 | 8 | beforeEach(() => { 9 | spyFn = jest.fn(); 10 | resolveParams = { 11 | query: { 12 | select: spyFn, 13 | }, 14 | }; 15 | }); 16 | 17 | it('should not call query.select if projection is empty', () => { 18 | projectionHelper(resolveParams); 19 | expect(spyFn).not.toBeCalled(); 20 | }); 21 | 22 | it('should call query.select if projection is provided', () => { 23 | resolveParams.projection = { name: 1, age: 1 }; 24 | projectionHelper(resolveParams, { name: 'n' }); 25 | expect(spyFn).toBeCalledWith({ n: true, age: true }); 26 | }); 27 | 28 | it('should make projection fields flat', () => { 29 | resolveParams.projection = { name: { first: 1, last: 1 } }; 30 | projectionHelper(resolveParams, { name: 'n' }); 31 | expect(spyFn).toBeCalledWith({ 'n.first': true, 'n.last': true }); 32 | }); 33 | 34 | it('should make projection fields flat with nested aliases', () => { 35 | resolveParams.projection = { name: { first: 1, last: 1 } }; 36 | projectionHelper(resolveParams, { name: { __selfAlias: 'n', first: 'f', last: 'l' } }); 37 | expect(spyFn).toBeCalledWith({ 'n.f': true, 'n.l': true }); 38 | }); 39 | 40 | it('should not call query.select if projection has * key', () => { 41 | resolveParams.projection = { '*': true }; 42 | projectionHelper(resolveParams); 43 | expect(spyFn).not.toBeCalled(); 44 | }); 45 | 46 | describe('projection operators', () => { 47 | // see more details here https://docs.mongodb.com/v3.2/reference/operator/projection/meta/ 48 | it('should pass $meta non-flatten', () => { 49 | resolveParams.projection = { score: { $meta: 'textScore' } }; 50 | projectionHelper(resolveParams); 51 | expect(spyFn).toBeCalledWith({ score: { $meta: 'textScore' } }); 52 | }); 53 | 54 | it('should pass $slice non-flatten', () => { 55 | resolveParams.projection = { comments: { $slice: 5 } }; 56 | projectionHelper(resolveParams); 57 | expect(spyFn).toBeCalledWith({ comments: { $slice: 5 } }); 58 | }); 59 | 60 | it('should pass $elemMatch non-flatten', () => { 61 | resolveParams.projection = { students: { $elemMatch: { school: 102 } } }; 62 | projectionHelper(resolveParams); 63 | expect(spyFn).toBeCalledWith({ students: { $elemMatch: { school: 102 } } }); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/resolvers/helpers/__tests__/record-test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | schemaComposer, 3 | NonNullComposer, 4 | InputTypeComposer, 5 | ObjectTypeComposer, 6 | } from 'graphql-compose'; 7 | import { recordHelperArgs } from '../record'; 8 | import { UserModel } from '../../../__mocks__/userModel'; 9 | import { convertModelToGraphQL } from '../../../fieldsConverter'; 10 | 11 | describe('Resolver helper `record` ->', () => { 12 | let UserTC: ObjectTypeComposer; 13 | 14 | beforeEach(() => { 15 | schemaComposer.clear(); 16 | UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); 17 | }); 18 | 19 | describe('recordHelperArgs()', () => { 20 | it('should throw error if provided empty opts', () => { 21 | expect(() => recordHelperArgs(UserTC)).toThrowError('provide non-empty options'); 22 | }); 23 | 24 | it('should return input field', () => { 25 | const args: any = recordHelperArgs(UserTC, { 26 | prefix: 'Record', 27 | suffix: 'Input', 28 | }); 29 | expect(args.record.type).toBeInstanceOf(InputTypeComposer); 30 | }); 31 | 32 | it('should reuse existed inputType', () => { 33 | const existedType = schemaComposer.createInputTC({ 34 | name: 'RecordUserInput', 35 | fields: {}, 36 | }); 37 | schemaComposer.set('RecordUserType', existedType); 38 | const args: any = recordHelperArgs(UserTC, { 39 | prefix: 'Record', 40 | suffix: 'Input', 41 | }); 42 | expect(args.record.type).toBe(existedType); 43 | }); 44 | 45 | it('should for opts.isRequired=true return NonNullComposer', () => { 46 | const args: any = recordHelperArgs(UserTC, { 47 | prefix: 'Record', 48 | suffix: 'Input', 49 | isRequired: true, 50 | }); 51 | expect(args.record.type).toBeInstanceOf(NonNullComposer); 52 | }); 53 | 54 | it('should remove fields via opts.removeFields', () => { 55 | const args: any = recordHelperArgs(UserTC, { 56 | prefix: 'Record', 57 | suffix: 'Input', 58 | removeFields: ['name', 'age'], 59 | }); 60 | const inputTypeComposer = args.record.type; 61 | expect(inputTypeComposer.hasField('name')).toBe(false); 62 | expect(inputTypeComposer.hasField('age')).toBe(false); 63 | expect(inputTypeComposer.hasField('gender')).toBe(true); 64 | }); 65 | 66 | it('should set required fields via opts.requiredFields', () => { 67 | const args: any = recordHelperArgs(UserTC, { 68 | prefix: 'Record', 69 | suffix: 'Input', 70 | requiredFields: ['name', 'age'], 71 | }); 72 | const inputTypeComposer = args.record.type; 73 | expect(inputTypeComposer.getField('name').type).toBeInstanceOf(NonNullComposer); 74 | expect(inputTypeComposer.getField('age').type).toBeInstanceOf(NonNullComposer); 75 | expect(inputTypeComposer.getField('gender').type).not.toBeInstanceOf(NonNullComposer); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/resolvers/helpers/__tests__/skip-test.ts: -------------------------------------------------------------------------------- 1 | import { skipHelperArgs, skipHelper } from '../skip'; 2 | 3 | describe('Resolver helper `skip` ->', () => { 4 | describe('limitHelperArgs()', () => { 5 | it('should return skip field', () => { 6 | const args: any = skipHelperArgs(); 7 | expect(args.skip.type).toBe('Int'); 8 | }); 9 | }); 10 | 11 | describe('skipHelper()', () => { 12 | let spyFn: jest.Mock; 13 | let resolveParams: any; 14 | 15 | beforeEach(() => { 16 | spyFn = jest.fn(); 17 | resolveParams = { 18 | query: { 19 | skip: spyFn, 20 | }, 21 | }; 22 | }); 23 | 24 | it('should not call query.skip if args.skip is empty', () => { 25 | skipHelper(resolveParams); 26 | expect(spyFn).not.toBeCalled(); 27 | }); 28 | it('should call query.skip if args.skip is provided', () => { 29 | resolveParams.args = { skip: 333 }; 30 | skipHelper(resolveParams); 31 | expect(spyFn).toBeCalledWith(333); 32 | }); 33 | it('should convert skip to int in args.skip', () => { 34 | resolveParams.args = { skip: '444' }; 35 | skipHelper(resolveParams); 36 | expect(spyFn).toBeCalledWith(444); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/resolvers/helpers/aliases.ts: -------------------------------------------------------------------------------- 1 | import type { Model, Schema } from 'mongoose'; 2 | import { isObject } from 'graphql-compose'; 3 | 4 | export interface NestedAliasesMap { 5 | __selfAlias?: string; // if object has alias, then it be written here and its field aliases below 6 | [userFieldName: string]: string | NestedAliasesMap | undefined; 7 | } 8 | 9 | export type AliasesMap = Record; 10 | 11 | export function prepareAliases(model: Model): AliasesMap | false { 12 | const aliases = (model?.schema as any)?.aliases || {}; 13 | 14 | if (model.discriminators) { 15 | Object.keys(model.discriminators).forEach((subModelName: string) => { 16 | const subModel: Model = (model.discriminators as any)[subModelName]; 17 | Object.assign(aliases, (subModel?.schema as any)?.aliases); 18 | }); 19 | } 20 | if (Object.keys(aliases).length > 0) { 21 | return aliases; 22 | } 23 | return false; 24 | } 25 | 26 | export function prepareAliasesReverse(schema: Schema): AliasesMap | false { 27 | const aliases = (schema as any)?.aliases; 28 | const keys = Object.keys(aliases); 29 | if (keys.length > 0) { 30 | const r = {} as AliasesMap; 31 | keys.forEach((k) => { 32 | r[aliases[k]] = k; 33 | }); 34 | return r; 35 | } 36 | return false; 37 | } 38 | 39 | export function replaceAliases( 40 | data: Record, 41 | aliases?: NestedAliasesMap 42 | ): Record { 43 | if (aliases) { 44 | const res = { ...data }; 45 | Object.keys(data).forEach((key) => { 46 | if (aliases?.[key]) { 47 | const alias = aliases?.[key]; 48 | let aliasValue; 49 | if (typeof alias === 'string') { 50 | aliasValue = alias; 51 | } else if (isObject(alias)) { 52 | aliasValue = alias?.__selfAlias; 53 | } 54 | 55 | res[aliasValue || key] = isObject(res[key]) 56 | ? replaceAliases(res[key], isObject(alias) ? alias : undefined) 57 | : res[key]; 58 | 59 | if (aliasValue) { 60 | delete res[key]; 61 | } 62 | } 63 | }); 64 | return res; 65 | } 66 | return data; 67 | } 68 | 69 | export function prepareNestedAliases( 70 | schema: Schema, 71 | preparedAliases = new Map, NestedAliasesMap | undefined>() 72 | ): NestedAliasesMap | undefined { 73 | if (preparedAliases.has(schema)) { 74 | return preparedAliases.get(schema); 75 | } 76 | 77 | const aliases = {} as NestedAliasesMap; 78 | preparedAliases.set(schema, aliases); 79 | 80 | const discriminators = (schema as any).discriminators; 81 | if (discriminators) { 82 | Object.keys(discriminators).forEach((discSchemaName: string) => { 83 | const discSchema: Schema = (discriminators as any)[discSchemaName]; 84 | const additionalAliases = prepareNestedAliases(discSchema, preparedAliases); 85 | Object.assign(aliases, additionalAliases); 86 | }); 87 | } 88 | 89 | Object.keys(schema.paths).forEach((path) => { 90 | const field = schema.paths[path]; 91 | let fieldName = path; 92 | if ((field as any)?.options?.alias) { 93 | fieldName = (field as any)?.options?.alias; 94 | aliases[fieldName] = path; 95 | } 96 | if ((field as any)?.schema) { 97 | const nestedSchema = (field as any)?.schema; 98 | const nestedAliases = prepareNestedAliases(nestedSchema, preparedAliases); 99 | if (nestedAliases) { 100 | const topKey = aliases[fieldName]; 101 | if (topKey && typeof topKey === 'string') { 102 | aliases[fieldName] = { 103 | __selfAlias: topKey, 104 | }; 105 | } 106 | aliases[fieldName] = Object.assign(aliases[fieldName] || {}, nestedAliases); 107 | } 108 | } 109 | }); 110 | 111 | if (!Object.keys(aliases).length) { 112 | preparedAliases.set(schema, undefined); 113 | return undefined; 114 | } 115 | return aliases; 116 | } 117 | -------------------------------------------------------------------------------- /src/resolvers/helpers/beforeQueryHelper.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendedResolveParams } from '../index'; 2 | 3 | export interface BeforeQueryHelperOpts { 4 | useLean?: boolean; 5 | } 6 | 7 | export async function beforeQueryHelper(resolveParams: ExtendedResolveParams): Promise { 8 | if (!resolveParams.query || typeof resolveParams.query.exec !== 'function') { 9 | throw new Error('beforeQueryHelper: expected resolveParams.query to be instance of Query'); 10 | } 11 | if (!resolveParams.beforeQuery) { 12 | return resolveParams.query.exec(); 13 | } 14 | 15 | const result = await resolveParams.beforeQuery(resolveParams.query, resolveParams); 16 | if (result !== undefined) { 17 | if (typeof result?.exec === 'function') { 18 | // if `beforeQuery` returns new `query` object 19 | return result.exec(); 20 | } else { 21 | // if `beforeQuery` returns data 22 | return result; 23 | } 24 | } else { 25 | // if `beforeQuery` modifies initial `query` object 26 | return resolveParams.query.exec(); 27 | } 28 | } 29 | 30 | export async function beforeQueryHelperLean(resolveParams: ExtendedResolveParams): Promise { 31 | if (!resolveParams.query || typeof resolveParams.query.lean !== 'function') { 32 | throw new Error('beforeQueryHelper: expected resolveParams.query to be instance of Query'); 33 | } 34 | 35 | if (!resolveParams.beforeQuery) { 36 | return resolveParams.query.lean(); 37 | } 38 | 39 | const result = await resolveParams.beforeQuery(resolveParams.query, resolveParams); 40 | if (result !== undefined) { 41 | if (typeof result?.lean === 'function') { 42 | // if `beforeQuery` returns new `query` object 43 | return result.lean(); 44 | } else { 45 | // if `beforeQuery` returns data 46 | return result; 47 | } 48 | } else { 49 | // if `beforeQuery` modifies initial `query` object 50 | return resolveParams.query.lean(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/resolvers/helpers/dataLoaderHelper.ts: -------------------------------------------------------------------------------- 1 | import DataLoader, { BatchLoadFn } from 'dataloader'; 2 | import { GraphQLResolveInfo } from 'graphql-compose/lib/graphql'; 3 | 4 | export function getDataLoader = any>( 5 | context: TContext, 6 | info: GraphQLResolveInfo, 7 | batchLoadFn: BatchLoadFn 8 | ): DataLoader { 9 | if (!context._gqlDataLoaders) (context as any)._gqlDataLoaders = new WeakMap(); 10 | const { _gqlDataLoaders } = context; 11 | 12 | // for different parts of GraphQL queries, key will be new 13 | const dlKey = info.fieldNodes; 14 | 15 | // get or create DataLoader in GraphQL context 16 | let dl: DataLoader = _gqlDataLoaders.get(dlKey); 17 | if (!dl) { 18 | const dataLoaderOptions = { 19 | cacheKeyFn: (k) => { 20 | if (k?.equals) { 21 | // Convert ObjectId to string for combining different instances of same ObjectIds. 22 | // Eg. you have 10 articles with same authorId. So in memory `authorId` for every record 23 | // will have its own instance of ObjectID. 24 | // 25 | // mongoose will convert them back to ObjectId automatically when call `find` method 26 | return k.toString(); 27 | } 28 | return k; 29 | }, 30 | } as DataLoader.Options; 31 | 32 | dl = new DataLoader(async (ids) => { 33 | const result = await batchLoadFn(ids); 34 | // return docs in the same order as were provided their ids 35 | return ids.map((id) => 36 | (result as any).find((doc: any) => { 37 | if (doc?._id?.equals) { 38 | // compare correctly MongoIDs via ObjectID.equals() method 39 | return doc._id.equals(id); 40 | } 41 | return doc._id === id; 42 | }) 43 | ); 44 | }, dataLoaderOptions); 45 | 46 | _gqlDataLoaders.set(dlKey, dl); 47 | } 48 | return dl; 49 | } 50 | -------------------------------------------------------------------------------- /src/resolvers/helpers/errorCatcher.ts: -------------------------------------------------------------------------------- 1 | import { Resolver } from 'graphql-compose'; 2 | import { getErrorInterface, ValidationError } from '../../errors'; 3 | import { GraphQLError } from 'graphql-compose/lib/graphql'; 4 | 5 | /** 6 | * This helper add `error` field in payload & wraps `resolve` method. 7 | * It catches exception and return it in payload if user 8 | * requested `err` field in GraphQL-query. 9 | */ 10 | export function addErrorCatcherField(resolver: Resolver): void { 11 | getErrorInterface(resolver.schemaComposer); 12 | 13 | const payloadTC = resolver.getOTC(); 14 | 15 | if (!payloadTC.hasField('error')) { 16 | payloadTC.setField('error', { 17 | type: 'ErrorInterface', 18 | description: 19 | 'Error that may occur during operation. If you request this field in GraphQL query, you will receive typed error in payload; otherwise error will be provided in root `errors` field of GraphQL response.', 20 | }); 21 | } 22 | 23 | const childResolve = resolver.resolve.bind(resolver); 24 | resolver.setResolve(async (rp) => { 25 | try { 26 | const res = await childResolve(rp); 27 | return res; 28 | } catch (e: any) { 29 | let error; 30 | if (e instanceof ValidationError) { 31 | error = { 32 | name: 'ValidationError', 33 | message: e.message, 34 | errors: e.errors, 35 | }; 36 | } else if (e?.constructor.name === 'MongoError') { 37 | error = { 38 | name: 'MongoError', 39 | message: e.message, 40 | code: e.code, 41 | }; 42 | } else { 43 | error = { 44 | message: e.message, 45 | }; 46 | } 47 | 48 | if (rp.projection?.error) { 49 | // User requested to return error in mutation payload.error field. 50 | // So do not throw error, just return it. 51 | return { error }; 52 | } else { 53 | // Rethrow GraphQLError helps to provide `extensions` data for Error record 54 | // in the top-level array of errors. 55 | // Delete `error.message` from `extensions`, because it already present on one level upper. 56 | delete error.message; 57 | throw new GraphQLError( 58 | e.message, 59 | undefined, 60 | undefined, 61 | undefined, 62 | undefined, 63 | undefined, 64 | error 65 | ); 66 | } 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/resolvers/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { getFilterHelperArgOptsMap } from './filter'; 2 | import { getLimitHelperArgsOptsMap } from './limit'; 3 | import { getRecordHelperArgsOptsMap } from './record'; 4 | 5 | export * from './aliases'; 6 | export * from './filter'; 7 | export * from './limit'; 8 | export * from './projection'; 9 | export * from './record'; 10 | export * from './skip'; 11 | export * from './sort'; 12 | 13 | export const MergeAbleHelperArgsOpts = { 14 | sort: 'boolean', 15 | skip: 'boolean', 16 | limit: getLimitHelperArgsOptsMap(), 17 | filter: getFilterHelperArgOptsMap(), 18 | record: getRecordHelperArgsOptsMap(), 19 | records: getRecordHelperArgsOptsMap(), 20 | }; 21 | -------------------------------------------------------------------------------- /src/resolvers/helpers/limit.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectTypeComposerArgumentConfigMapDefinition } from 'graphql-compose'; 2 | import type { ExtendedResolveParams } from '../index'; 3 | 4 | export type LimitHelperArgsOpts = { 5 | /** 6 | * Set limit for default number of returned records 7 | * if it does not provided in query. 8 | * By default: 100 9 | */ 10 | defaultValue?: number; 11 | }; 12 | 13 | // for merging, discriminators merge-able only 14 | export const getLimitHelperArgsOptsMap = (): Record => ({ defaultValue: 'number' }); 15 | 16 | export function limitHelperArgs( 17 | opts?: LimitHelperArgsOpts 18 | ): ObjectTypeComposerArgumentConfigMapDefinition<{ limit: any }> { 19 | return { 20 | limit: { 21 | type: 'Int', 22 | defaultValue: opts?.defaultValue || 100, 23 | }, 24 | }; 25 | } 26 | 27 | export function limitHelper(resolveParams: ExtendedResolveParams): void { 28 | const limit = parseInt(resolveParams.args?.limit, 10) || 0; 29 | if (limit > 0) { 30 | resolveParams.query = resolveParams.query.limit(limit); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/resolvers/helpers/payloadRecordId.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectTypeComposer, 3 | InterfaceTypeComposer, 4 | ComposeOutputTypeDefinition, 5 | ObjectTypeComposerFieldConfigMapDefinition, 6 | toInputType, 7 | } from 'graphql-compose'; 8 | 9 | export type PayloadRecordIdHelperOpts = { 10 | /** Custom function for id generation. By default: `doc._id`. */ 11 | fn?: (doc: any, context: any) => any; 12 | /** Custom output type for returned recordId */ 13 | type?: string | ComposeOutputTypeDefinition; 14 | }; 15 | 16 | export function payloadRecordId( 17 | tc: ObjectTypeComposer | InterfaceTypeComposer, 18 | opts?: PayloadRecordIdHelperOpts | false 19 | ): ObjectTypeComposerFieldConfigMapDefinition | null { 20 | if (opts === false) return null; 21 | 22 | return { 23 | recordId: { 24 | description: 'Document ID', 25 | type: opts?.type ? opts.type : tc.hasField('_id') ? tc.getFieldTC('_id') : 'MongoID', 26 | resolve: (source, _, context) => { 27 | const doc = (source as any)?.record; 28 | if (!doc) return; 29 | return opts?.fn ? opts.fn(doc, context) : doc?._id; 30 | }, 31 | }, 32 | }; 33 | } 34 | 35 | export type PayloadRecordIdsHelperOpts = { 36 | /** Custom function for id generation. By default: `doc._id`. */ 37 | fn?: (docs: any, context: any) => any; 38 | /** Custom output type for returned recordIds */ 39 | type?: string | ComposeOutputTypeDefinition; 40 | }; 41 | 42 | export function payloadRecordIds( 43 | tc: ObjectTypeComposer | InterfaceTypeComposer, 44 | opts?: PayloadRecordIdHelperOpts | false 45 | ): ObjectTypeComposerFieldConfigMapDefinition | null { 46 | if (opts === false) return null; 47 | 48 | return { 49 | recordIds: { 50 | description: 'Documents IDs', 51 | type: opts?.type 52 | ? opts.type 53 | : tc.hasField('_id') 54 | ? toInputType(tc.getFieldTC('_id')).NonNull.List.NonNull 55 | : '[MongoID!]!', 56 | resolve: (source, _, context) => { 57 | const docs = (source as any)?.records; 58 | if (opts?.fn) { 59 | return opts.fn(docs, context); 60 | } 61 | return docs ? docs.map((doc: any) => doc?._id) : []; 62 | }, 63 | }, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/resolvers/helpers/projection.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendedResolveParams } from '../index'; 2 | import { NestedAliasesMap } from './aliases'; 3 | import { isObject } from 'graphql-compose'; 4 | 5 | export function projectionHelper( 6 | resolveParams: ExtendedResolveParams, 7 | aliases?: NestedAliasesMap 8 | ): void { 9 | const projection = resolveParams.projection; 10 | if (projection) { 11 | // if projection has '*' key, then omit field projection (fetch all fields from database) 12 | if (projection['*']) { 13 | return; 14 | } 15 | 16 | const flatProjection = dotifyWithAliases(projection, aliases); 17 | 18 | if (Object.keys(flatProjection).length > 0) { 19 | resolveParams.query = resolveParams.query.select(flatProjection); 20 | } 21 | } 22 | } 23 | 24 | export type ProjectionOperator = Record; 25 | export type FlatDottedObject = Record; 26 | 27 | export function dotifyWithAliases( 28 | obj: Record, 29 | aliases?: NestedAliasesMap 30 | ): FlatDottedObject { 31 | const res: FlatDottedObject = {}; 32 | dotifyRecurse(obj, res, aliases); 33 | return res; 34 | } 35 | 36 | /** 37 | * Nested projection converts to `flat` dotted mongoose projection. 38 | * If you model has aliases, then pass here result from `prepareNestedAliases()` 39 | */ 40 | function dotifyRecurse( 41 | obj: Record, 42 | res: FlatDottedObject, 43 | aliases?: NestedAliasesMap | false, 44 | prefix?: string 45 | ) { 46 | Object.keys(obj).forEach((key: string) => { 47 | const value = obj[key]; 48 | 49 | let newKey; 50 | if (aliases && aliases?.[key]) { 51 | const alias = aliases?.[key]; 52 | let aliasValue; 53 | if (typeof alias === 'string') { 54 | aliasValue = alias; 55 | } else if (isObject(alias)) { 56 | aliasValue = alias?.__selfAlias; 57 | } 58 | newKey = aliasValue || key; 59 | } else { 60 | newKey = key; 61 | } 62 | 63 | if (prefix) { 64 | newKey = `${prefix}.${newKey}`; 65 | } 66 | 67 | if (value && (value.$meta || value.$slice || value.$elemMatch || value.$)) { 68 | // pass MongoDB projection operators https://docs.mongodb.com/v3.2/reference/operator/projection/meta/ 69 | res[newKey] = value; 70 | } else if (isObject(value) && Object.keys(value).length > 0) { 71 | let subAliases: NestedAliasesMap | undefined; 72 | if (aliases && isObject(aliases?.[key])) { 73 | subAliases = (aliases as any)[key]; 74 | } 75 | dotifyRecurse(value, res, subAliases, newKey); 76 | } else { 77 | // set `true` or `false` for projection 78 | res[newKey] = !!value; 79 | } 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /src/resolvers/helpers/record.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectTypeComposer, 3 | InterfaceTypeComposer, 4 | ObjectTypeComposerArgumentConfigMapDefinition, 5 | InputTypeComposer, 6 | } from 'graphql-compose'; 7 | import { makeFieldsRecursiveNullable } from '../../utils/makeFieldsRecursiveNullable'; 8 | import { Document } from 'mongoose'; 9 | 10 | export type RecordHelperArgsOpts = { 11 | /** 12 | * You an remove some fields from type via this option. 13 | */ 14 | removeFields?: string[]; 15 | /** 16 | * This option makes provided fieldNames as required 17 | */ 18 | requiredFields?: string[]; 19 | /** 20 | * This option makes all fields nullable by default. 21 | * May be overridden by `requiredFields` property 22 | */ 23 | allFieldsNullable?: boolean; 24 | /** 25 | * Provide custom prefix for Type name 26 | */ 27 | prefix?: string; 28 | /** 29 | * Provide custom suffix for Type name 30 | */ 31 | suffix?: string; 32 | /** 33 | * Make arg `record` as required if this option is true. 34 | */ 35 | isRequired?: boolean; 36 | }; 37 | 38 | // for merging, discriminators merge-able only 39 | export const getRecordHelperArgsOptsMap = (): Record => ({ 40 | isRequired: 'boolean', 41 | removeFields: 'string[]', 42 | requiredFields: 'string[]', 43 | }); 44 | 45 | export function recordHelperArgs( 46 | tc: ObjectTypeComposer | InterfaceTypeComposer, 47 | opts?: RecordHelperArgsOpts 48 | ): ObjectTypeComposerArgumentConfigMapDefinition<{ record: any }> { 49 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 50 | throw new Error('First arg for recordHelperArgs() should be instance of ObjectTypeComposer.'); 51 | } 52 | 53 | if (!opts) { 54 | throw new Error('You should provide non-empty options.'); 55 | } 56 | 57 | const { prefix, suffix } = opts; 58 | 59 | let recordITC; 60 | const recordTypeName = `${prefix}${tc.getTypeName()}${suffix}`; 61 | const schemaComposer = tc.schemaComposer; 62 | if (schemaComposer.hasInstance(recordTypeName, InputTypeComposer)) { 63 | recordITC = schemaComposer.getITC(recordTypeName); 64 | } else { 65 | recordITC = tc.getInputTypeComposer().clone(recordTypeName); 66 | } 67 | 68 | if (opts && opts.allFieldsNullable) { 69 | makeFieldsRecursiveNullable(recordITC, { prefix, suffix }); 70 | } 71 | 72 | if (opts && opts.removeFields) { 73 | recordITC.removeField(opts.removeFields); 74 | } 75 | 76 | if (opts && opts.requiredFields) { 77 | recordITC.makeRequired(opts.requiredFields); 78 | } 79 | 80 | return { 81 | record: { 82 | type: opts.isRequired ? recordITC.NonNull : recordITC, 83 | }, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/resolvers/helpers/skip.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectTypeComposerArgumentConfigMapDefinition } from 'graphql-compose'; 2 | import type { ExtendedResolveParams } from '../index'; 3 | 4 | export function skipHelperArgs(): ObjectTypeComposerArgumentConfigMapDefinition<{ skip: any }> { 5 | return { 6 | skip: { 7 | type: 'Int', 8 | }, 9 | }; 10 | } 11 | 12 | export function skipHelper(resolveParams: ExtendedResolveParams): void { 13 | const skip = parseInt(resolveParams && resolveParams.args && resolveParams.args.skip, 10); 14 | if (skip > 0) { 15 | resolveParams.query = resolveParams.query.skip(skip); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/resolvers/helpers/sort.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | 3 | import type { Document, Model } from 'mongoose'; 4 | import { 5 | EnumTypeComposer, 6 | InterfaceTypeComposer, 7 | isObject, 8 | ObjectTypeComposer, 9 | ObjectTypeComposerArgumentConfigMapDefinition, 10 | SchemaComposer, 11 | } from 'graphql-compose'; 12 | import { extendByReversedIndexes, getIndexesFromModel } from '../../utils'; 13 | import type { ExtendedResolveParams } from '../index'; 14 | 15 | export type SortHelperArgsOpts = { 16 | /** 17 | * Allow sort arg to be an array of enum values. Example [AGE_DESC, NAME_ASC, _ID_ASC]. 18 | * Note enum values will only ever be generated for *indexed fields*. 19 | */ 20 | multi?: boolean; 21 | /** 22 | * This option set custom type name for generated sort argument. 23 | */ 24 | sortTypeName?: string; 25 | }; 26 | 27 | export function sortHelperArgs( 28 | tc: ObjectTypeComposer | InterfaceTypeComposer, 29 | model: Model, 30 | opts?: SortHelperArgsOpts 31 | ): ObjectTypeComposerArgumentConfigMapDefinition<{ sort: any }> { 32 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 33 | throw new Error('First arg for sortHelperArgs() should be instance of ObjectTypeComposer.'); 34 | } 35 | 36 | if (!model || !model.modelName || !model.schema) { 37 | throw new Error('Second arg for sortHelperArgs() should be instance of Mongoose Model.'); 38 | } 39 | 40 | if (!opts || !opts.sortTypeName) { 41 | throw new Error('You should provide non-empty `sortTypeName` in options for sortHelperArgs().'); 42 | } 43 | 44 | const gqSortType = getSortTypeFromModel(opts.sortTypeName, model, tc.schemaComposer); 45 | 46 | return { 47 | sort: { 48 | type: opts?.multi ? gqSortType.NonNull.List : gqSortType, 49 | }, 50 | }; 51 | } 52 | 53 | export function sortHelper(resolveParams: ExtendedResolveParams): void { 54 | const _sort = resolveParams?.args?.sort; 55 | if (!_sort) return; 56 | 57 | let sort: Record; 58 | if (Array.isArray(_sort)) { 59 | sort = {}; 60 | // combine array in one object, 61 | // keep only first key occurrence (rest skip) 62 | _sort.forEach((o) => { 63 | if (isObject(o)) { 64 | Object.keys(o).forEach((key) => { 65 | if (!sort.hasOwnProperty(key)) { 66 | sort[key] = (o as any)[key]; 67 | } 68 | }); 69 | } 70 | }); 71 | } else { 72 | sort = _sort; 73 | } 74 | 75 | if (typeof sort === 'object' && Object.keys(sort).length > 0) { 76 | resolveParams.query = resolveParams.query.sort(sort); 77 | } 78 | } 79 | 80 | export function getSortTypeFromModel( 81 | typeName: string, 82 | model: Model, 83 | schemaComposer: SchemaComposer 84 | ): EnumTypeComposer { 85 | return schemaComposer.getOrCreateETC(typeName, (etc) => { 86 | const indexes = extendByReversedIndexes(getIndexesFromModel(model)); 87 | const fields: Record = {}; 88 | indexes.forEach((indexData) => { 89 | const keys = Object.keys(indexData); 90 | let name = keys 91 | .join('__') 92 | .toUpperCase() 93 | .replace(/[^_a-zA-Z0-9]/gi, '__'); 94 | if (indexData[keys[0]] === 1) { 95 | name = `${name}_ASC`; 96 | } else if (indexData[keys[0]] === -1) { 97 | name = `${name}_DESC`; 98 | } 99 | fields[name] = { 100 | value: indexData, 101 | }; 102 | }); 103 | 104 | etc.setFields(fields); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /src/resolvers/helpers/validate.ts: -------------------------------------------------------------------------------- 1 | import type { Document, Error as MongooseError } from 'mongoose'; 2 | import { version } from 'mongoose'; 3 | import { ValidationError } from '../../errors'; 4 | 5 | const versionNumber = Number(version.charAt(0)); 6 | 7 | export type ValidationErrorData = { 8 | path: string; 9 | message: string; 10 | value: any; 11 | /** 12 | * This `idx` property is used only for *Many operations. 13 | * It stores idx from received array of records which occurs Validation Error. 14 | */ 15 | idx?: number; 16 | }; 17 | 18 | export type ValidationsWithMessage = { 19 | message: string; 20 | errors: Array; 21 | }; 22 | 23 | export async function validateDoc(doc: Document): Promise { 24 | const validations: MongooseError.ValidationError | null = 25 | versionNumber >= 7 26 | ? doc.validateSync() 27 | : await new Promise((resolve) => doc.validate(resolve as any)); 28 | 29 | return validations?.errors 30 | ? { 31 | message: validations.message, 32 | errors: Object.keys(validations.errors).map((key) => { 33 | // transform object to array[{ path, message, value }, {}, ...] 34 | const { message, value } = validations.errors[key] as any; 35 | return { 36 | path: key, 37 | message, 38 | value, 39 | }; 40 | }), 41 | } 42 | : null; 43 | } 44 | 45 | /** 46 | * Make async validation for mongoose document. 47 | * And if it has validation errors then throw one Error with embedding all validation errors into it. 48 | */ 49 | export async function validateAndThrow(doc: Document): Promise { 50 | const validations: ValidationsWithMessage | null = await validateDoc(doc); 51 | if (validations) { 52 | throw new ValidationError(validations); 53 | } 54 | } 55 | 56 | /** 57 | * Make async validation for array of mongoose documents. 58 | * And if they have validation errors then throw one Error with embedding 59 | * all validator errors into one array with addition of `idx` property. 60 | * `idx` represent record index in array received from user. 61 | */ 62 | export async function validateManyAndThrow(docs: Document[]): Promise { 63 | const combinedValidators: Array = []; 64 | let hasValidationError = false; 65 | 66 | for (let idx = 0; idx < docs.length; idx++) { 67 | const validations: ValidationsWithMessage | null = await validateDoc(docs[idx]); 68 | 69 | if (validations) { 70 | validations.errors.forEach((validatorError) => { 71 | combinedValidators.push({ 72 | ...validatorError, 73 | idx, 74 | }); 75 | }); 76 | 77 | hasValidationError = true; 78 | } 79 | } 80 | 81 | if (hasValidationError) { 82 | throw new ValidationError({ 83 | message: 'Nothing has been saved. Some documents contain validation errors', 84 | errors: combinedValidators, 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import type { ResolverResolveParams } from 'graphql-compose'; 2 | import type { Query, Model, Document } from 'mongoose'; 3 | import { count, CountResolverOpts } from './count'; 4 | import { findById, FindByIdResolverOpts } from './findById'; 5 | import { findByIds, FindByIdsResolverOpts } from './findByIds'; 6 | import { findMany, FindManyResolverOpts } from './findMany'; 7 | import { findOne, FindOneResolverOpts } from './findOne'; 8 | import { createMany, CreateManyResolverOpts } from './createMany'; 9 | import { createOne, CreateOneResolverOpts } from './createOne'; 10 | import { updateById, UpdateByIdResolverOpts } from './updateById'; 11 | import { updateMany, UpdateManyResolverOpts } from './updateMany'; 12 | import { updateOne, UpdateOneResolverOpts } from './updateOne'; 13 | import { removeById, RemoveByIdResolverOpts } from './removeById'; 14 | import { removeMany, RemoveManyResolverOpts } from './removeMany'; 15 | import { removeOne, RemoveOneResolverOpts } from './removeOne'; 16 | import { dataLoader, DataLoaderResolverOpts } from './dataLoader'; 17 | import { dataLoaderMany, DataLoaderManyResolverOpts } from './dataLoaderMany'; 18 | import { pagination, PaginationResolverOpts } from './pagination'; 19 | import { connection, ConnectionResolverOpts } from './connection'; 20 | 21 | export type AllResolversOpts = { 22 | count?: false | CountResolverOpts; 23 | findById?: false | FindByIdResolverOpts; 24 | findByIds?: false | FindByIdsResolverOpts; 25 | findOne?: false | FindOneResolverOpts; 26 | findMany?: false | FindManyResolverOpts; 27 | dataLoader?: false | DataLoaderResolverOpts; 28 | dataLoaderMany?: false | DataLoaderManyResolverOpts; 29 | createOne?: false | CreateOneResolverOpts; 30 | createMany?: false | CreateManyResolverOpts; 31 | updateById?: false | UpdateByIdResolverOpts; 32 | updateOne?: false | UpdateOneResolverOpts; 33 | updateMany?: false | UpdateManyResolverOpts; 34 | removeById?: false | RemoveByIdResolverOpts; 35 | removeOne?: false | RemoveOneResolverOpts; 36 | removeMany?: false | RemoveManyResolverOpts; 37 | connection?: false | ConnectionResolverOpts; 38 | pagination?: false | PaginationResolverOpts; 39 | }; 40 | 41 | export type ExtendedResolveParams = Partial< 42 | ResolverResolveParams 43 | > & { 44 | query: Query; 45 | rawQuery: { [optName: string]: any }; 46 | beforeQuery?: (query: Query, rp: ExtendedResolveParams) => Promise; 47 | beforeRecordMutate?: (record: TDoc, rp: ExtendedResolveParams) => Promise; 48 | model: Model; 49 | }; 50 | 51 | export const resolverFactory = { 52 | count, 53 | findById, 54 | findByIds, 55 | findOne, 56 | findMany, 57 | dataLoader, 58 | dataLoaderMany, 59 | createOne, 60 | createMany, 61 | updateById, 62 | updateOne, 63 | updateMany, 64 | removeById, 65 | removeOne, 66 | removeMany, 67 | pagination, 68 | connection, 69 | }; 70 | 71 | export { 72 | CountResolverOpts, 73 | FindByIdResolverOpts, 74 | FindByIdsResolverOpts, 75 | FindManyResolverOpts, 76 | FindOneResolverOpts, 77 | CreateManyResolverOpts, 78 | CreateOneResolverOpts, 79 | UpdateByIdResolverOpts, 80 | UpdateManyResolverOpts, 81 | UpdateOneResolverOpts, 82 | RemoveByIdResolverOpts, 83 | RemoveManyResolverOpts, 84 | RemoveOneResolverOpts, 85 | DataLoaderResolverOpts, 86 | DataLoaderManyResolverOpts, 87 | PaginationResolverOpts, 88 | ConnectionResolverOpts, 89 | }; 90 | -------------------------------------------------------------------------------- /src/resolvers/pagination.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 2 | import type { Model, Document } from 'mongoose'; 3 | import { 4 | preparePaginationResolver, 5 | PaginationTArgs, 6 | PaginationResolverOpts as _PaginationResolverOpts, 7 | } from 'graphql-compose-pagination'; 8 | import { CountResolverOpts, count } from './count'; 9 | import { FindManyResolverOpts, findMany } from './findMany'; 10 | 11 | export type PaginationResolverOpts = Omit< 12 | _PaginationResolverOpts, 13 | 'countResolver' | 'findManyResolver' 14 | > & { 15 | findManyResolver?: Resolver; 16 | countResolver?: Resolver; 17 | countOpts?: CountResolverOpts; 18 | findManyOpts?: FindManyResolverOpts; 19 | }; 20 | 21 | export function pagination( 22 | model: Model, 23 | tc: ObjectTypeComposer | InterfaceTypeComposer, 24 | opts?: PaginationResolverOpts 25 | ): Resolver { 26 | const { countOpts, findManyOpts, findManyResolver, countResolver, ...restOpts } = opts || {}; 27 | const resolver = preparePaginationResolver(tc, { 28 | findManyResolver: findManyResolver || findMany(model, tc, findManyOpts), 29 | countResolver: countResolver || count(model, tc, countOpts), 30 | ...restOpts, 31 | }); 32 | return resolver; 33 | } 34 | -------------------------------------------------------------------------------- /src/resolvers/removeById.ts: -------------------------------------------------------------------------------- 1 | import { toInputType } from 'graphql-compose'; 2 | import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; 3 | import type { Model, Document } from 'mongoose'; 4 | import { findById } from './findById'; 5 | import type { ExtendedResolveParams } from './index'; 6 | import { addErrorCatcherField } from './helpers/errorCatcher'; 7 | import { PayloadRecordIdHelperOpts, payloadRecordId } from './helpers/payloadRecordId'; 8 | 9 | export interface RemoveByIdResolverOpts { 10 | /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */ 11 | suffix?: string; 12 | /** Customize payload.recordId field. If false, then this field will be removed. */ 13 | recordId?: PayloadRecordIdHelperOpts | false; 14 | /** Customize payload.error field. If true, then this field will be removed. */ 15 | disableErrorField?: boolean; 16 | } 17 | 18 | type TArgs = { 19 | _id: any; 20 | }; 21 | 22 | export function removeById( 23 | model: Model, 24 | tc: ObjectTypeComposer | InterfaceTypeComposer, 25 | opts?: RemoveByIdResolverOpts 26 | ): Resolver { 27 | if (!model || !model.modelName || !model.schema) { 28 | throw new Error('First arg for Resolver removeById() should be instance of Mongoose Model.'); 29 | } 30 | 31 | if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { 32 | throw new Error( 33 | 'Second arg for Resolver removeById() should be instance of ObjectTypeComposer.' 34 | ); 35 | } 36 | 37 | const findByIdResolver = findById(model, tc); 38 | 39 | const outputTypeName = `RemoveById${tc.getTypeName()}${opts?.suffix || ''}Payload`; 40 | const outputType = tc.schemaComposer.getOrCreateOTC(outputTypeName, (t) => { 41 | t.setFields({ 42 | ...payloadRecordId(tc, opts?.recordId), 43 | record: { 44 | type: tc, 45 | description: 'Removed document', 46 | }, 47 | }); 48 | }); 49 | 50 | const resolver = tc.schemaComposer.createResolver({ 51 | name: 'removeById', 52 | kind: 'mutation', 53 | description: 54 | 'Remove one document: ' + 55 | '1) Retrieve one document and remove with hooks via findByIdAndRemove. ' + 56 | '2) Return removed document.', 57 | type: outputType, 58 | args: { 59 | _id: tc.hasField('_id') ? toInputType(tc.getFieldTC('_id')).NonNull : 'MongoID!', 60 | }, 61 | resolve: (async (resolveParams: ExtendedResolveParams) => { 62 | const _id = resolveParams?.args?._id; 63 | 64 | if (!_id) { 65 | throw new Error(`${tc.getTypeName()}.removeById resolver requires args._id value`); 66 | } 67 | 68 | // We should get all data for document, cause Mongoose model may have hooks/middlewares 69 | // which required some fields which not in graphql projection 70 | // So empty projection returns all fields. 71 | let doc: TDoc = await findByIdResolver.resolve({ ...resolveParams, projection: {} }); 72 | 73 | if (resolveParams.beforeRecordMutate) { 74 | doc = await resolveParams.beforeRecordMutate(doc, resolveParams); 75 | } 76 | if (doc) { 77 | if ('remove' in doc && typeof doc.remove === 'function' && !doc.remove.length) { 78 | await doc.remove(); 79 | } else { 80 | await doc.deleteOne(); 81 | } 82 | 83 | return { 84 | record: doc, 85 | }; 86 | } 87 | 88 | return null; 89 | }) as any, 90 | }); 91 | 92 | if (!opts?.disableErrorField) { 93 | // Add `error` field to payload which can catch resolver Error 94 | // and return it in mutation payload 95 | addErrorCatcherField(resolver); 96 | } 97 | 98 | return resolver; 99 | } 100 | -------------------------------------------------------------------------------- /src/types/BSONDecimal.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { GraphQLScalarType, Kind } from 'graphql-compose/lib/graphql'; 3 | 4 | const Decimal128 = mongoose.Types.Decimal128; 5 | 6 | const GraphQLBSONDecimal = new GraphQLScalarType({ 7 | name: 'BSONDecimal', 8 | description: 9 | 'The `Decimal` scalar type uses the IEEE 754 decimal128 ' + 10 | 'decimal-based floating-point numbering format. ' + 11 | 'Supports 34 decimal digits of precision, a max value of ' + 12 | 'approximately 10^6145, and min value of approximately -10^6145', 13 | serialize: String, 14 | parseValue(value: any) { 15 | if (typeof value === 'string') { 16 | return Decimal128.fromString(value); 17 | } 18 | if (typeof value === 'number') { 19 | return Decimal128.fromString(value.toString()); 20 | } 21 | if (value instanceof Decimal128) { 22 | return value; 23 | } 24 | throw new TypeError('Field error: value is an invalid Decimal'); 25 | }, 26 | parseLiteral(ast) { 27 | if (ast.kind === Kind.STRING || ast.kind === Kind.INT) { 28 | return Decimal128.fromString(ast.value); 29 | } 30 | return null; 31 | }, 32 | }); 33 | 34 | export default GraphQLBSONDecimal; 35 | -------------------------------------------------------------------------------- /src/types/MongoID.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { GraphQLScalarType, Kind } from 'graphql-compose/lib/graphql'; 3 | 4 | const ObjectId = mongoose.Types.ObjectId; 5 | 6 | const GraphQLMongoID = new GraphQLScalarType({ 7 | name: 'MongoID', 8 | description: 9 | 'The `ID` scalar type represents a unique MongoDB identifier in collection. ' + 10 | 'MongoDB by default use 12-byte ObjectId value ' + 11 | '(https://docs.mongodb.com/manual/reference/bson-types/#objectid). ' + 12 | 'But MongoDB also may accepts string or integer as correct values for _id field.', 13 | serialize: String, 14 | parseValue(value: any) { 15 | if (!ObjectId.isValid(value) && typeof value !== 'string') { 16 | throw new TypeError('Field error: value is an invalid ObjectId'); 17 | } 18 | return value; 19 | }, 20 | parseLiteral(ast) { 21 | return ast.kind === Kind.STRING || ast.kind === Kind.INT ? ast.value : null; 22 | }, 23 | }); 24 | 25 | export default GraphQLMongoID; 26 | -------------------------------------------------------------------------------- /src/types/RegExpAsString.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, Kind } from 'graphql-compose/lib/graphql'; 2 | 3 | function parseStringWithRegExp(str: string): RegExp { 4 | if (str.startsWith('/')) { 5 | const m = str.match(/^\/(.+)\/([gimsuy]*)$/); 6 | if (m) { 7 | return new RegExp(m[1], m[2]); 8 | } 9 | throw new TypeError('Field error: cannot parse provided string as RegExp object'); 10 | } else { 11 | // simple regexp without expression flags 12 | return new RegExp(str); 13 | } 14 | } 15 | 16 | const GraphQLRegExpAsString = new GraphQLScalarType({ 17 | name: 'RegExpAsString', 18 | specifiedByURL: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf', 19 | description: 20 | 'The string representation of JavaScript regexp. You may provide it with flags "/^abc.*/i" or without flags like "^abc.*". More info about RegExp characters and flags: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions', 21 | serialize: String, 22 | parseValue(value: any) { 23 | if (typeof value !== 'string') { 24 | throw new TypeError( 25 | 'Field error: GraphQL RegExpAsString value should be provided as a string' 26 | ); 27 | } 28 | return parseStringWithRegExp(value); 29 | }, 30 | parseLiteral(ast) { 31 | if (ast.kind !== Kind.STRING) { 32 | throw new TypeError( 33 | 'Field error: GraphQL RegExpAsString value should be provided as a string' 34 | ); 35 | } 36 | return parseStringWithRegExp(ast.value); 37 | }, 38 | }); 39 | 40 | export default GraphQLRegExpAsString; 41 | -------------------------------------------------------------------------------- /src/types/__tests__/BSONDecimal-test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Kind } from 'graphql-compose/lib/graphql'; 3 | import GraphQLBSONDecimal from '../BSONDecimal'; 4 | 5 | const Decimal128 = mongoose.Types.Decimal128; 6 | 7 | describe('GraphQLBSONDecimal', () => { 8 | describe('serialize', () => { 9 | it('pass Decimal128', () => { 10 | const amount = Decimal128.fromString('90000000000000000000000000000000.09'); 11 | expect(GraphQLBSONDecimal.serialize(amount)).toBe('90000000000000000000000000000000.09'); 12 | }); 13 | 14 | it('pass String', () => { 15 | const amount = '90000000000000000000000000000000.09'; 16 | expect(GraphQLBSONDecimal.serialize(amount)).toBe('90000000000000000000000000000000.09'); 17 | }); 18 | }); 19 | 20 | describe('parseValue', () => { 21 | it('pass Decimal128', () => { 22 | const amount = Decimal128.fromString('90000000000000000000000000000000.09'); 23 | expect(GraphQLBSONDecimal.parseValue(amount)).toBeInstanceOf(Decimal128); 24 | }); 25 | 26 | it('pass String', () => { 27 | const amount = '90000000000000000000000000000000.09'; 28 | expect(GraphQLBSONDecimal.parseValue(amount)).toBeInstanceOf(Decimal128); 29 | }); 30 | 31 | it('pass Integer', () => { 32 | const amount = 123; 33 | expect(GraphQLBSONDecimal.parseValue(amount)).toBeInstanceOf(Decimal128); 34 | }); 35 | 36 | it('pass any custom string value', () => { 37 | const id = 'custom_id'; 38 | expect(() => GraphQLBSONDecimal.parseValue(id)).toThrow('not a valid Decimal128 string'); 39 | }); 40 | }); 41 | 42 | describe('parseLiteral', () => { 43 | it('parse a ast STRING literal', async () => { 44 | const ast = { 45 | kind: Kind.STRING, 46 | value: '90000000000000000000000000000000.09', 47 | } as any; 48 | const amount: any = GraphQLBSONDecimal.parseLiteral(ast, {}); 49 | expect(amount).toBeInstanceOf(Decimal128); 50 | expect(amount.toString()).toEqual('90000000000000000000000000000000.09'); 51 | }); 52 | 53 | it('parse a ast INT literal', async () => { 54 | const ast: any = { 55 | kind: Kind.INT, 56 | value: '123', 57 | }; 58 | const amount: any = GraphQLBSONDecimal.parseLiteral(ast, {}); 59 | expect(amount).toBeInstanceOf(Decimal128); 60 | expect(amount.toString()).toEqual('123'); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/types/__tests__/MongoID-test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Kind } from 'graphql-compose/lib/graphql'; 3 | import GraphQLMongoID from '../MongoID'; 4 | 5 | const ObjectId = mongoose.Types.ObjectId; 6 | 7 | describe('GraphQLMongoID', () => { 8 | describe('serialize', () => { 9 | it('pass ObjectId', () => { 10 | const id = new ObjectId('5a0d77aa7e65a808ad24937f'); 11 | expect(GraphQLMongoID.serialize(id)).toBe('5a0d77aa7e65a808ad24937f'); 12 | }); 13 | 14 | it('pass String', () => { 15 | const id = '5a0d77aa7e65a808ad249000'; 16 | expect(GraphQLMongoID.serialize(id)).toBe('5a0d77aa7e65a808ad249000'); 17 | }); 18 | }); 19 | 20 | describe('parseValue', () => { 21 | it('pass ObjectId', () => { 22 | const id = new ObjectId('5a0d77aa7e65a808ad24937f'); 23 | expect(GraphQLMongoID.parseValue(id)).toBe(id); 24 | }); 25 | 26 | it('pass ObjectId as string', () => { 27 | const id = '5a0d77aa7e65a808ad249000'; 28 | expect(GraphQLMongoID.parseValue(id)).toEqual(id); 29 | }); 30 | 31 | it('pass integer', () => { 32 | const id = 123; 33 | expect(GraphQLMongoID.parseValue(id)).toEqual(id); 34 | }); 35 | 36 | it('pass any custom string', () => { 37 | const id = 'custom_id'; 38 | expect(GraphQLMongoID.parseValue(id)).toEqual(id); 39 | }); 40 | }); 41 | 42 | describe('parseLiteral', () => { 43 | it('parse a ast STRING literal', async () => { 44 | const ast = { 45 | kind: Kind.STRING, 46 | value: '5a0d77aa7e65a808ad249000', 47 | } as any; 48 | const id: any = GraphQLMongoID.parseLiteral(ast, {}); 49 | expect(id).toEqual('5a0d77aa7e65a808ad249000'); 50 | }); 51 | 52 | it('parse a ast INT literal', async () => { 53 | const ast: any = { 54 | kind: Kind.INT, 55 | value: 123, 56 | }; 57 | const id: any = GraphQLMongoID.parseLiteral(ast, {}); 58 | expect(id).toEqual(123); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/types/__tests__/RegExpAsString-test.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from 'graphql-compose/lib/graphql'; 2 | import GraphQLRegExpAsString from '../RegExpAsString'; 3 | 4 | describe('GraphQLRegExpAsString', () => { 5 | describe('serialize', () => { 6 | it('pass RegExp without flags', () => { 7 | expect(GraphQLRegExpAsString.serialize(new RegExp('^Abc$'))).toBe('/^Abc$/'); 8 | expect(GraphQLRegExpAsString.serialize(new RegExp(/^Abc$/))).toBe('/^Abc$/'); 9 | }); 10 | 11 | it('pass RegExp with flags', () => { 12 | expect(GraphQLRegExpAsString.serialize(new RegExp('^Abc$', 'gm'))).toBe('/^Abc$/gm'); 13 | }); 14 | 15 | it('pass as String', () => { 16 | expect(GraphQLRegExpAsString.serialize('abc')).toBe('abc'); 17 | }); 18 | }); 19 | 20 | describe('parseValue', () => { 21 | it('pass as string', () => { 22 | expect(GraphQLRegExpAsString.parseValue('^Abc$')).toEqual(/^Abc$/); 23 | expect(GraphQLRegExpAsString.parseValue('/^Abc$/')).toEqual(/^Abc$/); 24 | expect(GraphQLRegExpAsString.parseValue('/^Abc$/gm')).toEqual(/^Abc$/gm); 25 | expect(GraphQLRegExpAsString.parseValue('so/me')).toEqual(/so\/me/); 26 | }); 27 | 28 | it('pass as wrong type', () => { 29 | expect(() => GraphQLRegExpAsString.parseValue(123)).toThrow( 30 | 'value should be provided as a string' 31 | ); 32 | }); 33 | }); 34 | 35 | describe('parseLiteral', () => { 36 | it('parse a ast STRING literal', async () => { 37 | const ast = { 38 | kind: Kind.STRING, 39 | value: '^Abc$', 40 | } as any; 41 | expect(GraphQLRegExpAsString.parseLiteral(ast, {})).toEqual(/^Abc$/); 42 | 43 | const ast2 = { 44 | kind: Kind.STRING, 45 | value: '/^Abc$/gm', 46 | } as any; 47 | expect(GraphQLRegExpAsString.parseLiteral(ast2, {})).toEqual(/^Abc$/gm); 48 | }); 49 | 50 | it('parse wrong ast literal', async () => { 51 | const ast: any = { 52 | kind: Kind.INT, 53 | value: 123, 54 | }; 55 | expect(() => GraphQLRegExpAsString.parseLiteral(ast, {})).toThrow( 56 | 'value should be provided as a string' 57 | ); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { isObject, upperFirst } from 'graphql-compose'; 2 | import { toMongoDottedObject, toMongoFilterDottedObject } from './toMongoDottedObject'; 3 | 4 | export { toMongoDottedObject, toMongoFilterDottedObject, isObject, upperFirst }; 5 | export * from './getIndexesFromModel'; 6 | -------------------------------------------------------------------------------- /src/utils/makeFieldsRecursiveNullable.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeComposer, AnyTypeComposer, replaceTC } from 'graphql-compose'; 2 | 3 | export function makeFieldsRecursiveNullable( 4 | itc: InputTypeComposer, 5 | opts: { prefix?: string; suffix?: string; skipTypes?: AnyTypeComposer[] } 6 | ): void { 7 | // clone all subtypes and make all its fields nullable 8 | itc.getFieldNames().forEach((fieldName) => { 9 | itc.makeFieldNullable(fieldName); 10 | let fieldTC = itc.getFieldTC(fieldName); 11 | if (fieldTC instanceof InputTypeComposer) { 12 | if (opts?.prefix || opts?.suffix) { 13 | const newName = dedupedName(fieldTC.getTypeName(), opts); 14 | fieldTC = fieldTC.clone(newName); 15 | // replace field type with keeping in place List & NonNull modificators if they are present 16 | itc.getField(fieldName).type = replaceTC(itc.getField(fieldName).type, fieldTC); 17 | } 18 | if (!opts.skipTypes) opts.skipTypes = []; 19 | if (!opts.skipTypes.includes(fieldTC)) { 20 | opts.skipTypes.push(fieldTC); 21 | makeFieldsRecursiveNullable(fieldTC, opts); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | function dedupedName(name: string, opts: { prefix?: string; suffix?: string }): string { 28 | let newName = name; 29 | const { prefix, suffix } = opts; 30 | 31 | if (prefix && !newName.startsWith(prefix)) { 32 | newName = `${prefix}${newName}`; 33 | } 34 | 35 | if (suffix && !newName.endsWith(suffix)) { 36 | newName = `${newName}${suffix}`; 37 | } 38 | 39 | return newName; 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/testHelpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SchemaComposer, 3 | Resolver, 4 | ObjectTypeComposerFieldConfigAsObjectDefinition, 5 | inspect, 6 | } from 'graphql-compose'; 7 | import { graphql, ExecutionResult } from 'graphql-compose/lib/graphql'; 8 | 9 | const FIELD = 'test_field'; 10 | 11 | interface TestOperationOpts { 12 | schemaComposer: SchemaComposer; 13 | operation: string; 14 | variables?: any; 15 | source?: Record; 16 | context?: any; 17 | } 18 | 19 | async function testOperation(opts: TestOperationOpts): Promise { 20 | const res = await graphql({ 21 | schema: opts.schemaComposer.buildSchema(), 22 | source: opts.operation, 23 | rootValue: opts?.source || {}, 24 | contextValue: opts?.context || {}, 25 | variableValues: opts?.variables, 26 | }); 27 | return res; 28 | } 29 | 30 | interface TestFieldConfigOpts { 31 | args?: TArgs; 32 | field: 33 | | ObjectTypeComposerFieldConfigAsObjectDefinition 34 | | Resolver; 35 | selection: string; 36 | source?: Record; 37 | context?: TContext; 38 | schemaComposer?: SchemaComposer; 39 | } 40 | 41 | export async function testFieldConfig( 42 | opts: TestFieldConfigOpts 43 | ): Promise { 44 | const { field, selection, args, ...restOpts } = opts; 45 | 46 | const sc = opts?.schemaComposer || new SchemaComposer(); 47 | sc.Query.setField(FIELD, field); 48 | 49 | const ac = _getArgsForQuery(field, args, sc); 50 | const selectionSet = selection.trim(); 51 | if (!selectionSet.startsWith('{') || !selectionSet.endsWith('}')) { 52 | throw new Error( 53 | `Error in testFieldConfig({ selection: '...' }) – selection must be a string started from "{" and ended with "}"` 54 | ); 55 | } 56 | const res = await testOperation({ 57 | ...restOpts, 58 | variables: args, 59 | operation: ` 60 | query ${ac.queryVars} { 61 | ${FIELD}${ac.fieldVars} ${selectionSet} 62 | } 63 | `, 64 | schemaComposer: sc, 65 | }); 66 | 67 | if (res.errors) { 68 | throw new Error((res?.errors?.[0] as any) || 'GraphQL Error'); 69 | } 70 | 71 | return res?.data?.[FIELD]; 72 | } 73 | 74 | function _getArgsForQuery( 75 | fc: ObjectTypeComposerFieldConfigAsObjectDefinition | Resolver, 76 | variables: any = {}, 77 | schemaComposer?: SchemaComposer 78 | ): { 79 | queryVars: string; 80 | fieldVars: string; 81 | } { 82 | const sc = schemaComposer || new SchemaComposer(); 83 | sc.Query.setField(FIELD, fc); 84 | 85 | const varNames = Object.keys(variables); 86 | 87 | const argNames = sc.Query.getFieldArgNames(FIELD); 88 | if (argNames.length === 0 && varNames.length > 0) { 89 | throw new Error( 90 | `FieldConfig does not have any arguments. But in test you provided the following variables: ${inspect( 91 | variables 92 | )}` 93 | ); 94 | } 95 | 96 | varNames.forEach((varName) => { 97 | if (!argNames.includes(varName)) { 98 | throw new Error( 99 | `FieldConfig does not have '${varName}' argument. Available arguments: '${argNames.join( 100 | "', '" 101 | )}'.` 102 | ); 103 | } 104 | }); 105 | 106 | argNames.forEach((argName) => { 107 | if (sc.Query.isFieldArgNonNull(FIELD, argName)) { 108 | const val = variables[argName]; 109 | if (val === null || val === undefined) { 110 | throw new Error( 111 | `FieldConfig has required argument '${argName}'. But you did not provide it in your test via variables: '${inspect( 112 | variables 113 | )}'.` 114 | ); 115 | } 116 | } 117 | }); 118 | 119 | const queryVars = varNames 120 | .map((n) => `$${n}: ${String(sc.Query.getFieldArgType(FIELD, n))}`) 121 | .join(' '); 122 | const fieldVars = varNames.map((n) => `${n}: $${n}`).join(' '); 123 | 124 | return { 125 | queryVars: queryVars ? `(${queryVars})` : '', 126 | fieldVars: fieldVars ? `(${fieldVars})` : '', 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "outDir": "./lib", 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["**/__tests__", "**/__mocks__"] 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "removeComments": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedParameters": true, 17 | "noUnusedLocals": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "lib": ["es2017", "esnext.asynciterable"], 20 | "types": ["node", "jest"], 21 | "baseUrl": ".", 22 | "rootDir": "./src", 23 | }, 24 | "include": ["src/**/*"], 25 | "exclude": [ 26 | "./node_modules" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------