├── .circleci └── config.yml ├── .commitlintrc.json ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .release-it.json ├── README.md ├── index.d.ts ├── index.js ├── index.ts ├── lib ├── decorators │ ├── args-type.decorator.ts │ ├── args.decorator.ts │ ├── context.decorator.ts │ ├── directive.decorator.ts │ ├── extensions.decorator.ts │ ├── field.decorator.ts │ ├── hide-field.decorator.ts │ ├── index.ts │ ├── info.decorator.ts │ ├── input-type.decorator.ts │ ├── interface-type.decorator.ts │ ├── mutation.decorator.ts │ ├── object-type.decorator.ts │ ├── param.utils.ts │ ├── parent.decorator.ts │ ├── query.decorator.ts │ ├── resolve-field.decorator.ts │ ├── resolver.decorator.ts │ ├── resolvers.utils.ts │ ├── root.decorator.ts │ ├── scalar.decorator.ts │ └── subscription.decorator.ts ├── enums │ ├── class-type.enum.ts │ ├── gql-paramtype.enum.ts │ └── resolver.enum.ts ├── factories │ └── params.factory.ts ├── fgql.constants.ts ├── fgql.module.ts ├── graphql │ ├── graphql-ast.explorer.ts │ ├── graphql-schema.builder.ts │ ├── graphql-schema.host.ts │ ├── graphql-types.loader.ts │ ├── graphql.factory.ts │ └── index.ts ├── index.ts ├── interfaces │ ├── base-type-options.interface.ts │ ├── build-schema-options.interface.ts │ ├── complexity.interface.ts │ ├── custom-scalar.interface.ts │ ├── fgql-module-options.interface.ts │ ├── gql-exception-filter.interface.ts │ ├── index.ts │ ├── resolve-type-fn.interface.ts │ ├── resolver-metadata.interface.ts │ ├── return-type-func.interface.ts │ └── type-options.interface.ts ├── plugin │ ├── compiler-plugin.ts │ ├── index.ts │ ├── merge-options.ts │ ├── plugin-constants.ts │ ├── utils │ │ ├── ast-utils.ts │ │ └── plugin-utils.ts │ └── visitors │ │ └── model-class.visitor.ts ├── scalars │ ├── index.ts │ ├── iso-date.scalar.ts │ └── timestamp.scalar.ts ├── schema-builder │ ├── errors │ │ ├── cannot-determine-input-type.error.ts │ │ ├── cannot-determine-output-type.error.ts │ │ ├── default-nullable-conflict.error.ts │ │ ├── default-values-conflict.error.ts │ │ ├── directive-parsing.error.ts │ │ ├── invalid-nullable-option.error.ts │ │ ├── return-type-cannot-be-resolved.error.ts │ │ ├── schema-generation.error.ts │ │ ├── unable-to-find-fields.error.ts │ │ ├── undefined-resolver-type.error.ts │ │ ├── undefined-return-type.error.ts │ │ └── undefined-type.error.ts │ ├── factories │ │ ├── args.factory.ts │ │ ├── ast-definition-node.factory.ts │ │ ├── enum-definition.factory.ts │ │ ├── factories.ts │ │ ├── input-type-definition.factory.ts │ │ ├── input-type.factory.ts │ │ ├── interface-definition.factory.ts │ │ ├── mutation-type.factory.ts │ │ ├── object-type-definition.factory.ts │ │ ├── orphaned-types.factory.ts │ │ ├── output-type.factory.ts │ │ ├── query-type.factory.ts │ │ ├── resolve-type.factory.ts │ │ ├── root-type.factory.ts │ │ ├── subscription-type.factory.ts │ │ └── union-definition.factory.ts │ ├── graphql-schema.factory.ts │ ├── helpers │ │ ├── file-system.helper.ts │ │ └── get-default-value.helper.ts │ ├── index.ts │ ├── metadata │ │ ├── class.metadata.ts │ │ ├── directive.metadata.ts │ │ ├── enum.metadata.ts │ │ ├── extensions.metadata.ts │ │ ├── index.ts │ │ ├── interface.metadata.ts │ │ ├── object-type.metadata.ts │ │ ├── param.metadata.ts │ │ ├── property.metadata.ts │ │ ├── resolver.metadata.ts │ │ └── union.metadata.ts │ ├── schema-builder.module.ts │ ├── services │ │ ├── orphaned-reference.registry.ts │ │ ├── type-fields.accessor.ts │ │ └── type-mapper.service.ts │ ├── storages │ │ ├── index.ts │ │ ├── lazy-metadata.storage.ts │ │ ├── type-definitions.storage.ts │ │ └── type-metadata.storage.ts │ ├── type-definitions.generator.ts │ └── utils │ │ ├── get-fields-and-decorator.util.ts │ │ ├── is-target-equal-util.ts │ │ └── is-throwing.util.ts ├── services │ ├── base-explorer.service.ts │ ├── gql-arguments-host.ts │ ├── gql-execution-context.ts │ ├── index.ts │ ├── resolvers-explorer.service.ts │ └── scalars-explorer.service.ts ├── type-factories │ ├── create-union-type.factory.ts │ ├── index.ts │ └── register-enum-type.factory.ts └── utils │ ├── add-class-type-metadata.util.ts │ ├── async-iterator.util.ts │ ├── extend.util.ts │ ├── extract-metadata.util.ts │ ├── generate-token.util.ts │ ├── index.ts │ ├── is-pipe.util.ts │ ├── merge-defaults.util.ts │ ├── normalize-route-path.util.ts │ ├── reflection.utilts.ts │ ├── remove-temp.util.ts │ └── scalar-types.utils.ts ├── package.json ├── plugin.js ├── plugin.ts ├── tests ├── code-first │ ├── app.module.ts │ ├── common │ │ ├── filters │ │ │ └── unauthorized.filter.ts │ │ ├── guards │ │ │ └── auth.guard.ts │ │ └── scalars │ │ │ └── date.scalar.ts │ ├── directions │ │ ├── directions.module.ts │ │ └── directions.resolver.ts │ ├── enums │ │ └── direction.enum.ts │ ├── main.ts │ ├── other │ │ ├── abstract.resolver.ts │ │ └── sample-orphaned.type.ts │ └── recipes │ │ ├── dto │ │ ├── filter-recipes-count.args.ts │ │ ├── new-recipe.input.ts │ │ └── recipes.args.ts │ │ ├── models │ │ ├── category.ts │ │ ├── ingredient.ts │ │ └── recipe.ts │ │ ├── recipes.module.ts │ │ ├── recipes.resolver.ts │ │ ├── recipes.service.ts │ │ └── unions │ │ └── search-result.union.ts ├── e2e │ ├── code-first-schema.spec.ts │ └── code-first.spec.ts └── utils │ ├── introspection-schema.utils.ts │ └── printed-schema.snapshot.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache 5 | restore_cache: 6 | key: dependency-cache-{{ checksum "package.json" }} 7 | - &install-deps 8 | run: 9 | name: Install dependencies 10 | command: yarn --frozen-lockfile 11 | - &build-packages 12 | run: 13 | name: Build 14 | command: yarn build 15 | 16 | jobs: 17 | build: 18 | working_directory: ~/nest 19 | docker: 20 | - image: circleci/node:12 21 | steps: 22 | - checkout 23 | - restore_cache: 24 | key: dependency-cache-{{ checksum "package.json" }} 25 | - run: 26 | name: Install dependencies 27 | command: yarn --frozen-lockfile 28 | - save_cache: 29 | key: dependency-cache-{{ checksum "package.json" }} 30 | paths: 31 | - ./node_modules 32 | - run: 33 | name: Build 34 | command: yarn build 35 | 36 | integration_tests: 37 | working_directory: ~/nest 38 | docker: 39 | - image: circleci/node:12 40 | steps: 41 | - checkout 42 | - *restore-cache 43 | - *install-deps 44 | - run: 45 | name: Integration tests 46 | command: yarn test:integration 47 | 48 | workflows: 49 | version: 2 50 | build-and-test: 51 | jobs: 52 | - build 53 | - integration_tests: 54 | requires: 55 | - build 56 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tests/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | '@typescript-eslint/no-use-before-define': 'off', 24 | '@typescript-eslint/no-unused-vars': 'off', 25 | '@typescript-eslint/explicit-module-boundary-types': 'off', 26 | '@typescript-eslint/ban-types': 'off', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # misc 10 | npm-debug.log 11 | yarn-error.log 12 | package-lock.json 13 | .DS_Store 14 | 15 | # tests 16 | /test 17 | /coverage 18 | /.nyc_output 19 | test-schema.graphql 20 | *.test-definitions.ts 21 | 22 | # dist 23 | /lib/src 24 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | tests 4 | index.ts 5 | package-lock.json 6 | tsconfig.json 7 | .prettierrc 8 | 9 | # misc 10 | .commitlintrc.json 11 | .release-it.json 12 | .eslintignore 13 | .eslintrc.js 14 | renovate.json 15 | .prettierignore 16 | .prettierrc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tests/generated-definitions/*.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(): release v${version}" 4 | }, 5 | "github": { 6 | "release": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nest-fgql 2 | ========= 3 | 4 | A fast and lightweight module to expose [GraphQL](https://graphql.org/) APIs in a [NestJS](https://nestjs.com) application. 5 | 6 |

7 | CircleCI 8 | npm (scoped) 9 |

10 | 11 | ## Description 12 | 13 | This is intended to be a drop-in replacement for [@nestjs/graphql](https://github.com/nestjs/graphql) that offers improved runtime performance. 14 | 15 | [Benchmarks](https://github.com/benawad/node-graphql-benchmarks) show that Apollo adds a noteable overhead regarding performance. Using [fastify](https://github.com/fastify/fastify) and [graphql-jit](https://github.com/zalando-incubator/graphql-jit) via [fastify-gql](https://github.com/mcollina/fastify-gql) performance is improved. 16 | 17 | ## Usage 18 | 19 | ### Requirements 20 | 21 | Must first follow the [Performance (Fastify) 22 | ](https://docs.nestjs.com/techniques/performance) guide to setup [fastify](https://github.com/fastify/fastify). 23 | 24 | ### Install 25 | 26 | ``` 27 | yarn add @mirco312312/nest-fgql 28 | ``` 29 | 30 | ### Code 31 | 32 | ``` 33 | import { Module } from '@nestjs/common'; 34 | import { FgqlModule } from '@mirco312312/nest-fgql'; 35 | // ... your other imports 36 | 37 | @Module({ 38 | imports: [ 39 | // ... preceeding modules 40 | FgqlModule.forRoot({ 41 | autoSchemaFile: true, 42 | // ... any other options 43 | }), 44 | // ... more modules 45 | ], 46 | }) 47 | export class ApplicationModule {} 48 | ``` 49 | 50 | ## Performance 51 | 52 | Start the test scenario for an environment. 53 | 54 | ### @nestjs/graphql 55 | 56 | ``` 57 | git clone https://github.com/nestjs/graphql 58 | cd graphql 59 | npm i 60 | npx ts-node tests/code-first/main.ts 61 | ``` 62 | 63 | ### nest-fgql 64 | 65 | ``` 66 | git clone https://github.com/mirco312312/nest-fgql 67 | cd nest-fgql 68 | yarn 69 | npx ts-node tests/code-first/main.ts 70 | ``` 71 | 72 | ### Execute [autocannon](https://github.com/mcollina/autocannon) 73 | 74 | ``` 75 | autocannon -d 10 -c100 \ 76 | -m POST \ 77 | -H 'Content-Type: application/json' \ 78 | -b '{"operationName":null,"variables":{},"query":"{\n categories {\n name\n description\n tags\n }\n recipes {\n id\n ingredients {\n name\n }\n rating\n averageRating\n }\n}\n"}' \ 79 | http://localhost:3000/graphql 80 | ``` 81 | 82 | ### Results 83 | 84 | MacBook Pro (16-inch, 2019) 85 | 86 | - 2,4 GHz 8-Core Intel Core i9 87 | - 64 GB 2667 MHz DDR4 88 | 89 | #### @nestjs/graphql 90 | 91 | ``` 92 | Running 10s test @ http://localhost:3000/graphql 93 | 100 connections 94 | 95 | ┌─────────┬───────┬───────┬───────┬───────┬──────────┬─────────┬───────────┐ 96 | │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ 97 | ├─────────┼───────┼───────┼───────┼───────┼──────────┼─────────┼───────────┤ 98 | │ Latency │ 25 ms │ 29 ms │ 61 ms │ 67 ms │ 30.89 ms │ 8.83 ms │ 176.23 ms │ 99 | └─────────┴───────┴───────┴───────┴───────┴──────────┴─────────┴───────────┘ 100 | ┌───────────┬────────┬────────┬─────────┬─────────┬─────────┬────────┬────────┐ 101 | │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ 102 | ├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤ 103 | │ Req/Sec │ 1613 │ 1613 │ 3429 │ 3579 │ 3183.2 │ 576.32 │ 1613 │ 104 | ├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤ 105 | │ Bytes/Sec │ 807 kB │ 807 kB │ 1.72 MB │ 1.79 MB │ 1.59 MB │ 288 kB │ 807 kB │ 106 | └───────────┴────────┴────────┴─────────┴─────────┴─────────┴────────┴────────┘ 107 | 108 | Req/Bytes counts sampled once per second. 109 | 110 | 32k requests in 10.05s, 15.9 MB read 111 | ``` 112 | 113 | #### nest-fgql 114 | 115 | ``` 116 | Running 10s test @ http://localhost:3000/graphql 117 | 100 connections 118 | 119 | ┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬──────────┐ 120 | │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ 121 | ├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼──────────┤ 122 | │ Latency │ 7 ms │ 8 ms │ 12 ms │ 13 ms │ 8.94 ms │ 1.52 ms │ 29.08 ms │ 123 | └─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴──────────┘ 124 | ┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ 125 | │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ 126 | ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ 127 | │ Req/Sec │ 10327 │ 10327 │ 10591 │ 10935 │ 10607.2 │ 164.39 │ 10326 │ 128 | ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ 129 | │ Bytes/Sec │ 4.15 MB │ 4.15 MB │ 4.26 MB │ 4.4 MB │ 4.26 MB │ 66.2 kB │ 4.15 MB │ 130 | └───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ 131 | 132 | Req/Bytes counts sampled once per second. 133 | 134 | 106k requests in 10.05s, 42.6 MB read 135 | ``` 136 | 137 | ## Credits 138 | 139 | Heavily based on [@nestjs/graphql](https://github.com/nestjs/graphql). 140 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | __export(require("./dist")); 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /lib/decorators/args-type.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from '../enums/class-type.enum'; 2 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 3 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 4 | import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util'; 5 | 6 | /** 7 | * Decorator that marks a class as a resolver arguments type. 8 | */ 9 | export function ArgsType(): ClassDecorator { 10 | return (target: Function) => { 11 | const metadata = { 12 | name: target.name, 13 | target, 14 | }; 15 | LazyMetadataStorage.store(() => 16 | TypeMetadataStorage.addArgsMetadata(metadata), 17 | ); 18 | addClassTypeMetadata(target, ClassType.ARGS); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /lib/decorators/args.decorator.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Type } from '@nestjs/common'; 2 | import { 3 | isFunction, 4 | isObject, 5 | isString, 6 | } from '@nestjs/common/utils/shared.utils'; 7 | import 'reflect-metadata'; 8 | import { GqlParamtype } from '../enums/gql-paramtype.enum'; 9 | import { BaseTypeOptions } from '../interfaces'; 10 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 11 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 12 | import { isPipe } from '../utils/is-pipe.util'; 13 | import { reflectTypeFromMetadata } from '../utils/reflection.utilts'; 14 | import { addPipesMetadata } from './param.utils'; 15 | 16 | /** 17 | * Interface defining options that can be passed to `@Args()` decorator. 18 | */ 19 | export interface ArgsOptions extends BaseTypeOptions { 20 | /** 21 | * Name of the argument. 22 | */ 23 | name?: string; 24 | /** 25 | * Description of the argument. 26 | */ 27 | description?: string; 28 | /** 29 | * Function that returns a reference to the arguments host class. 30 | */ 31 | type?: () => any; 32 | } 33 | 34 | /** 35 | * Resolver method parameter decorator. Extracts the arguments 36 | * object from the underlying platform and populates the decorated 37 | * parameter with the value of either all arguments or a single specified argument. 38 | */ 39 | export function Args(): ParameterDecorator; 40 | /** 41 | * Resolver method parameter decorator. Extracts the arguments 42 | * object from the underlying platform and populates the decorated 43 | * parameter with the value of either all arguments or a single specified argument. 44 | */ 45 | export function Args( 46 | ...pipes: (Type | PipeTransform)[] 47 | ): ParameterDecorator; 48 | /** 49 | * Resolver method parameter decorator. Extracts the arguments 50 | * object from the underlying platform and populates the decorated 51 | * parameter with the value of either all arguments or a single specified argument. 52 | */ 53 | export function Args( 54 | property: string, 55 | ...pipes: (Type | PipeTransform)[] 56 | ): ParameterDecorator; 57 | /** 58 | * Resolver method parameter decorator. Extracts the arguments 59 | * object from the underlying platform and populates the decorated 60 | * parameter with the value of either all arguments or a single specified argument. 61 | */ 62 | export function Args( 63 | options: ArgsOptions, 64 | ...pipes: (Type | PipeTransform)[] 65 | ): ParameterDecorator; 66 | /** 67 | * Resolver method parameter decorator. Extracts the arguments 68 | * object from the underlying platform and populates the decorated 69 | * parameter with the value of either all arguments or a single specified argument. 70 | */ 71 | export function Args( 72 | property: string, 73 | options: ArgsOptions, 74 | ...pipes: (Type | PipeTransform)[] 75 | ): ParameterDecorator; 76 | /** 77 | * Resolver method parameter decorator. Extracts the arguments 78 | * object from the underlying platform and populates the decorated 79 | * parameter with the value of either all arguments or a single specified argument. 80 | */ 81 | export function Args( 82 | propertyOrOptionsOrPipe?: 83 | | string 84 | | (Type | PipeTransform) 85 | | ArgsOptions, 86 | optionsOrPipe?: ArgsOptions | (Type | PipeTransform), 87 | ...pipes: (Type | PipeTransform)[] 88 | ): ParameterDecorator { 89 | const [property, argOptions, argPipes] = getArgsOptions( 90 | propertyOrOptionsOrPipe, 91 | optionsOrPipe, 92 | pipes, 93 | ); 94 | 95 | return (target: Object, key: string, index: number) => { 96 | addPipesMetadata(GqlParamtype.ARGS, property, argPipes, target, key, index); 97 | 98 | LazyMetadataStorage.store(target.constructor as Type, () => { 99 | const { typeFn: reflectedTypeFn, options } = reflectTypeFromMetadata({ 100 | metadataKey: 'design:paramtypes', 101 | prototype: target, 102 | propertyKey: key, 103 | index: index, 104 | explicitTypeFn: argOptions.type, 105 | typeOptions: argOptions, 106 | }); 107 | 108 | const metadata = { 109 | target: target.constructor, 110 | methodName: key, 111 | typeFn: reflectedTypeFn, 112 | index, 113 | options, 114 | }; 115 | 116 | if (property && isString(property)) { 117 | TypeMetadataStorage.addMethodParamMetadata({ 118 | kind: 'arg', 119 | name: property, 120 | description: argOptions.description, 121 | ...metadata, 122 | }); 123 | } else { 124 | TypeMetadataStorage.addMethodParamMetadata({ 125 | kind: 'args', 126 | ...metadata, 127 | }); 128 | } 129 | }); 130 | }; 131 | } 132 | 133 | function getArgsOptions( 134 | propertyOrOptionsOrPipe?: 135 | | string 136 | | (Type | PipeTransform) 137 | | ArgsOptions, 138 | optionsOrPipe?: ArgsOptions | (Type | PipeTransform), 139 | pipes?: (Type | PipeTransform)[], 140 | ): [string, ArgsOptions, (Type | PipeTransform)[]] { 141 | if (!propertyOrOptionsOrPipe || isString(propertyOrOptionsOrPipe)) { 142 | const propertyKey = propertyOrOptionsOrPipe as string; 143 | 144 | let options = {}; 145 | let argPipes = []; 146 | if (isPipe(optionsOrPipe)) { 147 | argPipes = [optionsOrPipe].concat(pipes); 148 | } else { 149 | options = optionsOrPipe || {}; 150 | argPipes = pipes; 151 | } 152 | return [propertyKey, options, argPipes]; 153 | } 154 | 155 | const isArgsOptionsObject = 156 | isObject(propertyOrOptionsOrPipe) && 157 | !isFunction((propertyOrOptionsOrPipe as PipeTransform).transform); 158 | if (isArgsOptionsObject) { 159 | const argOptions = propertyOrOptionsOrPipe as ArgsOptions; 160 | const propertyKey = argOptions.name; 161 | const argPipes = optionsOrPipe ? [optionsOrPipe].concat(pipes) : pipes; 162 | 163 | return [ 164 | propertyKey, 165 | argOptions, 166 | argPipes as (Type | PipeTransform)[], 167 | ]; 168 | } 169 | 170 | // concatenate all pipes 171 | let argPipes = [propertyOrOptionsOrPipe]; 172 | if (optionsOrPipe) { 173 | argPipes = argPipes.concat(optionsOrPipe); 174 | } 175 | argPipes = argPipes.concat(pipes); 176 | 177 | return [undefined, {}, argPipes as (Type | PipeTransform)[]]; 178 | } 179 | -------------------------------------------------------------------------------- /lib/decorators/context.decorator.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Type } from '@nestjs/common'; 2 | import 'reflect-metadata'; 3 | import { GqlParamtype } from '../enums/gql-paramtype.enum'; 4 | import { createGqlPipesParamDecorator } from './param.utils'; 5 | 6 | /** 7 | * Resolver method parameter decorator. Extracts the `Context` 8 | * object from the underlying platform and populates the decorated 9 | * parameter with the value of `Context`. 10 | */ 11 | export function Context(): ParameterDecorator; 12 | /** 13 | * Resolver method parameter decorator. Extracts the `Context` 14 | * object from the underlying platform and populates the decorated 15 | * parameter with the value of `Context`. 16 | */ 17 | export function Context( 18 | ...pipes: (Type | PipeTransform)[] 19 | ): ParameterDecorator; 20 | /** 21 | * Resolver method parameter decorator. Extracts the `Context` 22 | * object from the underlying platform and populates the decorated 23 | * parameter with the value of `Context`. 24 | */ 25 | export function Context( 26 | property: string, 27 | ...pipes: (Type | PipeTransform)[] 28 | ): ParameterDecorator; 29 | /** 30 | * Resolver method parameter decorator. Extracts the `Context` 31 | * object from the underlying platform and populates the decorated 32 | * parameter with the value of `Context`. 33 | */ 34 | export function Context( 35 | property?: string | (Type | PipeTransform), 36 | ...pipes: (Type | PipeTransform)[] 37 | ): ParameterDecorator { 38 | return createGqlPipesParamDecorator(GqlParamtype.CONTEXT)(property, ...pipes); 39 | } 40 | -------------------------------------------------------------------------------- /lib/decorators/directive.decorator.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql'; 2 | import { DirectiveParsingError } from '../schema-builder/errors/directive-parsing.error'; 3 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 4 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 5 | 6 | /** 7 | * Adds a directive to specified field, type, or handler. 8 | */ 9 | export function Directive( 10 | sdl: string, 11 | ): MethodDecorator & PropertyDecorator & ClassDecorator { 12 | return (target: Function | Object, key?: string | symbol) => { 13 | validateDirective(sdl); 14 | 15 | LazyMetadataStorage.store(() => { 16 | if (key) { 17 | TypeMetadataStorage.addDirectivePropertyMetadata({ 18 | target: target.constructor, 19 | fieldName: key as string, 20 | sdl, 21 | }); 22 | } else { 23 | TypeMetadataStorage.addDirectiveMetadata({ 24 | target: target as Function, 25 | sdl, 26 | }); 27 | } 28 | }); 29 | }; 30 | } 31 | 32 | function validateDirective(sdl: string) { 33 | try { 34 | parse(`type String ${sdl}`); 35 | } catch (err) { 36 | throw new DirectiveParsingError(sdl); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/decorators/extensions.decorator.ts: -------------------------------------------------------------------------------- 1 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 2 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 3 | 4 | /** 5 | * Adds arbitrary data accessible through the "extensions" property to specified field, type, or handler. 6 | */ 7 | export function Extensions( 8 | value: Record, 9 | ): MethodDecorator & ClassDecorator & PropertyDecorator { 10 | return (target: Function | object, propertyKey?: string | symbol) => { 11 | LazyMetadataStorage.store(() => { 12 | if (propertyKey) { 13 | TypeMetadataStorage.addExtensionsPropertyMetadata({ 14 | target: target.constructor, 15 | fieldName: propertyKey as string, 16 | value, 17 | }); 18 | } else { 19 | TypeMetadataStorage.addExtensionsMetadata({ 20 | target: target as Function, 21 | value, 22 | }); 23 | } 24 | }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /lib/decorators/field.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek. 3 | * In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module. 4 | * Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility. 5 | * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". 6 | */ 7 | 8 | import { isFunction } from '@nestjs/common/utils/shared.utils'; 9 | import { Complexity } from '../interfaces'; 10 | import { BaseTypeOptions } from '../interfaces/base-type-options.interface'; 11 | import { ReturnTypeFunc } from '../interfaces/return-type-func.interface'; 12 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 13 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 14 | import { reflectTypeFromMetadata } from '../utils/reflection.utilts'; 15 | 16 | /** 17 | * Interface defining options that can be passed to `@Field()` decorator. 18 | */ 19 | export interface FieldOptions extends BaseTypeOptions { 20 | /** 21 | * Name of the field. 22 | */ 23 | name?: string; 24 | /** 25 | * Description of the field. 26 | */ 27 | description?: string; 28 | /** 29 | * Field deprecation reason (if deprecated). 30 | */ 31 | deprecationReason?: string; 32 | /** 33 | * Field complexity options. 34 | */ 35 | complexity?: Complexity; 36 | } 37 | 38 | /** 39 | * @Field() decorator is used to mark a specific class property as a GraphQL field. 40 | * Only properties decorated with this decorator will be defined in the schema. 41 | */ 42 | export function Field(): PropertyDecorator & MethodDecorator; 43 | /** 44 | * @Field() decorator is used to mark a specific class property as a GraphQL field. 45 | * Only properties decorated with this decorator will be defined in the schema. 46 | */ 47 | export function Field( 48 | options: FieldOptions, 49 | ): PropertyDecorator & MethodDecorator; 50 | /** 51 | * @Field() decorator is used to mark a specific class property as a GraphQL field. 52 | * Only properties decorated with this decorator will be defined in the schema. 53 | */ 54 | export function Field( 55 | returnTypeFunction?: ReturnTypeFunc, 56 | options?: FieldOptions, 57 | ): PropertyDecorator & MethodDecorator; 58 | /** 59 | * @Field() decorator is used to mark a specific class property as a GraphQL field. 60 | * Only properties decorated with this decorator will be defined in the schema. 61 | */ 62 | export function Field( 63 | typeOrOptions?: ReturnTypeFunc | FieldOptions, 64 | fieldOptions?: FieldOptions, 65 | ): PropertyDecorator & MethodDecorator { 66 | return ( 67 | prototype: Object, 68 | propertyKey?: string, 69 | descriptor?: TypedPropertyDescriptor, 70 | ) => { 71 | addFieldMetadata( 72 | typeOrOptions, 73 | fieldOptions, 74 | prototype, 75 | propertyKey, 76 | descriptor, 77 | ); 78 | }; 79 | } 80 | 81 | export function addFieldMetadata( 82 | typeOrOptions: ReturnTypeFunc | FieldOptions, 83 | fieldOptions: FieldOptions, 84 | prototype: Object, 85 | propertyKey?: string, 86 | descriptor?: TypedPropertyDescriptor, 87 | loadEagerly?: boolean, 88 | ) { 89 | const [typeFunc, options = {}] = isFunction(typeOrOptions) 90 | ? [typeOrOptions, fieldOptions] 91 | : [undefined, typeOrOptions as any]; 92 | 93 | const applyMetadataFn = () => { 94 | const isResolver = !!descriptor; 95 | const isResolverMethod = !!(descriptor && descriptor.value); 96 | 97 | const { typeFn: getType, options: typeOptions } = reflectTypeFromMetadata({ 98 | metadataKey: isResolverMethod ? 'design:returntype' : 'design:type', 99 | prototype, 100 | propertyKey, 101 | explicitTypeFn: typeFunc as ReturnTypeFunc, 102 | typeOptions: options, 103 | }); 104 | 105 | TypeMetadataStorage.addClassFieldMetadata({ 106 | name: propertyKey, 107 | schemaName: options.name || propertyKey, 108 | typeFn: getType, 109 | options: typeOptions, 110 | target: prototype.constructor, 111 | description: options.description, 112 | deprecationReason: options.deprecationReason, 113 | complexity: options.complexity, 114 | }); 115 | 116 | if (isResolver) { 117 | TypeMetadataStorage.addResolverPropertyMetadata({ 118 | kind: 'internal', 119 | methodName: propertyKey, 120 | schemaName: options.name || propertyKey, 121 | target: prototype.constructor, 122 | complexity: options.complexity, 123 | }); 124 | } 125 | }; 126 | if (loadEagerly) { 127 | applyMetadataFn(); 128 | } else { 129 | LazyMetadataStorage.store(applyMetadataFn); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/decorators/hide-field.decorator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-empty-function */ 3 | export function HideField(): PropertyDecorator { 4 | return (target: Record, propertyKey: string | symbol) => {}; 5 | } 6 | -------------------------------------------------------------------------------- /lib/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './args-type.decorator'; 2 | export * from './args.decorator'; 3 | export * from './context.decorator'; 4 | export * from './directive.decorator'; 5 | export * from './extensions.decorator'; 6 | export * from './field.decorator'; 7 | export * from './hide-field.decorator'; 8 | export * from './info.decorator'; 9 | export * from './input-type.decorator'; 10 | export * from './interface-type.decorator'; 11 | export * from './mutation.decorator'; 12 | export * from './object-type.decorator'; 13 | export * from './parent.decorator'; 14 | export * from './query.decorator'; 15 | export * from './resolve-field.decorator'; 16 | export * from './resolver.decorator'; 17 | export * from './root.decorator'; 18 | export * from './scalar.decorator'; 19 | export * from './subscription.decorator'; 20 | -------------------------------------------------------------------------------- /lib/decorators/info.decorator.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Type } from '@nestjs/common'; 2 | import 'reflect-metadata'; 3 | import { GqlParamtype } from '../enums/gql-paramtype.enum'; 4 | import { createGqlPipesParamDecorator } from './param.utils'; 5 | 6 | /** 7 | * Resolver method parameter decorator. Extracts the `Info` 8 | * object from the underlying platform and populates the decorated 9 | * parameter with the value of `Info`. 10 | */ 11 | export function Info(...pipes: (Type | PipeTransform)[]) { 12 | return createGqlPipesParamDecorator(GqlParamtype.INFO)(undefined, ...pipes); 13 | } 14 | -------------------------------------------------------------------------------- /lib/decorators/input-type.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek. 3 | * In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module. 4 | * Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility. 5 | * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". 6 | */ 7 | 8 | import { isString } from '@nestjs/common/utils/shared.utils'; 9 | import { ClassType } from '../enums/class-type.enum'; 10 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 11 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 12 | import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util'; 13 | 14 | /** 15 | * Interface defining options that can be passed to `@InputType()` decorator. 16 | */ 17 | export interface InputTypeOptions { 18 | /** 19 | * Description of the input type. 20 | */ 21 | description?: string; 22 | /** 23 | * If `true`, type will not be registered in the schema. 24 | */ 25 | isAbstract?: boolean; 26 | } 27 | 28 | /** 29 | * Decorator that marks a class as a GraphQL input type. 30 | */ 31 | export function InputType(): ClassDecorator; 32 | /** 33 | * Decorator that marks a class as a GraphQL input type. 34 | */ 35 | export function InputType(options: InputTypeOptions): ClassDecorator; 36 | /** 37 | * Decorator that marks a class as a GraphQL input type. 38 | */ 39 | export function InputType( 40 | name: string, 41 | options?: InputTypeOptions, 42 | ): ClassDecorator; 43 | /** 44 | * Decorator that marks a class as a GraphQL input type. 45 | */ 46 | export function InputType( 47 | nameOrOptions?: string | InputTypeOptions, 48 | inputTypeOptions?: InputTypeOptions, 49 | ): ClassDecorator { 50 | const [name, options = {}] = isString(nameOrOptions) 51 | ? [nameOrOptions, inputTypeOptions] 52 | : [undefined, nameOrOptions]; 53 | 54 | return (target) => { 55 | const metadata = { 56 | target, 57 | name: name || target.name, 58 | description: options.description, 59 | isAbstract: options.isAbstract, 60 | }; 61 | LazyMetadataStorage.store(() => 62 | TypeMetadataStorage.addInputTypeMetadata(metadata), 63 | ); 64 | addClassTypeMetadata(target, ClassType.INPUT); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /lib/decorators/interface-type.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek. 3 | * In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module. 4 | * Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility. 5 | * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". 6 | */ 7 | 8 | import { isString } from '@nestjs/common/utils/shared.utils'; 9 | import { ClassType } from '../enums/class-type.enum'; 10 | import { ResolveTypeFn } from '../interfaces'; 11 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 12 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 13 | import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util'; 14 | 15 | /** 16 | * Interface defining options that can be passed to `@InterfaceType()` decorator. 17 | */ 18 | export interface InterfaceTypeOptions { 19 | /** 20 | * Description of the argument. 21 | */ 22 | description?: string; 23 | /** 24 | * If `true`, type will not be registered in the schema. 25 | */ 26 | isAbstract?: boolean; 27 | /** 28 | * Custom implementation of the "resolveType" function. 29 | */ 30 | resolveType?: ResolveTypeFn; 31 | } 32 | 33 | /** 34 | * Decorator that marks a class as a GraphQL interface type. 35 | */ 36 | export function InterfaceType(options?: InterfaceTypeOptions): ClassDecorator; 37 | /** 38 | * Decorator that marks a class as a GraphQL interface type. 39 | */ 40 | export function InterfaceType( 41 | name: string, 42 | options?: InterfaceTypeOptions, 43 | ): ClassDecorator; 44 | /** 45 | * Decorator that marks a class as a GraphQL interface type. 46 | */ 47 | export function InterfaceType( 48 | nameOrOptions?: string | InterfaceTypeOptions, 49 | interfaceOptions?: InterfaceTypeOptions, 50 | ): ClassDecorator { 51 | const [name, options = {}] = isString(nameOrOptions) 52 | ? [nameOrOptions, interfaceOptions] 53 | : [undefined, nameOrOptions]; 54 | 55 | return (target) => { 56 | const metadata = { 57 | name: name || target.name, 58 | target, 59 | ...options, 60 | }; 61 | LazyMetadataStorage.store(() => 62 | TypeMetadataStorage.addInterfaceMetadata(metadata), 63 | ); 64 | 65 | addClassTypeMetadata(target, ClassType.INTERFACE); 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /lib/decorators/mutation.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { isString } from '@nestjs/common/utils/shared.utils'; 3 | import 'reflect-metadata'; 4 | import { Resolver } from '../enums/resolver.enum'; 5 | import { BaseTypeOptions } from '../interfaces/base-type-options.interface'; 6 | import { ReturnTypeFunc } from '../interfaces/return-type-func.interface'; 7 | import { UndefinedReturnTypeError } from '../schema-builder/errors/undefined-return-type.error'; 8 | import { ResolverTypeMetadata } from '../schema-builder/metadata'; 9 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 10 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 11 | import { reflectTypeFromMetadata } from '../utils/reflection.utilts'; 12 | import { addResolverMetadata } from './resolvers.utils'; 13 | 14 | /** 15 | * Interface defining options that can be passed to `@Mutation()` decorator. 16 | */ 17 | export interface MutationOptions extends BaseTypeOptions { 18 | /** 19 | * Name of the mutation. 20 | */ 21 | name?: string; 22 | /** 23 | * Description of the mutation. 24 | */ 25 | description?: string; 26 | /** 27 | * Mutation deprecation reason (if deprecated). 28 | */ 29 | deprecationReason?: string; 30 | } 31 | 32 | /** 33 | * Mutation handler (method) Decorator. Routes specified mutation to this method. 34 | */ 35 | export function Mutation(): MethodDecorator; 36 | /** 37 | * Mutation handler (method) Decorator. Routes specified mutation to this method. 38 | */ 39 | export function Mutation(name: string): MethodDecorator; 40 | /** 41 | * Mutation handler (method) Decorator. Routes specified mutation to this method. 42 | */ 43 | export function Mutation( 44 | typeFunc: ReturnTypeFunc, 45 | options?: MutationOptions, 46 | ): MethodDecorator; 47 | /** 48 | * Mutation handler (method) Decorator. Routes specified mutation to this method. 49 | */ 50 | export function Mutation( 51 | nameOrType?: string | ReturnTypeFunc, 52 | options: MutationOptions = {}, 53 | ): MethodDecorator { 54 | return (target: Object | Function, key?: string, descriptor?: any) => { 55 | const name = isString(nameOrType) 56 | ? nameOrType 57 | : (options && options.name) || undefined; 58 | 59 | addResolverMetadata(Resolver.MUTATION, name, target, key, descriptor); 60 | 61 | LazyMetadataStorage.store(target.constructor as Type, () => { 62 | if (!nameOrType || isString(nameOrType)) { 63 | throw new UndefinedReturnTypeError(Mutation.name, key); 64 | } 65 | 66 | const { typeFn, options: typeOptions } = reflectTypeFromMetadata({ 67 | metadataKey: 'design:returntype', 68 | prototype: target, 69 | propertyKey: key, 70 | explicitTypeFn: nameOrType, 71 | typeOptions: options, 72 | }); 73 | const metadata: ResolverTypeMetadata = { 74 | methodName: key, 75 | schemaName: options.name || key, 76 | target: target.constructor, 77 | typeFn, 78 | returnTypeOptions: typeOptions, 79 | description: options.description, 80 | deprecationReason: options.deprecationReason, 81 | }; 82 | TypeMetadataStorage.addMutationMetadata(metadata); 83 | }); 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /lib/decorators/object-type.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek. 3 | * In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module. 4 | * Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility. 5 | * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". 6 | */ 7 | 8 | import { isString } from '@nestjs/common/utils/shared.utils'; 9 | import { ClassType } from '../enums/class-type.enum'; 10 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 11 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 12 | import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util'; 13 | 14 | /** 15 | * Interface defining options that can be passed to `@ObjectType()` decorator 16 | */ 17 | export interface ObjectTypeOptions { 18 | /** 19 | * Description of the input type. 20 | */ 21 | description?: string; 22 | /** 23 | * If `true`, type will not be registered in the schema. 24 | */ 25 | isAbstract?: boolean; 26 | /** 27 | * Interfaces implemented by this object. 28 | */ 29 | implements?: Function | Function[]; 30 | } 31 | 32 | /** 33 | * Decorator that marks a class as a GraphQL type. 34 | */ 35 | export function ObjectType(): ClassDecorator; 36 | /** 37 | * Decorator that marks a class as a GraphQL type. 38 | */ 39 | export function ObjectType(options: ObjectTypeOptions): ClassDecorator; 40 | /** 41 | * Decorator that marks a class as a GraphQL type. 42 | */ 43 | export function ObjectType( 44 | name: string, 45 | options?: ObjectTypeOptions, 46 | ): ClassDecorator; 47 | /** 48 | * Decorator that marks a class as a GraphQL type. 49 | */ 50 | export function ObjectType( 51 | nameOrOptions?: string | ObjectTypeOptions, 52 | objectTypeOptions?: ObjectTypeOptions, 53 | ): ClassDecorator { 54 | const [name, options = {}] = isString(nameOrOptions) 55 | ? [nameOrOptions, objectTypeOptions] 56 | : [undefined, nameOrOptions]; 57 | 58 | const interfaces = options.implements 59 | ? [].concat(options.implements) 60 | : undefined; 61 | return (target) => { 62 | const addObjectTypeMetadata = () => 63 | TypeMetadataStorage.addObjectTypeMetadata({ 64 | name: name || target.name, 65 | target, 66 | description: options.description, 67 | interfaces, 68 | isAbstract: options.isAbstract, 69 | }); 70 | 71 | // This function must be called eagerly to allow resolvers 72 | // accessing the "name" property 73 | addObjectTypeMetadata(); 74 | LazyMetadataStorage.store(addObjectTypeMetadata); 75 | 76 | addClassTypeMetadata(target, ClassType.OBJECT); 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /lib/decorators/param.utils.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Type } from '@nestjs/common'; 2 | import { isNil, isString } from '@nestjs/common/utils/shared.utils'; 3 | import 'reflect-metadata'; 4 | import { GqlParamtype } from '../enums/gql-paramtype.enum'; 5 | import { PARAM_ARGS_METADATA } from '../fgql.constants'; 6 | 7 | export type ParamData = object | string | number; 8 | export type ParamsMetadata = Record< 9 | number, 10 | { 11 | index: number; 12 | data?: ParamData; 13 | } 14 | >; 15 | 16 | function assignMetadata( 17 | args: ParamsMetadata, 18 | paramtype: GqlParamtype, 19 | index: number, 20 | data?: ParamData, 21 | ...pipes: (Type | PipeTransform)[] 22 | ) { 23 | return { 24 | ...args, 25 | [`${paramtype}:${index}`]: { 26 | index, 27 | data, 28 | pipes, 29 | }, 30 | }; 31 | } 32 | 33 | export const createGqlParamDecorator = (paramtype: GqlParamtype) => { 34 | return (data?: ParamData): ParameterDecorator => (target, key, index) => { 35 | const args = 36 | Reflect.getMetadata(PARAM_ARGS_METADATA, target.constructor, key) || {}; 37 | Reflect.defineMetadata( 38 | PARAM_ARGS_METADATA, 39 | assignMetadata(args, paramtype, index, data), 40 | target.constructor, 41 | key, 42 | ); 43 | }; 44 | }; 45 | 46 | export const addPipesMetadata = ( 47 | paramtype: GqlParamtype, 48 | data: any, 49 | pipes: (Type | PipeTransform)[], 50 | target: Record, 51 | key: string | symbol, 52 | index: number, 53 | ) => { 54 | const args = 55 | Reflect.getMetadata(PARAM_ARGS_METADATA, target.constructor, key) || {}; 56 | const hasParamData = isNil(data) || isString(data); 57 | const paramData = hasParamData ? data : undefined; 58 | const paramPipes = hasParamData ? pipes : [data, ...pipes]; 59 | 60 | Reflect.defineMetadata( 61 | PARAM_ARGS_METADATA, 62 | assignMetadata(args, paramtype, index, paramData, ...paramPipes), 63 | target.constructor, 64 | key, 65 | ); 66 | }; 67 | 68 | export const createGqlPipesParamDecorator = (paramtype: GqlParamtype) => ( 69 | data?: any, 70 | ...pipes: (Type | PipeTransform)[] 71 | ): ParameterDecorator => (target, key, index) => { 72 | addPipesMetadata(paramtype, data, pipes, target, key, index); 73 | }; 74 | -------------------------------------------------------------------------------- /lib/decorators/parent.decorator.ts: -------------------------------------------------------------------------------- 1 | import { GqlParamtype } from '../enums/gql-paramtype.enum'; 2 | import { createGqlParamDecorator } from './param.utils'; 3 | 4 | /** 5 | * Resolver method parameter decorator. Extracts the parent/root 6 | * object from the underlying platform and populates the decorated 7 | * parameter with the value of parent/root. 8 | */ 9 | export const Parent: () => ParameterDecorator = createGqlParamDecorator( 10 | GqlParamtype.ROOT, 11 | ); 12 | -------------------------------------------------------------------------------- /lib/decorators/query.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { isString } from '@nestjs/common/utils/shared.utils'; 3 | import 'reflect-metadata'; 4 | import { Resolver } from '../enums/resolver.enum'; 5 | import { BaseTypeOptions } from '../interfaces/base-type-options.interface'; 6 | import { ReturnTypeFunc } from '../interfaces/return-type-func.interface'; 7 | import { UndefinedReturnTypeError } from '../schema-builder/errors/undefined-return-type.error'; 8 | import { ResolverTypeMetadata } from '../schema-builder/metadata'; 9 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 10 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 11 | import { reflectTypeFromMetadata } from '../utils/reflection.utilts'; 12 | import { addResolverMetadata } from './resolvers.utils'; 13 | 14 | /** 15 | * Interface defining options that can be passed to `@Query()` decorator. 16 | */ 17 | export interface QueryOptions extends BaseTypeOptions { 18 | /** 19 | * Name of the query. 20 | */ 21 | name?: string; 22 | /** 23 | * Description of the query. 24 | */ 25 | description?: string; 26 | /** 27 | * Query deprecation reason (if deprecated). 28 | */ 29 | deprecationReason?: string; 30 | } 31 | 32 | /** 33 | * Query handler (method) Decorator. Routes specified query to this method. 34 | */ 35 | export function Query(): MethodDecorator; 36 | /** 37 | * Query handler (method) Decorator. Routes specified query to this method. 38 | */ 39 | export function Query(name: string): MethodDecorator; 40 | /** 41 | * Query handler (method) Decorator. Routes specified query to this method. 42 | */ 43 | export function Query( 44 | typeFunc: ReturnTypeFunc, 45 | options?: QueryOptions, 46 | ): MethodDecorator; 47 | /** 48 | * Query handler (method) Decorator. Routes specified query to this method. 49 | */ 50 | export function Query( 51 | nameOrType?: string | ReturnTypeFunc, 52 | options: QueryOptions = {}, 53 | ): MethodDecorator { 54 | return (target: Object | Function, key?: string, descriptor?: any) => { 55 | const name = isString(nameOrType) 56 | ? nameOrType 57 | : (options && options.name) || undefined; 58 | 59 | addResolverMetadata(Resolver.QUERY, name, target, key, descriptor); 60 | 61 | LazyMetadataStorage.store(target.constructor as Type, () => { 62 | if (!nameOrType || isString(nameOrType)) { 63 | throw new UndefinedReturnTypeError(Query.name, key); 64 | } 65 | 66 | const { typeFn, options: typeOptions } = reflectTypeFromMetadata({ 67 | metadataKey: 'design:returntype', 68 | prototype: target, 69 | propertyKey: key, 70 | explicitTypeFn: nameOrType, 71 | typeOptions: options || {}, 72 | }); 73 | const metadata: ResolverTypeMetadata = { 74 | methodName: key, 75 | schemaName: options.name || key, 76 | target: target.constructor, 77 | typeFn, 78 | returnTypeOptions: typeOptions, 79 | description: options.description, 80 | deprecationReason: options.deprecationReason, 81 | }; 82 | TypeMetadataStorage.addQueryMetadata(metadata); 83 | }); 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /lib/decorators/resolve-field.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata, Type } from '@nestjs/common'; 2 | import { isFunction, isObject } from '@nestjs/common/utils/shared.utils'; 3 | import { 4 | RESOLVER_NAME_METADATA, 5 | RESOLVER_PROPERTY_METADATA, 6 | } from '../fgql.constants'; 7 | import { Complexity } from '../interfaces'; 8 | import { BaseTypeOptions } from '../interfaces/base-type-options.interface'; 9 | import { 10 | GqlTypeReference, 11 | ReturnTypeFunc, 12 | } from '../interfaces/return-type-func.interface'; 13 | import { TypeOptions } from '../interfaces/type-options.interface'; 14 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 15 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 16 | import { reflectTypeFromMetadata } from '../utils/reflection.utilts'; 17 | 18 | /** 19 | * Interface defining options that can be passed to `@ResolveField()` decorator. 20 | */ 21 | export interface ResolveFieldOptions extends BaseTypeOptions { 22 | /** 23 | * Name of the field. 24 | */ 25 | name?: string; 26 | /** 27 | * Description of the field. 28 | */ 29 | description?: string; 30 | /** 31 | * Field deprecation reason (if deprecated). 32 | */ 33 | deprecationReason?: string; 34 | /** 35 | * Field complexity options. 36 | */ 37 | complexity?: Complexity; 38 | } 39 | 40 | /** 41 | * Field resolver (method) Decorator. 42 | */ 43 | export function ResolveField( 44 | typeFunc?: ReturnTypeFunc, 45 | options?: ResolveFieldOptions, 46 | ): MethodDecorator; 47 | /** 48 | * Property resolver (method) Decorator. 49 | */ 50 | export function ResolveField( 51 | propertyName?: string, 52 | typeFunc?: ReturnTypeFunc, 53 | options?: ResolveFieldOptions, 54 | ): MethodDecorator; 55 | /** 56 | * Property resolver (method) Decorator. 57 | */ 58 | export function ResolveField( 59 | propertyNameOrFunc?: string | ReturnTypeFunc, 60 | typeFuncOrOptions?: ReturnTypeFunc | ResolveFieldOptions, 61 | resolveFieldOptions?: ResolveFieldOptions, 62 | ): MethodDecorator { 63 | return ( 64 | target: Function | Record, 65 | key?: string, 66 | descriptor?: any, 67 | ) => { 68 | // eslint-disable-next-line prefer-const 69 | let [propertyName, typeFunc, options] = isFunction(propertyNameOrFunc) 70 | ? typeFuncOrOptions && typeFuncOrOptions.name 71 | ? [typeFuncOrOptions.name, propertyNameOrFunc, typeFuncOrOptions] 72 | : [undefined, propertyNameOrFunc, typeFuncOrOptions] 73 | : [propertyNameOrFunc, typeFuncOrOptions, resolveFieldOptions]; 74 | 75 | SetMetadata(RESOLVER_NAME_METADATA, propertyName)(target, key, descriptor); 76 | SetMetadata(RESOLVER_PROPERTY_METADATA, true)(target, key, descriptor); 77 | 78 | options = isObject(options) 79 | ? { 80 | name: propertyName as string, 81 | ...options, 82 | } 83 | : propertyName 84 | ? { name: propertyName as string } 85 | : {}; 86 | 87 | LazyMetadataStorage.store(target.constructor as Type, () => { 88 | let typeOptions: TypeOptions, typeFn: (type?: any) => GqlTypeReference; 89 | try { 90 | const implicitTypeMetadata = reflectTypeFromMetadata({ 91 | metadataKey: 'design:returntype', 92 | prototype: target, 93 | propertyKey: key, 94 | explicitTypeFn: typeFunc as ReturnTypeFunc, 95 | typeOptions: options as any, 96 | }); 97 | typeOptions = implicitTypeMetadata.options; 98 | typeFn = implicitTypeMetadata.typeFn; 99 | } catch {} 100 | 101 | TypeMetadataStorage.addResolverPropertyMetadata({ 102 | kind: 'external', 103 | methodName: key, 104 | schemaName: options.name || key, 105 | target: target.constructor, 106 | typeFn, 107 | typeOptions, 108 | description: (options as ResolveFieldOptions).description, 109 | deprecationReason: (options as ResolveFieldOptions).deprecationReason, 110 | complexity: (options as ResolveFieldOptions).complexity, 111 | }); 112 | }); 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /lib/decorators/resolver.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { isFunction, isString } from '@nestjs/common/utils/shared.utils'; 3 | import 'reflect-metadata'; 4 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 5 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 6 | import { 7 | addResolverMetadata, 8 | getClassName, 9 | getClassOrUndefined, 10 | getResolverTypeFn, 11 | } from './resolvers.utils'; 12 | 13 | export type ResolverTypeFn = (of?: void) => Type; 14 | 15 | /** 16 | * Extracts the name property set through the @ObjectType() decorator (if specified) 17 | * @param nameOrType type reference 18 | */ 19 | function getObjectTypeNameIfExists(nameOrType: Function): string | undefined { 20 | const ctor = getClassOrUndefined(nameOrType); 21 | const objectTypesMetadata = TypeMetadataStorage.getObjectTypesMetadata(); 22 | const objectMetadata = objectTypesMetadata.find(type => type.target === ctor); 23 | if (!objectMetadata) { 24 | return; 25 | } 26 | return objectMetadata.name; 27 | } 28 | 29 | /** 30 | * Interface defining options that can be passed to `@Resolve()` decorator 31 | */ 32 | export interface ResolverOptions { 33 | /** 34 | * If `true`, type will not be registered in the schema. 35 | */ 36 | isAbstract?: boolean; 37 | } 38 | 39 | /** 40 | * Object resolver decorator. 41 | */ 42 | export function Resolver(): MethodDecorator & ClassDecorator; 43 | /** 44 | * Object resolver decorator. 45 | */ 46 | export function Resolver(name: string): MethodDecorator & ClassDecorator; 47 | /** 48 | * Object resolver decorator. 49 | */ 50 | export function Resolver( 51 | options: ResolverOptions, 52 | ): MethodDecorator & ClassDecorator; 53 | /** 54 | * Object resolver decorator. 55 | */ 56 | export function Resolver( 57 | classType: Type, 58 | options?: ResolverOptions, 59 | ): MethodDecorator & ClassDecorator; 60 | /** 61 | * Object resolver decorator. 62 | */ 63 | export function Resolver( 64 | typeFunc: ResolverTypeFn, 65 | options?: ResolverOptions, 66 | ): MethodDecorator & ClassDecorator; 67 | /** 68 | * Object resolver decorator. 69 | */ 70 | export function Resolver( 71 | nameOrTypeOrOptions?: string | ResolverTypeFn | Type | ResolverOptions, 72 | options?: ResolverOptions, 73 | ): MethodDecorator & ClassDecorator { 74 | return ( 75 | target: Object | Function, 76 | key?: string | symbol, 77 | descriptor?: any, 78 | ) => { 79 | const [nameOrType, resolverOptions] = 80 | typeof nameOrTypeOrOptions === 'object' && nameOrTypeOrOptions !== null 81 | ? [undefined, nameOrTypeOrOptions] 82 | : [nameOrTypeOrOptions as string | ResolverTypeFn | Type, options]; 83 | 84 | let name = nameOrType && getClassName(nameOrType); 85 | 86 | if (isFunction(nameOrType)) { 87 | const objectName = getObjectTypeNameIfExists(nameOrType as Function); 88 | objectName && (name = objectName); 89 | } 90 | addResolverMetadata(undefined, name, target, key, descriptor); 91 | 92 | if (!isString(nameOrType)) { 93 | LazyMetadataStorage.store(target as Type, () => { 94 | const typeFn = getResolverTypeFn(nameOrType, target as Function); 95 | 96 | TypeMetadataStorage.addResolverMetadata({ 97 | target: target as Function, 98 | typeFn: typeFn, 99 | isAbstract: (resolverOptions && resolverOptions.isAbstract) || false, 100 | }); 101 | }); 102 | } 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /lib/decorators/resolvers.utils.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata, Type } from '@nestjs/common'; 2 | import { isFunction, isString } from '@nestjs/common/utils/shared.utils'; 3 | import { Resolver } from '../enums/resolver.enum'; 4 | import { 5 | RESOLVER_NAME_METADATA, 6 | RESOLVER_TYPE_METADATA, 7 | } from '../fgql.constants'; 8 | import { UndefinedResolverTypeError } from '../schema-builder/errors/undefined-resolver-type.error'; 9 | import { ResolverTypeFn } from './resolver.decorator'; 10 | 11 | export function addResolverMetadata( 12 | resolver: Resolver | string | undefined, 13 | name: string | undefined, 14 | target?: Record | Function, 15 | key?: string | symbol, 16 | descriptor?: any, 17 | ) { 18 | SetMetadata(RESOLVER_TYPE_METADATA, resolver || name)( 19 | target, 20 | key, 21 | descriptor, 22 | ); 23 | SetMetadata(RESOLVER_NAME_METADATA, name)(target, key, descriptor); 24 | } 25 | 26 | export function getClassName(nameOrType: string | Function | Type) { 27 | if (isString(nameOrType)) { 28 | return nameOrType; 29 | } 30 | const classOrUndefined = getClassOrUndefined(nameOrType); 31 | return classOrUndefined && classOrUndefined.name; 32 | } 33 | 34 | export function getResolverTypeFn(nameOrType: Function, target: Function) { 35 | return nameOrType 36 | ? nameOrType.prototype 37 | ? () => nameOrType as Type 38 | : (nameOrType as ResolverTypeFn) 39 | : () => { 40 | throw new UndefinedResolverTypeError(target.name); 41 | }; 42 | } 43 | 44 | export function getClassOrUndefined(typeOrFunc: Function | Type) { 45 | return isConstructor(typeOrFunc) 46 | ? typeOrFunc 47 | : isFunction(typeOrFunc) 48 | ? (typeOrFunc as Function)() 49 | : undefined; 50 | } 51 | 52 | function isConstructor(obj: any) { 53 | return ( 54 | !!obj.prototype && 55 | !!obj.prototype.constructor && 56 | !!obj.prototype.constructor.name 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /lib/decorators/root.decorator.ts: -------------------------------------------------------------------------------- 1 | import { GqlParamtype } from '../enums/gql-paramtype.enum'; 2 | import { createGqlParamDecorator } from './param.utils'; 3 | 4 | /** 5 | * Resolver method parameter decorator. Extracts the parent/root 6 | * object from the underlying platform and populates the decorated 7 | * parameter with the value of parent/root. 8 | */ 9 | export const Root: () => ParameterDecorator = createGqlParamDecorator( 10 | GqlParamtype.ROOT, 11 | ); 12 | -------------------------------------------------------------------------------- /lib/decorators/scalar.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { 3 | SCALAR_NAME_METADATA, 4 | SCALAR_TYPE_METADATA, 5 | } from '../fgql.constants'; 6 | import { ReturnTypeFunc } from '../interfaces/return-type-func.interface'; 7 | 8 | /** 9 | * Decorator that marks a class as a GraphQL scalar. 10 | */ 11 | export function Scalar(name: string): ClassDecorator; 12 | /** 13 | * Decorator that marks a class as a GraphQL scalar. 14 | */ 15 | export function Scalar(name: string, typeFunc: ReturnTypeFunc): ClassDecorator; 16 | /** 17 | * Decorator that marks a class as a GraphQL scalar. 18 | */ 19 | export function Scalar( 20 | name: string, 21 | typeFunc?: ReturnTypeFunc, 22 | ): ClassDecorator { 23 | return (target, key?, descriptor?) => { 24 | SetMetadata(SCALAR_NAME_METADATA, name)(target, key, descriptor); 25 | SetMetadata(SCALAR_TYPE_METADATA, typeFunc)(target, key, descriptor); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /lib/decorators/subscription.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata, Type } from '@nestjs/common'; 2 | import { isString } from '@nestjs/common/utils/shared.utils'; 3 | import 'reflect-metadata'; 4 | import { Resolver } from '../enums/resolver.enum'; 5 | import { SUBSCRIPTION_OPTIONS_METADATA } from '../fgql.constants'; 6 | import { BaseTypeOptions, ReturnTypeFunc } from '../interfaces'; 7 | import { UndefinedReturnTypeError } from '../schema-builder/errors/undefined-return-type.error'; 8 | import { ResolverTypeMetadata } from '../schema-builder/metadata'; 9 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 10 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 11 | import { reflectTypeFromMetadata } from '../utils/reflection.utilts'; 12 | import { addResolverMetadata } from './resolvers.utils'; 13 | 14 | /** 15 | * Interface defining options that can be passed to `@Subscription()` decorator. 16 | */ 17 | export interface SubscriptionOptions extends BaseTypeOptions { 18 | /** 19 | * Name of the subscription. 20 | */ 21 | name?: string; 22 | /** 23 | * Description of the subscription. 24 | */ 25 | description?: string; 26 | /** 27 | * Subscription deprecation reason (if deprecated). 28 | */ 29 | deprecationReason?: string; 30 | /** 31 | * Filter messages function. 32 | */ 33 | filter?: ( 34 | payload: any, 35 | variables: any, 36 | context: any, 37 | ) => boolean | Promise; 38 | /** 39 | * Resolve messages function (to transform payload/message shape). 40 | */ 41 | resolve?: ( 42 | payload: any, 43 | args: any, 44 | context: any, 45 | info: any, 46 | ) => any | Promise; 47 | } 48 | 49 | /** 50 | * Subscription handler (method) Decorator. Routes subscriptions to this method. 51 | */ 52 | export function Subscription(): MethodDecorator; 53 | /** 54 | * Subscription handler (method) Decorator. Routes subscriptions to this method. 55 | */ 56 | export function Subscription(name: string): MethodDecorator; 57 | /** 58 | * Subscription handler (method) Decorator. Routes subscriptions to this method. 59 | */ 60 | export function Subscription( 61 | name: string, 62 | options: Pick, 63 | ): MethodDecorator; 64 | /** 65 | * Subscription handler (method) Decorator. Routes subscriptions to this method. 66 | */ 67 | export function Subscription( 68 | typeFunc: ReturnTypeFunc, 69 | options?: SubscriptionOptions, 70 | ): MethodDecorator; 71 | /** 72 | * Subscription handler (method) Decorator. Routes subscriptions to this method. 73 | */ 74 | export function Subscription( 75 | nameOrType?: string | ReturnTypeFunc, 76 | options: SubscriptionOptions = {}, 77 | ): MethodDecorator { 78 | return ( 79 | target: Record | Function, 80 | key?: string, 81 | descriptor?: any, 82 | ) => { 83 | const name = isString(nameOrType) 84 | ? nameOrType 85 | : (options && options.name) || undefined; 86 | 87 | addResolverMetadata(Resolver.SUBSCRIPTION, name, target, key, descriptor); 88 | SetMetadata(SUBSCRIPTION_OPTIONS_METADATA, options)( 89 | target, 90 | key, 91 | descriptor, 92 | ); 93 | 94 | LazyMetadataStorage.store(target.constructor as Type, () => { 95 | if (!nameOrType || isString(nameOrType)) { 96 | throw new UndefinedReturnTypeError(Subscription.name, key); 97 | } 98 | 99 | const { typeFn, options: typeOptions } = reflectTypeFromMetadata({ 100 | metadataKey: 'design:returntype', 101 | prototype: target, 102 | propertyKey: key, 103 | explicitTypeFn: nameOrType, 104 | typeOptions: options, 105 | }); 106 | const metadata: ResolverTypeMetadata = { 107 | methodName: key, 108 | schemaName: options.name || key, 109 | target: target.constructor, 110 | typeFn, 111 | returnTypeOptions: typeOptions, 112 | description: options.description, 113 | deprecationReason: options.deprecationReason, 114 | }; 115 | TypeMetadataStorage.addSubscriptionMetadata(metadata); 116 | }); 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /lib/enums/class-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ClassType { 2 | ARGS = 'args', 3 | OBJECT = 'objectType', 4 | INPUT = 'inputType', 5 | INTERFACE = 'interface', 6 | } 7 | -------------------------------------------------------------------------------- /lib/enums/gql-paramtype.enum.ts: -------------------------------------------------------------------------------- 1 | export enum GqlParamtype { 2 | ROOT, 3 | ARGS = 3, 4 | CONTEXT = 1, 5 | INFO = 2, 6 | } 7 | -------------------------------------------------------------------------------- /lib/enums/resolver.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Resolver { 2 | QUERY = 'Query', 3 | MUTATION = 'Mutation', 4 | SUBSCRIPTION = 'Subscription', 5 | } 6 | -------------------------------------------------------------------------------- /lib/factories/params.factory.ts: -------------------------------------------------------------------------------- 1 | import { ParamData } from '@nestjs/common'; 2 | import { ParamsFactory } from '@nestjs/core/helpers/external-context-creator'; 3 | import { GqlParamtype } from '../enums/gql-paramtype.enum'; 4 | 5 | export class GqlParamsFactory implements ParamsFactory { 6 | exchangeKeyForValue(type: number, data: ParamData, args: any) { 7 | if (!args) { 8 | return null; 9 | } 10 | switch (type as GqlParamtype) { 11 | case GqlParamtype.ROOT: 12 | return args[0]; 13 | case GqlParamtype.ARGS: 14 | return data && args[1] ? args[1][data as string] : args[1]; 15 | case GqlParamtype.CONTEXT: 16 | return data && args[2] ? args[2][data as string] : args[2]; 17 | case GqlParamtype.INFO: 18 | return data && args[3] ? args[3][data as string] : args[3]; 19 | default: 20 | return null; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/fgql.constants.ts: -------------------------------------------------------------------------------- 1 | export const GRAPHQL_MODULE_OPTIONS = 'FgqlModuleOptions'; 2 | 3 | export const RESOLVER_TYPE_METADATA = 'graphql:resolver_type'; 4 | export const RESOLVER_NAME_METADATA = 'graphql:resolver_name'; 5 | export const RESOLVER_PROPERTY_METADATA = 'graphql:resolve_property'; 6 | export const RESOLVER_DELEGATE_METADATA = 'graphql:delegate_property'; 7 | export const SCALAR_NAME_METADATA = 'graphql:scalar_name'; 8 | export const SCALAR_TYPE_METADATA = 'graphql:scalar_type'; 9 | export const PLUGIN_METADATA = 'graphql:plugin'; 10 | export const PARAM_ARGS_METADATA = '__routeArguments__'; 11 | export const SUBSCRIPTION_OPTIONS_METADATA = 'graphql:subscription_options;'; 12 | export const CLASS_TYPE_METADATA = 'graphql:class_type'; 13 | 14 | export const FIELD_TYPENAME = '__resolveType'; 15 | export const GRAPHQL_MODULE_ID = 'GqlModuleId'; 16 | export const SUBSCRIPTION_TYPE = 'Subscription'; 17 | 18 | export const DEFINITIONS_FILE_HEADER = ` 19 | /** ------------------------------------------------------ 20 | * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 21 | * ------------------------------------------------------- 22 | */ 23 | 24 | /* tslint:disable */ 25 | /* eslint-disable */\n`; 26 | 27 | export const SDL_FILE_HEADER = `\ 28 | # ------------------------------------------------------ 29 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 30 | # ------------------------------------------------------ 31 | 32 | `; 33 | -------------------------------------------------------------------------------- /lib/fgql.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | Inject, 4 | Module, 5 | OnModuleInit, 6 | Provider, 7 | } from '@nestjs/common'; 8 | import { HttpAdapterHost, MetadataScanner } from '@nestjs/core'; 9 | import * as GQL from 'fastify-gql'; 10 | import { printSchema } from 'graphql'; 11 | import { GRAPHQL_MODULE_ID, GRAPHQL_MODULE_OPTIONS } from './fgql.constants'; 12 | import { 13 | GraphQLAstExplorer, 14 | GraphQLFactory, 15 | GraphQLSchemaBuilder, 16 | GraphQLSchemaHost, 17 | GraphQLTypesLoader, 18 | } from './graphql'; 19 | import { 20 | FgqlModuleOptions, 21 | GqlModuleAsyncOptions, 22 | GqlOptionsFactory, 23 | } from './interfaces/fgql-module-options.interface'; 24 | import { GraphQLSchemaBuilderModule } from './schema-builder'; 25 | import { ResolversExplorerService, ScalarsExplorerService } from './services'; 26 | import { extend, generateString, mergeDefaults } from './utils'; 27 | 28 | @Module({ 29 | imports: [GraphQLSchemaBuilderModule], 30 | providers: [ 31 | GraphQLFactory, 32 | MetadataScanner, 33 | ResolversExplorerService, 34 | ScalarsExplorerService, 35 | GraphQLAstExplorer, 36 | GraphQLTypesLoader, 37 | GraphQLSchemaBuilder, 38 | GraphQLSchemaHost, 39 | ], 40 | exports: [GraphQLTypesLoader, GraphQLAstExplorer, GraphQLSchemaHost], 41 | }) 42 | export class FgqlModule implements OnModuleInit { 43 | constructor( 44 | private readonly httpAdapterHost: HttpAdapterHost, 45 | private readonly graphqlFactory: GraphQLFactory, 46 | private readonly graphqlTypesLoader: GraphQLTypesLoader, 47 | @Inject(GRAPHQL_MODULE_OPTIONS) 48 | private readonly options: FgqlModuleOptions, 49 | ) {} 50 | 51 | static forRoot(options: FgqlModuleOptions = {}): DynamicModule { 52 | options = mergeDefaults(options); 53 | return { 54 | module: FgqlModule, 55 | providers: [ 56 | { 57 | provide: GRAPHQL_MODULE_OPTIONS, 58 | useValue: options, 59 | }, 60 | ], 61 | }; 62 | } 63 | 64 | static forRootAsync(options: GqlModuleAsyncOptions): DynamicModule { 65 | return { 66 | module: FgqlModule, 67 | imports: options.imports, 68 | providers: [ 69 | ...this.createAsyncProviders(options), 70 | { 71 | provide: GRAPHQL_MODULE_ID, 72 | useValue: generateString(), 73 | }, 74 | ], 75 | }; 76 | } 77 | 78 | private static createAsyncProviders( 79 | options: GqlModuleAsyncOptions, 80 | ): Provider[] { 81 | if (options.useExisting || options.useFactory) { 82 | return [this.createAsyncOptionsProvider(options)]; 83 | } 84 | return [ 85 | this.createAsyncOptionsProvider(options), 86 | { 87 | provide: options.useClass, 88 | useClass: options.useClass, 89 | }, 90 | ]; 91 | } 92 | 93 | private static createAsyncOptionsProvider( 94 | options: GqlModuleAsyncOptions, 95 | ): Provider { 96 | if (options.useFactory) { 97 | return { 98 | provide: GRAPHQL_MODULE_OPTIONS, 99 | useFactory: async (...args: any[]) => 100 | mergeDefaults(await options.useFactory(...args)), 101 | inject: options.inject || [], 102 | }; 103 | } 104 | return { 105 | provide: GRAPHQL_MODULE_OPTIONS, 106 | useFactory: async (optionsFactory: GqlOptionsFactory) => 107 | mergeDefaults(await optionsFactory.createGqlOptions()), 108 | inject: [options.useExisting || options.useClass], 109 | }; 110 | } 111 | 112 | async onModuleInit() { 113 | if (!this.httpAdapterHost) { 114 | return; 115 | } 116 | const httpAdapter = this.httpAdapterHost.httpAdapter; 117 | if (!httpAdapter) { 118 | return; 119 | } 120 | const typeDefs = 121 | (await this.graphqlTypesLoader.mergeTypesByPaths( 122 | this.options.typePaths, 123 | )) || []; 124 | 125 | const mergedTypeDefs = extend(typeDefs, this.options.typeDefs); 126 | const options = await this.graphqlFactory.mergeOptions({ 127 | ...this.options, 128 | typeDefs: mergedTypeDefs, 129 | }); 130 | 131 | if (this.options.definitions && this.options.definitions.path) { 132 | await this.graphqlFactory.generateDefinitions( 133 | printSchema(options.schema), 134 | this.options, 135 | ); 136 | } 137 | 138 | this.registerGqlServer(options); 139 | } 140 | 141 | private registerGqlServer(options: FgqlModuleOptions) { 142 | const httpAdapter = this.httpAdapterHost.httpAdapter; 143 | const platformName = httpAdapter.getType(); 144 | 145 | if (platformName !== 'fastify') { 146 | throw new Error(`No support for current HttpAdapter: ${platformName}`); 147 | } 148 | 149 | const app = httpAdapter.getInstance(); 150 | 151 | app.register(GQL, { 152 | graphiql: true, 153 | jit: 1, 154 | ...options, 155 | }); 156 | 157 | app.addHook('preHandler', async (request, reply) => { 158 | // make sure that operationName is null, if empty string is passed 159 | if (request?.body?.operationName === '') { 160 | request.body.operationName = null; 161 | } 162 | return; 163 | }); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lib/graphql/graphql-schema.builder.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isString } from '@nestjs/common/utils/shared.utils'; 3 | import { GraphQLSchema, printSchema, specifiedDirectives } from 'graphql'; 4 | import { resolve } from 'path'; 5 | import { SDL_FILE_HEADER } from '../fgql.constants'; 6 | import { BuildSchemaOptions } from '../interfaces/build-schema-options.interface'; 7 | import { FgqlModuleOptions } from '../interfaces/fgql-module-options.interface'; 8 | import { GraphQLSchemaFactory } from '../schema-builder/graphql-schema.factory'; 9 | import { FileSystemHelper } from '../schema-builder/helpers/file-system.helper'; 10 | import { ScalarsExplorerService } from '../services'; 11 | 12 | @Injectable() 13 | export class GraphQLSchemaBuilder { 14 | constructor( 15 | private readonly scalarsExplorerService: ScalarsExplorerService, 16 | private readonly gqlSchemaFactory: GraphQLSchemaFactory, 17 | private readonly fileSystemHelper: FileSystemHelper, 18 | ) {} 19 | 20 | async build( 21 | autoSchemaFile: string | boolean, 22 | options: FgqlModuleOptions, 23 | resolvers: Function[], 24 | ): Promise { 25 | const scalarsMap = this.scalarsExplorerService.getScalarsMap(); 26 | try { 27 | const buildSchemaOptions = options.buildSchemaOptions || {}; 28 | return await this.buildSchema(resolvers, autoSchemaFile, { 29 | ...buildSchemaOptions, 30 | scalarsMap, 31 | schemaDirectives: options.schemaDirectives, 32 | }); 33 | } catch (err) { 34 | if (err && err.details) { 35 | console.error(err.details); 36 | } 37 | throw err; 38 | } 39 | } 40 | 41 | async buildFederatedSchema( 42 | autoSchemaFile: string | boolean, 43 | options: FgqlModuleOptions, 44 | resolvers: Function[], 45 | ) { 46 | const scalarsMap = this.scalarsExplorerService.getScalarsMap(); 47 | try { 48 | const buildSchemaOptions = options.buildSchemaOptions || {}; 49 | return await this.buildSchema(resolvers, autoSchemaFile, { 50 | ...buildSchemaOptions, 51 | directives: [ 52 | ...specifiedDirectives, 53 | ...((buildSchemaOptions && buildSchemaOptions.directives) || []), 54 | ], 55 | scalarsMap, 56 | schemaDirectives: options.schemaDirectives, 57 | skipCheck: true, 58 | }); 59 | } catch (err) { 60 | if (err && err.details) { 61 | console.error(err.details); 62 | } 63 | throw err; 64 | } 65 | } 66 | 67 | private async buildSchema( 68 | resolvers: Function[], 69 | autoSchemaFile: boolean | string, 70 | options: BuildSchemaOptions = {}, 71 | ): Promise { 72 | const schema = await this.gqlSchemaFactory.create(resolvers, options); 73 | if (typeof autoSchemaFile !== 'boolean') { 74 | const filename = isString(autoSchemaFile) 75 | ? autoSchemaFile 76 | : resolve(process.cwd(), 'schema.gql'); 77 | 78 | const fileContent = SDL_FILE_HEADER + printSchema(schema); 79 | await this.fileSystemHelper.writeFile(filename, fileContent); 80 | } 81 | return schema; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/graphql/graphql-schema.host.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLSchema } from 'graphql'; 3 | 4 | @Injectable() 5 | export class GraphQLSchemaHost { 6 | private _schema: GraphQLSchema; 7 | 8 | set schema(schemaRef: GraphQLSchema) { 9 | this._schema = schemaRef; 10 | } 11 | 12 | get schema(): GraphQLSchema { 13 | if (!this._schema) { 14 | throw new Error( 15 | 'GraphQL schema has not yet been created. ' + 16 | 'Make sure to call the "GraphQLSchemaHost#schema" getter when the application is already initialized (after the "onModuleInit" hook triggered by either "app.listen()" or "app.init()" method).', 17 | ); 18 | } 19 | return this._schema; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/graphql/graphql-types.loader.ts: -------------------------------------------------------------------------------- 1 | import { mergeTypeDefs } from '@graphql-tools/merge'; 2 | import { Injectable } from '@nestjs/common'; 3 | import * as glob from 'fast-glob'; 4 | import * as fs from 'fs'; 5 | import { flatten } from 'lodash'; 6 | import * as util from 'util'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires 9 | const normalize = require('normalize-path'); 10 | const readFile = util.promisify(fs.readFile); 11 | 12 | @Injectable() 13 | export class GraphQLTypesLoader { 14 | async mergeTypesByPaths(paths: string | string[]): Promise { 15 | if (!paths || paths.length === 0) { 16 | return null; 17 | } 18 | const types = await this.getTypesFromPaths(paths); 19 | const flatTypes = flatten(types); 20 | 21 | return mergeTypeDefs(flatTypes, { 22 | throwOnConflict: true, 23 | commentDescriptions: true, 24 | reverseDirectives: true, 25 | }); 26 | } 27 | 28 | private async getTypesFromPaths(paths: string | string[]): Promise { 29 | paths = util.isArray(paths) 30 | ? paths.map((path) => normalize(path)) 31 | : normalize(paths); 32 | 33 | const filePaths = await glob(paths, { 34 | ignore: ['node_modules'], 35 | }); 36 | if (filePaths.length === 0) { 37 | throw new Error( 38 | `No type definitions were found with the specified file name patterns: "${paths}". Please make sure there is at least one file that matches the given patterns.`, 39 | ); 40 | } 41 | const fileContentsPromises = filePaths.sort().map((filePath) => { 42 | return readFile(filePath.toString(), 'utf8'); 43 | }); 44 | 45 | return Promise.all(fileContentsPromises); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/graphql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './graphql-ast.explorer'; 2 | export * from './graphql-schema.builder'; 3 | export * from './graphql-schema.host'; 4 | export * from './graphql-types.loader'; 5 | export * from './graphql.factory'; 6 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators'; 2 | export * from './fgql.module'; 3 | export * from './graphql'; 4 | export * from './interfaces'; 5 | export * from './scalars'; 6 | export * from './schema-builder'; 7 | export * from './services/gql-arguments-host'; 8 | export * from './services/gql-execution-context'; 9 | export * from './type-factories'; 10 | -------------------------------------------------------------------------------- /lib/interfaces/base-type-options.interface.ts: -------------------------------------------------------------------------------- 1 | export type NullableList = 'items' | 'itemsAndList'; 2 | export interface BaseTypeOptions { 3 | /** 4 | * Determines whether field/argument/etc is nullable. 5 | */ 6 | nullable?: boolean | NullableList; 7 | /** 8 | * Default value. 9 | */ 10 | defaultValue?: any; 11 | } 12 | -------------------------------------------------------------------------------- /lib/interfaces/build-schema-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLDirective, GraphQLScalarType } from 'graphql'; 2 | 3 | export type DateScalarMode = 'isoDate' | 'timestamp'; 4 | 5 | export interface ScalarsTypeMap { 6 | type: Function; 7 | scalar: GraphQLScalarType; 8 | } 9 | 10 | export interface BuildSchemaOptions { 11 | /** 12 | * Date scalar mode 13 | */ 14 | dateScalarMode?: DateScalarMode; 15 | 16 | /** 17 | * Scalars map 18 | */ 19 | scalarsMap?: ScalarsTypeMap[]; 20 | 21 | /** 22 | * Orphaned type classes that are not explicitly used in GraphQL types definitions 23 | */ 24 | orphanedTypes?: Function[]; 25 | 26 | /** 27 | * Disable checking on build the correctness of a schema 28 | */ 29 | skipCheck?: boolean; 30 | 31 | /** 32 | * GraphQL directives 33 | */ 34 | directives?: GraphQLDirective[]; 35 | 36 | /** 37 | * GraphQL schema directives mapping 38 | */ 39 | schemaDirectives?: Record; 40 | } 41 | -------------------------------------------------------------------------------- /lib/interfaces/complexity.interface.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLCompositeType, GraphQLField } from 'graphql'; 2 | 3 | export type ComplexityEstimatorArgs = { 4 | type: GraphQLCompositeType; 5 | field: GraphQLField; 6 | args: { [key: string]: any }; 7 | childComplexity: number; 8 | }; 9 | 10 | export type ComplexityEstimator = ( 11 | options: ComplexityEstimatorArgs, 12 | ) => number | void; 13 | export type Complexity = ComplexityEstimator | number; 14 | -------------------------------------------------------------------------------- /lib/interfaces/custom-scalar.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLScalarLiteralParser, 3 | GraphQLScalarSerializer, 4 | GraphQLScalarValueParser, 5 | } from 'graphql'; 6 | 7 | export interface CustomScalar { 8 | description?: string; 9 | parseValue: GraphQLScalarValueParser; 10 | serialize: GraphQLScalarSerializer; 11 | parseLiteral: GraphQLScalarLiteralParser; 12 | } 13 | -------------------------------------------------------------------------------- /lib/interfaces/fgql-module-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; 2 | import * as GQL from 'fastify-gql'; 3 | import { GraphQLSchema } from 'graphql'; 4 | import { IResolvers } from 'graphql-tools'; 5 | import { DefinitionsGeneratorOptions } from '../graphql/graphql-ast.explorer'; 6 | import { BuildSchemaOptions } from './build-schema-options.interface'; 7 | 8 | export interface FgqlModuleOptions 9 | extends Omit { 10 | schema?: GraphQLSchema; 11 | resolvers?: IResolvers; 12 | typeDefs?: string | string[]; 13 | typePaths?: string[]; 14 | transformSchema?: ( 15 | schema: GraphQLSchema, 16 | ) => GraphQLSchema | Promise; 17 | definitions?: { 18 | path?: string; 19 | outputAs?: 'class' | 'interface'; 20 | } & DefinitionsGeneratorOptions; 21 | autoSchemaFile?: string | boolean; 22 | buildSchemaOptions?: BuildSchemaOptions; 23 | include?: Function[]; 24 | schemaDirectives?: Record; 25 | resolverValidationOptions?: IResolverValidationOptions; 26 | directiveResolvers?: any; 27 | /** 28 | * Enable/disable enhancers for @ResolveField() 29 | */ 30 | fieldResolverEnhancers?: Enhancer[]; 31 | } 32 | 33 | export type Enhancer = 'guards' | 'interceptors' | 'filters'; 34 | 35 | export interface IResolverValidationOptions { 36 | requireResolversForArgs?: boolean; 37 | requireResolversForNonScalar?: boolean; 38 | requireResolversForAllFields?: boolean; 39 | requireResolversForResolveType?: boolean; 40 | allowResolversNotInSchema?: boolean; 41 | } 42 | 43 | export interface GqlOptionsFactory { 44 | createGqlOptions(): Promise | FgqlModuleOptions; 45 | } 46 | 47 | export interface GqlModuleAsyncOptions extends Pick { 48 | useExisting?: Type; 49 | useClass?: Type; 50 | useFactory?: ( 51 | ...args: any[] 52 | ) => Promise | FgqlModuleOptions; 53 | inject?: any[]; 54 | } 55 | -------------------------------------------------------------------------------- /lib/interfaces/gql-exception-filter.interface.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost } from '@nestjs/common'; 2 | 3 | export interface GqlExceptionFilter { 4 | catch(exception: TInput, host: ArgumentsHost): TOutput; 5 | } 6 | -------------------------------------------------------------------------------- /lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-type-options.interface'; 2 | export * from './build-schema-options.interface'; 3 | export * from './complexity.interface'; 4 | export * from './custom-scalar.interface'; 5 | export * from './fgql-module-options.interface'; 6 | export * from './gql-exception-filter.interface'; 7 | export * from './resolve-type-fn.interface'; 8 | export * from './return-type-func.interface'; 9 | -------------------------------------------------------------------------------- /lib/interfaces/resolve-type-fn.interface.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLTypeResolver } from 'graphql'; 2 | 3 | export type ResolveTypeFn = ( 4 | ...args: Parameters> 5 | ) => any; 6 | -------------------------------------------------------------------------------- /lib/interfaces/resolver-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ResolverMetadata { 2 | name: string; 3 | type: string; 4 | methodName: string; 5 | callback?: Function | Record; 6 | } 7 | -------------------------------------------------------------------------------- /lib/interfaces/return-type-func.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { GraphQLScalarType } from 'graphql'; 3 | 4 | export type GqlTypeReference = 5 | | Type 6 | | GraphQLScalarType 7 | | Function 8 | | object 9 | | symbol; 10 | export type ReturnTypeFuncValue = GqlTypeReference | [GqlTypeReference]; 11 | export type ReturnTypeFunc = (returns?: void) => ReturnTypeFuncValue; 12 | -------------------------------------------------------------------------------- /lib/interfaces/type-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { BaseTypeOptions } from './base-type-options.interface'; 2 | 3 | export interface TypeOptions extends BaseTypeOptions { 4 | isArray?: boolean; 5 | arrayDepth?: number; 6 | } 7 | -------------------------------------------------------------------------------- /lib/plugin/compiler-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { mergePluginOptions } from './merge-options'; 3 | import { ModelClassVisitor } from './visitors/model-class.visitor'; 4 | 5 | const typeClassVisitor = new ModelClassVisitor(); 6 | const isFilenameMatched = (patterns: string[], filename: string) => 7 | patterns.some(path => filename.includes(path)); 8 | 9 | export const before = (options?: Record, program?: ts.Program) => { 10 | options = mergePluginOptions(options); 11 | 12 | return (ctx: ts.TransformationContext): ts.Transformer => { 13 | return (sf: ts.SourceFile) => { 14 | if (isFilenameMatched(options.typeFileNameSuffix, sf.fileName)) { 15 | return typeClassVisitor.visit(sf, ctx, program); 16 | } 17 | return sf; 18 | }; 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compiler-plugin'; 2 | -------------------------------------------------------------------------------- /lib/plugin/merge-options.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@nestjs/common/utils/shared.utils'; 2 | 3 | export interface PluginOptions { 4 | typeFileNameSuffix?: string | string[]; 5 | } 6 | 7 | const defaultOptions: PluginOptions = { 8 | typeFileNameSuffix: ['.input.ts', '.args.ts', '.entity.ts', '.model.ts'], 9 | }; 10 | 11 | export const mergePluginOptions = ( 12 | options: Record = {}, 13 | ): PluginOptions => { 14 | if (isString(options.typeFileNameSuffix)) { 15 | options.typeFileNameSuffix = [options.typeFileNameSuffix]; 16 | } 17 | return { 18 | ...defaultOptions, 19 | ...options, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/plugin/plugin-constants.ts: -------------------------------------------------------------------------------- 1 | export const GRAPHQL_PACKAGE_NAMESPACE = 'graphql'; 2 | export const GRAPHQL_PACKAGE_NAME = '@nestjs/graphql'; 3 | export const METADATA_FACTORY_NAME = '_GRAPHQL_METADATA_FACTORY'; 4 | -------------------------------------------------------------------------------- /lib/plugin/utils/ast-utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallExpression, 3 | Decorator, 4 | Identifier, 5 | LeftHandSideExpression, 6 | Node, 7 | ObjectFlags, 8 | ObjectType, 9 | PropertyAccessExpression, 10 | SyntaxKind, 11 | Type, 12 | TypeChecker, 13 | TypeFlags, 14 | TypeFormatFlags, 15 | } from 'typescript'; 16 | import { isDynamicallyAdded } from './plugin-utils'; 17 | 18 | export function isArray(type: Type) { 19 | const symbol = type.getSymbol(); 20 | if (!symbol) { 21 | return false; 22 | } 23 | return symbol.getName() === 'Array' && getTypeArguments(type).length === 1; 24 | } 25 | 26 | export function getTypeArguments(type: Type) { 27 | return (type as any).typeArguments || []; 28 | } 29 | 30 | export function isBoolean(type: Type) { 31 | return hasFlag(type, TypeFlags.Boolean); 32 | } 33 | 34 | export function isString(type: Type) { 35 | return hasFlag(type, TypeFlags.String); 36 | } 37 | 38 | export function isNumber(type: Type) { 39 | return hasFlag(type, TypeFlags.Number); 40 | } 41 | 42 | export function isInterface(type: Type) { 43 | return hasObjectFlag(type, ObjectFlags.Interface); 44 | } 45 | 46 | export function isEnum(type: Type) { 47 | const hasEnumFlag = hasFlag(type, TypeFlags.Enum); 48 | if (hasEnumFlag) { 49 | return true; 50 | } 51 | if (isEnumLiteral(type)) { 52 | return false; 53 | } 54 | const symbol = type.getSymbol(); 55 | if (!symbol) { 56 | return false; 57 | } 58 | const valueDeclaration = symbol.valueDeclaration; 59 | if (!valueDeclaration) { 60 | return false; 61 | } 62 | return valueDeclaration.kind === SyntaxKind.EnumDeclaration; 63 | } 64 | 65 | export function isEnumLiteral(type: Type) { 66 | return hasFlag(type, TypeFlags.EnumLiteral) && !type.isUnion(); 67 | } 68 | 69 | export function hasFlag(type: Type, flag: TypeFlags) { 70 | return (type.flags & flag) === flag; 71 | } 72 | 73 | export function hasObjectFlag(type: Type, flag: ObjectFlags) { 74 | return ((type as ObjectType).objectFlags & flag) === flag; 75 | } 76 | 77 | export function getText( 78 | type: Type, 79 | typeChecker: TypeChecker, 80 | enclosingNode?: Node, 81 | typeFormatFlags?: TypeFormatFlags, 82 | ) { 83 | if (!typeFormatFlags) { 84 | typeFormatFlags = getDefaultTypeFormatFlags(enclosingNode); 85 | } 86 | const compilerNode = !enclosingNode ? undefined : enclosingNode; 87 | return typeChecker.typeToString(type, compilerNode, typeFormatFlags); 88 | } 89 | 90 | export function getDefaultTypeFormatFlags(enclosingNode: Node) { 91 | let formatFlags = 92 | TypeFormatFlags.UseTypeOfFunction | 93 | TypeFormatFlags.NoTruncation | 94 | TypeFormatFlags.UseFullyQualifiedType | 95 | TypeFormatFlags.WriteTypeArgumentsOfSignature; 96 | if (enclosingNode && enclosingNode.kind === SyntaxKind.TypeAliasDeclaration) 97 | formatFlags |= TypeFormatFlags.InTypeAlias; 98 | return formatFlags; 99 | } 100 | 101 | export function getDecoratorArguments(decorator: Decorator) { 102 | const callExpression = decorator.expression; 103 | return (callExpression && (callExpression as CallExpression).arguments) || []; 104 | } 105 | 106 | export function getDecoratorName(decorator: Decorator) { 107 | const isDecoratorFactory = 108 | decorator.expression.kind === SyntaxKind.CallExpression; 109 | if (isDecoratorFactory) { 110 | const callExpression = decorator.expression; 111 | const identifier = (callExpression as CallExpression) 112 | .expression as Identifier; 113 | if (isDynamicallyAdded(identifier)) { 114 | return undefined; 115 | } 116 | return getIdentifierFromName( 117 | (callExpression as CallExpression).expression, 118 | ).getText(); 119 | } 120 | return getIdentifierFromName(decorator.expression).getText(); 121 | } 122 | 123 | function getIdentifierFromName(expression: LeftHandSideExpression) { 124 | const identifier = getNameFromExpression(expression); 125 | if (expression && expression.kind !== SyntaxKind.Identifier) { 126 | throw new Error(); 127 | } 128 | return identifier; 129 | } 130 | 131 | function getNameFromExpression(expression: LeftHandSideExpression) { 132 | if (expression && expression.kind === SyntaxKind.PropertyAccessExpression) { 133 | return (expression as PropertyAccessExpression).name; 134 | } 135 | return expression; 136 | } 137 | -------------------------------------------------------------------------------- /lib/scalars/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFloat, GraphQLID, GraphQLInt } from 'graphql'; 2 | 3 | export * from './iso-date.scalar'; 4 | export * from './timestamp.scalar'; 5 | 6 | export const Int = GraphQLInt; 7 | export const Float = GraphQLFloat; 8 | export const ID = GraphQLID; 9 | -------------------------------------------------------------------------------- /lib/scalars/iso-date.scalar.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, Kind, ValueNode } from 'graphql'; 2 | 3 | export const GraphQLISODateTime = new GraphQLScalarType({ 4 | name: 'DateTime', 5 | description: 6 | 'A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.', 7 | parseValue(value: string) { 8 | return new Date(value); 9 | }, 10 | serialize(value: Date) { 11 | return value instanceof Date ? value.toISOString() : null; 12 | }, 13 | parseLiteral(ast: ValueNode) { 14 | return ast.kind === Kind.STRING ? new Date(ast.value) : null; 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /lib/scalars/timestamp.scalar.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, Kind, ValueNode } from 'graphql'; 2 | 3 | export const GraphQLTimestamp = new GraphQLScalarType({ 4 | name: 'Timestamp', 5 | description: 6 | '`Date` type as integer. Type represents date and time as number of milliseconds from start of UNIX epoch.', 7 | serialize(value: Date) { 8 | return value instanceof Date ? value.getTime() : null; 9 | }, 10 | parseValue(value: string | null) { 11 | try { 12 | return value !== null ? new Date(value) : null; 13 | } catch { 14 | return null; 15 | } 16 | }, 17 | parseLiteral(ast: ValueNode) { 18 | if (ast.kind === Kind.INT) { 19 | const num = parseInt(ast.value, 10); 20 | return new Date(num); 21 | } else if (ast.kind === Kind.STRING) { 22 | return this.parseValue(ast.value); 23 | } 24 | return null; 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/cannot-determine-input-type.error.ts: -------------------------------------------------------------------------------- 1 | export class CannotDetermineInputTypeError extends Error { 2 | constructor(hostType: string) { 3 | super( 4 | `Cannot determine a GraphQL input type for the "${hostType}". Make sure your class is decorated with an appropriate decorator.`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/cannot-determine-output-type.error.ts: -------------------------------------------------------------------------------- 1 | export class CannotDetermineOutputTypeError extends Error { 2 | constructor(hostType: string) { 3 | super( 4 | `Cannot determine a GraphQL output type for the "${hostType}". Make sure your class is decorated with an appropriate decorator.`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/default-nullable-conflict.error.ts: -------------------------------------------------------------------------------- 1 | import { NullableList } from '../../interfaces'; 2 | 3 | export class DefaultNullableConflictError extends Error { 4 | constructor( 5 | hostTypeName: string, 6 | defaultVal: any, 7 | isNullable: boolean | NullableList, 8 | ) { 9 | super( 10 | `Incorrect "nullable" option value set for ${hostTypeName}. Do not combine "defaultValue: ${defaultVal}" with "nullable: ${isNullable}".`, 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/default-values-conflict.error.ts: -------------------------------------------------------------------------------- 1 | export class DefaultValuesConflictError extends Error { 2 | constructor( 3 | hostTypeName: string, 4 | fieldName: string, 5 | decoratorDefaultVal: unknown, 6 | initializerDefaultVal: unknown, 7 | ) { 8 | super( 9 | `Error caused by mis-matched default values for the "${fieldName}" field of "${hostTypeName}". The default value from the decorator "${decoratorDefaultVal}" is not equal to the property initializer value "${initializerDefaultVal}". Ensure that these values match.`, 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/directive-parsing.error.ts: -------------------------------------------------------------------------------- 1 | export class DirectiveParsingError extends Error { 2 | constructor(sdl: string) { 3 | super( 4 | `Directive SDL "${sdl}" is invalid. Please, pass a valid directive definition.`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/invalid-nullable-option.error.ts: -------------------------------------------------------------------------------- 1 | import { NullableList } from '../../interfaces'; 2 | 3 | export class InvalidNullableOptionError extends Error { 4 | constructor(name: string, nullable?: boolean | NullableList) { 5 | super( 6 | `Incorrect nullable option set for ${name}. Do not combine non-list type with nullable "${nullable}".`, 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/return-type-cannot-be-resolved.error.ts: -------------------------------------------------------------------------------- 1 | export class ReturnTypeCannotBeResolvedError extends Error { 2 | constructor(hostTypeName: string) { 3 | super( 4 | `Return type for "${hostTypeName}" cannot be resolved. If you did not pass a custom implementation (the "resolveType" function), you must return an instance of a class instead of a plain JavaScript object.`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/schema-generation.error.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | 3 | export class SchemaGenerationError extends Error { 4 | constructor(public readonly details: ReadonlyArray) { 5 | super('Schema generation error (code-first approach)'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/unable-to-find-fields.error.ts: -------------------------------------------------------------------------------- 1 | export class UnableToFindFieldsError extends Error { 2 | constructor(name: string) { 3 | super( 4 | `Unable to find fields for GraphQL type ${name}. Is your class annotated with an appropriate decorator?`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/undefined-resolver-type.error.ts: -------------------------------------------------------------------------------- 1 | export class UndefinedResolverTypeError extends Error { 2 | constructor(name: string) { 3 | super( 4 | `Undefined resolver type error. Make sure you are providing an explicit object type for the "${name}" (e.g., "@Resolver(() => Cat)").`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/undefined-return-type.error.ts: -------------------------------------------------------------------------------- 1 | export class UndefinedReturnTypeError extends Error { 2 | constructor(decoratorName: string, methodKey: string) { 3 | super( 4 | `"${decoratorName}.${methodKey}" was defined in resolvers, but not in schema. If you use the @${decoratorName}() decorator with the code first approach enabled, remember to explicitly provide a return type function, e.g. @${decoratorName}(returns => Author).`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/errors/undefined-type.error.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from '@nestjs/common/utils/shared.utils'; 2 | 3 | export class UndefinedTypeError extends Error { 4 | constructor(name: string, key: string, index?: number) { 5 | super( 6 | `Undefined type error. Make sure you are providing an explicit type for the "${key}" ${ 7 | isUndefined(index) ? '' : `(parameter at index [${index}]) ` 8 | }of the "${name}" class.`, 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/args.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isUndefined } from '@nestjs/common/utils/shared.utils'; 3 | import { GraphQLFieldConfigArgumentMap } from 'graphql'; 4 | import { BuildSchemaOptions } from '../../interfaces'; 5 | import { getDefaultValue } from '../helpers/get-default-value.helper'; 6 | import { ClassMetadata, MethodArgsMetadata } from '../metadata'; 7 | import { TypeMetadataStorage } from '../storages/type-metadata.storage'; 8 | import { InputTypeFactory } from './input-type.factory'; 9 | 10 | @Injectable() 11 | export class ArgsFactory { 12 | constructor(private readonly inputTypeFactory: InputTypeFactory) {} 13 | 14 | public create( 15 | args: MethodArgsMetadata[], 16 | options: BuildSchemaOptions, 17 | ): GraphQLFieldConfigArgumentMap { 18 | const fieldConfigMap: GraphQLFieldConfigArgumentMap = {}; 19 | args.forEach(param => { 20 | if (param.kind === 'arg') { 21 | fieldConfigMap[param.name] = { 22 | description: param.description, 23 | type: this.inputTypeFactory.create( 24 | param.name, 25 | param.typeFn(), 26 | options, 27 | param.options, 28 | ), 29 | defaultValue: param.options.defaultValue, 30 | }; 31 | } else if (param.kind === 'args') { 32 | const argumentTypes = TypeMetadataStorage.getArgumentsMetadata(); 33 | const hostType = param.typeFn(); 34 | const argumentType = argumentTypes.find( 35 | item => item.target === hostType, 36 | )!; 37 | 38 | let parent = Object.getPrototypeOf(argumentType.target); 39 | while (!isUndefined(parent.prototype)) { 40 | const parentArgType = argumentTypes.find( 41 | item => item.target === parent, 42 | ); 43 | if (parentArgType) { 44 | this.inheritParentArgs(parentArgType, options, fieldConfigMap); 45 | } 46 | parent = Object.getPrototypeOf(parent); 47 | } 48 | this.inheritParentArgs(argumentType, options, fieldConfigMap); 49 | } 50 | }); 51 | return fieldConfigMap; 52 | } 53 | 54 | private inheritParentArgs( 55 | argType: ClassMetadata, 56 | options: BuildSchemaOptions, 57 | fieldConfigMap: GraphQLFieldConfigArgumentMap = {}, 58 | ) { 59 | const argumentInstance = new (argType.target as any)(); 60 | argType.properties.forEach(field => { 61 | field.options.defaultValue = getDefaultValue( 62 | argumentInstance, 63 | field.options, 64 | field.name, 65 | argType.name, 66 | ); 67 | 68 | const { schemaName } = field; 69 | fieldConfigMap[schemaName] = { 70 | description: field.description, 71 | type: this.inputTypeFactory.create( 72 | field.name, 73 | field.typeFn(), 74 | options, 75 | field.options, 76 | ), 77 | defaultValue: field.options.defaultValue, 78 | }; 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/ast-definition-node.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isEmpty } from '@nestjs/common/utils/shared.utils'; 3 | import { 4 | DirectiveNode, 5 | FieldDefinitionNode, 6 | GraphQLInputType, 7 | GraphQLOutputType, 8 | InputObjectTypeDefinitionNode, 9 | InputValueDefinitionNode, 10 | ObjectTypeDefinitionNode, 11 | parse, 12 | } from 'graphql'; 13 | import { head } from 'lodash'; 14 | import { DirectiveParsingError } from '../errors/directive-parsing.error'; 15 | import { DirectiveMetadata } from '../metadata/directive.metadata'; 16 | 17 | @Injectable() 18 | export class AstDefinitionNodeFactory { 19 | /** 20 | * The implementation of this class has been heavily inspired by the folllowing code: 21 | * @ref https://github.com/MichalLytek/type-graphql/blob/master/src/schema/definition-node.ts 22 | * implemented in this PR https://github.com/MichalLytek/type-graphql/pull/369 by Jordan Stous (https://github.com/j) 23 | */ 24 | 25 | createObjectTypeNode( 26 | name: string, 27 | directiveMetadata?: DirectiveMetadata[], 28 | ): ObjectTypeDefinitionNode | undefined { 29 | if (isEmpty(directiveMetadata)) { 30 | return; 31 | } 32 | return { 33 | kind: 'ObjectTypeDefinition', 34 | name: { 35 | kind: 'Name', 36 | value: name, 37 | }, 38 | directives: directiveMetadata.map(this.createDirectiveNode), 39 | }; 40 | } 41 | 42 | createInputObjectTypeNode( 43 | name: string, 44 | directiveMetadata?: DirectiveMetadata[], 45 | ): InputObjectTypeDefinitionNode | undefined { 46 | if (isEmpty(directiveMetadata)) { 47 | return; 48 | } 49 | return { 50 | kind: 'InputObjectTypeDefinition', 51 | name: { 52 | kind: 'Name', 53 | value: name, 54 | }, 55 | directives: directiveMetadata.map(this.createDirectiveNode), 56 | }; 57 | } 58 | 59 | createFieldNode( 60 | name: string, 61 | type: GraphQLOutputType, 62 | directiveMetadata?: DirectiveMetadata[], 63 | ): FieldDefinitionNode | undefined { 64 | if (isEmpty(directiveMetadata)) { 65 | return; 66 | } 67 | return { 68 | kind: 'FieldDefinition', 69 | type: { 70 | kind: 'NamedType', 71 | name: { 72 | kind: 'Name', 73 | value: type.toString(), 74 | }, 75 | }, 76 | name: { 77 | kind: 'Name', 78 | value: name, 79 | }, 80 | directives: directiveMetadata.map(this.createDirectiveNode), 81 | }; 82 | } 83 | 84 | createInputValueNode( 85 | name: string, 86 | type: GraphQLInputType, 87 | directiveMetadata?: DirectiveMetadata[], 88 | ): InputValueDefinitionNode | undefined { 89 | if (isEmpty(directiveMetadata)) { 90 | return; 91 | } 92 | return { 93 | kind: 'InputValueDefinition', 94 | type: { 95 | kind: 'NamedType', 96 | name: { 97 | kind: 'Name', 98 | value: type.toString(), 99 | }, 100 | }, 101 | name: { 102 | kind: 'Name', 103 | value: name, 104 | }, 105 | directives: directiveMetadata.map(this.createDirectiveNode), 106 | }; 107 | } 108 | 109 | private createDirectiveNode(directive: DirectiveMetadata): DirectiveNode { 110 | const parsed = parse(`type String ${directive.sdl}`); 111 | const definitions = parsed.definitions as ObjectTypeDefinitionNode[]; 112 | const directives = definitions 113 | .filter(item => item.directives && item.directives.length > 0) 114 | .map(({ directives }) => directives) 115 | .reduce((acc, item) => [...acc, ...item]); 116 | 117 | if (directives.length !== 1) { 118 | throw new DirectiveParsingError(directive.sdl); 119 | } 120 | return head(directives); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/enum-definition.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLEnumType } from 'graphql'; 3 | import { EnumMetadata } from '../metadata'; 4 | 5 | export interface EnumDefinition { 6 | enumRef: object; 7 | type: GraphQLEnumType; 8 | } 9 | 10 | @Injectable() 11 | export class EnumDefinitionFactory { 12 | public create(metadata: EnumMetadata): EnumDefinition { 13 | const enumValues = this.getEnumValues(metadata.ref); 14 | 15 | return { 16 | enumRef: metadata.ref, 17 | type: new GraphQLEnumType({ 18 | name: metadata.name, 19 | description: metadata.description, 20 | values: Object.keys(enumValues).reduce((prevValue, key) => { 21 | prevValue[key] = { 22 | value: enumValues[key], 23 | }; 24 | return prevValue; 25 | }, {}), 26 | }), 27 | }; 28 | } 29 | 30 | private getEnumValues(enumObject: Record) { 31 | const enumKeys = Object.keys(enumObject).filter(key => 32 | isNaN(parseInt(key, 10)), 33 | ); 34 | return enumKeys.reduce((prev, nextKey) => { 35 | prev[nextKey] = enumObject[nextKey]; 36 | return prev; 37 | }, {}); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/factories.ts: -------------------------------------------------------------------------------- 1 | import { ArgsFactory } from './args.factory'; 2 | import { AstDefinitionNodeFactory } from './ast-definition-node.factory'; 3 | import { EnumDefinitionFactory } from './enum-definition.factory'; 4 | import { InputTypeDefinitionFactory } from './input-type-definition.factory'; 5 | import { InputTypeFactory } from './input-type.factory'; 6 | import { InterfaceDefinitionFactory } from './interface-definition.factory'; 7 | import { MutationTypeFactory } from './mutation-type.factory'; 8 | import { ObjectTypeDefinitionFactory } from './object-type-definition.factory'; 9 | import { OrphanedTypesFactory } from './orphaned-types.factory'; 10 | import { OutputTypeFactory } from './output-type.factory'; 11 | import { QueryTypeFactory } from './query-type.factory'; 12 | import { ResolveTypeFactory } from './resolve-type.factory'; 13 | import { RootTypeFactory } from './root-type.factory'; 14 | import { SubscriptionTypeFactory } from './subscription-type.factory'; 15 | import { UnionDefinitionFactory } from './union-definition.factory'; 16 | 17 | export const schemaBuilderFactories = [ 18 | EnumDefinitionFactory, 19 | InputTypeDefinitionFactory, 20 | ArgsFactory, 21 | InputTypeFactory, 22 | InterfaceDefinitionFactory, 23 | MutationTypeFactory, 24 | ObjectTypeDefinitionFactory, 25 | OutputTypeFactory, 26 | OrphanedTypesFactory, 27 | OutputTypeFactory, 28 | QueryTypeFactory, 29 | ResolveTypeFactory, 30 | RootTypeFactory, 31 | SubscriptionTypeFactory, 32 | UnionDefinitionFactory, 33 | AstDefinitionNodeFactory, 34 | ]; 35 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/input-type-definition.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common'; 2 | import { isUndefined } from '@nestjs/common/utils/shared.utils'; 3 | import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; 4 | import { BuildSchemaOptions } from '../../interfaces'; 5 | import { getDefaultValue } from '../helpers/get-default-value.helper'; 6 | import { ClassMetadata } from '../metadata'; 7 | import { TypeFieldsAccessor } from '../services/type-fields.accessor'; 8 | import { TypeDefinitionsStorage } from '../storages/type-definitions.storage'; 9 | import { AstDefinitionNodeFactory } from './ast-definition-node.factory'; 10 | import { InputTypeFactory } from './input-type.factory'; 11 | 12 | export interface InputTypeDefinition { 13 | target: Function; 14 | type: GraphQLInputObjectType; 15 | isAbstract: boolean; 16 | } 17 | 18 | @Injectable() 19 | export class InputTypeDefinitionFactory { 20 | constructor( 21 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 22 | private readonly inputTypeFactory: InputTypeFactory, 23 | private readonly typeFieldsAccessor: TypeFieldsAccessor, 24 | private readonly astDefinitionNodeFactory: AstDefinitionNodeFactory, 25 | ) {} 26 | 27 | public create( 28 | metadata: ClassMetadata, 29 | options: BuildSchemaOptions, 30 | ): InputTypeDefinition { 31 | return { 32 | target: metadata.target, 33 | isAbstract: metadata.isAbstract || false, 34 | type: new GraphQLInputObjectType({ 35 | name: metadata.name, 36 | description: metadata.description, 37 | fields: this.generateFields(metadata, options), 38 | /** 39 | * AST node has to be manually created in order to define directives 40 | * (more on this topic here: https://github.com/graphql/graphql-js/issues/1343) 41 | */ 42 | astNode: this.astDefinitionNodeFactory.createInputObjectTypeNode( 43 | metadata.name, 44 | metadata.directives, 45 | ), 46 | extensions: metadata.extensions, 47 | }), 48 | }; 49 | } 50 | 51 | private generateFields( 52 | metadata: ClassMetadata, 53 | options: BuildSchemaOptions, 54 | ): () => GraphQLInputFieldConfigMap { 55 | const instance = new (metadata.target as Type)(); 56 | const prototype = Object.getPrototypeOf(metadata.target); 57 | 58 | const getParentType = () => { 59 | const parentTypeDefinition = this.typeDefinitionsStorage.getInputTypeByTarget( 60 | prototype, 61 | ); 62 | return parentTypeDefinition ? parentTypeDefinition.type : undefined; 63 | }; 64 | return () => { 65 | let fields: GraphQLInputFieldConfigMap = {}; 66 | metadata.properties.forEach((property) => { 67 | property.options.defaultValue = getDefaultValue( 68 | instance, 69 | property.options, 70 | property.name, 71 | metadata.name, 72 | ); 73 | 74 | const type = this.inputTypeFactory.create( 75 | property.name, 76 | property.typeFn(), 77 | options, 78 | property.options, 79 | ); 80 | fields[property.schemaName] = { 81 | description: property.description, 82 | type, 83 | defaultValue: property.options.defaultValue, 84 | /** 85 | * AST node has to be manually created in order to define directives 86 | * (more on this topic here: https://github.com/graphql/graphql-js/issues/1343) 87 | */ 88 | astNode: this.astDefinitionNodeFactory.createInputValueNode( 89 | property.name, 90 | type, 91 | property.directives, 92 | ), 93 | extensions: metadata.extensions, 94 | }; 95 | }); 96 | 97 | if (!isUndefined(prototype.prototype)) { 98 | const parentClassRef = getParentType(); 99 | if (parentClassRef) { 100 | const parentFields = this.typeFieldsAccessor.extractFromInputType( 101 | parentClassRef, 102 | ); 103 | fields = { 104 | ...parentFields, 105 | ...fields, 106 | }; 107 | } 108 | } 109 | return fields; 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/input-type.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLInputType } from 'graphql'; 3 | import { BuildSchemaOptions, GqlTypeReference } from '../../interfaces'; 4 | import { TypeOptions } from '../../interfaces/type-options.interface'; 5 | import { CannotDetermineInputTypeError } from '../errors/cannot-determine-input-type.error'; 6 | import { TypeMapperSevice } from '../services/type-mapper.service'; 7 | import { TypeDefinitionsStorage } from '../storages/type-definitions.storage'; 8 | 9 | @Injectable() 10 | export class InputTypeFactory { 11 | constructor( 12 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 13 | private readonly typeMapperService: TypeMapperSevice, 14 | ) {} 15 | 16 | public create( 17 | hostType: string, 18 | typeRef: GqlTypeReference, 19 | buildOptions: BuildSchemaOptions, 20 | typeOptions: TypeOptions = {}, 21 | ): GraphQLInputType { 22 | let inputType: 23 | | GraphQLInputType 24 | | undefined = this.typeMapperService.mapToScalarType( 25 | typeRef, 26 | buildOptions.scalarsMap, 27 | buildOptions.dateScalarMode, 28 | ); 29 | if (!inputType) { 30 | inputType = this.typeDefinitionsStorage.getInputTypeAndExtract( 31 | typeRef as any, 32 | ); 33 | if (!inputType) { 34 | throw new CannotDetermineInputTypeError(hostType); 35 | } 36 | } 37 | return this.typeMapperService.mapToGqlType( 38 | hostType, 39 | inputType, 40 | typeOptions, 41 | true, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/interface-definition.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isUndefined } from '@nestjs/common/utils/shared.utils'; 3 | import { GraphQLFieldConfigMap, GraphQLInterfaceType } from 'graphql'; 4 | import { BuildSchemaOptions } from '../../interfaces'; 5 | import { ReturnTypeCannotBeResolvedError } from '../errors/return-type-cannot-be-resolved.error'; 6 | import { InterfaceMetadata } from '../metadata/interface.metadata'; 7 | import { TypeFieldsAccessor } from '../services/type-fields.accessor'; 8 | import { TypeDefinitionsStorage } from '../storages/type-definitions.storage'; 9 | import { TypeMetadataStorage } from '../storages/type-metadata.storage'; 10 | import { OutputTypeFactory } from './output-type.factory'; 11 | import { ResolveTypeFactory } from './resolve-type.factory'; 12 | 13 | export interface InterfaceTypeDefinition { 14 | target: Function; 15 | type: GraphQLInterfaceType; 16 | isAbstract: boolean; 17 | } 18 | 19 | @Injectable() 20 | export class InterfaceDefinitionFactory { 21 | constructor( 22 | private readonly resolveTypeFactory: ResolveTypeFactory, 23 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 24 | private readonly outputTypeFactory: OutputTypeFactory, 25 | private readonly typeFieldsAccessor: TypeFieldsAccessor, 26 | ) {} 27 | 28 | public create( 29 | metadata: InterfaceMetadata, 30 | options: BuildSchemaOptions, 31 | ): InterfaceTypeDefinition { 32 | const resolveType = this.createResolveTypeFn(metadata); 33 | return { 34 | target: metadata.target, 35 | isAbstract: metadata.isAbstract || false, 36 | type: new GraphQLInterfaceType({ 37 | name: metadata.name, 38 | description: metadata.description, 39 | fields: this.generateFields(metadata, options), 40 | resolveType, 41 | }), 42 | }; 43 | } 44 | 45 | private createResolveTypeFn(metadata: InterfaceMetadata) { 46 | const objectTypesMetadata = TypeMetadataStorage.getObjectTypesMetadata(); 47 | const implementedTypes = objectTypesMetadata 48 | .filter( 49 | objectType => 50 | objectType.interfaces && 51 | objectType.interfaces.includes(metadata.target), 52 | ) 53 | .map(objectType => objectType.target); 54 | 55 | return metadata.resolveType 56 | ? this.resolveTypeFactory.getResolveTypeFunction(metadata.resolveType) 57 | : (instance: any) => { 58 | const target = implementedTypes.find( 59 | Type => instance instanceof Type, 60 | ); 61 | if (!target) { 62 | throw new ReturnTypeCannotBeResolvedError(metadata.name); 63 | } 64 | return this.typeDefinitionsStorage.getObjectTypeByTarget(target).type; 65 | }; 66 | } 67 | 68 | private generateFields( 69 | metadata: InterfaceMetadata, 70 | options: BuildSchemaOptions, 71 | ): () => GraphQLFieldConfigMap { 72 | const prototype = Object.getPrototypeOf(metadata.target); 73 | const getParentType = () => { 74 | const parentTypeDefinition = this.typeDefinitionsStorage.getInterfaceByTarget( 75 | prototype, 76 | ); 77 | return parentTypeDefinition ? parentTypeDefinition.type : undefined; 78 | }; 79 | 80 | return () => { 81 | let fields: GraphQLFieldConfigMap = {}; 82 | metadata.properties.forEach(property => { 83 | fields[property.schemaName] = { 84 | description: property.description, 85 | type: this.outputTypeFactory.create( 86 | property.name, 87 | property.typeFn(), 88 | options, 89 | property.options, 90 | ), 91 | }; 92 | }); 93 | 94 | if (!isUndefined(prototype.prototype)) { 95 | const parentClassRef = getParentType(); 96 | if (parentClassRef) { 97 | const parentFields = this.typeFieldsAccessor.extractFromInterfaceOrObjectType( 98 | parentClassRef, 99 | ); 100 | fields = { 101 | ...parentFields, 102 | ...fields, 103 | }; 104 | } 105 | } 106 | return fields; 107 | }; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/mutation-type.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | import { BuildSchemaOptions } from '../../interfaces'; 4 | import { TypeMetadataStorage } from '../storages/type-metadata.storage'; 5 | import { RootTypeFactory } from './root-type.factory'; 6 | 7 | @Injectable() 8 | export class MutationTypeFactory { 9 | constructor(private readonly rootTypeFactory: RootTypeFactory) {} 10 | 11 | public create( 12 | typeRefs: Function[], 13 | options: BuildSchemaOptions, 14 | ): GraphQLObjectType { 15 | const objectTypeName = 'Mutation'; 16 | const mutationsMetadata = TypeMetadataStorage.getMutationsMetadata(); 17 | 18 | return this.rootTypeFactory.create( 19 | typeRefs, 20 | mutationsMetadata, 21 | objectTypeName, 22 | options, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/orphaned-types.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLNamedType } from 'graphql'; 3 | import { OrphanedReferenceRegistry } from '../services/orphaned-reference.registry'; 4 | import { TypeDefinitionsStorage } from '../storages/type-definitions.storage'; 5 | import { ObjectTypeDefinition } from './object-type-definition.factory'; 6 | 7 | @Injectable() 8 | export class OrphanedTypesFactory { 9 | constructor( 10 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 11 | private readonly orphanedReferenceRegistry: OrphanedReferenceRegistry, 12 | ) {} 13 | 14 | public create(types: Function[]): GraphQLNamedType[] { 15 | types = (types || []).concat(this.orphanedReferenceRegistry.getAll()); 16 | 17 | if (types.length === 0) { 18 | return []; 19 | } 20 | const interfaceTypeDefs = this.typeDefinitionsStorage.getAllInterfaceDefinitions(); 21 | const objectTypeDefs = this.typeDefinitionsStorage.getAllObjectTypeDefinitions(); 22 | const inputTypeDefs = this.typeDefinitionsStorage.getAllInputTypeDefinitions(); 23 | const classTypeDefs = [ 24 | ...interfaceTypeDefs, 25 | ...objectTypeDefs, 26 | ...inputTypeDefs, 27 | ]; 28 | return classTypeDefs 29 | .filter(item => !item.isAbstract) 30 | .filter(item => { 31 | const implementsReferencedInterface = 32 | (item as ObjectTypeDefinition).interfaces && 33 | (item as ObjectTypeDefinition).interfaces.some(i => 34 | types.includes(i), 35 | ); 36 | return types.includes(item.target) || implementsReferencedInterface; 37 | }) 38 | .map(({ type }) => type); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/output-type.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLOutputType } from 'graphql'; 3 | import { BuildSchemaOptions, GqlTypeReference } from '../../interfaces'; 4 | import { TypeOptions } from '../../interfaces/type-options.interface'; 5 | import { CannotDetermineOutputTypeError } from '../errors/cannot-determine-output-type.error'; 6 | import { TypeMapperSevice } from '../services/type-mapper.service'; 7 | import { TypeDefinitionsStorage } from '../storages/type-definitions.storage'; 8 | 9 | @Injectable() 10 | export class OutputTypeFactory { 11 | constructor( 12 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 13 | private readonly typeMapperService: TypeMapperSevice, 14 | ) {} 15 | 16 | public create( 17 | hostType: string, 18 | typeRef: GqlTypeReference, 19 | buildOptions: BuildSchemaOptions, 20 | typeOptions: TypeOptions = {}, 21 | ): GraphQLOutputType { 22 | let gqlType: 23 | | GraphQLOutputType 24 | | undefined = this.typeMapperService.mapToScalarType( 25 | typeRef, 26 | buildOptions.scalarsMap, 27 | buildOptions.dateScalarMode, 28 | ); 29 | if (!gqlType) { 30 | gqlType = this.typeDefinitionsStorage.getOutputTypeAndExtract(typeRef); 31 | if (!gqlType) { 32 | throw new CannotDetermineOutputTypeError(hostType); 33 | } 34 | } 35 | return this.typeMapperService.mapToGqlType( 36 | hostType, 37 | gqlType, 38 | typeOptions, 39 | false, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/query-type.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | import { BuildSchemaOptions } from '../../interfaces'; 4 | import { TypeMetadataStorage } from '../storages/type-metadata.storage'; 5 | import { RootTypeFactory } from './root-type.factory'; 6 | 7 | @Injectable() 8 | export class QueryTypeFactory { 9 | constructor(private readonly rootTypeFactory: RootTypeFactory) {} 10 | 11 | public create( 12 | typeRefs: Function[], 13 | options: BuildSchemaOptions, 14 | ): GraphQLObjectType { 15 | const objectTypeName = 'Query'; 16 | const queriesMetadata = TypeMetadataStorage.getQueriesMetadata(); 17 | 18 | return this.rootTypeFactory.create( 19 | typeRefs, 20 | queriesMetadata, 21 | objectTypeName, 22 | options, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/resolve-type.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isString } from '@nestjs/common/utils/shared.utils'; 3 | import { GraphQLTypeResolver } from 'graphql'; 4 | import { ResolveTypeFn } from '../../interfaces'; 5 | import { TypeDefinitionsStorage } from '../storages/type-definitions.storage'; 6 | 7 | @Injectable() 8 | export class ResolveTypeFactory { 9 | constructor( 10 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 11 | ) {} 12 | 13 | public getResolveTypeFunction( 14 | resolveType: ResolveTypeFn, 15 | ): GraphQLTypeResolver { 16 | return async (...args) => { 17 | const resolvedType = await resolveType(...args); 18 | if (isString(resolvedType)) { 19 | return resolvedType; 20 | } 21 | const typeDef = this.typeDefinitionsStorage.getObjectTypeByTarget( 22 | resolvedType, 23 | ); 24 | return typeDef?.type; 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/root-type.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; 3 | import { BuildSchemaOptions } from '../../interfaces'; 4 | import { ResolverTypeMetadata } from '../metadata/resolver.metadata'; 5 | import { OrphanedReferenceRegistry } from '../services/orphaned-reference.registry'; 6 | import { ArgsFactory } from './args.factory'; 7 | import { AstDefinitionNodeFactory } from './ast-definition-node.factory'; 8 | import { OutputTypeFactory } from './output-type.factory'; 9 | 10 | export type FieldsFactory = ( 11 | handlers: ResolverTypeMetadata[], 12 | options: BuildSchemaOptions, 13 | ) => GraphQLFieldConfigMap; 14 | 15 | @Injectable() 16 | export class RootTypeFactory { 17 | constructor( 18 | private readonly outputTypeFactory: OutputTypeFactory, 19 | private readonly argsFactory: ArgsFactory, 20 | private readonly astDefinitionNodeFactory: AstDefinitionNodeFactory, 21 | private readonly orphanedReferenceRegistry: OrphanedReferenceRegistry, 22 | ) {} 23 | 24 | public create( 25 | typeRefs: Function[], 26 | resolversMetadata: ResolverTypeMetadata[], 27 | objectTypeName: 'Subscription' | 'Mutation' | 'Query', 28 | options: BuildSchemaOptions, 29 | fieldsFactory: FieldsFactory = (handlers) => 30 | this.generateFields(handlers, options), 31 | ): GraphQLObjectType { 32 | const handlers = typeRefs 33 | ? resolversMetadata.filter((query) => typeRefs.includes(query.target)) 34 | : resolversMetadata; 35 | 36 | if (handlers.length === 0) { 37 | return; 38 | } 39 | return new GraphQLObjectType({ 40 | name: objectTypeName, 41 | fields: fieldsFactory(handlers, options), 42 | }); 43 | } 44 | 45 | generateFields( 46 | handlers: ResolverTypeMetadata[], 47 | options: BuildSchemaOptions, 48 | ): GraphQLFieldConfigMap { 49 | const fieldConfigMap: GraphQLFieldConfigMap = {}; 50 | 51 | handlers 52 | .filter( 53 | (handler) => 54 | !(handler.classMetadata && handler.classMetadata.isAbstract), 55 | ) 56 | .forEach((handler) => { 57 | this.orphanedReferenceRegistry.addToRegistryIfOrphaned( 58 | handler.typeFn(), 59 | ); 60 | 61 | const type = this.outputTypeFactory.create( 62 | handler.methodName, 63 | handler.typeFn(), 64 | options, 65 | handler.returnTypeOptions, 66 | ); 67 | 68 | const key = handler.schemaName; 69 | fieldConfigMap[key] = { 70 | type, 71 | args: this.argsFactory.create(handler.methodArgs, options), 72 | resolve: undefined, 73 | description: handler.description, 74 | deprecationReason: handler.deprecationReason, 75 | /** 76 | * AST node has to be manually created in order to define directives 77 | * (more on this topic here: https://github.com/graphql/graphql-js/issues/1343) 78 | */ 79 | astNode: this.astDefinitionNodeFactory.createFieldNode( 80 | key, 81 | type, 82 | handler.directives, 83 | ), 84 | extensions: { 85 | complexity: handler.complexity, 86 | ...handler.extensions, 87 | }, 88 | }; 89 | }); 90 | return fieldConfigMap; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/subscription-type.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | import { BuildSchemaOptions } from '../../interfaces'; 4 | import { TypeMetadataStorage } from '../storages/type-metadata.storage'; 5 | import { RootTypeFactory } from './root-type.factory'; 6 | 7 | @Injectable() 8 | export class SubscriptionTypeFactory { 9 | constructor(private readonly rootTypeFactory: RootTypeFactory) {} 10 | 11 | public create( 12 | typeRefs: Function[], 13 | options: BuildSchemaOptions, 14 | ): GraphQLObjectType { 15 | const objectTypeName = 'Subscription'; 16 | const subscriptionsMetadata = TypeMetadataStorage.getSubscriptionsMetadata(); 17 | 18 | return this.rootTypeFactory.create( 19 | typeRefs, 20 | subscriptionsMetadata, 21 | objectTypeName, 22 | options, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/schema-builder/factories/union-definition.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common'; 2 | import { GraphQLUnionType } from 'graphql'; 3 | import { ReturnTypeCannotBeResolvedError } from '../errors/return-type-cannot-be-resolved.error'; 4 | import { UnionMetadata } from '../metadata'; 5 | import { TypeDefinitionsStorage } from '../storages/type-definitions.storage'; 6 | import { ResolveTypeFactory } from './resolve-type.factory'; 7 | 8 | export interface UnionDefinition { 9 | id: symbol; 10 | type: GraphQLUnionType; 11 | } 12 | 13 | @Injectable() 14 | export class UnionDefinitionFactory { 15 | constructor( 16 | private readonly resolveTypeFactory: ResolveTypeFactory, 17 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 18 | ) {} 19 | 20 | public create(metadata: UnionMetadata): UnionDefinition { 21 | const getObjectType = (item: Type) => 22 | this.typeDefinitionsStorage.getObjectTypeByTarget(item).type; 23 | const types = () => metadata.typesFn().map(item => getObjectType(item)); 24 | 25 | return { 26 | id: metadata.id, 27 | type: new GraphQLUnionType({ 28 | name: metadata.name, 29 | description: metadata.description, 30 | types, 31 | resolveType: this.createResolveTypeFn(metadata), 32 | }), 33 | }; 34 | } 35 | 36 | private createResolveTypeFn(metadata: UnionMetadata) { 37 | return metadata.resolveType 38 | ? this.resolveTypeFactory.getResolveTypeFunction(metadata.resolveType) 39 | : (instance: any) => { 40 | const target = metadata 41 | .typesFn() 42 | .find(Type => instance instanceof Type); 43 | 44 | if (!target) { 45 | throw new ReturnTypeCannotBeResolvedError(metadata.name); 46 | } 47 | const objectDef = this.typeDefinitionsStorage.getObjectTypeByTarget( 48 | target, 49 | ); 50 | return objectDef.type; 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/schema-builder/graphql-schema.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, Type } from '@nestjs/common'; 2 | import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils'; 3 | import { getIntrospectionQuery, graphql, GraphQLSchema } from 'graphql'; 4 | import { SCALAR_NAME_METADATA, SCALAR_TYPE_METADATA } from '../fgql.constants'; 5 | import { BuildSchemaOptions, ScalarsTypeMap } from '../interfaces'; 6 | import { createScalarType } from '../utils/scalar-types.utils'; 7 | import { SchemaGenerationError } from './errors/schema-generation.error'; 8 | import { MutationTypeFactory } from './factories/mutation-type.factory'; 9 | import { OrphanedTypesFactory } from './factories/orphaned-types.factory'; 10 | import { QueryTypeFactory } from './factories/query-type.factory'; 11 | import { SubscriptionTypeFactory } from './factories/subscription-type.factory'; 12 | import { LazyMetadataStorage } from './storages/lazy-metadata.storage'; 13 | import { TypeMetadataStorage } from './storages/type-metadata.storage'; 14 | import { TypeDefinitionsGenerator } from './type-definitions.generator'; 15 | 16 | @Injectable() 17 | export class GraphQLSchemaFactory { 18 | private readonly logger = new Logger(GraphQLSchemaFactory.name); 19 | 20 | constructor( 21 | private readonly queryTypeFactory: QueryTypeFactory, 22 | private readonly mutationTypeFactory: MutationTypeFactory, 23 | private readonly subscriptionTypeFactory: SubscriptionTypeFactory, 24 | private readonly orphanedTypesFactory: OrphanedTypesFactory, 25 | private readonly typeDefinitionsGenerator: TypeDefinitionsGenerator, 26 | ) {} 27 | 28 | async create(resolvers: Function[]): Promise; 29 | async create( 30 | resolvers: Function[], 31 | scalarClasses: Function[], 32 | ): Promise; 33 | async create( 34 | resolvers: Function[], 35 | options: BuildSchemaOptions, 36 | ): Promise; 37 | async create( 38 | resolvers: Function[], 39 | scalarClasses: Function[], 40 | options: BuildSchemaOptions, 41 | ): Promise; 42 | async create( 43 | resolvers: Function[], 44 | scalarsOrOptions: Function[] | BuildSchemaOptions = [], 45 | options: BuildSchemaOptions = {}, 46 | ): Promise { 47 | if (Array.isArray(scalarsOrOptions)) { 48 | this.assignScalarObjects(scalarsOrOptions, options); 49 | } else { 50 | options = scalarsOrOptions; 51 | } 52 | 53 | TypeMetadataStorage.clear(); 54 | LazyMetadataStorage.load(resolvers); 55 | TypeMetadataStorage.compile(options.orphanedTypes); 56 | 57 | this.typeDefinitionsGenerator.generate(options); 58 | 59 | const schema = new GraphQLSchema({ 60 | mutation: this.mutationTypeFactory.create(resolvers, options), 61 | query: this.queryTypeFactory.create(resolvers, options), 62 | subscription: this.subscriptionTypeFactory.create(resolvers, options), 63 | types: this.orphanedTypesFactory.create(options.orphanedTypes), 64 | directives: options.directives, 65 | }); 66 | 67 | if (!options.skipCheck) { 68 | const introspectionQuery = getIntrospectionQuery(); 69 | const { errors } = await graphql(schema, introspectionQuery); 70 | if (errors) { 71 | throw new SchemaGenerationError(errors); 72 | } 73 | } 74 | 75 | return schema; 76 | } 77 | 78 | private assignScalarObjects( 79 | scalars: Function[], 80 | options: BuildSchemaOptions, 81 | ) { 82 | if (isEmpty(scalars)) { 83 | return; 84 | } 85 | const scalarsMap = options.scalarsMap || []; 86 | scalars 87 | .filter((classRef) => classRef) 88 | .forEach((classRef) => 89 | this.addScalarTypeByClassRef(classRef as Type, scalarsMap), 90 | ); 91 | 92 | options.scalarsMap = scalarsMap; 93 | } 94 | 95 | private addScalarTypeByClassRef( 96 | classRef: Type, 97 | scalarsMap: ScalarsTypeMap[], 98 | ) { 99 | try { 100 | const scalarNameMetadata = Reflect.getMetadata( 101 | SCALAR_NAME_METADATA, 102 | classRef, 103 | ); 104 | const scalarTypeMetadata = Reflect.getMetadata( 105 | SCALAR_TYPE_METADATA, 106 | classRef, 107 | ); 108 | if (!scalarNameMetadata) { 109 | return; 110 | } 111 | const instance = new (classRef as Type)(); 112 | const type = 113 | (isFunction(scalarTypeMetadata) && scalarTypeMetadata()) || classRef; 114 | 115 | scalarsMap.push({ 116 | type, 117 | scalar: createScalarType(scalarNameMetadata, instance), 118 | }); 119 | } catch { 120 | this.logger.error( 121 | `Cannot generate a GraphQLScalarType for "${classRef.name}" scalar. Make sure to put any initialization logic in the lifecycle hooks instead of a constructor.`, 122 | ); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/schema-builder/helpers/file-system.helper.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as fs from 'fs'; 3 | import { join, parse, resolve, sep } from 'path'; 4 | 5 | @Injectable() 6 | export class FileSystemHelper { 7 | async writeFile(path: string, content: string) { 8 | try { 9 | await fs.promises.writeFile(path, content); 10 | } catch (err) { 11 | if (err.code !== 'ENOENT') { 12 | throw err; 13 | } 14 | await this.mkdirRecursive(path); 15 | await fs.promises.writeFile(path, content); 16 | } 17 | } 18 | 19 | async mkdirRecursive(path: string) { 20 | for (const dir of this.getDirs(path)) { 21 | try { 22 | await fs.promises.mkdir(dir); 23 | } catch (err) { 24 | if (err.code !== 'EEXIST') { 25 | throw err; 26 | } 27 | } 28 | } 29 | } 30 | 31 | getDirs(path: string): string[] { 32 | const parsedPath = parse(resolve(path)); 33 | const chunks = parsedPath.dir.split(sep); 34 | if (parsedPath.root === '/') { 35 | chunks[0] = `/${chunks[0]}`; 36 | } 37 | const dirs = new Array(); 38 | chunks.reduce((previous: string, next: string) => { 39 | const directory = join(previous, next); 40 | dirs.push(directory); 41 | return join(directory); 42 | }); 43 | return dirs; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/schema-builder/helpers/get-default-value.helper.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from '@nestjs/common/utils/shared.utils'; 2 | import { TypeOptions } from '../../interfaces/type-options.interface'; 3 | import { DefaultValuesConflictError } from '../errors/default-values-conflict.error'; 4 | 5 | export function getDefaultValue( 6 | instance: object, 7 | options: TypeOptions, 8 | key: string, 9 | typeName: string, 10 | ): T | undefined { 11 | const initializerValue = instance[key]; 12 | if (isUndefined(options.defaultValue)) { 13 | return initializerValue; 14 | } 15 | if ( 16 | options.defaultValue !== initializerValue && 17 | !isUndefined(initializerValue) 18 | ) { 19 | throw new DefaultValuesConflictError( 20 | typeName, 21 | key, 22 | options.defaultValue, 23 | initializerValue, 24 | ); 25 | } 26 | return options.defaultValue; 27 | } 28 | -------------------------------------------------------------------------------- /lib/schema-builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storages'; 2 | export * from './graphql-schema.factory'; 3 | export * from './schema-builder.module'; 4 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/class.metadata.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveMetadata } from './directive.metadata'; 2 | import { PropertyMetadata } from './property.metadata'; 3 | 4 | export interface ClassMetadata { 5 | target: Function; 6 | name: string; 7 | description?: string; 8 | isAbstract?: boolean; 9 | directives?: DirectiveMetadata[]; 10 | extensions?: Record; 11 | properties?: PropertyMetadata[]; 12 | } 13 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/directive.metadata.ts: -------------------------------------------------------------------------------- 1 | export interface DirectiveMetadata { 2 | sdl: string; 3 | target: Function; 4 | } 5 | 6 | export type ClassDirectiveMetadata = DirectiveMetadata; 7 | 8 | export interface PropertyDirectiveMetadata extends DirectiveMetadata { 9 | fieldName: string; 10 | } 11 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/enum.metadata.ts: -------------------------------------------------------------------------------- 1 | export interface EnumMetadata { 2 | ref: object; 3 | name: string; 4 | description?: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/extensions.metadata.ts: -------------------------------------------------------------------------------- 1 | export interface ExtensionsMetadata { 2 | target: Function; 3 | value: Record; 4 | } 5 | 6 | export type ClassExtensionsMetadata = ExtensionsMetadata; 7 | 8 | export interface PropertyExtensionsMetadata extends ExtensionsMetadata { 9 | fieldName: string; 10 | } 11 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './class.metadata'; 2 | export * from './directive.metadata'; 3 | export * from './enum.metadata'; 4 | export * from './extensions.metadata'; 5 | export * from './param.metadata'; 6 | export * from './property.metadata'; 7 | export * from './resolver.metadata'; 8 | export * from './union.metadata'; 9 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/interface.metadata.ts: -------------------------------------------------------------------------------- 1 | import { ResolveTypeFn } from '../../interfaces'; 2 | import { ClassMetadata } from './class.metadata'; 3 | 4 | export interface InterfaceMetadata extends ClassMetadata { 5 | resolveType?: ResolveTypeFn; 6 | } 7 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/object-type.metadata.ts: -------------------------------------------------------------------------------- 1 | import { ClassMetadata } from './class.metadata'; 2 | 3 | export interface ObjectTypeMetadata extends ClassMetadata { 4 | interfaces?: Function[]; 5 | } 6 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/param.metadata.ts: -------------------------------------------------------------------------------- 1 | import { GqlTypeReference } from '../../interfaces'; 2 | import { TypeOptions } from '../../interfaces/type-options.interface'; 3 | 4 | export interface BaseArgMetadata { 5 | target: Function; 6 | methodName: string; 7 | typeFn: (type?: any) => GqlTypeReference; 8 | options: TypeOptions; 9 | index: number; 10 | } 11 | 12 | export interface ArgParamMetadata extends BaseArgMetadata { 13 | kind: 'arg'; 14 | name: string; 15 | description?: string; 16 | } 17 | 18 | export interface ArgsParamMetadata extends BaseArgMetadata { 19 | kind: 'args'; 20 | } 21 | 22 | export type MethodArgsMetadata = ArgParamMetadata | ArgsParamMetadata; 23 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/property.metadata.ts: -------------------------------------------------------------------------------- 1 | import { Complexity, GqlTypeReference } from '../../interfaces'; 2 | import { TypeOptions } from '../../interfaces/type-options.interface'; 3 | import { DirectiveMetadata } from './directive.metadata'; 4 | import { MethodArgsMetadata } from './param.metadata'; 5 | 6 | export interface PropertyMetadata { 7 | schemaName: string; 8 | name: string; 9 | typeFn: () => GqlTypeReference; 10 | target: Function; 11 | options: TypeOptions; 12 | description?: string; 13 | deprecationReason?: string; 14 | methodArgs?: MethodArgsMetadata[]; 15 | directives?: DirectiveMetadata[]; 16 | extensions?: Record; 17 | complexity?: Complexity; 18 | } 19 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/resolver.metadata.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { Complexity, GqlTypeReference } from '../../interfaces'; 3 | import { TypeOptions } from '../../interfaces/type-options.interface'; 4 | import { DirectiveMetadata } from './directive.metadata'; 5 | import { MethodArgsMetadata } from './param.metadata'; 6 | 7 | export interface ResolverClassMetadata { 8 | target: Function; 9 | typeFn: (of?: void) => Type; 10 | isAbstract?: boolean; 11 | parent?: ResolverClassMetadata; 12 | } 13 | 14 | export interface BaseResolverMetadata { 15 | target: Function; 16 | methodName: string; 17 | schemaName: string; 18 | description?: string; 19 | deprecationReason?: string; 20 | methodArgs?: MethodArgsMetadata[]; 21 | classMetadata?: ResolverClassMetadata; 22 | directives?: DirectiveMetadata[]; 23 | extensions?: Record; 24 | complexity?: Complexity; 25 | } 26 | 27 | export interface ResolverTypeMetadata extends BaseResolverMetadata { 28 | typeFn: (type?: void) => GqlTypeReference; 29 | returnTypeOptions: TypeOptions; 30 | } 31 | 32 | export interface FieldResolverMetadata extends BaseResolverMetadata { 33 | kind: 'internal' | 'external'; 34 | typeOptions?: TypeOptions; 35 | typeFn?: (type?: void) => GqlTypeReference; 36 | objectTypeFn?: (of?: void) => Type; 37 | } 38 | -------------------------------------------------------------------------------- /lib/schema-builder/metadata/union.metadata.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ResolveTypeFn } from '../../interfaces'; 3 | 4 | export interface UnionMetadata[] = Type[]> { 5 | name: string; 6 | typesFn: () => T; 7 | id?: symbol; 8 | description?: string; 9 | resolveType?: ResolveTypeFn; 10 | } 11 | -------------------------------------------------------------------------------- /lib/schema-builder/schema-builder.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek. 3 | * In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module. 4 | * Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility. 5 | * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". 6 | */ 7 | 8 | import { Module } from '@nestjs/common'; 9 | import { schemaBuilderFactories } from './factories/factories'; 10 | import { GraphQLSchemaFactory } from './graphql-schema.factory'; 11 | import { FileSystemHelper } from './helpers/file-system.helper'; 12 | import { OrphanedReferenceRegistry } from './services/orphaned-reference.registry'; 13 | import { TypeFieldsAccessor } from './services/type-fields.accessor'; 14 | import { TypeMapperSevice } from './services/type-mapper.service'; 15 | import { TypeDefinitionsStorage } from './storages/type-definitions.storage'; 16 | import { TypeDefinitionsGenerator } from './type-definitions.generator'; 17 | 18 | @Module({ 19 | providers: [ 20 | ...schemaBuilderFactories, 21 | GraphQLSchemaFactory, 22 | TypeDefinitionsGenerator, 23 | FileSystemHelper, 24 | TypeDefinitionsStorage, 25 | TypeMapperSevice, 26 | TypeFieldsAccessor, 27 | OrphanedReferenceRegistry, 28 | ], 29 | exports: [GraphQLSchemaFactory, FileSystemHelper], 30 | }) 31 | export class GraphQLSchemaBuilderModule {} 32 | -------------------------------------------------------------------------------- /lib/schema-builder/services/orphaned-reference.registry.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isFunction } from '@nestjs/common/utils/shared.utils'; 3 | import { GqlTypeReference } from '../../interfaces'; 4 | 5 | const BANNED_TYPES: Function[] = [String, Date, Number, Boolean]; 6 | 7 | @Injectable() 8 | export class OrphanedReferenceRegistry { 9 | private readonly registry = new Set(); 10 | 11 | addToRegistryIfOrphaned(typeRef: GqlTypeReference) { 12 | if (!isFunction(typeRef)) { 13 | return; 14 | } 15 | if (BANNED_TYPES.includes(typeRef as Function)) { 16 | return; 17 | } 18 | this.registry.add(typeRef as Function); 19 | } 20 | 21 | getAll(): Function[] { 22 | return [...this.registry.values()]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/schema-builder/services/type-fields.accessor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | GraphQLFieldConfigArgumentMap, 4 | GraphQLFieldConfigMap, 5 | GraphQLInputFieldConfigMap, 6 | GraphQLInputObjectType, 7 | GraphQLInterfaceType, 8 | GraphQLObjectType, 9 | } from 'graphql'; 10 | import { omit } from 'lodash'; 11 | 12 | @Injectable() 13 | export class TypeFieldsAccessor { 14 | extractFromInputType( 15 | gqlType: GraphQLInputObjectType, 16 | ): GraphQLInputFieldConfigMap { 17 | const fieldsMap = gqlType.getFields(); 18 | const fieldsConfig: GraphQLInputFieldConfigMap = {}; 19 | 20 | for (const key in fieldsMap) { 21 | const targetField = fieldsMap[key]; 22 | fieldsConfig[key] = { 23 | type: targetField.type, 24 | description: targetField.description, 25 | defaultValue: targetField.defaultValue, 26 | astNode: targetField.astNode, 27 | extensions: targetField.extensions, 28 | }; 29 | } 30 | return fieldsConfig; 31 | } 32 | 33 | extractFromInterfaceOrObjectType( 34 | type: GraphQLInterfaceType | GraphQLObjectType, 35 | ): GraphQLFieldConfigMap { 36 | const fieldsMap = type.getFields(); 37 | const fieldsConfig: GraphQLFieldConfigMap = {}; 38 | 39 | for (const key in fieldsMap) { 40 | const targetField = fieldsMap[key]; 41 | const args: GraphQLFieldConfigArgumentMap = {}; 42 | targetField.args.forEach((item) => { 43 | args[item.name] = omit(item, 'name'); 44 | }); 45 | 46 | fieldsConfig[key] = { 47 | type: targetField.type, 48 | description: targetField.description, 49 | deprecationReason: targetField.deprecationReason, 50 | extensions: targetField.extensions, 51 | astNode: targetField.astNode, 52 | resolve: targetField.resolve, 53 | args, 54 | }; 55 | } 56 | 57 | return fieldsConfig; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/schema-builder/services/type-mapper.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common'; 2 | import { isUndefined } from '@nestjs/common/utils/shared.utils'; 3 | import { 4 | GraphQLBoolean, 5 | GraphQLFloat, 6 | GraphQLList, 7 | GraphQLNonNull, 8 | GraphQLScalarType, 9 | GraphQLString, 10 | GraphQLType, 11 | } from 'graphql'; 12 | import { 13 | DateScalarMode, 14 | GqlTypeReference, 15 | ScalarsTypeMap, 16 | } from '../../interfaces'; 17 | import { TypeOptions } from '../../interfaces/type-options.interface'; 18 | import { GraphQLISODateTime, GraphQLTimestamp } from '../../scalars'; 19 | import { DefaultNullableConflictError } from '../errors/default-nullable-conflict.error'; 20 | import { InvalidNullableOptionError } from '../errors/invalid-nullable-option.error'; 21 | 22 | @Injectable() 23 | export class TypeMapperSevice { 24 | mapToScalarType>( 25 | typeRef: T, 26 | scalarsMap: ScalarsTypeMap[] = [], 27 | dateScalarMode: DateScalarMode = 'isoDate', 28 | ): GraphQLScalarType | undefined { 29 | if (typeRef instanceof GraphQLScalarType) { 30 | return typeRef; 31 | } 32 | const scalarHost = scalarsMap.find((item) => item.type === typeRef); 33 | if (scalarHost) { 34 | return scalarHost.scalar; 35 | } 36 | const dateScalar = 37 | dateScalarMode === 'timestamp' ? GraphQLTimestamp : GraphQLISODateTime; 38 | const typeScalarMapping = new Map([ 39 | [String, GraphQLString], 40 | [Number, GraphQLFloat], 41 | [Boolean, GraphQLBoolean], 42 | [Date, dateScalar], 43 | ]); 44 | return typeScalarMapping.get(typeRef as Function); 45 | } 46 | 47 | mapToGqlType( 48 | hostType: string, 49 | typeRef: T, 50 | options: TypeOptions, 51 | isInputTypeCtx: boolean, 52 | ): T { 53 | this.validateTypeOptions(hostType, options); 54 | let graphqlType: T | GraphQLList | GraphQLNonNull = typeRef; 55 | 56 | if (options.isArray) { 57 | graphqlType = this.mapToGqlList( 58 | graphqlType, 59 | options.arrayDepth!, 60 | this.hasArrayOptions(options), 61 | ); 62 | } 63 | 64 | let isNotNullable: boolean; 65 | if (isInputTypeCtx) { 66 | /** 67 | * The input values (e.g., args) remain "nullable" 68 | * even if the "defaultValue" is specified. 69 | */ 70 | isNotNullable = 71 | isUndefined(options.defaultValue) && 72 | (!options.nullable || options.nullable === 'items'); 73 | } else { 74 | isNotNullable = !options.nullable || options.nullable === 'items'; 75 | } 76 | return isNotNullable 77 | ? (new GraphQLNonNull(graphqlType) as T) 78 | : (graphqlType as T); 79 | } 80 | 81 | private validateTypeOptions(hostType: string, options: TypeOptions) { 82 | if (!options.isArray && this.hasArrayOptions(options)) { 83 | throw new InvalidNullableOptionError(hostType, options.nullable); 84 | } 85 | 86 | const isNotNullable = options.nullable === 'items'; 87 | if (!isUndefined(options.defaultValue) && isNotNullable) { 88 | throw new DefaultNullableConflictError( 89 | hostType, 90 | options.defaultValue, 91 | options.nullable, 92 | ); 93 | } 94 | return true; 95 | } 96 | 97 | private mapToGqlList( 98 | targetType: T, 99 | depth: number, 100 | nullable: boolean, 101 | ): GraphQLList { 102 | const targetTypeNonNull = nullable 103 | ? targetType 104 | : new GraphQLNonNull(targetType); 105 | 106 | if (depth === 0) { 107 | return targetType as GraphQLList; 108 | } 109 | return this.mapToGqlList( 110 | new GraphQLList(targetTypeNonNull) as T, 111 | depth - 1, 112 | nullable, 113 | ); 114 | } 115 | 116 | private hasArrayOptions(options: TypeOptions) { 117 | return options.nullable === 'items' || options.nullable === 'itemsAndList'; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/schema-builder/storages/index.ts: -------------------------------------------------------------------------------- 1 | export { TypeMetadataStorage } from './type-metadata.storage'; 2 | -------------------------------------------------------------------------------- /lib/schema-builder/storages/lazy-metadata.storage.ts: -------------------------------------------------------------------------------- 1 | import { flatten, Type } from '@nestjs/common'; 2 | import { isUndefined } from '@nestjs/common/utils/shared.utils'; 3 | 4 | interface LazyMetadataHost { 5 | func: Function; 6 | target?: Type; 7 | } 8 | 9 | export class LazyMetadataStorageHost { 10 | private readonly storage = new Array(); 11 | 12 | store(func: Function): void; 13 | store(target: Type, func: Function): void; 14 | store(targetOrFn: Type | Function, func?: Function) { 15 | if (func) { 16 | this.storage.push({ target: targetOrFn as Type, func }); 17 | } else { 18 | this.storage.push({ func: targetOrFn }); 19 | } 20 | } 21 | 22 | load(types: Function[] = []) { 23 | types = this.concatPrototypes(types); 24 | this.storage.forEach(({ func, target }) => { 25 | if (target && types.includes(target)) { 26 | func(); 27 | } else if (!target) { 28 | func(); 29 | } 30 | }); 31 | } 32 | 33 | private concatPrototypes(types: Function[]): Function[] { 34 | const typesWithPrototypes = types 35 | .filter((type) => type && type.prototype) 36 | .map((type) => { 37 | const parentTypes = []; 38 | 39 | let parent: Function = type; 40 | while (!isUndefined(parent.prototype)) { 41 | parent = Object.getPrototypeOf(parent); 42 | if (parent === Function.prototype) { 43 | break; 44 | } 45 | parentTypes.push(parent); 46 | } 47 | parentTypes.push(type); 48 | return parentTypes; 49 | }); 50 | 51 | return flatten(typesWithPrototypes); 52 | } 53 | } 54 | 55 | const globalRef = global as any; 56 | export const LazyMetadataStorage: LazyMetadataStorageHost = 57 | globalRef.GqlLazyMetadataStorageHost || 58 | (globalRef.GqlLazyMetadataStorageHost = new LazyMetadataStorageHost()); 59 | -------------------------------------------------------------------------------- /lib/schema-builder/storages/type-definitions.storage.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | GraphQLEnumType, 4 | GraphQLInputObjectType, 5 | GraphQLInterfaceType, 6 | GraphQLObjectType, 7 | GraphQLUnionType, 8 | } from 'graphql'; 9 | import { EnumDefinition } from '../factories/enum-definition.factory'; 10 | import { InputTypeDefinition } from '../factories/input-type-definition.factory'; 11 | import { InterfaceTypeDefinition } from '../factories/interface-definition.factory'; 12 | import { ObjectTypeDefinition } from '../factories/object-type-definition.factory'; 13 | import { UnionDefinition } from '../factories/union-definition.factory'; 14 | 15 | export type GqlInputTypeKey = Function | object; 16 | export type GqlInputType = InputTypeDefinition | EnumDefinition; 17 | 18 | export type GqlOutputTypeKey = Function | object | symbol; 19 | export type GqlOutputType = 20 | | InterfaceTypeDefinition 21 | | ObjectTypeDefinition 22 | | EnumDefinition 23 | | UnionDefinition; 24 | 25 | @Injectable() 26 | export class TypeDefinitionsStorage { 27 | private readonly interfaceTypeDefinitions = new Map< 28 | Function, 29 | InterfaceTypeDefinition 30 | >(); 31 | private readonly enumTypeDefinitions = new Map(); 32 | private readonly unionTypeDefinitions = new Map(); 33 | private readonly objectTypeDefinitions = new Map< 34 | Function, 35 | ObjectTypeDefinition 36 | >(); 37 | private readonly inputTypeDefinitions = new Map< 38 | Function, 39 | InputTypeDefinition 40 | >(); 41 | private inputTypeDefinitionsLinks?: Map; 42 | private outputTypeDefinitionsLinks?: Map; 43 | 44 | addEnums(enumDefs: EnumDefinition[]) { 45 | enumDefs.forEach(item => this.enumTypeDefinitions.set(item.enumRef, item)); 46 | } 47 | 48 | getEnumByObject(obj: object): EnumDefinition { 49 | return this.enumTypeDefinitions.get(obj); 50 | } 51 | 52 | addUnions(unionDefs: UnionDefinition[]) { 53 | unionDefs.forEach(item => this.unionTypeDefinitions.set(item.id, item)); 54 | } 55 | 56 | getUnionBySymbol(key: symbol): UnionDefinition { 57 | return this.unionTypeDefinitions.get(key); 58 | } 59 | 60 | addInterfaces(interfaceDefs: InterfaceTypeDefinition[]) { 61 | interfaceDefs.forEach(item => 62 | this.interfaceTypeDefinitions.set(item.target, item), 63 | ); 64 | } 65 | 66 | getInterfaceByTarget(type: Function): InterfaceTypeDefinition { 67 | return this.interfaceTypeDefinitions.get(type); 68 | } 69 | 70 | getAllInterfaceDefinitions(): InterfaceTypeDefinition[] { 71 | return Array.from(this.interfaceTypeDefinitions.values()); 72 | } 73 | 74 | addInputTypes(inputDefs: InputTypeDefinition[]) { 75 | inputDefs.forEach(item => this.inputTypeDefinitions.set(item.target, item)); 76 | } 77 | 78 | getInputTypeByTarget(type: Function): InputTypeDefinition { 79 | return this.inputTypeDefinitions.get(type); 80 | } 81 | 82 | getAllInputTypeDefinitions(): InputTypeDefinition[] { 83 | return Array.from(this.inputTypeDefinitions.values()); 84 | } 85 | 86 | addObjectTypes(objectDefs: ObjectTypeDefinition[]) { 87 | objectDefs.forEach(item => 88 | this.objectTypeDefinitions.set(item.target, item), 89 | ); 90 | } 91 | 92 | getObjectTypeByTarget(type: Function): ObjectTypeDefinition { 93 | return this.objectTypeDefinitions.get(type); 94 | } 95 | 96 | getAllObjectTypeDefinitions(): ObjectTypeDefinition[] { 97 | return Array.from(this.objectTypeDefinitions.values()); 98 | } 99 | 100 | getInputTypeAndExtract( 101 | key: GqlInputTypeKey, 102 | ): GraphQLInputObjectType | GraphQLEnumType | undefined { 103 | if (!this.inputTypeDefinitionsLinks) { 104 | this.inputTypeDefinitionsLinks = new Map([ 105 | ...this.enumTypeDefinitions.entries(), 106 | ...this.inputTypeDefinitions.entries(), 107 | ]); 108 | } 109 | const definition = this.inputTypeDefinitionsLinks.get(key); 110 | if (definition) { 111 | return definition.type; 112 | } 113 | return; 114 | } 115 | 116 | getOutputTypeAndExtract( 117 | key: GqlOutputTypeKey, 118 | ): 119 | | GraphQLEnumType 120 | | GraphQLUnionType 121 | | GraphQLInterfaceType 122 | | GraphQLObjectType 123 | | undefined { 124 | if (!this.outputTypeDefinitionsLinks) { 125 | this.outputTypeDefinitionsLinks = new Map< 126 | GqlOutputTypeKey, 127 | GqlOutputType 128 | >([ 129 | ...this.objectTypeDefinitions.entries(), 130 | ...this.interfaceTypeDefinitions.entries(), 131 | ...this.enumTypeDefinitions.entries(), 132 | ...this.unionTypeDefinitions.entries(), 133 | ]); 134 | } 135 | const definition = this.outputTypeDefinitionsLinks.get(key); 136 | if (definition) { 137 | return definition.type; 138 | } 139 | return; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/schema-builder/type-definitions.generator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { BuildSchemaOptions } from '../interfaces'; 3 | import { EnumDefinitionFactory } from './factories/enum-definition.factory'; 4 | import { InputTypeDefinitionFactory } from './factories/input-type-definition.factory'; 5 | import { InterfaceDefinitionFactory } from './factories/interface-definition.factory'; 6 | import { ObjectTypeDefinitionFactory } from './factories/object-type-definition.factory'; 7 | import { UnionDefinitionFactory } from './factories/union-definition.factory'; 8 | import { TypeDefinitionsStorage } from './storages/type-definitions.storage'; 9 | import { TypeMetadataStorage } from './storages/type-metadata.storage'; 10 | 11 | @Injectable() 12 | export class TypeDefinitionsGenerator { 13 | constructor( 14 | private readonly typeDefinitionsStorage: TypeDefinitionsStorage, 15 | private readonly enumDefinitionFactory: EnumDefinitionFactory, 16 | private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory, 17 | private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory, 18 | private readonly interfaceDefinitionFactory: InterfaceDefinitionFactory, 19 | private readonly unionDefinitionFactory: UnionDefinitionFactory, 20 | ) {} 21 | 22 | generate(options: BuildSchemaOptions) { 23 | this.generateUnionDefs(); 24 | this.generateEnumDefs(); 25 | this.generateInterfaceDefs(options); 26 | this.generateObjectTypeDefs(options); 27 | this.generateInputTypeDefs(options); 28 | } 29 | 30 | private generateInputTypeDefs(options: BuildSchemaOptions) { 31 | const metadata = TypeMetadataStorage.getInputTypesMetadata(); 32 | const inputTypeDefs = metadata.map(metadata => 33 | this.inputTypeDefinitionFactory.create(metadata, options), 34 | ); 35 | this.typeDefinitionsStorage.addInputTypes(inputTypeDefs); 36 | } 37 | 38 | private generateObjectTypeDefs(options: BuildSchemaOptions) { 39 | const metadata = TypeMetadataStorage.getObjectTypesMetadata(); 40 | const objectTypeDefs = metadata.map(metadata => 41 | this.objectTypeDefinitionFactory.create(metadata, options), 42 | ); 43 | this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs); 44 | } 45 | 46 | private generateInterfaceDefs(options: BuildSchemaOptions) { 47 | const metadata = TypeMetadataStorage.getInterfacesMetadata(); 48 | const interfaceDefs = metadata.map(metadata => 49 | this.interfaceDefinitionFactory.create(metadata, options), 50 | ); 51 | this.typeDefinitionsStorage.addInterfaces(interfaceDefs); 52 | } 53 | 54 | private generateEnumDefs() { 55 | const metadata = TypeMetadataStorage.getEnumsMetadata(); 56 | const enumDefs = metadata.map(metadata => 57 | this.enumDefinitionFactory.create(metadata), 58 | ); 59 | this.typeDefinitionsStorage.addEnums(enumDefs); 60 | } 61 | 62 | private generateUnionDefs() { 63 | const metadata = TypeMetadataStorage.getUnionsMetadata(); 64 | const unionDefs = metadata.map(metadata => 65 | this.unionDefinitionFactory.create(metadata), 66 | ); 67 | this.typeDefinitionsStorage.addUnions(unionDefs); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/schema-builder/utils/get-fields-and-decorator.util.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import 'reflect-metadata'; 3 | import { 4 | ArgsType, 5 | InputType, 6 | InterfaceType, 7 | ObjectType, 8 | } from '../../decorators'; 9 | import { ClassType } from '../../enums/class-type.enum'; 10 | import { CLASS_TYPE_METADATA } from '../../fgql.constants'; 11 | import { UnableToFindFieldsError } from '../errors/unable-to-find-fields.error'; 12 | import { ClassMetadata, PropertyMetadata } from '../metadata'; 13 | import { LazyMetadataStorage } from '../storages/lazy-metadata.storage'; 14 | import { TypeMetadataStorage } from '../storages/type-metadata.storage'; 15 | 16 | export function getFieldsAndDecoratorForType(objType: Type) { 17 | const classType = Reflect.getMetadata(CLASS_TYPE_METADATA, objType); 18 | if (!classType) { 19 | throw new UnableToFindFieldsError(objType.name); 20 | } 21 | 22 | LazyMetadataStorage.load([objType]); 23 | TypeMetadataStorage.compile(); 24 | 25 | const [ 26 | classMetadata, 27 | decoratorFactory, 28 | ] = getClassMetadataAndFactoryByTargetAndType(classType, objType); 29 | 30 | let fields = classMetadata?.properties; 31 | if (!fields) { 32 | throw new UnableToFindFieldsError(objType.name); 33 | } 34 | fields = inheritClassFields(objType, fields); 35 | 36 | return { 37 | fields, 38 | decoratorFactory, 39 | }; 40 | } 41 | 42 | type ClassDecorator = 43 | | typeof ArgsType 44 | | typeof InterfaceType 45 | | typeof ObjectType 46 | | typeof InputType; 47 | type MetadataAndFactoryTuple = [ClassMetadata | undefined, ClassDecorator]; 48 | 49 | function getClassMetadataAndFactoryByTargetAndType( 50 | classType: ClassType, 51 | objType: Type, 52 | ): MetadataAndFactoryTuple { 53 | switch (classType) { 54 | case ClassType.ARGS: 55 | return [ 56 | TypeMetadataStorage.getArgumentsMetadataByTarget(objType), 57 | ArgsType, 58 | ]; 59 | case ClassType.OBJECT: 60 | return [ 61 | TypeMetadataStorage.getObjectTypeMetadataByTarget(objType), 62 | ObjectType, 63 | ]; 64 | case ClassType.INPUT: 65 | return [ 66 | TypeMetadataStorage.getInputTypeMetadataByTarget(objType), 67 | InputType, 68 | ]; 69 | case ClassType.INTERFACE: 70 | return [ 71 | TypeMetadataStorage.getInterfaceMetadataByTarget(objType), 72 | InterfaceType, 73 | ]; 74 | } 75 | } 76 | 77 | function inheritClassFields( 78 | objType: Type, 79 | fields: PropertyMetadata[], 80 | ) { 81 | try { 82 | const parentClass = Object.getPrototypeOf(objType); 83 | if (parentClass === Function) { 84 | return fields; 85 | } 86 | const { fields: parentFields } = getFieldsAndDecoratorForType( 87 | parentClass as Type, 88 | ); 89 | return inheritClassFields(parentClass, [...parentFields, ...fields]); 90 | } catch (err) { 91 | return fields; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/schema-builder/utils/is-target-equal-util.ts: -------------------------------------------------------------------------------- 1 | export type TargetHost = Record<'target', Function>; 2 | export function isTargetEqual( 3 | a: T, 4 | b: U, 5 | ) { 6 | return a.target === b.target; 7 | } 8 | -------------------------------------------------------------------------------- /lib/schema-builder/utils/is-throwing.util.ts: -------------------------------------------------------------------------------- 1 | export function isThrowing(func: () => unknown) { 2 | try { 3 | func(); 4 | return false; 5 | } catch { 6 | return true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/services/base-explorer.service.ts: -------------------------------------------------------------------------------- 1 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 2 | import { Module } from '@nestjs/core/injector/module'; 3 | import { flattenDeep, groupBy, identity, isEmpty, mapValues } from 'lodash'; 4 | import { ResolverMetadata } from '../interfaces/resolver-metadata.interface'; 5 | 6 | export class BaseExplorerService { 7 | getModules( 8 | modulesContainer: Map, 9 | include: Function[], 10 | ): Module[] { 11 | if (!include || isEmpty(include)) { 12 | return [...modulesContainer.values()]; 13 | } 14 | const whitelisted = this.includeWhitelisted(modulesContainer, include); 15 | return whitelisted; 16 | } 17 | 18 | includeWhitelisted( 19 | modulesContainer: Map, 20 | include: Function[], 21 | ): Module[] { 22 | const modules = [...modulesContainer.values()]; 23 | return modules.filter(({ metatype }) => 24 | include.some(item => item === metatype), 25 | ); 26 | } 27 | 28 | flatMap( 29 | modules: Module[], 30 | callback: (instance: InstanceWrapper, moduleRef: Module) => T | T[], 31 | ): T[] { 32 | const invokeMap = () => { 33 | return modules.map(moduleRef => { 34 | const providers = [...moduleRef.providers.values()]; 35 | return providers.map(wrapper => callback(wrapper, moduleRef)); 36 | }); 37 | }; 38 | return flattenDeep(invokeMap()).filter(identity); 39 | } 40 | 41 | groupMetadata(resolvers: ResolverMetadata[]) { 42 | const groupByType = groupBy( 43 | resolvers, 44 | (metadata: ResolverMetadata) => metadata.type, 45 | ); 46 | const groupedMetadata = mapValues( 47 | groupByType, 48 | (resolversArr: ResolverMetadata[]) => 49 | resolversArr.reduce( 50 | (prev, curr) => ({ 51 | ...prev, 52 | [curr.name]: curr.callback, 53 | }), 54 | {}, 55 | ), 56 | ); 57 | return groupedMetadata; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/services/gql-arguments-host.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost } from '@nestjs/common'; 2 | import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; 3 | 4 | export interface GraphQLArgumentsHost extends ArgumentsHost { 5 | getRoot(): T; 6 | getInfo(): T; 7 | getArgs(): T; 8 | getContext(): T; 9 | } 10 | 11 | export class GqlArgumentsHost extends ExecutionContextHost 12 | implements GraphQLArgumentsHost { 13 | static create(context: ArgumentsHost): GqlArgumentsHost { 14 | const type = context.getType(); 15 | const gqlContext = new GqlArgumentsHost(context.getArgs()); 16 | gqlContext.setType(type); 17 | return gqlContext; 18 | } 19 | 20 | getRoot(): T { 21 | return this.getArgByIndex(0); 22 | } 23 | 24 | getArgs(): T { 25 | return this.getArgByIndex(1); 26 | } 27 | 28 | getContext(): T { 29 | return this.getArgByIndex(2); 30 | } 31 | 32 | getInfo(): T { 33 | return this.getArgByIndex(3); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/services/gql-execution-context.ts: -------------------------------------------------------------------------------- 1 | import { ContextType, ExecutionContext } from '@nestjs/common'; 2 | import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; 3 | import { GraphQLArgumentsHost } from './gql-arguments-host'; 4 | 5 | export type GqlContextType = 'graphql' | ContextType; 6 | export type GraphQLExecutionContext = GqlExecutionContext; 7 | 8 | export class GqlExecutionContext extends ExecutionContextHost 9 | implements GraphQLArgumentsHost { 10 | static create(context: ExecutionContext): GqlExecutionContext { 11 | const type = context.getType(); 12 | const gqlContext = new GqlExecutionContext( 13 | context.getArgs(), 14 | context.getClass(), 15 | context.getHandler(), 16 | ); 17 | gqlContext.setType(type); 18 | return gqlContext; 19 | } 20 | 21 | getType(): TContext { 22 | return super.getType(); 23 | } 24 | 25 | getRoot(): T { 26 | return this.getArgByIndex(0); 27 | } 28 | 29 | getArgs(): T { 30 | return this.getArgByIndex(1); 31 | } 32 | 33 | getContext(): T { 34 | return this.getArgByIndex(2); 35 | } 36 | 37 | getInfo(): T { 38 | return this.getArgByIndex(3); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-explorer.service'; 2 | export * from './gql-arguments-host'; 3 | export * from './gql-execution-context'; 4 | export * from './resolvers-explorer.service'; 5 | export * from './scalars-explorer.service'; 6 | -------------------------------------------------------------------------------- /lib/services/scalars-explorer.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { isFunction } from '@nestjs/common/utils/shared.utils'; 3 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 4 | import { ModulesContainer } from '@nestjs/core/injector/modules-container'; 5 | import { 6 | GRAPHQL_MODULE_OPTIONS, 7 | SCALAR_NAME_METADATA, 8 | SCALAR_TYPE_METADATA, 9 | } from '../fgql.constants'; 10 | import { ScalarsTypeMap, FgqlModuleOptions } from '../interfaces'; 11 | import { createScalarType } from '../utils/scalar-types.utils'; 12 | import { BaseExplorerService } from './base-explorer.service'; 13 | 14 | @Injectable() 15 | export class ScalarsExplorerService extends BaseExplorerService { 16 | constructor( 17 | private readonly modulesContainer: ModulesContainer, 18 | @Inject(GRAPHQL_MODULE_OPTIONS) 19 | private readonly gqlOptions: FgqlModuleOptions, 20 | ) { 21 | super(); 22 | } 23 | 24 | explore() { 25 | const modules = this.getModules( 26 | this.modulesContainer, 27 | this.gqlOptions.include || [], 28 | ); 29 | return this.flatMap(modules, (instance) => 30 | this.filterSchemaFirstScalar(instance), 31 | ); 32 | } 33 | 34 | filterSchemaFirstScalar = any>( 35 | wrapper: InstanceWrapper, 36 | ) { 37 | const { instance } = wrapper; 38 | if (!instance) { 39 | return undefined; 40 | } 41 | const scalarName: string = Reflect.getMetadata( 42 | SCALAR_NAME_METADATA, 43 | instance.constructor, 44 | ); 45 | if (!scalarName) { 46 | return; 47 | } 48 | return { 49 | [scalarName]: createScalarType(scalarName, instance), 50 | }; 51 | } 52 | 53 | getScalarsMap(): ScalarsTypeMap[] { 54 | const modules = this.getModules( 55 | this.modulesContainer, 56 | this.gqlOptions.include || [], 57 | ); 58 | return this.flatMap(modules, (instance) => 59 | this.filterCodeFirstScalar(instance), 60 | ); 61 | } 62 | 63 | filterCodeFirstScalar = any>( 64 | wrapper: InstanceWrapper, 65 | ) { 66 | const { instance } = wrapper; 67 | if (!instance) { 68 | return undefined; 69 | } 70 | const scalarNameMetadata = Reflect.getMetadata( 71 | SCALAR_NAME_METADATA, 72 | instance.constructor, 73 | ); 74 | const scalarTypeMetadata = Reflect.getMetadata( 75 | SCALAR_TYPE_METADATA, 76 | instance.constructor, 77 | ); 78 | if (!scalarNameMetadata) { 79 | return; 80 | } 81 | const typeRef = 82 | (isFunction(scalarTypeMetadata) && scalarTypeMetadata()) || 83 | instance.constructor; 84 | 85 | return { 86 | type: typeRef, 87 | scalar: createScalarType(scalarNameMetadata, instance), 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/type-factories/create-union-type.factory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek. 3 | * In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module. 4 | * Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility. 5 | * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". 6 | */ 7 | 8 | import { Type } from '@nestjs/common'; 9 | import { ResolveTypeFn } from '../interfaces/resolve-type-fn.interface'; 10 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 11 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 12 | 13 | /** 14 | * Interface defining options that can be passed to `createUnionType` function. 15 | */ 16 | export interface UnionOptions[] = any[]> { 17 | /** 18 | * Name of the union. 19 | */ 20 | name: string; 21 | /** 22 | * Description of the union. 23 | */ 24 | description?: string; 25 | /** 26 | * Custom implementation of the "resolveType" function. 27 | */ 28 | resolveType?: ResolveTypeFn; 29 | /** 30 | * Types that the union consist of. 31 | */ 32 | types: () => T; 33 | } 34 | 35 | export type ArrayElement< 36 | ArrayType extends readonly unknown[] 37 | > = ArrayType[number]; 38 | export type Union = InstanceType>; 39 | 40 | /** 41 | * Creates a GraphQL union type composed of types references. 42 | * @param options 43 | */ 44 | export function createUnionType[] = any[]>( 45 | options: UnionOptions, 46 | ): Union { 47 | const { name, description, types, resolveType } = options; 48 | const id = Symbol(name); 49 | 50 | LazyMetadataStorage.store(() => 51 | TypeMetadataStorage.addUnionMetadata({ 52 | id, 53 | name, 54 | description, 55 | typesFn: types, 56 | resolveType, 57 | }), 58 | ); 59 | return id as any; 60 | } 61 | -------------------------------------------------------------------------------- /lib/type-factories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-union-type.factory'; 2 | export * from './register-enum-type.factory'; 3 | -------------------------------------------------------------------------------- /lib/type-factories/register-enum-type.factory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek. 3 | * In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module. 4 | * Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility. 5 | * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". 6 | */ 7 | 8 | import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; 9 | import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; 10 | 11 | /** 12 | * Interface defining options that can be passed to `registerEnumType` function. 13 | */ 14 | export interface EnumOptions { 15 | /** 16 | * Name of the enum. 17 | */ 18 | name: string; 19 | /** 20 | * Description of the enum. 21 | */ 22 | description?: string; 23 | } 24 | 25 | /** 26 | * Registers a GraphqQL enum type based on the passed enumerator reference. 27 | * @param options 28 | */ 29 | export function registerEnumType( 30 | enumRef: T, 31 | options: EnumOptions, 32 | ) { 33 | LazyMetadataStorage.store(() => 34 | TypeMetadataStorage.addEnumMetadata({ 35 | ref: enumRef, 36 | name: options.name, 37 | description: options.description, 38 | }), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /lib/utils/add-class-type-metadata.util.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { ClassType } from '../enums/class-type.enum'; 3 | import { CLASS_TYPE_METADATA } from '../fgql.constants'; 4 | 5 | export function addClassTypeMetadata(target: Function, classType: ClassType) { 6 | const decoratorFactory: ClassDecorator = SetMetadata( 7 | CLASS_TYPE_METADATA, 8 | classType, 9 | ); 10 | decoratorFactory(target); 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils/async-iterator.util.ts: -------------------------------------------------------------------------------- 1 | import { $$asyncIterator } from 'iterall'; 2 | 3 | type AsyncIterator = { 4 | next(value?: any): Promise>; 5 | return(): any; 6 | throw(error: any): any; 7 | [$$asyncIterator](): AsyncIterator; 8 | }; 9 | 10 | export const createAsyncIterator = async ( 11 | lazyFactory: Promise>, 12 | filterFn: Function, 13 | ): Promise> => { 14 | const asyncIterator = await lazyFactory; 15 | const getNextValue = async () => { 16 | if (!asyncIterator || typeof asyncIterator.next !== 'function') { 17 | return Promise.reject(asyncIterator); 18 | } 19 | 20 | const payload = await asyncIterator.next(); 21 | if (payload.done === true) { 22 | return payload; 23 | } 24 | return Promise.resolve(filterFn(payload.value)) 25 | .catch(() => false) 26 | .then(result => (result ? payload : getNextValue())); 27 | }; 28 | 29 | return { 30 | next() { 31 | return getNextValue(); 32 | }, 33 | return() { 34 | const isAsyncIterator = 35 | asyncIterator && typeof asyncIterator.return === 'function'; 36 | 37 | return isAsyncIterator 38 | ? asyncIterator.return() 39 | : Promise.resolve({ 40 | done: true, 41 | value: asyncIterator, 42 | }); 43 | }, 44 | throw(error: any) { 45 | return asyncIterator.throw(error); 46 | }, 47 | [$$asyncIterator]() { 48 | return this; 49 | }, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/utils/extend.util.ts: -------------------------------------------------------------------------------- 1 | import { defaultTo, isArray, isString } from 'lodash'; 2 | 3 | export function extend(obj1: unknown, obj2: unknown): any { 4 | if (isString(obj1)) { 5 | return isString(obj2) 6 | ? [defaultTo(obj1, ''), defaultTo(obj2, '')] 7 | : ([defaultTo(obj1, '')] as any[]).concat(defaultTo(obj2, [])); 8 | } 9 | if (isArray(obj1)) { 10 | return defaultTo(obj1, []).concat(defaultTo(obj2, [])); 11 | } 12 | return { 13 | ...((obj1 as object) || {}), 14 | ...((obj2 as object) || {}), 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /lib/utils/extract-metadata.util.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { 3 | RESOLVER_NAME_METADATA, 4 | RESOLVER_PROPERTY_METADATA, 5 | RESOLVER_TYPE_METADATA, 6 | } from '../fgql.constants'; 7 | import { ResolverMetadata } from '../interfaces/resolver-metadata.interface'; 8 | 9 | export function extractMetadata( 10 | instance: Record, 11 | prototype: any, 12 | methodName: string, 13 | filterPredicate: ( 14 | resolverType: string, 15 | isReferenceResolver?: boolean, 16 | isPropertyResolver?: boolean, 17 | ) => boolean, 18 | ): ResolverMetadata { 19 | const callback = prototype[methodName]; 20 | const resolverType = 21 | Reflect.getMetadata(RESOLVER_TYPE_METADATA, callback) || 22 | Reflect.getMetadata(RESOLVER_TYPE_METADATA, instance.constructor); 23 | 24 | const isPropertyResolver = !!Reflect.getMetadata( 25 | RESOLVER_PROPERTY_METADATA, 26 | callback, 27 | ); 28 | 29 | const resolverName = Reflect.getMetadata(RESOLVER_NAME_METADATA, callback); 30 | if (filterPredicate(resolverType, false, isPropertyResolver)) { 31 | return null; 32 | } 33 | 34 | const name = resolverName || methodName; 35 | return { 36 | type: resolverType, 37 | methodName, 38 | name, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/utils/generate-token.util.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | 3 | export const generateString = () => v4(); 4 | -------------------------------------------------------------------------------- /lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './async-iterator.util'; 2 | export * from './extend.util'; 3 | export * from './extract-metadata.util'; 4 | export * from './generate-token.util'; 5 | export * from './merge-defaults.util'; 6 | export * from './normalize-route-path.util'; 7 | export * from './remove-temp.util'; 8 | -------------------------------------------------------------------------------- /lib/utils/is-pipe.util.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Type } from '@nestjs/common'; 2 | import { isFunction } from '@nestjs/common/utils/shared.utils'; 3 | 4 | export function isPipe( 5 | value: unknown, 6 | ): value is PipeTransform | Type { 7 | if (!value) { 8 | return false; 9 | } 10 | if (isFunction(value)) { 11 | return true; 12 | } 13 | return isFunction((value as PipeTransform).transform); 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/merge-defaults.util.ts: -------------------------------------------------------------------------------- 1 | import { FastifyReply, FastifyRequest } from 'fastify'; 2 | import { isFunction } from '@nestjs/common/utils/shared.utils'; 3 | import { FgqlModuleOptions } from '../interfaces/fgql-module-options.interface'; 4 | 5 | const defaultOptions: FgqlModuleOptions = { 6 | path: '/graphql', 7 | fieldResolverEnhancers: [], 8 | }; 9 | 10 | export function mergeDefaults( 11 | options: FgqlModuleOptions, 12 | defaults: FgqlModuleOptions = defaultOptions, 13 | ): FgqlModuleOptions { 14 | const moduleOptions = { 15 | ...defaults, 16 | ...options, 17 | }; 18 | if (!moduleOptions.context) { 19 | moduleOptions.context = ( 20 | request: FastifyRequest, 21 | reply: FastifyReply, 22 | ) => Promise.resolve({ req: request }); 23 | } else if (isFunction(moduleOptions.context)) { 24 | moduleOptions.context = async ( 25 | request: FastifyRequest, 26 | reply: FastifyReply, 27 | ) => { 28 | const ctx = await (options.context as Function)(request, reply); 29 | return assignReqProperty(ctx, request); 30 | }; 31 | } else { 32 | moduleOptions.context = ( 33 | request: FastifyRequest, 34 | reply: FastifyReply, 35 | ) => { 36 | return Promise.resolve( 37 | assignReqProperty(options.context as Record, request), 38 | ); 39 | }; 40 | } 41 | return moduleOptions; 42 | } 43 | 44 | function assignReqProperty( 45 | ctx: Record | undefined, 46 | req: unknown, 47 | ) { 48 | if (!ctx) { 49 | return { req }; 50 | } 51 | if ( 52 | typeof ctx !== 'object' || 53 | (ctx && ctx.req && typeof ctx.req === 'object') 54 | ) { 55 | return ctx; 56 | } 57 | ctx.req = req; 58 | return ctx; 59 | } 60 | -------------------------------------------------------------------------------- /lib/utils/normalize-route-path.util.ts: -------------------------------------------------------------------------------- 1 | function addStartingSlash(text: string) { 2 | if (!text) { 3 | return text; 4 | } 5 | return text[0] !== '/' ? '/' + text : text; 6 | } 7 | 8 | function stripEndingSlash(text: string) { 9 | if (!text) { 10 | return text; 11 | } 12 | return text[text.length - 1] === '/' && text.length > 1 13 | ? text.slice(0, text.length - 1) 14 | : text; 15 | } 16 | 17 | export function normalizeRoutePath(path: string) { 18 | path = addStartingSlash(path); 19 | return stripEndingSlash(path); 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/reflection.utilts.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { get } from 'lodash'; 3 | import { 4 | GqlTypeReference, 5 | ReturnTypeFunc, 6 | } from '../interfaces/return-type-func.interface'; 7 | import { TypeOptions } from '../interfaces/type-options.interface'; 8 | import { UndefinedTypeError } from '../schema-builder/errors/undefined-type.error'; 9 | 10 | const NOT_ALLOWED_TYPES: Type[] = [Promise, Array, Object, Function]; 11 | 12 | export interface ReflectTypeOptions { 13 | metadataKey: 'design:type' | 'design:returntype' | 'design:paramtypes'; 14 | prototype: Object; 15 | propertyKey: string; 16 | explicitTypeFn?: ReturnTypeFunc; 17 | typeOptions?: TypeOptions; 18 | index?: number; 19 | } 20 | 21 | export interface TypeMetadata { 22 | typeFn: (type?: any) => GqlTypeReference; 23 | options: TypeOptions; 24 | } 25 | 26 | export function reflectTypeFromMetadata( 27 | reflectOptions: ReflectTypeOptions, 28 | ): TypeMetadata { 29 | const { 30 | metadataKey, 31 | prototype, 32 | propertyKey, 33 | explicitTypeFn, 34 | typeOptions = {}, 35 | index, 36 | } = reflectOptions; 37 | 38 | const options = { ...typeOptions }; 39 | const reflectedType: Type[] | Type = Reflect.getMetadata( 40 | metadataKey, 41 | prototype, 42 | propertyKey, 43 | ); 44 | const implicitType = extractTypeIfArray(metadataKey, reflectedType, index); 45 | const isNotAllowed = implicitType && NOT_ALLOWED_TYPES.includes(implicitType); 46 | 47 | if ( 48 | (!explicitTypeFn && (!implicitType || isNotAllowed)) || 49 | (!implicitType && !explicitTypeFn) 50 | ) { 51 | throw new UndefinedTypeError( 52 | get(prototype, 'constructor.name'), 53 | propertyKey, 54 | index, 55 | ); 56 | } 57 | if (explicitTypeFn) { 58 | return { 59 | typeFn: createWrappedExplicitTypeFn(explicitTypeFn, options), 60 | options, 61 | }; 62 | } 63 | return { 64 | typeFn: () => implicitType, 65 | options: 66 | implicitType === Array 67 | ? { 68 | ...options, 69 | isArray: true, 70 | arrayDepth: 1, 71 | } 72 | : options, 73 | }; 74 | } 75 | 76 | function extractTypeIfArray( 77 | metadataKey: 'design:type' | 'design:returntype' | 'design:paramtypes', 78 | reflectedType: Type | Type[], 79 | index: number, 80 | ): Type { 81 | if (metadataKey === 'design:paramtypes') { 82 | return (reflectedType as Type[])[index]; 83 | } 84 | return reflectedType as Type; 85 | } 86 | 87 | type DeepArray = Array | T>; 88 | 89 | function getTypeReferenceAndArrayDepth( 90 | [typeOrArray]: DeepArray, 91 | depth = 1, 92 | ) { 93 | if (!Array.isArray(typeOrArray)) { 94 | return { depth, typeRef: typeOrArray }; 95 | } 96 | return getTypeReferenceAndArrayDepth(typeOrArray, depth + 1); 97 | } 98 | 99 | function createWrappedExplicitTypeFn( 100 | explicitTypeFn: ReturnTypeFunc, 101 | options: TypeOptions, 102 | ) { 103 | return () => { 104 | const explicitTypeRef = explicitTypeFn(); 105 | if (Array.isArray(explicitTypeRef)) { 106 | const { depth, typeRef } = getTypeReferenceAndArrayDepth(explicitTypeRef); 107 | options.isArray = true; 108 | options.arrayDepth = depth; 109 | return typeRef; 110 | } 111 | return explicitTypeRef; 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /lib/utils/remove-temp.util.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | /** 4 | * Removes "temp__" field from schema added 5 | * because of "merge-graphql-schemas" library issues. 6 | **/ 7 | export function removeTempField(schema: GraphQLSchema): GraphQLSchema { 8 | const queryTypeRef = schema.getQueryType(); 9 | if (!queryTypeRef) { 10 | return schema; 11 | } 12 | const fields = queryTypeRef.getFields(); 13 | if (!fields) { 14 | return schema; 15 | } 16 | delete fields['temp__']; 17 | return schema; 18 | } 19 | -------------------------------------------------------------------------------- /lib/utils/scalar-types.utils.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | 3 | function bindInstanceContext( 4 | instance: Partial, 5 | funcKey: keyof GraphQLScalarType, 6 | ) { 7 | return instance[funcKey] 8 | ? (instance[funcKey] as Function).bind(instance) 9 | : undefined; 10 | } 11 | 12 | export function createScalarType( 13 | name: string, 14 | instance: Partial, 15 | ) { 16 | return new GraphQLScalarType({ 17 | name, 18 | description: instance.description as string, 19 | parseValue: bindInstanceContext(instance, 'parseValue'), 20 | serialize: bindInstanceContext(instance, 'serialize'), 21 | parseLiteral: bindInstanceContext(instance, 'parseLiteral'), 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirco312312/nest-fgql", 3 | "version": "1.0.10", 4 | "description": "Fast, lightweight GraphQL module to be used in NestJS", 5 | "author": "Mirco Bartels", 6 | "license": "MIT", 7 | "keywords": [ 8 | "nest", 9 | "nestjs", 10 | "graphql", 11 | "graphql-jit", 12 | "fastify", 13 | "fastify-gql" 14 | ], 15 | "scripts": { 16 | "prebuild": "rimraf dist", 17 | "build": "tsc -p tsconfig.json", 18 | "format": "prettier **/**/*.ts --ignore-path ./.prettierignore --write", 19 | "lint": "eslint 'lib/**/*.ts' --fix", 20 | "prepublish:npm": "npm run build", 21 | "publish:npm": "npm publish --access public", 22 | "prepublish:next": "npm run build", 23 | "publish:next": "npm publish --access public --tag next", 24 | "test:integration": "jest --runInBand", 25 | "test:integration:dev": "jest --runInBand --watch", 26 | "prerelease": "npm run build", 27 | "release": "release-it" 28 | }, 29 | "devDependencies": { 30 | "@commitlint/cli": "9.0.1", 31 | "@commitlint/config-angular": "9.0.1", 32 | "@nestjs/common": "7.2.0", 33 | "@nestjs/core": "7.2.0", 34 | "@nestjs/platform-fastify": "7.2.0", 35 | "@nestjs/testing": "7.2.0", 36 | "@types/jest": "26.0.3", 37 | "@types/lodash": "^4.14.157", 38 | "@types/node": "12.12.31", 39 | "@types/node-fetch": "2.5.7", 40 | "@types/supertest": "^2.0.9", 41 | "@typescript-eslint/eslint-plugin": "3.4.0", 42 | "@typescript-eslint/parser": "3.4.0", 43 | "class-transformer": "0.2.3", 44 | "class-validator": "0.12.2", 45 | "eslint": "7.3.1", 46 | "eslint-config-prettier": "6.11.0", 47 | "eslint-plugin-import": "2.21.2", 48 | "husky": "4.2.5", 49 | "jest": "26.1.0", 50 | "lint-staged": "10.2.11", 51 | "prettier": "2.0.5", 52 | "reflect-metadata": "0.1.13", 53 | "release-it": "12.6.3", 54 | "rimraf": "3.0.2", 55 | "supertest": "4.0.2", 56 | "ts-jest": "26.1.1", 57 | "ts-morph": "^7.1.2", 58 | "ts-node": "8.10.2", 59 | "typescript": "3.9.3" 60 | }, 61 | "dependencies": { 62 | "fastify-gql": "mirco312312/fastify-gql#feature/keep-alive", 63 | "graphql": "^15.3.0", 64 | "graphql-tag": "^2.10.3", 65 | "graphql-tools": "^6.0.11", 66 | "iterall": "^1.3.0", 67 | "lodash": "^4.17.15", 68 | "normalize-path": "^3.0.0" 69 | }, 70 | "peerDependencies": { 71 | "@nestjs/common": "^7.0.0", 72 | "@nestjs/core": "^7.0.0", 73 | "@nestjs/platform-fastify": "^7.3.2", 74 | "reflect-metadata": "^0.1.12" 75 | }, 76 | "lint-staged": { 77 | "*.ts": [ 78 | "prettier --write" 79 | ] 80 | }, 81 | "husky": { 82 | "hooks": { 83 | "commit-msg": "commitlint -c .commitlintrc.json -E HUSKY_GIT_PARAMS", 84 | "pre-commit": "lint-staged" 85 | } 86 | }, 87 | "repository": { 88 | "type": "git", 89 | "url": "git+https://github.com/mirco312312/nest-fgql.git" 90 | }, 91 | "jest": { 92 | "moduleFileExtensions": [ 93 | "js", 94 | "json", 95 | "ts" 96 | ], 97 | "rootDir": ".", 98 | "testEnvironment": "node", 99 | "testRegex": ".spec.ts$", 100 | "transform": { 101 | "^.+\\.(t|j)s$": "ts-jest" 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | __export(require('./dist/plugin')); 7 | -------------------------------------------------------------------------------- /plugin.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/plugin'; 2 | -------------------------------------------------------------------------------- /tests/code-first/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FgqlModule } from '../../lib'; 3 | import { DirectionsModule } from './directions/directions.module'; 4 | import { RecipesModule } from './recipes/recipes.module'; 5 | 6 | @Module({ 7 | imports: [ 8 | RecipesModule, 9 | DirectionsModule, 10 | FgqlModule.forRoot({ 11 | debug: false, 12 | installSubscriptionHandlers: true, 13 | autoSchemaFile: true, 14 | }), 15 | ], 16 | }) 17 | export class ApplicationModule {} 18 | -------------------------------------------------------------------------------- /tests/code-first/common/filters/unauthorized.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, UnauthorizedException } from '@nestjs/common'; 2 | import { GqlExceptionFilter } from '../../../../lib'; 3 | 4 | @Catch(UnauthorizedException) 5 | export class UnauthorizedFilter implements GqlExceptionFilter { 6 | catch(exception: any, host: ArgumentsHost) { 7 | return new Error('Unauthorized error'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/code-first/common/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { GqlExecutionContext } from '../../../../lib'; 8 | 9 | @Injectable() 10 | export class AuthGuard implements CanActivate { 11 | async canActivate(context: ExecutionContext): Promise { 12 | const gqlContext = GqlExecutionContext.create(context); 13 | if (gqlContext) { 14 | throw new UnauthorizedException(); 15 | } 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/code-first/common/scalars/date.scalar.ts: -------------------------------------------------------------------------------- 1 | import { Kind, ValueNode } from 'graphql'; 2 | import { Scalar } from '../../../../lib'; 3 | 4 | @Scalar('Date', type => Date) 5 | export class DateScalar { 6 | description = 'Date custom scalar type'; 7 | 8 | parseValue(value: any) { 9 | return new Date(value); // value from the client 10 | } 11 | 12 | serialize(value: any) { 13 | return value.getTime(); // value sent to the client 14 | } 15 | 16 | parseLiteral(ast: ValueNode) { 17 | if (ast.kind === Kind.INT) { 18 | return parseInt(ast.value, 10); // ast value is always in string format 19 | } 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/code-first/directions/directions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DirectionsResolver } from './directions.resolver'; 3 | 4 | @Module({ 5 | providers: [DirectionsResolver], 6 | }) 7 | export class DirectionsModule {} 8 | -------------------------------------------------------------------------------- /tests/code-first/directions/directions.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '../../../lib'; 2 | import { Direction } from '../enums/direction.enum'; 3 | 4 | @Resolver() 5 | export class DirectionsResolver { 6 | @Query(returns => Direction) 7 | move( 8 | @Args({ name: 'direction', type: () => Direction }) 9 | direction: Direction, 10 | ): Direction { 11 | return direction; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/code-first/enums/direction.enum.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from '../../../lib'; 2 | 3 | export enum Direction { 4 | Up = 'UP', 5 | Down = 'DOWN', 6 | Left = 'LEFT', 7 | Right = 'RIGHT', 8 | } 9 | 10 | registerEnumType(Direction, { 11 | name: 'Direction', // this one is mandatory 12 | description: 'The basic directions', // this one is optional 13 | }); 14 | -------------------------------------------------------------------------------- /tests/code-first/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { 4 | FastifyAdapter, 5 | NestFastifyApplication, 6 | } from '@nestjs/platform-fastify'; 7 | import { ApplicationModule } from './app.module'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create( 11 | ApplicationModule, 12 | new FastifyAdapter(), 13 | ); 14 | app.useGlobalPipes(new ValidationPipe()); 15 | await app.listen(3000); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /tests/code-first/other/abstract.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '../../../lib'; 2 | import { RecipesArgs } from './../recipes/dto/recipes.args'; 3 | import { Recipe } from './../recipes/models/recipe'; 4 | 5 | @Resolver(() => Recipe, { isAbstract: true }) 6 | export class AbstractResolver { 7 | @Query(returns => [Recipe]) 8 | abstractRecipes(@Args() recipesArgs: RecipesArgs): Recipe[] { 9 | return []; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/code-first/other/sample-orphaned.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '../../../lib'; 2 | 3 | @ObjectType({ description: 'orphaned type' }) 4 | export class SampleOrphanedType { 5 | @Field(type => ID) 6 | id: string; 7 | 8 | @Field() 9 | title: string; 10 | 11 | @Field({ nullable: true }) 12 | description?: string; 13 | 14 | @Field() 15 | creationDate: Date; 16 | 17 | @Field() 18 | get averageRating(): number { 19 | return 0.5; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/code-first/recipes/dto/filter-recipes-count.args.ts: -------------------------------------------------------------------------------- 1 | import { ArgsType, Field } from '../../../../lib'; 2 | 3 | @ArgsType() 4 | export class FilterRecipesCountArgs { 5 | @Field({ nullable: true }) 6 | type?: string; 7 | 8 | @Field({ nullable: true }) 9 | status?: string; 10 | } 11 | -------------------------------------------------------------------------------- /tests/code-first/recipes/dto/new-recipe.input.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { Length, MaxLength } from 'class-validator'; 3 | import { Field, InputType } from '../../../../lib'; 4 | 5 | @InputType({ description: 'new recipe input' }) 6 | export class NewRecipeInput { 7 | @Field({ description: 'recipe title' }) 8 | @MaxLength(30) 9 | title: string; 10 | 11 | @Field({ nullable: true }) 12 | @Length(30, 255) 13 | description?: string; 14 | 15 | @Type(() => String) 16 | @Field(type => [String]) 17 | ingredients: string[]; 18 | } 19 | -------------------------------------------------------------------------------- /tests/code-first/recipes/dto/recipes.args.ts: -------------------------------------------------------------------------------- 1 | import { Max, Min } from 'class-validator'; 2 | import { ArgsType, Field, Int } from '../../../../lib'; 3 | 4 | @ArgsType() 5 | export class RecipesArgs { 6 | @Field(type => Int, { description: 'number of items to skip' }) 7 | @Min(0) 8 | skip: number = 0; 9 | 10 | @Field(type => Int) 11 | @Min(1) 12 | @Max(50) 13 | take: number = 25; 14 | } 15 | -------------------------------------------------------------------------------- /tests/code-first/recipes/models/category.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '../../../../lib/'; 2 | 3 | @ObjectType() 4 | export class Category { 5 | @Field((type) => String) 6 | name: string; 7 | 8 | @Field((type) => String, { defaultValue: 'default value' }) 9 | description: string; 10 | 11 | @Field((type) => [String], { defaultValue: [] }) 12 | tags: string[]; 13 | 14 | constructor(category: Partial) { 15 | Object.assign(this, category); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/code-first/recipes/models/ingredient.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '../../../../lib'; 2 | 3 | @ObjectType() 4 | export class Ingredient { 5 | @Field(type => ID) 6 | id: string; 7 | 8 | @Field({ 9 | defaultValue: 'default', 10 | deprecationReason: 'is deprecated', 11 | description: 'ingredient name', 12 | nullable: true, 13 | }) 14 | name: string; 15 | 16 | constructor(ingredient: Partial) { 17 | Object.assign(this, ingredient); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/code-first/recipes/models/recipe.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, InterfaceType, ObjectType } from '../../../../lib'; 2 | 3 | @InterfaceType({ 4 | description: 'example interface', 5 | resolveType: (value) => { 6 | return Recipe; 7 | }, 8 | }) 9 | export abstract class IRecipe { 10 | @Field((type) => ID) 11 | id: string; 12 | 13 | @Field() 14 | title: string; 15 | } 16 | 17 | @ObjectType({ implements: IRecipe, description: 'recipe object type' }) 18 | export class Recipe { 19 | @Field((type) => ID) 20 | id: string; 21 | 22 | @Field() 23 | title: string; 24 | 25 | @Field({ nullable: true }) 26 | description?: string; 27 | 28 | @Field() 29 | creationDate: Date; 30 | 31 | @Field() 32 | get averageRating(): number { 33 | return 0.5; 34 | } 35 | 36 | @Field({ nullable: true }) 37 | get lastRate(): number | undefined { 38 | return undefined; 39 | } 40 | 41 | @Field((type) => [String]) 42 | get tags(): string[] { 43 | return []; 44 | } 45 | 46 | constructor(recipe: Partial) { 47 | Object.assign(this, recipe); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/code-first/recipes/recipes.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_FILTER } from '@nestjs/core'; 3 | import { UnauthorizedFilter } from '../common/filters/unauthorized.filter'; 4 | import { DateScalar } from '../common/scalars/date.scalar'; 5 | import { RecipesResolver } from './recipes.resolver'; 6 | import { RecipesService } from './recipes.service'; 7 | 8 | @Module({ 9 | providers: [ 10 | RecipesResolver, 11 | RecipesService, 12 | DateScalar, 13 | { 14 | provide: APP_FILTER, 15 | useClass: UnauthorizedFilter, 16 | }, 17 | ], 18 | }) 19 | export class RecipesModule {} 20 | -------------------------------------------------------------------------------- /tests/code-first/recipes/recipes.resolver.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UseGuards } from '@nestjs/common'; 2 | import { 3 | Args, 4 | Mutation, 5 | Parent, 6 | Query, 7 | ResolveField, 8 | Resolver, 9 | Subscription, 10 | } from '../../../lib'; 11 | import { AuthGuard } from '../common/guards/auth.guard'; 12 | import { FilterRecipesCountArgs } from './dto/filter-recipes-count.args'; 13 | import { NewRecipeInput } from './dto/new-recipe.input'; 14 | import { RecipesArgs } from './dto/recipes.args'; 15 | import { Ingredient } from './models/ingredient'; 16 | import { IRecipe, Recipe } from './models/recipe'; 17 | import { RecipesService } from './recipes.service'; 18 | import { SearchResultUnion } from './unions/search-result.union'; 19 | import { Category } from './models/category'; 20 | 21 | @Resolver((of) => Recipe) 22 | export class RecipesResolver { 23 | constructor(private readonly recipesService: RecipesService) {} 24 | 25 | @UseGuards(AuthGuard) 26 | @Query((returns) => IRecipe, { description: 'get recipe by id' }) 27 | async recipe( 28 | @Args('id', { 29 | defaultValue: '1', 30 | description: 'recipe id', 31 | }) 32 | id: string, 33 | ): Promise { 34 | const recipe = await this.recipesService.findOneById(id); 35 | if (!recipe) { 36 | throw new NotFoundException(id); 37 | } 38 | return recipe; 39 | } 40 | 41 | @Query((returns) => [SearchResultUnion], { deprecationReason: 'test' }) 42 | async search(): Promise> { 43 | return [ 44 | new Recipe({ title: 'recipe' }), 45 | new Ingredient({ 46 | name: 'test', 47 | }), 48 | ]; 49 | } 50 | 51 | @Query((returns) => [Category]) 52 | categories() { 53 | return [new Category({ name: 'Category #1' })]; 54 | } 55 | 56 | @Query((returns) => [Recipe]) 57 | recipes(@Args() recipesArgs: RecipesArgs): Promise { 58 | return this.recipesService.findAll(recipesArgs); 59 | } 60 | 61 | @Mutation((returns) => Recipe) 62 | async addRecipe( 63 | @Args('newRecipeData') newRecipeData: NewRecipeInput, 64 | ): Promise { 65 | const recipe = await this.recipesService.create(newRecipeData); 66 | // pubSub.publish('recipeAdded', { recipeAdded: recipe }); 67 | return recipe; 68 | } 69 | 70 | @ResolveField('ingredients', () => [Ingredient]) 71 | getIngredients(@Parent() root) { 72 | return [new Ingredient({ name: 'cherry' })]; 73 | } 74 | 75 | @ResolveField((type) => Number) 76 | count(@Args() filters: FilterRecipesCountArgs) { 77 | return 10; 78 | } 79 | 80 | @ResolveField() 81 | rating(): number { 82 | return 10; 83 | } 84 | 85 | @Mutation((returns) => Boolean) 86 | async removeRecipe(@Args('id') id: string) { 87 | return this.recipesService.remove(id); 88 | } 89 | 90 | @Subscription((returns) => Recipe, { 91 | description: 'subscription description', 92 | }) 93 | recipeAdded() { 94 | // return pubSub.asyncIterator('recipeAdded'); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/code-first/recipes/recipes.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { NewRecipeInput } from './dto/new-recipe.input'; 3 | import { RecipesArgs } from './dto/recipes.args'; 4 | import { Recipe } from './models/recipe'; 5 | 6 | @Injectable() 7 | export class RecipesService { 8 | /** 9 | * MOCK 10 | * Put some real business logic here 11 | * Left for demonstration purposes 12 | */ 13 | 14 | async create(data: NewRecipeInput): Promise { 15 | return { 16 | id: 3, 17 | ...data, 18 | } as any; 19 | } 20 | 21 | async findOneById(id: string): Promise { 22 | return {} as any; 23 | } 24 | 25 | async findAll(recipesArgs: RecipesArgs): Promise { 26 | return [ 27 | new Recipe({ 28 | id: '1', 29 | title: 'Pizza', 30 | creationDate: new Date(), 31 | }), 32 | new Recipe({ 33 | id: '2', 34 | title: 'Spaghetti', 35 | creationDate: new Date(), 36 | }), 37 | ] as Recipe[]; 38 | } 39 | 40 | async remove(id: string): Promise { 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/code-first/recipes/unions/search-result.union.ts: -------------------------------------------------------------------------------- 1 | import { createUnionType } from '../../../../lib'; 2 | import { Ingredient } from '../models/ingredient'; 3 | import { Recipe } from '../models/recipe'; 4 | 5 | export const SearchResultUnion = createUnionType({ 6 | name: 'SearchResultUnion', 7 | description: 'Search result description', 8 | types: () => [Ingredient, Recipe], 9 | resolveType: value => { 10 | if ('name' in value) { 11 | return Ingredient; 12 | } 13 | if ('title' in value) { 14 | return Recipe; 15 | } 16 | return undefined; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /tests/e2e/code-first.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from '@nestjs/platform-fastify'; 6 | import { Test } from '@nestjs/testing'; 7 | import * as request from 'supertest'; 8 | import { ApplicationModule } from '../code-first/app.module'; 9 | 10 | describe('Code-first', () => { 11 | let app: INestApplication; 12 | 13 | beforeEach(async () => { 14 | const module = await Test.createTestingModule({ 15 | imports: [ApplicationModule], 16 | }).compile(); 17 | 18 | app = module.createNestApplication( 19 | new FastifyAdapter(), 20 | ); 21 | 22 | await app.init(); 23 | 24 | await app.getHttpAdapter().getInstance().ready(); 25 | }); 26 | 27 | it('should return the categories result', () => { 28 | return request(app.getHttpServer()) 29 | .post('/graphql') 30 | .send({ 31 | operationName: null, 32 | variables: {}, 33 | query: ` 34 | { 35 | categories { 36 | name 37 | description 38 | tags 39 | } 40 | }`, 41 | }) 42 | .expect(200, { 43 | data: { 44 | categories: [ 45 | { 46 | name: 'Category #1', 47 | description: 'default value', 48 | tags: [], 49 | }, 50 | ], 51 | }, 52 | }); 53 | }); 54 | 55 | it('should return the search result', () => { 56 | return request(app.getHttpServer()) 57 | .post('/graphql') 58 | .send({ 59 | operationName: null, 60 | variables: {}, 61 | query: ` 62 | { 63 | search { 64 | ... on Recipe { 65 | title 66 | } 67 | ... on Ingredient { 68 | name 69 | } 70 | } 71 | }`, 72 | }) 73 | .expect(200, { 74 | data: { 75 | search: [ 76 | { 77 | title: 'recipe', 78 | }, 79 | { 80 | name: 'test', 81 | }, 82 | ], 83 | }, 84 | }); 85 | }); 86 | 87 | it(`should return query result`, () => { 88 | return request(app.getHttpServer()) 89 | .post('/graphql') 90 | .send({ 91 | operationName: null, 92 | variables: {}, 93 | query: ` 94 | { 95 | recipes { 96 | id, 97 | ingredients { 98 | name 99 | }, 100 | rating, 101 | averageRating 102 | } 103 | }`, 104 | }) 105 | .expect(200, { 106 | data: { 107 | recipes: [ 108 | { 109 | id: '1', 110 | ingredients: [ 111 | { 112 | name: 'cherry', 113 | }, 114 | ], 115 | rating: 10, 116 | averageRating: 0.5, 117 | }, 118 | { 119 | id: '2', 120 | ingredients: [ 121 | { 122 | name: 'cherry', 123 | }, 124 | ], 125 | rating: 10, 126 | averageRating: 0.5, 127 | }, 128 | ], 129 | }, 130 | }); 131 | }); 132 | 133 | it(`should return query result`, () => { 134 | return request(app.getHttpServer()) 135 | .post('/graphql') 136 | .send({ 137 | operationName: null, 138 | variables: {}, 139 | query: ` 140 | { 141 | recipes { 142 | id, 143 | ingredients { 144 | name 145 | }, 146 | rating, 147 | averageRating 148 | } 149 | }`, 150 | }) 151 | .expect(200, { 152 | data: { 153 | recipes: [ 154 | { 155 | id: '1', 156 | ingredients: [ 157 | { 158 | name: 'cherry', 159 | }, 160 | ], 161 | rating: 10, 162 | averageRating: 0.5, 163 | }, 164 | { 165 | id: '2', 166 | ingredients: [ 167 | { 168 | name: 'cherry', 169 | }, 170 | ], 171 | rating: 10, 172 | averageRating: 0.5, 173 | }, 174 | ], 175 | }, 176 | }); 177 | }); 178 | 179 | afterEach(async () => { 180 | await app.close(); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /tests/utils/introspection-schema.utils.ts: -------------------------------------------------------------------------------- 1 | import { IntrospectionObjectType, IntrospectionSchema } from 'graphql'; 2 | 3 | export function getQuery( 4 | introspectionSchema: IntrospectionSchema, 5 | ): IntrospectionObjectType { 6 | return introspectionSchema.types.find( 7 | item => item.name === introspectionSchema.queryType.name, 8 | ) as IntrospectionObjectType; 9 | } 10 | 11 | export function getMutation( 12 | introspectionSchema: IntrospectionSchema, 13 | ): IntrospectionObjectType { 14 | return introspectionSchema.types.find( 15 | item => item.name === introspectionSchema.mutationType.name, 16 | ) as IntrospectionObjectType; 17 | } 18 | 19 | export function getSubscription( 20 | introspectionSchema: IntrospectionSchema, 21 | ): IntrospectionObjectType { 22 | return introspectionSchema.types.find( 23 | item => item.name === introspectionSchema.subscriptionType.name, 24 | ) as IntrospectionObjectType; 25 | } 26 | 27 | export function getQueryByName( 28 | introspectionSchema: IntrospectionSchema, 29 | name: string, 30 | ) { 31 | const queryType = getQuery(introspectionSchema); 32 | return queryType.fields.find(item => item.name === name); 33 | } 34 | 35 | export function getMutationByName( 36 | introspectionSchema: IntrospectionSchema, 37 | name: string, 38 | ) { 39 | const mutationType = getMutation(introspectionSchema); 40 | return mutationType.fields.find(item => item.name === name); 41 | } 42 | 43 | export function getSubscriptionByName( 44 | introspectionSchema: IntrospectionSchema, 45 | name: string, 46 | ) { 47 | const subscriptionType = getSubscription(introspectionSchema); 48 | return subscriptionType.fields.find(item => item.name === name); 49 | } 50 | -------------------------------------------------------------------------------- /tests/utils/printed-schema.snapshot.ts: -------------------------------------------------------------------------------- 1 | export const printedSchemaSnapshot = `# ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | """example interface""" 6 | interface IRecipe { 7 | id: ID! 8 | title: String! 9 | } 10 | 11 | """recipe object type""" 12 | type Recipe implements IRecipe { 13 | id: ID! 14 | title: String! 15 | description: String 16 | creationDate: DateTime! 17 | averageRating: Float! 18 | lastRate: Float 19 | tags: [String!]! 20 | ingredients: [Ingredient!]! 21 | count(type: String, status: String): Float! 22 | rating: Float! 23 | } 24 | 25 | """ 26 | A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. 27 | """ 28 | scalar DateTime 29 | 30 | """orphaned type""" 31 | type SampleOrphanedType { 32 | id: ID! 33 | title: String! 34 | description: String 35 | creationDate: DateTime! 36 | averageRating: Float! 37 | } 38 | 39 | type Ingredient { 40 | id: ID! 41 | 42 | """ingredient name""" 43 | name: String @deprecated(reason: "is deprecated") 44 | } 45 | 46 | type Category { 47 | name: String! 48 | description: String! 49 | tags: [String!]! 50 | } 51 | 52 | type Query { 53 | move(direction: Direction!): Direction! 54 | 55 | """get recipe by id""" 56 | recipe( 57 | """recipe id""" 58 | id: String = "1" 59 | ): IRecipe! 60 | search: [SearchResultUnion!]! @deprecated(reason: "test") 61 | categories: [Category!]! 62 | recipes( 63 | """number of items to skip""" 64 | skip: Int = 0 65 | take: Int = 25 66 | ): [Recipe!]! 67 | } 68 | 69 | """The basic directions""" 70 | enum Direction { 71 | Up 72 | Down 73 | Left 74 | Right 75 | } 76 | 77 | """Search result description""" 78 | union SearchResultUnion = Ingredient | Recipe 79 | 80 | type Mutation { 81 | addRecipe(newRecipeData: NewRecipeInput!): Recipe! 82 | removeRecipe(id: String!): Boolean! 83 | } 84 | 85 | """new recipe input""" 86 | input NewRecipeInput { 87 | """recipe title""" 88 | title: String! 89 | description: String 90 | ingredients: [String!]! 91 | } 92 | 93 | type Subscription { 94 | """subscription description""" 95 | recipeAdded: Recipe! 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@mirco312312/nest-fgql": ["*"] 5 | }, 6 | "module": "commonjs", 7 | "declaration": true, 8 | "noImplicitAny": false, 9 | "importHelpers": true, 10 | "removeComments": true, 11 | "noLib": false, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es6", 15 | "sourceMap": false, 16 | "outDir": "./dist", 17 | "rootDir": "./lib", 18 | "baseUrl": "./", 19 | "skipLibCheck": true 20 | }, 21 | "include": ["lib/**/*"], 22 | "exclude": ["node_modules", "**/*.spec.ts"] 23 | } 24 | --------------------------------------------------------------------------------