Repository: MichalLytek/type-graphql Branch: master Commit: 0a35c2b8244b Files: 873 Total size: 2.5 MB Directory structure: gitextract_cv2dczwf/ ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── documentation-issue-or-request.md │ │ └── feature_request.md │ ├── configs/ │ │ └── changelog.json │ ├── dependabot.yml │ └── workflows/ │ ├── check.yml │ ├── codeql.yml │ ├── license.yml │ ├── release.yml │ ├── sponsor.yml │ └── website.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .lintstagedrc ├── .markdownlint.json ├── .markdownlintignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .shellcheckrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchmarks/ │ ├── .eslintrc │ ├── array/ │ │ ├── graphql-js/ │ │ │ ├── async.ts │ │ │ └── standard.ts │ │ ├── results.txt │ │ ├── run.ts │ │ └── type-graphql/ │ │ ├── async-field-resolvers.ts │ │ ├── simple-resolvers.ts │ │ ├── standard.ts │ │ ├── sync-field-resolvers.ts │ │ ├── sync-getters.ts │ │ └── with-global-middleware.ts │ ├── simple/ │ │ ├── graphql-js.ts │ │ ├── results.txt │ │ ├── run.ts │ │ └── type-graphql.ts │ └── tsconfig.json ├── cspell.json ├── docs/ │ ├── README.md │ ├── authorization.md │ ├── aws-lambda.md │ ├── azure-functions.md │ ├── bootstrap.md │ ├── browser-usage.md │ ├── complexity.md │ ├── custom-decorators.md │ ├── dependency-injection.md │ ├── directives.md │ ├── emit-schema.md │ ├── enums.md │ ├── esm.md │ ├── examples.md │ ├── extensions.md │ ├── faq.md │ ├── generic-types.md │ ├── getting-started.md │ ├── inheritance.md │ ├── installation.md │ ├── interfaces.md │ ├── introduction.md │ ├── middlewares.md │ ├── migration-guide.md │ ├── nestjs.md │ ├── performance.md │ ├── prisma.md │ ├── resolvers.md │ ├── scalars.md │ ├── subscriptions.md │ ├── types-and-fields.md │ ├── unions.md │ └── validation.md ├── examples/ │ ├── .eslintrc │ ├── README.md │ ├── apollo-cache/ │ │ ├── cache-control.ts │ │ ├── examples.graphql │ │ ├── helpers/ │ │ │ ├── RequireAtLeastOne.d.ts │ │ │ └── getTime.ts │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ ├── apollo-federation/ │ │ ├── accounts/ │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ ├── resolver.ts │ │ │ ├── user.reference.ts │ │ │ └── user.ts │ │ ├── examples.graphql │ │ ├── helpers/ │ │ │ ├── buildFederatedSchema.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── inventory/ │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ ├── product.reference.ts │ │ │ ├── product.ts │ │ │ └── resolver.ts │ │ ├── products/ │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ ├── product.reference.ts │ │ │ ├── product.ts │ │ │ └── resolver.ts │ │ ├── reviews/ │ │ │ ├── index.ts │ │ │ ├── product/ │ │ │ │ ├── index.ts │ │ │ │ ├── product.ts │ │ │ │ └── resolver.ts │ │ │ ├── review/ │ │ │ │ ├── data.ts │ │ │ │ ├── index.ts │ │ │ │ ├── resolver.ts │ │ │ │ └── review.ts │ │ │ └── user/ │ │ │ ├── index.ts │ │ │ ├── resolver.ts │ │ │ └── user.ts │ │ └── schema.graphql │ ├── apollo-federation-2/ │ │ ├── accounts/ │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ ├── resolver.ts │ │ │ ├── user.reference.ts │ │ │ └── user.ts │ │ ├── examples.graphql │ │ ├── helpers/ │ │ │ └── buildFederatedSchema.ts │ │ ├── index.ts │ │ ├── inventory/ │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ ├── product.reference.ts │ │ │ ├── product.ts │ │ │ └── resolver.ts │ │ ├── products/ │ │ │ ├── data.ts │ │ │ ├── dining.ts │ │ │ ├── index.ts │ │ │ ├── product.reference.ts │ │ │ ├── product.ts │ │ │ ├── resolver.ts │ │ │ └── seating.ts │ │ ├── reviews/ │ │ │ ├── index.ts │ │ │ ├── product/ │ │ │ │ ├── product.ts │ │ │ │ └── resolver.ts │ │ │ ├── review/ │ │ │ │ ├── data.ts │ │ │ │ ├── resolver.ts │ │ │ │ └── review.ts │ │ │ └── user/ │ │ │ ├── resolver.ts │ │ │ └── user.ts │ │ └── schema.graphql │ ├── authorization/ │ │ ├── auth-checker.ts │ │ ├── context.type.ts │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ ├── schema.graphql │ │ └── user.type.ts │ ├── automatic-validation/ │ │ ├── examples.graphql │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── recipe.input.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ ├── recipes.arguments.ts │ │ └── schema.graphql │ ├── custom-validation/ │ │ ├── examples.graphql │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── recipe.input.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ ├── recipes.arguments.ts │ │ └── schema.graphql │ ├── enums-and-unions/ │ │ ├── cook.data.ts │ │ ├── cook.type.ts │ │ ├── difficulty.enum.ts │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.type.ts │ │ ├── resolver.ts │ │ ├── schema.graphql │ │ └── search-result.union.ts │ ├── extensions/ │ │ ├── context.type.ts │ │ ├── examples.graphql │ │ ├── helpers/ │ │ │ └── config.extractors.ts │ │ ├── index.ts │ │ ├── log-message.decorator.ts │ │ ├── logger.middleware.ts │ │ ├── logger.service.ts │ │ ├── recipe.data.ts │ │ ├── recipe.type.ts │ │ ├── resolver.ts │ │ ├── schema.graphql │ │ └── user.type.ts │ ├── generic-types/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── paginated-response.type.ts │ │ ├── recipe.data.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ ├── graphql-scalars/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.input.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ ├── interfaces-inheritance/ │ │ ├── employee/ │ │ │ ├── employee.input.ts │ │ │ ├── employee.type.ts │ │ │ └── index.ts │ │ ├── examples.graphql │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── person/ │ │ │ ├── index.ts │ │ │ ├── person.input.ts │ │ │ ├── person.interface.ts │ │ │ └── person.type.ts │ │ ├── resolver.ts │ │ ├── resource/ │ │ │ ├── index.ts │ │ │ └── resource.interface.ts │ │ ├── schema.graphql │ │ └── student/ │ │ ├── index.ts │ │ ├── student.input.ts │ │ └── student.type.ts │ ├── middlewares-custom-decorators/ │ │ ├── context.type.ts │ │ ├── decorators/ │ │ │ ├── current-user.ts │ │ │ ├── index.ts │ │ │ ├── random-id-arg.ts │ │ │ └── validate-args.ts │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── middlewares/ │ │ │ ├── error-logger.ts │ │ │ ├── index.ts │ │ │ ├── log-access.ts │ │ │ ├── number-interceptor.ts │ │ │ └── resolve-time.ts │ │ ├── recipe/ │ │ │ ├── index.ts │ │ │ ├── recipe.args.ts │ │ │ ├── recipe.data.ts │ │ │ ├── recipe.resolver.ts │ │ │ └── recipe.type.ts │ │ ├── schema.graphql │ │ └── user.type.ts │ ├── mikro-orm/ │ │ ├── context.type.ts │ │ ├── entities/ │ │ │ ├── index.ts │ │ │ ├── rating.ts │ │ │ ├── recipe.ts │ │ │ └── user.ts │ │ ├── examples.graphql │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── resolvers/ │ │ │ ├── index.ts │ │ │ ├── rating.resolver.ts │ │ │ ├── recipe.resolver.ts │ │ │ └── types/ │ │ │ ├── index.ts │ │ │ ├── rating.input.ts │ │ │ └── recipe.input.ts │ │ └── schema.graphql │ ├── mixin-classes/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── inputs/ │ │ │ ├── amend.user.input.ts │ │ │ ├── create.user.input.ts │ │ │ └── index.ts │ │ ├── mixins/ │ │ │ ├── index.ts │ │ │ ├── with.id.ts │ │ │ └── with.password.ts │ │ ├── resolver.ts │ │ ├── schema.graphql │ │ └── types/ │ │ ├── index.ts │ │ ├── user.details.ts │ │ └── user.ts │ ├── query-complexity/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ ├── redis-subscriptions/ │ │ ├── comment.input.ts │ │ ├── comment.type.ts │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── pubsub.ts │ │ ├── recipe.data.ts │ │ ├── recipe.resolver.args.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ ├── resolvers-inheritance/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── person/ │ │ │ ├── index.ts │ │ │ ├── person.resolver.ts │ │ │ ├── person.role.ts │ │ │ └── person.type.ts │ │ ├── recipe/ │ │ │ ├── index.ts │ │ │ ├── recipe.resolver.ts │ │ │ └── recipe.type.ts │ │ ├── resource/ │ │ │ ├── index.ts │ │ │ ├── resource.args.ts │ │ │ ├── resource.resolver.ts │ │ │ ├── resource.service.factory.ts │ │ │ ├── resource.service.ts │ │ │ └── resource.ts │ │ └── schema.graphql │ ├── simple-subscriptions/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── notification.resolver.ts │ │ ├── notification.type.ts │ │ ├── pubsub.ts │ │ └── schema.graphql │ ├── simple-usage/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.input.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ ├── tsconfig.json │ ├── tsyringe/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.input.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.service.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ ├── typegoose/ │ │ ├── .eslintrc │ │ ├── context.type.ts │ │ ├── entities/ │ │ │ ├── index.ts │ │ │ ├── rating.ts │ │ │ ├── recipe.ts │ │ │ └── user.ts │ │ ├── examples.graphql │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── object-id.scalar.ts │ │ ├── resolvers/ │ │ │ ├── index.ts │ │ │ ├── rating.resolver.ts │ │ │ ├── recipe.resolver.ts │ │ │ └── types/ │ │ │ ├── index.ts │ │ │ ├── rating.input.ts │ │ │ └── recipe.input.ts │ │ ├── schema.graphql │ │ ├── typegoose.middleware.ts │ │ └── types.ts │ ├── typeorm-basic-usage/ │ │ ├── context.type.ts │ │ ├── datasource.ts │ │ ├── entities/ │ │ │ ├── index.ts │ │ │ ├── rating.ts │ │ │ ├── recipe.ts │ │ │ └── user.ts │ │ ├── examples.graphql │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── resolvers/ │ │ │ ├── index.ts │ │ │ ├── rating.resolver.ts │ │ │ ├── recipe.resolver.ts │ │ │ └── types/ │ │ │ ├── index.ts │ │ │ ├── rating.input.ts │ │ │ └── recipe.input.ts │ │ └── schema.graphql │ ├── typeorm-lazy-relations/ │ │ ├── context.type.ts │ │ ├── datasource.ts │ │ ├── entities/ │ │ │ ├── index.ts │ │ │ ├── rating.ts │ │ │ ├── recipe.ts │ │ │ └── user.ts │ │ ├── examples.graphql │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── resolvers/ │ │ │ ├── index.ts │ │ │ ├── recipe.resolver.ts │ │ │ └── types/ │ │ │ ├── index.ts │ │ │ ├── rating.input.ts │ │ │ └── recipe.input.ts │ │ └── schema.graphql │ ├── using-container/ │ │ ├── examples.graphql │ │ ├── index.ts │ │ ├── recipe.data.ts │ │ ├── recipe.input.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.service.ts │ │ ├── recipe.type.ts │ │ └── schema.graphql │ └── using-scoped-container/ │ ├── context.type.ts │ ├── examples.graphql │ ├── index.ts │ ├── logger.ts │ ├── recipe/ │ │ ├── recipe.data.ts │ │ ├── recipe.input.ts │ │ ├── recipe.resolver.ts │ │ ├── recipe.service.ts │ │ └── recipe.type.ts │ └── schema.graphql ├── images/ │ └── payid_pookies.avif ├── jest.config.cts ├── package.json ├── scripts/ │ ├── .eslintrc │ ├── markdown.ts │ ├── package.json.ts │ ├── tsconfig.json │ └── version.ts ├── sponsorkit.config.mts ├── src/ │ ├── @types/ │ │ └── Reflect.d.ts │ ├── decorators/ │ │ ├── Arg.ts │ │ ├── Args.ts │ │ ├── ArgsType.ts │ │ ├── Authorized.ts │ │ ├── Ctx.ts │ │ ├── Directive.ts │ │ ├── Extensions.ts │ │ ├── Field.ts │ │ ├── FieldResolver.ts │ │ ├── Info.ts │ │ ├── InputType.ts │ │ ├── InterfaceType.ts │ │ ├── Mutation.ts │ │ ├── ObjectType.ts │ │ ├── Query.ts │ │ ├── Resolver.ts │ │ ├── Root.ts │ │ ├── Subscription.ts │ │ ├── UseMiddleware.ts │ │ ├── createMethodMiddlewareDecorator.ts │ │ ├── createParameterDecorator.ts │ │ ├── createResolverClassMiddlewareDecorator.ts │ │ ├── enums.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── unions.ts │ ├── errors/ │ │ ├── CannotDetermineGraphQLTypeError.ts │ │ ├── ConflictingDefaultValuesError.ts │ │ ├── GeneratingSchemaError.ts │ │ ├── InterfaceResolveTypeError.ts │ │ ├── InvalidDirectiveError.ts │ │ ├── MissingPubSubError.ts │ │ ├── MissingSubscriptionTopicsError.ts │ │ ├── NoExplicitTypeError.ts │ │ ├── ReflectMetadataMissingError.ts │ │ ├── SymbolKeysNotSupportedError.ts │ │ ├── UnionResolveTypeError.ts │ │ ├── UnmetGraphQLPeerDependencyError.ts │ │ ├── WrongNullableListOptionError.ts │ │ ├── graphql/ │ │ │ ├── ArgumentValidationError.ts │ │ │ ├── AuthenticationError.ts │ │ │ ├── AuthorizationError.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── helpers/ │ │ ├── auth-middleware.ts │ │ ├── decorators.ts │ │ ├── filesystem.ts │ │ ├── findType.ts │ │ ├── isThrowing.ts │ │ ├── params.ts │ │ ├── resolver-metadata.ts │ │ ├── returnTypes.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── index.ts │ ├── metadata/ │ │ ├── definitions/ │ │ │ ├── authorized-metadata.ts │ │ │ ├── class-metadata.ts │ │ │ ├── directive-metadata.ts │ │ │ ├── enum-metadata.ts │ │ │ ├── extensions-metadata.ts │ │ │ ├── field-metadata.ts │ │ │ ├── index.ts │ │ │ ├── interface-class-metadata.ts │ │ │ ├── middleware-metadata.ts │ │ │ ├── object-class-metadata.ts │ │ │ ├── param-metadata.ts │ │ │ ├── resolver-metadata.ts │ │ │ └── union-metadata.ts │ │ ├── getMetadataStorage.ts │ │ ├── index.ts │ │ ├── metadata-storage.ts │ │ └── utils.ts │ ├── resolvers/ │ │ ├── convert-args.ts │ │ ├── create.ts │ │ ├── helpers.ts │ │ └── validate-arg.ts │ ├── scalars/ │ │ ├── aliases.ts │ │ └── index.ts │ ├── schema/ │ │ ├── build-context.ts │ │ ├── definition-node.ts │ │ ├── schema-generator.ts │ │ └── utils.ts │ ├── shim.ts │ ├── typings/ │ │ ├── Complexity.ts │ │ ├── ResolverInterface.ts │ │ ├── SubscribeResolverData.ts │ │ ├── SubscriptionHandlerData.ts │ │ ├── TypeResolver.ts │ │ ├── ValidatorFn.ts │ │ ├── auth-checker.ts │ │ ├── index.ts │ │ ├── legacy-decorators.ts │ │ ├── middleware.ts │ │ ├── resolver-data.ts │ │ ├── resolvers-map.ts │ │ ├── subscriptions.ts │ │ └── utils/ │ │ ├── ClassType.ts │ │ ├── Constructor.ts │ │ ├── Except.ts │ │ ├── IsEqual.ts │ │ ├── Maybe.ts │ │ ├── MaybePromise.ts │ │ ├── MergeExclusive.ts │ │ ├── NonEmptyArray.ts │ │ ├── SetRequired.ts │ │ ├── Simplify.ts │ │ └── index.ts │ └── utils/ │ ├── buildSchema.ts │ ├── buildTypeDefsAndResolvers.ts │ ├── container.ts │ ├── createResolversMap.ts │ ├── emitSchemaDefinitionFile.ts │ ├── graphql-version.ts │ ├── index.ts │ └── isPromiseLike.ts ├── tests/ │ ├── .eslintrc │ ├── functional/ │ │ ├── authorization.ts │ │ ├── circular-refs.ts │ │ ├── default-nullable.ts │ │ ├── default-values.ts │ │ ├── deprecation.ts │ │ ├── description.ts │ │ ├── directives.ts │ │ ├── emit-schema-sdl.ts │ │ ├── enums.ts │ │ ├── errors/ │ │ │ └── metadata-polyfill.ts │ │ ├── extensions.ts │ │ ├── fields.ts │ │ ├── generic-types.ts │ │ ├── inputtype-enumerable-properties.ts │ │ ├── interface-resolvers-args.ts │ │ ├── interfaces-and-inheritance.ts │ │ ├── ioc-container.ts │ │ ├── manual-decorators.ts │ │ ├── metadata-storage.ts │ │ ├── middlewares.ts │ │ ├── nested-interface-inheritance.ts │ │ ├── peer-dependency.ts │ │ ├── query-complexity.ts │ │ ├── resolvers.ts │ │ ├── scalars.ts │ │ ├── simple-resolvers.ts │ │ ├── subscriptions.ts │ │ ├── typedefs-resolvers.ts │ │ ├── unions.ts │ │ └── validation.ts │ ├── helpers/ │ │ ├── circular-refs/ │ │ │ ├── good/ │ │ │ │ ├── CircularRef1.ts │ │ │ │ └── CircularRef2.ts │ │ │ └── wrong/ │ │ │ ├── CircularRef1.ts │ │ │ └── CircularRef2.ts │ │ ├── customScalar.ts │ │ ├── directives/ │ │ │ ├── TestDirective.ts │ │ │ └── assertValidDirective.ts │ │ ├── expectToThrow.ts │ │ ├── getInnerFieldType.ts │ │ ├── getSampleObjectFieldType.ts │ │ ├── getSchemaInfo.ts │ │ ├── getTypeField.ts │ │ └── sleep.ts │ └── tsconfig.json ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json ├── tsconfig.typings.json └── website/ ├── .gitignore ├── blog/ │ ├── 2018-03-25-medium-article.md │ └── 2020-08-19-devto-article.md ├── core/ │ └── Footer.js ├── i18n/ │ └── en.json ├── package.json ├── pages/ │ ├── en/ │ │ ├── help.js │ │ ├── index.js │ │ ├── users.js │ │ └── versions.js │ └── snippets/ │ ├── object-type.md │ ├── testability.md │ ├── typeorm.md │ └── validation.md ├── sidebars.json ├── siteConfig.js ├── static/ │ ├── css/ │ │ ├── custom.css │ │ └── prism-theme.css │ └── img/ │ └── payid_pookies.avif ├── versioned_docs/ │ ├── version-0.16.0/ │ │ ├── authorization.md │ │ ├── bootstrap.md │ │ ├── browser-usage.md │ │ ├── complexity.md │ │ ├── dependency-injection.md │ │ ├── emit-schema.md │ │ ├── enums.md │ │ ├── examples.md │ │ ├── faq.md │ │ ├── getting-started.md │ │ ├── interfaces-and-inheritance.md │ │ ├── introduction.md │ │ ├── middlewares.md │ │ ├── resolvers.md │ │ ├── scalars.md │ │ ├── subscriptions.md │ │ ├── types-and-fields.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-0.17.0/ │ │ ├── authorization.md │ │ ├── bootstrap.md │ │ ├── browser-usage.md │ │ ├── complexity.md │ │ ├── dependency-injection.md │ │ ├── emit-schema.md │ │ ├── enums.md │ │ ├── examples.md │ │ ├── faq.md │ │ ├── generic-types.md │ │ ├── getting-started.md │ │ ├── inheritance.md │ │ ├── installation.md │ │ ├── interfaces.md │ │ ├── introduction.md │ │ ├── middlewares.md │ │ ├── resolvers.md │ │ ├── scalars.md │ │ ├── subscriptions.md │ │ ├── types-and-fields.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-0.17.1/ │ │ ├── authorization.md │ │ ├── bootstrap.md │ │ ├── browser-usage.md │ │ ├── complexity.md │ │ ├── dependency-injection.md │ │ ├── emit-schema.md │ │ ├── enums.md │ │ ├── examples.md │ │ ├── faq.md │ │ ├── generic-types.md │ │ ├── getting-started.md │ │ ├── inheritance.md │ │ ├── installation.md │ │ ├── interfaces.md │ │ ├── introduction.md │ │ ├── middlewares.md │ │ ├── resolvers.md │ │ ├── scalars.md │ │ ├── subscriptions.md │ │ ├── types-and-fields.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-0.17.2/ │ │ ├── examples.md │ │ ├── getting-started.md │ │ ├── interfaces.md │ │ ├── introduction.md │ │ ├── types-and-fields.md │ │ └── unions.md │ ├── version-0.17.4/ │ │ ├── custom-decorators.md │ │ ├── examples.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── subscriptions.md │ │ └── unions.md │ ├── version-0.17.5/ │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── enums.md │ │ ├── examples.md │ │ ├── faq.md │ │ ├── generic-types.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── subscriptions.md │ │ ├── types-and-fields.md │ │ └── unions.md │ ├── version-0.17.6/ │ │ └── custom-decorators.md │ ├── version-1.0.0/ │ │ ├── bootstrap.md │ │ ├── browser-usage.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── directives.md │ │ ├── emit-schema.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── getting-started.md │ │ ├── installation.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── nestjs.md │ │ ├── performance.md │ │ ├── prisma.md │ │ ├── resolvers.md │ │ ├── scalars.md │ │ ├── subscriptions.md │ │ ├── types-and-fields.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-1.1.0/ │ │ ├── enums.md │ │ ├── examples.md │ │ └── validation.md │ ├── version-1.1.1/ │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── performance.md │ │ ├── resolvers.md │ │ ├── subscriptions.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-1.2.0-rc.1/ │ │ ├── authorization.md │ │ ├── bootstrap.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── directives.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── performance.md │ │ ├── resolvers.md │ │ ├── subscriptions.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-2.0.0-beta.3/ │ │ ├── authorization.md │ │ ├── bootstrap.md │ │ ├── browser-usage.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── directives.md │ │ ├── emit-schema.md │ │ ├── enums.md │ │ ├── esm.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── faq.md │ │ ├── generic-types.md │ │ ├── getting-started.md │ │ ├── inheritance.md │ │ ├── installation.md │ │ ├── interfaces.md │ │ ├── introduction.md │ │ ├── middlewares.md │ │ ├── nestjs.md │ │ ├── performance.md │ │ ├── prisma.md │ │ ├── resolvers.md │ │ ├── scalars.md │ │ ├── subscriptions.md │ │ ├── types-and-fields.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-2.0.0-beta.4/ │ │ ├── authorization.md │ │ ├── aws-lambda.md │ │ ├── browser-usage.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── directives.md │ │ ├── emit-schema.md │ │ ├── esm.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── inheritance.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── migration-guide.md │ │ ├── resolvers.md │ │ ├── subscriptions.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-2.0.0-beta.6/ │ │ ├── authorization.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── inheritance.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── resolvers.md │ │ ├── subscriptions.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-2.0.0-rc.1/ │ │ ├── authorization.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── inheritance.md │ │ ├── installation.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── migration-guide.md │ │ ├── resolvers.md │ │ ├── subscriptions.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-2.0.0-rc.2/ │ │ ├── authorization.md │ │ ├── azure-functions.md │ │ ├── browser-usage.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── inheritance.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── resolvers.md │ │ ├── subscriptions.md │ │ ├── unions.md │ │ └── validation.md │ ├── version-2.0.0-rc.3/ │ │ ├── authorization.md │ │ ├── complexity.md │ │ ├── custom-decorators.md │ │ ├── dependency-injection.md │ │ ├── emit-schema.md │ │ ├── examples.md │ │ ├── extensions.md │ │ ├── generic-types.md │ │ ├── inheritance.md │ │ ├── installation.md │ │ ├── interfaces.md │ │ ├── middlewares.md │ │ ├── resolvers.md │ │ ├── subscriptions.md │ │ ├── unions.md │ │ └── validation.md │ └── version-2.0.0-rc.4/ │ ├── authorization.md │ ├── complexity.md │ ├── custom-decorators.md │ ├── dependency-injection.md │ ├── examples.md │ ├── extensions.md │ ├── generic-types.md │ ├── inheritance.md │ ├── interfaces.md │ ├── middlewares.md │ ├── resolvers.md │ ├── subscriptions.md │ ├── unions.md │ └── validation.md ├── versioned_sidebars/ │ ├── version-0.16.0-sidebars.json │ ├── version-0.17.0-sidebars.json │ ├── version-0.17.4-sidebars.json │ ├── version-1.0.0-sidebars.json │ ├── version-2.0.0-beta.3-sidebars.json │ ├── version-2.0.0-beta.4-sidebars.json │ └── version-2.0.0-rc.2-sidebars.json └── versions.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ **/build/ **/dist/ **/coverage/ **/node_modules/ jest.config.cts sponsorkit.config.mts # FIXME: Remove website/ ================================================ FILE: .eslintrc ================================================ { "root": true, "env": { "node": true }, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module", "project": [ "./tsconfig.json", "./benchmarks/tsconfig.json", "./examples/tsconfig.json", "./scripts/tsconfig.json", "./tests/tsconfig.json" ] }, "settings": { "import/parsers": { "@typescript-eslint/parser": [".ts"] }, "import/resolver": { "typescript": { "alwaysTryTypes": true, "project": [ "./tsconfig.json", "./benchmarks/tsconfig.json", "./examples/tsconfig.json", "./scripts/tsconfig.json", "./tests/tsconfig.json" ] } } }, "reportUnusedDisableDirectives": true, "plugins": ["import", "@typescript-eslint", "eslint-plugin-tsdoc", "jest"], "extends": [ "airbnb-base", "airbnb-typescript/base", "eslint:recommended", "plugin:@cspell/recommended", "plugin:@typescript-eslint/recommended", // "plugin:@typescript-eslint/recommended-type-checked", "plugin:@typescript-eslint/stylistic", // "plugin:@typescript-eslint/stylistic-type-checked", "plugin:import/recommended", "plugin:import/typescript", "plugin:jest/recommended", "prettier" ], "rules": { "tsdoc/syntax": "warn", "semi": "off", "@typescript-eslint/semi": "error", "no-restricted-syntax": "off", "curly": ["error", "all"], "nonblock-statement-body-position": ["error", "below"], "sort-imports": ["error", { "ignoreDeclarationSort": true }], "import/order": [ "error", { "alphabetize": { "caseInsensitive": true, "order": "asc" }, "groups": ["builtin", "external", "internal", ["sibling", "parent"], "index", "unknown"], "newlines-between": "never", "pathGroups": [ { "pattern": "@/**", "group": "internal", "position": "before" }, { "pattern": "type-graphql", "group": "external" } ], "pathGroupsExcludedImportTypes": ["builtin"] } ], "import/no-default-export": "error", "import/prefer-default-export": "off", "no-unused-vars": "off", "no-duplicate-imports": "error", "@typescript-eslint/no-unused-vars": [ "error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" } ], "@typescript-eslint/array-type": ["error", { "default": "array-simple" }], "@typescript-eslint/consistent-type-imports": [ "error", { "disallowTypeAnnotations": false, "fixStyle": "inline-type-imports", "prefer": "type-imports" } ], "@typescript-eslint/consistent-type-exports": "error", "@typescript-eslint/consistent-type-definitions": ["error", "interface"], "@typescript-eslint/no-inferrable-types": [ "error", { "ignoreParameters": true, "ignoreProperties": true } ], // FIXME: Remove "@typescript-eslint/ban-types": [ "error", { "types": { "Function": false, "Object": false, "{}": false }, "extendDefaults": true } ], // FIXME: Remove "@typescript-eslint/no-explicit-any": "off" } } ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf # Source code *.bash text eol=lf *.bat text eol=crlf *.cmd text eol=crlf *.coffee text *.css text diff=css *.htm text diff=html *.html text diff=html *.inc text *.ini text *.js text *.json text *.jsx text *.less text *.ls text *.map text -diff *.od text *.onlydata text *.php text diff=php *.pl text *.ps1 text eol=crlf *.py text diff=python *.rb text diff=ruby *.sass text *.scm text *.scss text diff=css *.sh text eol=lf *.sql text *.styl text *.tag text *.ts text *.tsx text *.xml text *.xhtml text diff=html # Docker Dockerfile text # Documentation *.ipynb text *.markdown text diff=markdown *.md text diff=markdown *.mdwn text diff=markdown *.mdown text diff=markdown *.mkd text diff=markdown *.mkdn text diff=markdown *.mdtxt text *.mdtext text *.txt text AUTHORS text CHANGELOG text CHANGES text CONTRIBUTING text COPYING text copyright text *COPYRIGHT* text INSTALL text license text LICENSE text NEWS text readme text *README* text TODO text # Templates *.dot text *.ejs text *.erb text *.haml text *.handlebars text *.hbs text *.hbt text *.jade text *.latte text *.mustache text *.njk text *.phtml text *.svelte text *.tmpl text *.tpl text *.twig text *.vue text # Configs *.cnf text *.conf text *.config text .editorconfig text .env text .gitattributes text .gitconfig text .htaccess text *.lock text -diff package.json text eol=lf package-lock.json text -diff pnpm-lock.yaml text eol=lf -diff .prettierrc text yarn.lock text -diff *.toml text *.yaml text *.yml text browserslist text Makefile text makefile text # Heroku Procfile text # Graphics *.ai binary *.bmp binary *.eps binary *.gif binary *.gifv binary *.ico binary *.jng binary *.jp2 binary *.jpg binary *.jpeg binary *.jpx binary *.jxr binary *.pdf binary *.png binary *.psb binary *.psd binary *.svg text *.svgz binary *.tif binary *.tiff binary *.wbmp binary *.webp binary # Audio *.kar binary *.m4a binary *.mid binary *.midi binary *.mp3 binary *.ogg binary *.ra binary # Video *.3gpp binary *.3gp binary *.as binary *.asf binary *.asx binary *.avi binary *.fla binary *.flv binary *.m4v binary *.mng binary *.mov binary *.mp4 binary *.mpeg binary *.mpg binary *.ogv binary *.swc binary *.swf binary *.webm binary # Archives *.7z binary *.gz binary *.jar binary *.rar binary *.tar binary *.zip binary # Fonts *.ttf binary *.eot binary *.otf binary *.woff binary *.woff2 binary # Executables *.exe binary *.pyc binary # RC files *.*rc text # Ignore files *.*ignore text ================================================ FILE: .github/CODEOWNERS ================================================ * @MichalLytek ================================================ FILE: .github/FUNDING.yml ================================================ github: typegraphql open_collective: typegraphql ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Something works incorrectly or doesn't work at all --- **Describe the Bug** A clear and concise description of what the bug is. **To Reproduce** A quick guide how to reproduce the bug. You can paste here code snippets or even better, provide a link to the repository with minimal reproducible code example. **Expected Behavior** A clear and concise description of what you expected to happen. **Logs** If applicable, add some console logs to help explain your problem. You can paste the errors with stack trace that were printed when the error occurred. **Environment (please complete the following information):** - OS: [e.g. Windows] - Node (e.g. 10.5.0) - Package version [e.g. 0.12.2] (please check if the bug still exist in newest release) - TypeScript version (e.g. 2.8.2) **Additional Context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Question or help request url: https://github.com/MichalLytek/type-graphql/discussions about: Github Discussions is the place to ask a question or discuss with other community members - name: Prisma 2 integration url: https://github.com/MichalLytek/typegraphql-prisma about: All problems or a questions about `typegraphql-prisma` package should be placed in the separate repository ================================================ FILE: .github/ISSUE_TEMPLATE/documentation-issue-or-request.md ================================================ --- name: Documentation issue or request about: There's something wrong in docs or something is missing --- **Describe the issue** A clear and concise description of what is wrong or what feature is missing. You may ask here for guides, e.g. "How to run TypeGraphQL on AWS Lamda?" if nobody helped you on Github Discussions or StackOverflow. **Are you able to make a PR that fix this?** If you can, it would be great if you create a pull request that fixes the docs, fills the gap with new chapter or new code example. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: You want to suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. You can also propose how the new API should looks like. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/configs/changelog.json ================================================ { "categories": [ { "title": "## 🚀 Enhancements", "labels": ["Enhancement 🆕"] }, { "title": "## 🐛 Fixes", "labels": ["Bugfix 🐛 🔨"] }, { "title": "## 🧪 Tests", "labels": ["Test 🧪"] }, { "title": "## 📦 Dependencies", "labels": ["Dependencies 📦"] }, { "title": "## 📚 Documentation", "labels": ["Documentation 📖"] }, { "title": "## 🏠 Internal", "labels": ["Chore 🔨", "Internal 🏠"] } ] } ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "npm" directory: "/" versioning-strategy: increase schedule: interval: "weekly" ignore: - dependency-name: "*" update-types: ["version-update:semver-patch"] - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/check.yml ================================================ name: check on: push: branches: - master tags: - v* pull_request: branches: - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: version: name: Ensure package version match if: startsWith(github.ref_name, 'v') runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check Git tag format env: TYPE_GRAPHQL_REF_NAME: ${{ github.ref_name }} run: | _tag="$TYPE_GRAPHQL_REF_NAME" if ! printf "%s\n" "$_tag" | grep -q -P '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(alpha|beta|rc)\.(0|[1-9][0-9]*))?$'; then printf '[ERROR]: Git tag (%s) wrong format\n' "$_tag" exit 1 fi - name: Read package.json version uses: sergeysova/jq-action@v2 id: version_package with: cmd: jq --raw-output .version package.json - name: Read GitHub version uses: pozetroninc/github-action-get-latest-release@master id: version_v_github with: owner: MichalLytek repo: type-graphql excludes: prerelease, draft - name: Remove leading v* from GitHub version id: version_github env: TYPE_GRAPHQL_VERSION: ${{ steps.version_v_github.outputs.release }} run: | _version="$TYPE_GRAPHQL_VERSION" printf 'value=%s\n' "${_version#?}" >> "$GITHUB_OUTPUT" - name: Read Git tag version id: version_gittag env: TYPE_GRAPHQL_REF_NAME: ${{ github.ref_name }} run: | _version="$TYPE_GRAPHQL_REF_NAME" printf 'value=%s\n' "${_version#?}" >> "$GITHUB_OUTPUT" - name: Compare package.json with Git tag uses: madhead/semver-utils@latest id: comparison_package_gittag with: version: ${{ steps.version_package.outputs.value }} compare-to: ${{ steps.version_gittag.outputs.value }} lenient: false - name: Compare Git tag with GitHub uses: madhead/semver-utils@latest id: comparison_gittag_github with: version: ${{ steps.version_gittag.outputs.value }} compare-to: ${{ steps.version_github.outputs.value }} lenient: false - name: Check package.json == Git tag env: TYPE_GRAPHQL_COMPARISON: ${{ steps.comparison_package_gittag.outputs.comparison-result }} TYPE_GRAPHQL_VERSION_PACKAGE: ${{ steps.version_package.outputs.value }} TYPE_GRAPHQL_VERSION_TAG: ${{ steps.version_gittag.outputs.value }} run: | if [ ! "$TYPE_GRAPHQL_COMPARISON" = "=" ]; then printf '[ERROR]: package.json (%s) != Git tag (%s)\n' "$TYPE_GRAPHQL_VERSION_PACKAGE" "$TYPE_GRAPHQL_VERSION_TAG" exit 1 fi - name: Check Git tag > GitHub env: TYPE_GRAPHQL_COMPARISON: ${{ steps.comparison_gittag_github.outputs.comparison-result }} TYPE_GRAPHQL_VERSION_TAG: ${{ steps.version_gittag.outputs.value }} TYPE_GRAPHQL_VERSION_GITHUB: ${{ steps.version_github.outputs.value }} run: | if [ ! "$TYPE_GRAPHQL_COMPARISON" = ">" ]; then printf '[ERROR]: Git tag (%s) !> GitHub (%s)\n' "$TYPE_GRAPHQL_VERSION_TAG" "$TYPE_GRAPHQL_VERSION_GITHUB" exit 1 fi check: name: Build & Lint & Test needs: version if: always() && (needs.version.result == 'success' || needs.version.result == 'skipped') runs-on: ubuntu-latest strategy: fail-fast: true matrix: node-version: [20.x, 22.x, 24.x, 25.x] steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install Dependencies run: | npm ci - name: Build run: | npm run build npm run build:benchmarks - name: Check run: | npm run check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Test run: npm run test:ci env: CI: true - name: Upload code coverage uses: codecov/codecov-action@v4 if: matrix.node-version == '24.x' ================================================ FILE: .github/workflows/codeql.yml ================================================ name: codeql on: push: branches: - master pull_request: branches: - master schedule: - cron: "0 0 * * 0" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: - javascript-typescript steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: security-and-quality config: | paths-ignore: - "**/node_modules" - "**/*.test.ts" - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/license.yml ================================================ name: license on: schedule: - cron: "0 0 1 1 *" concurrency: group: ${{ github.workflow }} cancel-in-progress: true jobs: license: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - uses: FantasticFiasco/action-update-license-year@v3 with: token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: workflow_run: workflows: - check types: - completed permissions: id-token: write # Required for OIDC contents: read concurrency: group: ${{ github.workflow }} cancel-in-progress: true jobs: release: name: Release package on NPM runs-on: ubuntu-latest if: github.event.workflow_run.conclusion == 'success' && github.ref_name == 'master' && startsWith(github.event.workflow_run.head_branch, 'v') permissions: contents: write id-token: write steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_branch }} - name: Determine if version is prerelease id: prerelease env: TYPE_GRAPHQL_VERSION: ${{ github.event.workflow_run.head_branch }} run: | _prerelease= if printf "%s\n" "$TYPE_GRAPHQL_VERSION" | grep -q -P '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then _prerelease=false else _prerelease=true fi printf 'value=%s\n' "$_prerelease" >> "$GITHUB_OUTPUT" - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 24.x registry-url: "https://registry.npmjs.org" - name: Install latest npm run: | npm install -g npm@latest - name: Install Dependencies run: | npm ci - name: Prepare package run: | npm run prepublishOnly env: TYPE_GRAPHQL_REF: ${{ github.event.workflow_run.head_branch }} - name: Build Changelog id: changelog uses: mikepenz/release-changelog-builder-action@v5 with: configuration: "./.github/configs/changelog.json" failOnError: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.workflow_run.head_branch }} body: ${{ steps.changelog.outputs.changelog }} prerelease: ${{ steps.prerelease.outputs.value == 'true' }} - name: Publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} TYPE_GRAPHQL_PRERELEASE: ${{ steps.prerelease.outputs.value }} run: | _tag= if [ "$TYPE_GRAPHQL_PRERELEASE" = "true" ]; then _tag="next" else _tag="latest" fi npm publish --ignore-scripts --tag "$_tag" ================================================ FILE: .github/workflows/sponsor.yml ================================================ name: sponsor on: schedule: - cron: "0 0 * * *" concurrency: group: ${{ github.workflow }} cancel-in-progress: true jobs: sponsor: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "24.x" - name: Install Dependencies run: | npm ci - name: Regenerate sponsors images run: npm run gen:sponsorkit env: SPONSORKIT_GITHUB_TOKEN: ${{ secrets.SPONSORKIT_GITHUB_TOKEN }} # SPONSORKIT_OPENCOLLECTIVE_KEY: ${{ secrets.SPONSORKIT_GITHUB_TOKEN_OPENCOLLECTIVE_TOKEN }} - name: Commit updated images uses: EndBug/add-and-commit@v9 with: add: "img/github-sponsors.svg website/static/img/github-sponsors.svg" message: "chore(sponsors): update sponsors image" push: true committer_name: github-actions[bot] committer_email: github-actions[bot]@users.noreply.github.com env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/website.yml ================================================ name: website on: workflow_run: workflows: - check types: - completed concurrency: group: ${{ github.workflow }} cancel-in-progress: true jobs: website: name: Publish website runs-on: ubuntu-latest if: github.event.workflow_run.conclusion == 'success' && github.ref_name == 'master' && (github.event.workflow_run.head_branch == 'master' || startsWith(github.event.workflow_run.head_branch, 'v')) steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_branch }} fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 24.x - name: Install Dependencies run: | npm ci npm ci --prefix ./website - name: Build run: | npm run build --prefix ./website - name: Publish uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./website/build/type-graphql user_name: "github-actions[bot]" user_email: "github-actions[bot]@users.noreply.github.com" full_commit_message: | Deploy website based on ${{ github.event.workflow_run.head_sha }} ================================================ FILE: .gitignore ================================================ # Node.js modules **/node_modules/ # Builded sources **/build/ **/dist/ # Coverage **/coverage/ # IntelliJ stuffs .idea/ # Parcel cache .cache .parcel-cache # Sponsorkit cache ./images/.cache.json # Environments .env # Archives *.tar.gz *.tgz ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh # shellcheck disable=SC1007 source=SCRIPTDIR/_/husky.sh . "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/_/husky.sh" npx --no -- lint-staged ================================================ FILE: .lintstagedrc ================================================ { "**/*.ts": ["eslint --fix", "prettier --write"], "**/*.md": ["markdownlint --fix", "prettier --write"], "!**/*.{ts,md}": "prettier --write --ignore-unknown" } ================================================ FILE: .markdownlint.json ================================================ { "default": true, "line-length": false, "no-blanks-blockquote": false } ================================================ FILE: .markdownlintignore ================================================ **/build/ **/dist/ **/coverage/ **/node_modules/ website/blog/2018-03-25-medium-article.md website/blog/2020-08-19-devto-article.md website/versioned_docs/* # FIXME: Remove CHANGELOG.md ================================================ FILE: .npmrc ================================================ engine-strict=true ================================================ FILE: .nvmrc ================================================ 25 ================================================ FILE: .prettierignore ================================================ **/build/ **/dist/ **/coverage/ **/node_modules/ /.husky/_/ /.gitattributes website/versioned_docs website/versioned_sidebars ================================================ FILE: .prettierrc ================================================ { "endOfLine": "lf", "trailingComma": "all", "tabWidth": 2, "printWidth": 100, "bracketSpacing": true, "semi": true, "singleQuote": false, "arrowParens": "avoid", "useTabs": false } ================================================ FILE: .shellcheckrc ================================================ external-sources=true ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "bierner.comment-tagged-templates", "DavidAnson.vscode-markdownlint", "dbaeumer.vscode-eslint", "EditorConfig.EditorConfig", "esbenp.prettier-vscode", "GitHub.vscode-github-actions", "GraphQL.vscode-graphql", "streetsidesoftware.code-spell-checker", "timonwong.shellcheck", "tlent.jest-snapshot-language-support", "wayou.vscode-todo-highlight" ] } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Run example project", "type": "node", "request": "launch", "cwd": "${workspaceFolder}/examples/${input:exampleProjectName}", "args": ["./index.ts"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], "skipFiles": ["/**"] } ], "inputs": [ { "id": "exampleProjectName", "description": "Choose an example to run", "type": "pickString", // TODO: add new examples here "options": [ "apollo-cache", "apollo-federation", "apollo-federation-2", "authorization", "automatic-validation", "custom-validation", "enums-and-unions", "extensions", "generic-types", "graphql-scalars", "interfaces-inheritance", "middlewares-custom-decorators", "mikro-orm", "mixin-classes", "query-complexity", "redis-subscriptions", "resolvers-inheritance", "simple-subscriptions", "simple-usage", "tsyringe", "typegoose", "typeorm-basic-usage", "typeorm-lazy-relations", "using-container", "using-scoped-container" ] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "typescript.tsdk": "${workspaceFolder}/node_modules/typescript/lib", "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" } } ================================================ FILE: CHANGELOG.md ================================================ # Changelog and release notes ## Unreleased ### Fixes - support resolver inheritance with dynamic field resolvers name by matching field resolvers by schemaName instead of methodName (#1806) ## v2.0.0-rc.4 ### Fixes - prevent duplicated params when building schema multiple times (#1803) ## v2.0.0-rc.3 ### Features - optimize performance of building metadata storage with HashMap caching for O(1) lookups (#1779) ### Fixes - prevent enumerable undefined properties in input types instances (#1789) - add missing support for GraphQL extensions in interface types (#1776) ### Others - **Breaking Change**: update `graphql-js` peer dependency to `^16.12.0` - **Breaking Change**: drop support for Node.js < 20.11.1 - **Breaking Change**: update `graphql-scalars` peer dependency to `^1.25.0` - **Breaking Change**: update `class-validator` peer dependency to `>=0.14.3` ## v2.0.0-rc.2 ### Features - support declaring middlewares on resolver class level (#620) - support declaring auth roles on resolver class level (#620) - make possible creating custom decorators on resolver class level - `createResolverClassMiddlewareDecorator` - support registering custom arg decorator via `createParameterDecorator` and its second argument `CustomParameterOptions` - `arg` (#1325) ### Fixes - properly build multiple schemas with generic resolvers, args and field resolvers (#1321) ### Others - **Breaking Change**: update `graphql-scalars` peer dependency to `^1.23.0` - **Breaking Change**: rename `createMethodDecorator` into `createMethodMiddlewareDecorator` - **Breaking Change**: rename `createParamDecorator` to `createParameterDecorator` ## v2.0.0-rc.1 ### Features - support other `Reflect` polyfills than `reflect-metadata` by checking only used `Reflect` API (#1102) ### Fixes - properly override fields of `@ArgsType` classes in deeply nested inheritance chain (#1644) - allow for leading spaces and multiline directives definitions in `@Directive` decorator (#1423) ## v2.0.0-beta.6 ### Fixes - allow overriding field resolver method with different `name` arguments (#1284) - allow object type's `name` argument string contain a double underscore (`__`) when using `buildTypeDefsAndResolvers()` (#1309) ### Others - **Breaking Change**: update `graphql-scalars` peer dependency to `^1.22.4` - properly configure esm build pipeline to publish working esm version of the package ## v2.0.0-beta.4 ### Features - **Breaking Change**: expose shim as a package entry point `type-graphql/shim` (and `/node_modules/type-graphql/build/typings/shim.ts`) - **Breaking Change**: update `graphql-js` peer dependency to `^16.8.1` - **Breaking Change**: use `@graphql-yoga` instead of `graphql-subscriptions` as the subscriptions engine - **Breaking Change**: require providing `PubSub` implementation into `buildSchema` option when using `@Subscription` - **Breaking Change**: remove `@PubSub` in favor of directly importing created `PubSub` implementation - **Breaking Change**: remove `Publisher` and `PubSubEngine` types - **Breaking Change**: rename interface `ResolverFilterData` into `SubscriptionHandlerData` and `ResolverTopicData` into `SubscribeResolverData` - support defining directives on `@Field` of `@Args` - support defining directives on inline `@Arg` - allow passing custom validation function as `validateFn` option of `@Arg` and `@Args` decorators - add support for dynamic topic id function in `@Subscription` decorator option ## v2.0.0-beta.3 ### Features - **Breaking Change**: update `graphql-js` peer dependency to `^16.7.1` - **Breaking Change**: upgrade `ArgumentValidationError` and replace `UnauthorizedError` and `ForbiddenError` with `AuthenticationError`, `AuthorizationError` that are extending `GraphQLError` to let the error details be accessible in the `extensions` property - **Breaking Change**: change `ClassType` constraint from `ClassType` to `ClassType` in order to make it work properly with new TS features - **Breaking Change**: remove `dateScalarMode` option from `buildSchema` - **Breaking Change**: make `graphql-scalars` package a peer dependency and use date scalars from it instead of custom ones - **Breaking Change**: exported `GraphQLISODateTime` scalar has now a name `DateTimeISO` - **Breaking Change**: change `ValidatorFn` signature from `ValidatorFn` to `ValidatorFn` - support custom validation function getting resolver data on validate - bring compatibility with the ESM ecosystem by exposing the double bundle of the `type-graphql` package (CJS and ESM versions) ### Fixes - allow `ValidatorFn` to accept array of values (instead of only `object | undefined`) ## v2.0.0-beta.2 ### Features - **Breaking Change**: `AuthChecker` type is now "function or class" - update to `AuthCheckerFn` if the function form is needed in the code - **Breaking Change**: update `graphql-js` peer dependency to `^16.6.0` - **Breaking Change**: `buildSchemaSync` is now also checking the generated schema for errors - **Breaking Change**: `validate` option of `buildSchema` is set to `false` by default - integration with `class-validator` has to be turned on explicitly - **Breaking Change**: `validate` option of `buildSchema` doesn't accept anymore a custom validation function - use `validateFn` option instead - support class-based auth checker, which allows for dependency injection - allow defining directives for interface types and theirs fields, with inheritance for object types fields (#744) - allow deprecating input fields and args (#794) - support disabling inferring default values (#793) - support readonly arrays for roles of `@Authorized` decorator (#935) - add sync version of `buildTypeDefsAndResolvers` function (#803) - lift restriction of listing all interfaces from inheritance chain in `implements` option of `@ObjectType` decorator (#1425) ### Fixes - **Breaking Change**: properly emit types nullability when `defaultValue` is provided and remove `ConflictingDefaultWithNullableError` error (#751) - allow defining extension on field resolver level for fields also defined as a property of the class (#776) - fix throwing error when schema with dynamic default value was built again (#787) - fix converting inputs with fields of nested array type (#801) - disable broken exposing input types fields under a changed name via `@Field({ name: "..." })` - support overwriting fields of extended types (#1109) - properly execute args validation for nullable items array (#1328) ### Others - **Breaking Change**: update `class-validator` peer dependency to `>=0.14.0` - **Breaking Change**: change build config to ES2021 - drop support for Node.js < 16.16.0 - **Breaking Change**: remove support for loading resolvers by glob paths (`resolvers: string[]` build schema option) - **Breaking Change**: remove `isAbstract` legacy decorator option - **Breaking Change**: remove the `commentDescriptions` option from `PrintSchemaOptions` (no more support for `#` comments in SDL by GraphQL v16) ## v1.1.1 ### Fixes - fix crashing when of union's or interface type's `resolveType` function returns `undefined` or `null` (#731) - fix crashing when no reflected type available for fields with params decorators (#724) - fix not registering object types implementing interface type when interface type is used as object type field type (#736) - properly transform nested array of input type classes (#737) ## v1.1.0 ### Features - allow passing custom validation function as `validate` option to `buildSchema` - support defining deprecation reason and description of enum members (#714) ### Fixes - **Breaking Change**: throw error when wrong type of value provided as arg or input for `GraphQLISODateTime` and `GraphQLTimestamp` scalars - don't include in schema the fields declared as `@FieldResolver` when that resolvers classes aren't provided in `resolvers` array - fix grammar in `CannotDetermineGraphQLTypeError` error message - properly inherit extensions from parent class and its fields ## v1.0.0 ### Features - **Breaking Change**: emit in schema only types actually used by provided resolvers classes (#415) - **Breaking Change**: update `graphql-js` peer dependency to `^15.3.0` - **Breaking Change**: update `graphql-query-complexity` dependency to `^0.7.0` and drop support for `fieldConfigEstimator` (use `fieldExtensionsEstimator` instead) - **Breaking Change**: introduce `sortedSchema` option in `PrintSchemaOptions` and emit sorted schema file by default - **Breaking Change**: make `class-validator` a peer dependency of version `>=0.12.0` that needs to be installed manually (#366) - **Breaking Change**: remove `CannotDetermineTypeError` and make other error messages more detailed and specific - **Breaking Change**: remove legacy array inference - now explicit array syntax (`[Item]`) is required - update `TypeResolver` interface to match with `GraphQLTypeResolver` from `graphql-js` - add basic support for directives with `@Directive()` decorator (#369) - add possibility to tune up the performance and disable auth & middlewares stack for simple field resolvers (#479) - optimize resolvers execution paths to speed up a lot basic scenarios (#488) - add `@Extensions` decorator for putting metadata into GraphQL types config (#521) - add support for defining arguments and implementing resolvers for interface types fields (#579) - add `{ autoRegisterImplementations: false }` option to prevent automatic emitting in schema all the object types that implements used interface type (#595) - allow interfaces to implement other interfaces (#602) - expose `createResolversMap` utility that generates apollo-like resolvers object - support IoC containers which `.get()` method returns a `Promise` of resolver instance - update deps to newest major versions (`tslib`, `graphql-query-complexity`) ### Fixes - **Breaking Change**: stop returning null for `GraphQLTimestamp` and `GraphQLISODateTime` scalars when returned value is not a `Date` instance - now it throws explicit error instead - **Breaking Change**: fix transforming and validating nested inputs and arrays (#462) - refactor union types function syntax handling to prevent possible errors with circular refs - remove duplicated entries for resolver classes that use inheritance (#499) - fix using `name` option on interface fields (#567) - fix not calling `authChecker` during subscribe phase for subscriptions (#578) - fix using shared union type in multiple schemas - fix using shared interface type in multiple schemas - fix calling field resolver without providing resolver class to `buildSchema` - fix generated TS union type for union type of object type classes extending themselves (#587) - fix using shared union and interface types in multiple schemas when `resolveType` is used - properly inherit directives while extending `@InputType` or `@ObjectType` classes (#626) - skip transforming empty array items into input classes ### Others - **Breaking Change**: change build config to ES2018 - drop support for Node.js < 10.3 - **Breaking Change**: remove deprecated `DepreciationOptions` interface - **Breaking Change**: remove deprecated direct array syntax for declaring union types ## v0.17.6 ### Fixes - fix leaking resolver source code in `MissingSubscriptionTopicsError` error message (#489) ## v0.17.5 ### Features - rename `DepreciationOptions` interface to `DeprecationOptions` and deprecate the old one - update deps to newest minor versions (`tslib`, `semver`, `graphql-query-complexity` and `glob`) - support nested array types (`@Field(type => [[Int]])`) (#393) - deprecate the direct array syntax for union types ### Fixes - fix errors on circular refs in union types (#364) by adding the function syntax (`() => TClassTypes`) ## v0.17.4 ### Features - add support for creating custom parameter decorators (#329) - allow to provide custom `subscribe` function in `@Subscription` decorator (#328) ## v0.17.3 ### Features - update packages `semver` to `^6.0.0` and `graphql-subscriptions` to `^1.1.0` ### Fixes - fix broken compatibility with newer `@types/graphql` due to using removed private types (e.g. `MaybePromise`) (#320) ## v0.17.2 ### Features - add support for defining `resolveType` function for interfaces and unions (#319) - add support for setting default nullability for fields and return types (#297) - add `skipCheck` option in `buildSchema` to disable checking the correctness of a schema - add postinstall script for printing info on console about supporting the project ### Fixes - fix generating plain resolvers for queries and mutations (compatibility with Apollo client state) ## v0.17.1 ### Features - add support for emitting schema file in not existing directory (#269) - drop support for Node.js v6 (end of LTS in April 2019) ### Fixes - fix typings discovery support for WebStorm (#276) - allow for returning plain objects when using `ObjectType`s that implements `InterfaceType`s or extends other classes (#160) ## v0.17.0 ### Features - **Breaking Change**: make `graphql-js` packages a peer dependencies, bump `graphql` to `^14.1.1` and `@types/graphql` to `^14.0.7` (#239) - **Breaking Change**: remove `useContainer` function and allow to register container by `buildSchema` options (#241) - **Breaking Change**: change the default `PrintSchemaOptions` option `commentDescriptions` to false (no more `#` comments in SDL) - add support for passing `PrintSchemaOptions` in `buildSchema.emitSchemaFile` (e.g. `commentDescriptions: true` to restore previous behavior) - add `buildTypeDefsAndResolvers` utils function for generating apollo-like `typeDefs` and `resolvers` pair (#233) - add support for generic types (#255) ### Fixes - **Breaking Change**: remove the `formatArgumentValidationError` helper as it's not compatible and not needed in new Apollo Server (#258) - fix calling return type getter function `@Field(type => Foo)` before finishing module evaluation (allow for extending circular classes using `require`) - fix nullifying other custom method decorators - call the method on target instance, not the stored reference to original function (#247) - fix throwing error when extending non args class in the `@ArgsType()` class - prevent unnecessary conversion of an object that is already an instance of the requested type (avoid constructor side-effects) ## v0.16.0 ### Features - add support for default values in schema (#203) - add support for lists with nullable items (#211) ### Fixes - fix browser shim (compatibility with polyfills for decorator support) ## v0.15.0 ### Features - **Breaking Change**: upgrade `graphql` to `^14.0.2`, `graphql-subscriptions` to `^1.0.0` and `@types/graphql` to `^14.0.2` - update all other dependencies - drop support for Node.js v9 - add capability to emit the schema definition file (\*.gql) as a `buildSchema` option - add `emitSchemaDefinitionFile` helper function for emitting the schema SDL ## v0.14.0 ### Features - **Breaking Change**: change `ClassType` type and export it in package index - **Breaking Change**: refactor generic `createUnionType` to remove the 10 types limit (note: requires TypeScript >=3.0.1) - add support for subscribing to dynamic topics - based on args/ctx/root (#137) - add support for query complexity analysis - integration with `graphql-query-complexity` (#139) ## v0.13.1 ### Fixes - fix missing loosely typed overload signature for `createUnionType` (remove the 10 types limit) ## v0.13.0 ### Features - make `class-validator` a virtual peer dependency and update it to newest `0.9.1` version - add support for creating scoped containers (#113) ## v0.12.3 ### Features - add reflect-metadata checks and informative error if no polyfill provided - update `@types/graphql` to latest version (`^0.13.3`) ### Fixes - fix throwing error when `of => objectType` wasn't provided in abstract resolver class - fix calling `Object.assign` with boolean arguments (#111) ## v0.12.2 ### Features - add support for using type classes in browser (configure webpack to use decorators shim) ### Fixes - fix swallowing false argument value (#101) ## v0.12.1 ### Fixes - fix bug with overriding methods from parent resolver class (#95) ## v0.12.0 ### Features - **Breaking Change**: remove deprecated `ActionData` and `FilterActionData` interfaces - add support for resolver classes inheritance - add `name` decorator option for `@Field` and `@FieldResolver` decorators that allows to set the schema name different than the property name ## v0.11.3 ### Features - make auth checker feature generic typed (default `string` for backward compatibility) ## v0.11.2 ### Features - attach `MetadataStorage` to global scope (support multiple packages/modules) - rename and deprecate `ActionData` and `FilterActionData` interfaces to `ResolverData` and `ResolverFilterData` ## v0.11.1 ### Features - add support for returning null instead of throwing authorization error (`authMode` property of `buildSchema` config) - add support for generating object type field in schema from method with `@FieldResolver` ### Fixes - fix bug when converting object scalars to target class instance (#65) ## v0.11.0 ### Features - add support for creating and attaching middlewares, guards and interceptors to fields and resolvers - **Breaking Change**: remove deprecated decorators with `GraphQL` prefix and `{ array: true }` type option ## v0.10.0 ### Features - add `buildSchemaSync` function to build the schema synchronously (unsafe! without additional errors checks) - update package dependencies - **Breaking Change**: update `@types/graphql` to `0.13.0` ### Fixes - decorator option `validate` is now merged with `buildSchema`'s `validate` config instead of overwriting it ## v0.9.1 ### Fixes - fix bug with extending non-TypeGraphQL classes ## v0.9.0 ### Features - add support for GraphQL subscriptions using `graphql-subscriptions` - update package dependencies - deprecate `{ array: true }` type option ## v0.8.1 ### Features - add `@Info()` decorator for injecting GraphQL resolve info to resolvers - add support for injecting parts of `root` and `context` objects with `@Root("field")` and `@Ctx("field")` decorators ## v0.8.0 ### Features - add base support for GraphQL enums using TypeScript enums - add support for defining GraphQL unions - add support for importing resolvers from file path glob - deprecate decorators with `GraphQL` prefix - use `@ArgsType`, `@InputType`, `@InterfaceType`, `@ObjectType` and `@Resolver` instead ### Fixes - fix not working array type notation in circular dependencies (correct thunk generation) ## v0.7.0 ### Features - add authorization feature - `@Authorized` decorator and `authChecker` function in schema options ([see docs](https://github.com/MichalLytek/type-graphql/blob/master/docs/authorization.md)) - add support for defining array type using mongoose-like notation `[Type]` - **Breaking Change**: remove deprecated `@GraphQLArgumentType` decorator - use `@GraphQLArgsType` instead ## v0.6.0 ### Features - add support for defining GraphQL interfaces and implementing it by object types - add support for extending input, args, object and interface types classes - add support for implementing GraphQL interfaces without decorators duplication - **Breaking Change**: make `buildSchema` async - now it returns a Promise of `GraphQLSchema` - rename and deprecate `@GraphQLArgumentType` decorator - use `@GraphQLArgsType` instead ### Fixes - allow for no args in `@GraphQLResolver` decorator to keep consistency with other resolver classes ## v0.5.0 ### Features - create instance of root object when it's type provided in resolver - change `Date` scalar names to `GraphQLISODateTime` and `GraphQLTimestamp` - support only `Date` objects (instances) serialization in `GraphQLTimestamp` (and in `GraphQLISODateTime` too) - update package dependencies - add test suite with 92%+ coverage ### Fixes - **Breaking change**: switch array `nullable` option behavior from `[Type]!` to `[Type!]` - add more detailed type reflection error message (parameters support) - fix `ResolverInterface` resolver function type (allow additional parameters) - add support for named param in `@GraphQLResolver` lambda and for object class as param ## v0.4.0 ### Features - add basic support for automatic arguments and inputs validation using `class-validator` - add interface `ResolverInterface` for type checking of resolver class methods (field resolvers) - update `graphql` dependency from `^0.12.3` to `^0.13.0` ### Fixes - fix default values for arg/input fields (class property initializers) - use `new` instead of `Object.create` ## v0.3.0 ### Features - add support for descriptions in schema (types, args, queries, etc.) - add support for declaring deprecation reason on object fields and queries/mutations ### Fixes - fix scalars ID alias (GraphQLID not GraphQLString) ## v0.2.0 ### Features - add support for Date type (built-in scalar) - add support for custom scalars (and mapping it to TS types) - change `@Context` decorator name to `@Ctx` ## v0.1.2 ### Fixes - fix missing type args in schema when declared in field resolver - fix missing resolver function when defined as type field method - fix creating instances of root object when internal fields are Promises (switch from `plainToClass` to vanilla JS) - fix converting field and resolvers args errors while converting gql objects (weird `prototype` stuffs) ## v0.1.1 ### Features - add support for omitting return type when use type options, in selected decorators (`@Field`, `@Arg`) ### Fixes - fix class getter resolvers bug - missing fields from prototype (`plainToClass` bug) ## v0.1.0 ### Initial release ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to TypeGraphQL We would love for you to contribute to TypeGraphQL and help make it even better than it is today! As a contributor, here are the guidelines we would like you to follow: - [Question or Problem?](#question) - [Issues and Bugs](#issue) - [Feature Requests](#feature) - [Submission Guidelines](#submit) - [Coding Rules](#rules) - [Commit Message Guidelines](#commit)

Got a Question or Problem?

Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. Instead, consider using [Stack Overflow](https://stackoverflow.com/questions/tagged/typegraphql) to ask support-related questions. When creating a new question on Stack Overflow, make sure to add the `typegraphql` tag. You can also ask community for help using the [Github Discussion platform][discussions].

Found a Bug?

If you find a bug in the source code, you can help us by [submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can [submit a Pull Request](#submit-pr) with a failing test case that reproduces the issue.

Missing a Feature?

You can _request_ a new feature by [submitting an issue](#submit-issue) to our GitHub Repository. If you would like to _implement_ a new feature, please consider the size of the change in order to determine the right steps to proceed: - For a **Major Feature**, first open an issue and outline your proposal so that it can be discussed. This process allows us to better coordinate our efforts, prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project. - **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).

Submission Guidelines

Submitting an Issue

Before you submit an issue, please search the issue tracker. An issue for your problem may already exist and the discussion might inform you of workarounds readily available. You can file new issues by selecting from our [new issue templates](https://github.com/MichalLytek/type-graphql/issues/new/choose) and filling out the issue template.

Submitting a Pull Request (PR)

Before you submit your Pull Request (PR) consider the following guidelines: 1. Search [GitHub](https://github.com/MichalLytek/type-graphql/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate existing efforts. 2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. Discussing the design upfront helps to ensure that we're ready to accept your work. 3. Fork this repo. 4. Make your changes in a new git branch: ```shell git checkout -b my-fix-branch master ``` 5. Create your patch, **including appropriate test cases**. 6. Follow our [Coding Rules](#rules). 7. Run the full test suite, and ensure that all tests pass. 8. Commit your changes using a descriptive commit message that follows our [commit message guidelines](#commit). Adherence to these conventions is necessary because release notes are automatically generated from these messages. ```shell git commit -a ``` Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 9. Push your branch to GitHub: ```shell git push origin my-fix-branch ``` 10. In GitHub, send a pull request to `type-graphql:master`. Make sure to [allow edits from maintainers][allow-maintainer-edits]. If we ask for changes via code reviews then: - Make the required updates. - Re-run the test suites to ensure tests are still passing. - Rebase your branch and force push to your GitHub repository (this will update your Pull Request): ```shell git rebase master -i git push -f ``` That's it! Thank you for your contribution! ### After your pull request is merged After your pull request is merged, you can safely delete your branch and pull the changes from the main (upstream) repository: - Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: ```shell git push origin --delete my-fix-branch ``` - Check out the master branch: ```shell git checkout master -f ``` - Delete the local branch: ```shell git branch -D my-fix-branch ``` - Update your master with the latest upstream version: ```shell git pull --ff upstream master ```

Coding Rules

To ensure consistency throughout the source code, keep these rules in mind as you are working: - All features or bug fixes **must be covered by tests**. - The code must pass type checking and fullfil all the ESLint rules.

Commit Message Guidelines

For more information checkout this [commit rules guide](https://www.conventionalcommits.org/en/v1.0.0). [github]: https://github.com/MichalLytek/type-graphql [discussions]: https://github.com/MichalLytek/type-graphql/discussions [allow-maintainer-edits]: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork#enabling-repository-maintainer-permissions-on-existing-pull-requests ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-2026 Michał Lytek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![logo](./images/logo.png) # TypeGraphQL [![release](https://github.com/MichalLytek/type-graphql/actions/workflows/release.yml/badge.svg)](https://github.com/MichalLytek/type-graphql/actions/workflows/release.yml) [![website](https://github.com/MichalLytek/type-graphql/actions/workflows/website.yml/badge.svg)](https://github.com/MichalLytek/type-graphql/actions/workflows/website.yml) [![codeql](https://github.com/MichalLytek/type-graphql/actions/workflows/codeql.yml/badge.svg)](https://github.com/MichalLytek/type-graphql/actions/workflows/codeql.yml) [![discord](https://img.shields.io/discord/1195751245386875040?logo=discord&color=%237289da)](https://discord.gg/cWnBAQcbg2) [![codecov](https://codecov.io/gh/MichalLytek/type-graphql/branch/master/graph/badge.svg)](https://codecov.io/gh/MichalLytek/type-graphql) [![npm](https://img.shields.io/npm/v/type-graphql?logo=npm&color=%23CC3534)](https://www.npmjs.com/package/type-graphql) [![open collective](https://opencollective.com/typegraphql/tiers/badge.svg)](https://opencollective.com/typegraphql) Create [GraphQL](https://graphql.org) schema and resolvers with [TypeScript](https://www.typescriptlang.org), using classes and decorators! **** [![donate](https://opencollective.com/typegraphql/donate/button.png?color=black)](https://opencollective.com/typegraphql) ## Introduction **TypeGraphQL** makes developing GraphQL APIs an enjoyable process, i.e. by defining the schema using only classes and a bit of decorator magic. So, to create types like object type or input type, we use a kind of DTO class. For example, to declare `Recipe` type we simply create a class and annotate it with decorators: ```ts @ObjectType() class Recipe { @Field(type => ID) id: string; @Field() title: string; @Field(type => [Rate]) ratings: Rate[]; @Field({ nullable: true }) averageRating?: number; } ``` And we get the corresponding part of the schema in SDL: ```graphql type Recipe { id: ID! title: String! ratings: [Rate!]! averageRating: Float } ``` Then we can create queries, mutations and field resolvers. For this purpose, we use controller-like classes that are called "resolvers" by convention. We can also use awesome features like dependency injection and auth guards: ```ts @Resolver(Recipe) class RecipeResolver { // dependency injection constructor(private recipeService: RecipeService) {} @Query(returns => [Recipe]) recipes() { return this.recipeService.findAll(); } @Mutation() @Authorized(Roles.Admin) // auth guard removeRecipe(@Arg("id") id: string): boolean { return this.recipeService.removeById(id); } @FieldResolver() averageRating(@Root() recipe: Recipe) { return recipe.ratings.reduce((a, b) => a + b, 0) / recipe.ratings.length; } } ``` And in this simple way, we get this part of the schema in SDL: ```graphql type Query { recipes: [Recipe!]! } type Mutation { removeRecipe(id: String!): Boolean! } ``` ## Motivation We all know that GraphQL is great and solves many problems we have with REST APIs, like Over-Fetching and Under-Fetching. But developing a GraphQL API in Node.js with TypeScript is sometimes a bit of a pain. Why? Let's take a look at the steps we usually have to take. First, we create all the GraphQL types in `schema.graphql` using SDL. Then we create our data models using [ORM classes](https://github.com/typeorm/typeorm), which represent our DB entities. Then we start to write resolvers for our queries, mutations and fields, but this forces us to first create TS interfaces for all arguments, inputs, and even object types. Only then can we implement the resolvers using weird generic signatures and manually performing common tasks, like validation, authorization and loading dependencies: ```ts export const getRecipesResolver: GraphQLFieldResolver = async ( _, args, ctx, ) => { // common tasks repeatable for almost every resolver const repository = TypeORM.getRepository(Recipe); const auth = Container.get(AuthService); await joi.validate(getRecipesSchema, args); if (!auth.check(ctx.user)) { throw new NotAuthorizedError(); } // our business logic, e.g.: return repository.find({ skip: args.offset, take: args.limit }); }; ``` The biggest problem is redundancy in our codebase, which makes it difficult to keep things in sync. To add a new field to our entity, we have to jump through all the files - modify an entity class, the schema, as well as the interface. The same goes for inputs or arguments. It's easy to forget to update one piece or make a mistake with a single type. Also, what if we've made a typo in the field name? The rename feature (F2) won't work correctly. Tools like [GraphQL Code Generator](https://github.com/dotansimha/graphql-code-generator) or [graphqlgen](https://github.com/prisma/graphqlgen) only solve the first part - they generate the corresponding interfaces (and resolvers skeletons) for our GraphQL schema but they don't fix the schema <--> models redundancy and developer experience (F2 rename won't work, you have to remember about the codegen watch task in the background, etc.), as well as common tasks like validation, authorization, etc. **TypeGraphQL** comes to address these issues, based on experience from a few years of developing GraphQL APIs in TypeScript. The main idea is to have only one source of truth by defining the schema using classes and some help from decorators. Additional features like dependency injection, validation and auth guards help with common tasks that normally we would have to handle ourselves. ## Documentation The documentation, installation guide, and detailed description of the API and all of its features are [available on the website](https://typegraphql.com). ### Getting started A full getting started guide with a simple walkthrough (tutorial) can be found at [getting started docs](https://typegraphql.com/docs/getting-started.html). ### Video tutorial If you prefer video tutorials, you can watch [Ben Awad](https://github.com/benawad)'s [TypeGraphQL video series](https://www.youtube.com/playlist?list=PLN3n1USn4xlma1bBu3Tloe4NyYn9Ko8Gs) on YouTube. ### Examples You can also check the [examples folder](./examples) in this repository for more examples of usage: simple fields resolvers, DI Container support, TypeORM integration, automatic validation, etc. The [Tests folder](./tests) might also give you some tips on how to get various things done. ## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ## The future The currently released version is a stable 1.0.0 release. It is well-tested (97% coverage, ~500 test cases) and has most of the planned features already implemented. Plenty of companies and independent developers are using it in production with success. However, there are also plans for a lot more features like better TypeORM, Prisma and DataLoader integration, custom decorators and metadata annotations support - [the full list of ideas](https://github.com/MichalLytek/type-graphql/issues?q=is%3Aissue+is%3Aopen+label%3A"Enhancement+%3Anew%3A") is available on the GitHub repo. You can also keep track of [development's progress on the project board](https://github.com/MichalLytek/type-graphql/projects/1). If you have any interesting feature requests, feel free to open an issue on GitHub so we can discuss that! ## Support **TypeGraphQL** is an MIT-licensed open-source project. This framework is a result of the tremendous amount of work - sleepless nights, busy evenings and weekends. It doesn't have a large company that sits behind it - its ongoing development is possible only thanks to the support of the community. [![donate](https://opencollective.com/typegraphql/donate/button.png?color=blue)](https://opencollective.com/typegraphql) ### Gold Sponsors 🏆 > Please ask your company to support this open source project by [becoming a gold sponsor](https://opencollective.com/typegraphql/contribute/gold-sponsors-8340) and getting a premium technical support from our core contributors. ### Silver Sponsors 🥈 | [Leofame](https://leofame.com/buy-instagram-followers) | | :---------------------------------------------------------------------------------------------------------: | | [**Leofame**](https://leofame.com/buy-instagram-followers) | ### Bronze Sponsors 🥉 | [live graphic systems](https://www.ligrsystems.com) | [Felix Technologies](https://github.com/joinlifex) | [instinctools](https://instinctools.com/manufacturing) | | :------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------: | | [**Live Graphic Systems**](https://www.ligrsystems.com) | [**Felix Technologies**](https://github.com/joinlifex/) | [**instinctools**](https://instinctools.com/manufacturing) | | [BetWinner](https://guidebook.betwinner.com/) | [BuzzVoice](https://buzzvoice.com/) | [SocialWick](https://www.socialwick.com/) | [C19](https://www.c19.cl/) | [Nove Casino](https://novecasino.net/) | [Play Fortune](https://play-fortune.pl/gry-online/jednoreki-bandyta/) | [MoonKasyno](https://wechoosethemoon.org/kasyna-online/) | [Kasyno Online](https://www.casinobillions.com/pl/) | [FBPostLikes](https://www.fbpostlikes.com/) | | :--------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | | [**BetWinner**](https://guidebook.betwinner.com/) | [**BuzzVoice**](https://buzzvoice.com/) | [**SocialWick**](https://www.socialwick.com/) | [**C19**](https://www.c19.cl/) | [**Nove Casino**](https://novecasino.net/) | [**Play Fortune**](https://play-fortune.pl/gry-online/jednoreki-bandyta/) | [**MoonKasyno**](https://wechoosethemoon.org/kasyna-online/) | [**Kasyno Online**](https://www.casinobillions.com/pl/) | [**FBPostLikes**](https://www.fbpostlikes.com/) | | [SidesMedia](https://sidesmedia.com/) | [Social Followers](https://www.socialfollowers.uk/buy-tiktok-followers/) | [IG Comment](https://igcomment.com/buy-instagram-comments/) | [Twicsy](https://twicsy.com/buy-instagram-followers) | [Buzzoid](https://buzzoid.com/buy-instagram-followers/) | [Views4You](https://views4you.com/) | [PayID Pokies](https://au.trustpilot.com/review/bestpayidpokies.net) | [Fun88](https://global.fun88.com/) | | :------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------: | | [**SidesMedia**](https://sidesmedia.com/) | [**Social Followers**](https://www.socialfollowers.uk/buy-tiktok-followers/) | [**IG Comment**](https://igcomment.com/buy-instagram-comments/) | [**Twicsy**](https://twicsy.com/buy-instagram-followers) | [**Buzzoid**](https://buzzoid.com/buy-instagram-followers/) | [**Views4You**](https://views4you.com/) | [**PayID Pokies**](https://au.trustpilot.com/review/bestpayidpokies.net) | [**Fun88**](https://global.fun88.com/) | [![become a sponsor](https://opencollective.com/static/images/become_sponsor.svg)](https://opencollective.com/typegraphql) ### Members 💪 [![Members](https://opencollective.com/typegraphql/tiers/members.svg?avatarHeight=45&width=320&button=false)](https://opencollective.com/typegraphql#contributors) ### GitHub Sponsors [![GitHub Sponsors](./images/github-sponsors.svg)](https://github.com/sponsors/TypeGraphQL) ## Community - Visit the [Official Website](https://typegraphql.com) - Chat on [Discord](https://discord.gg/cWnBAQcbg2) ## Want to help? Want to file a bug, contribute some code, or improve the documentation? Great! Please read our guidelines for [CONTRIBUTING](./CONTRIBUTING.md) and then check out one of our [help-wanted issues](https://github.com/MichalLytek/type-graphql/labels/Help%20Wanted%20%3Asos%3A). ================================================ FILE: benchmarks/.eslintrc ================================================ { "rules": { "no-console": "off", "max-classes-per-file": "off", "class-methods-use-this": "off", "import/no-extraneous-dependencies": [ "error", { "devDependencies": true } ] } } ================================================ FILE: benchmarks/array/graphql-js/async.ts ================================================ import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, } from "graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; const SampleObjectType: GraphQLObjectType = new GraphQLObjectType({ name: "SampleObject", fields: () => ({ stringField: { type: new GraphQLNonNull(GraphQLString), resolve: async source => source.stringField, }, numberField: { type: new GraphQLNonNull(GraphQLInt), resolve: async source => source.numberField, }, booleanField: { type: new GraphQLNonNull(GraphQLBoolean), resolve: async source => source.booleanField, }, nestedField: { type: SampleObjectType, resolve: async source => source.nestedField, }, }), }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: { multipleNestedObjects: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SampleObjectType))), resolve: () => Array.from({ length: ARRAY_ITEMS }, (_, index) => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, }, })), }, }, }), }); runBenchmark(schema).catch(console.error); ================================================ FILE: benchmarks/array/graphql-js/standard.ts ================================================ import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, } from "graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; const SampleObjectType: GraphQLObjectType = new GraphQLObjectType({ name: "SampleObject", fields: () => ({ stringField: { type: new GraphQLNonNull(GraphQLString), }, numberField: { type: new GraphQLNonNull(GraphQLInt), }, booleanField: { type: new GraphQLNonNull(GraphQLBoolean), }, nestedField: { type: SampleObjectType, }, }), }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: { multipleNestedObjects: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SampleObjectType))), resolve: () => Array.from({ length: ARRAY_ITEMS }, (_, index) => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, }, })), }, }, }), }); runBenchmark(schema).catch(console.error); ================================================ FILE: benchmarks/array/results.txt ================================================ Core i7 2700K @ 3.5GHz Windows 10 x64 25 000 array items | 50 iterations Node.js v13.5 ----- TypeGraphQL standard - 15.518s using sync field resolvers - 18.180s using async field resolvers - 39.934s using getters - 31.207s standard with global middleware - 62.664s with `simpleResolvers: true` - 14.980s ----- `graphql-js` standard - 13.276s async field resolvers - 25.630s ================================================ FILE: benchmarks/array/run.ts ================================================ import { type GraphQLSchema, execute } from "graphql"; import { gql } from "graphql-tag"; const BENCHMARK_ITERATIONS = 50; export const ARRAY_ITEMS = 25000; export async function runBenchmark(schema: GraphQLSchema) { const multipleNestedObjectsQuery = gql` query { multipleNestedObjects { stringField booleanField numberField nestedField { stringField booleanField numberField } } } `; console.time("multipleNestedObjects"); for (let i = 0; i < BENCHMARK_ITERATIONS; i += 1) { // eslint-disable-next-line no-await-in-loop const result = await execute({ schema, document: multipleNestedObjectsQuery }); console.assert(result.data !== undefined, "result data is undefined"); console.assert( (result.data?.multipleNestedObjects as unknown[]).length === ARRAY_ITEMS, "result data is not a proper array", ); console.assert( (result.data?.multipleNestedObjects as any[])[0].nestedField.booleanField === true, "data nestedField are incorrect", ); } console.timeEnd("multipleNestedObjects"); } ================================================ FILE: benchmarks/array/type-graphql/async-field-resolvers.ts ================================================ import "reflect-metadata"; import { Field, FieldResolver, Int, ObjectType, Query, Resolver, Root, buildSchema, } from "type-graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; @ObjectType() class SampleObject { @Field() stringField!: string; @Field(() => Int) numberField!: number; @Field() booleanField!: boolean; @Field({ nullable: true }) nestedField?: SampleObject; } @Resolver(SampleObject) class SampleResolver { @Query(() => [SampleObject]) multipleNestedObjects(): SampleObject[] { return Array.from( { length: ARRAY_ITEMS }, (_, index): SampleObject => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, }, }), ); } @FieldResolver() async stringField(@Root() source: SampleObject) { return source.stringField; } @FieldResolver() async numberField(@Root() source: SampleObject) { return source.numberField; } @FieldResolver() async booleanField(@Root() source: SampleObject) { return source.booleanField; } @FieldResolver() async nestedField(@Root() source: SampleObject) { return source.nestedField; } } async function main() { const schema = await buildSchema({ resolvers: [SampleResolver], }); await runBenchmark(schema); } main().catch(console.error); ================================================ FILE: benchmarks/array/type-graphql/simple-resolvers.ts ================================================ import "reflect-metadata"; import { Field, Int, type MiddlewareFn, ObjectType, Query, Resolver, buildSchema, } from "type-graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; @ObjectType({ simpleResolvers: true }) class SampleObject { @Field() stringField!: string; @Field(() => Int) numberField!: number; @Field() booleanField!: boolean; @Field({ nullable: true }) nestedField?: SampleObject; } @Resolver() class SampleResolver { @Query(() => [SampleObject]) multipleNestedObjects(): SampleObject[] { return Array.from( { length: ARRAY_ITEMS }, (_, index): SampleObject => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, }, }), ); } } const log = (..._: unknown[]) => undefined; // noop const loggingMiddleware: MiddlewareFn = ({ info }, next) => { log(`${info.parentType.name}.${info.fieldName} accessed`); return next(); }; async function main() { const schema = await buildSchema({ resolvers: [SampleResolver], globalMiddlewares: [loggingMiddleware], }); await runBenchmark(schema); } main().catch(console.error); ================================================ FILE: benchmarks/array/type-graphql/standard.ts ================================================ import "reflect-metadata"; import { Field, Int, ObjectType, Query, Resolver, buildSchema } from "type-graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; @ObjectType() class SampleObject { @Field() stringField!: string; @Field(() => Int) numberField!: number; @Field() booleanField!: boolean; @Field({ nullable: true }) nestedField?: SampleObject; } @Resolver() class SampleResolver { @Query(() => [SampleObject]) multipleNestedObjects(): SampleObject[] { return Array.from( { length: ARRAY_ITEMS }, (_, index): SampleObject => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, }, }), ); } } async function main() { const schema = await buildSchema({ resolvers: [SampleResolver], }); await runBenchmark(schema); } main().catch(console.error); ================================================ FILE: benchmarks/array/type-graphql/sync-field-resolvers.ts ================================================ import "reflect-metadata"; import { Field, FieldResolver, Int, ObjectType, Query, Resolver, Root, buildSchema, } from "type-graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; @ObjectType() class SampleObject { @Field() stringField!: string; @Field(() => Int) numberField!: number; @Field() booleanField!: boolean; @Field({ nullable: true }) nestedField?: SampleObject; } @Resolver(SampleObject) class SampleResolver { @Query(() => [SampleObject]) multipleNestedObjects(): SampleObject[] { return Array.from( { length: ARRAY_ITEMS }, (_, index): SampleObject => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, }, }), ); } @FieldResolver() stringField(@Root() source: SampleObject) { return source.stringField; } @FieldResolver() numberField(@Root() source: SampleObject) { return source.numberField; } @FieldResolver() booleanField(@Root() source: SampleObject) { return source.booleanField; } @FieldResolver() nestedField(@Root() source: SampleObject) { return source.nestedField; } } async function main() { const schema = await buildSchema({ resolvers: [SampleResolver], }); await runBenchmark(schema); } main().catch(console.error); ================================================ FILE: benchmarks/array/type-graphql/sync-getters.ts ================================================ import "reflect-metadata"; import { Field, Int, ObjectType, Query, Resolver, buildSchema } from "type-graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; @ObjectType() class SampleObject { stringField!: string; @Field({ name: "stringField" }) get getStringField(): string { return this.stringField; } numberField!: number; @Field(() => Int, { name: "numberField" }) get getNumberField(): number { return this.numberField; } booleanField!: boolean; @Field({ name: "booleanField" }) get getBooleanField(): boolean { return this.booleanField; } nestedField?: SampleObject; @Field(() => SampleObject, { name: "nestedField", nullable: true }) get getNestedField(): SampleObject | undefined { return this.nestedField; } } @Resolver(SampleObject) class SampleResolver { @Query(() => [SampleObject]) multipleNestedObjects(): SampleObject[] { return Array.from( { length: ARRAY_ITEMS }, (_, index): SampleObject => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, } as SampleObject, }) as SampleObject, ); } } async function main() { const schema = await buildSchema({ resolvers: [SampleResolver], }); await runBenchmark(schema); } main().catch(console.error); ================================================ FILE: benchmarks/array/type-graphql/with-global-middleware.ts ================================================ import "reflect-metadata"; import { Field, Int, type MiddlewareFn, ObjectType, Query, Resolver, buildSchema, } from "type-graphql"; import { ARRAY_ITEMS, runBenchmark } from "../run"; @ObjectType() class SampleObject { @Field() stringField!: string; @Field(() => Int) numberField!: number; @Field() booleanField!: boolean; @Field({ nullable: true }) nestedField?: SampleObject; } @Resolver() class SampleResolver { @Query(() => [SampleObject]) multipleNestedObjects(): SampleObject[] { return Array.from( { length: ARRAY_ITEMS }, (_, index): SampleObject => ({ stringField: "stringField", booleanField: true, numberField: index, nestedField: { stringField: "stringField", booleanField: true, numberField: index, }, }), ); } } const log = (..._: unknown[]) => undefined; // noop const loggingMiddleware: MiddlewareFn = ({ info }, next) => { log(`${info.parentType.name}.${info.fieldName} accessed`); return next(); }; async function main() { const schema = await buildSchema({ resolvers: [SampleResolver], globalMiddlewares: [loggingMiddleware], }); await runBenchmark(schema); } main().catch(console.error); ================================================ FILE: benchmarks/simple/graphql-js.ts ================================================ import { GraphQLObjectType, GraphQLSchema, GraphQLString } from "graphql"; import { runBenchmark } from "./run"; const SampleObject: GraphQLObjectType = new GraphQLObjectType({ name: "SampleObject", fields: () => ({ sampleField: { type: GraphQLString, }, nestedField: { type: SampleObject, }, }), }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: { singleObject: { type: SampleObject, resolve: () => ({ sampleField: "sampleField" }), }, nestedObject: { type: SampleObject, resolve: () => ({ sampleField: "sampleField", nestedField: { sampleField: "sampleField", nestedField: { sampleField: "sampleField", nestedField: { sampleField: "sampleField", nestedField: { sampleField: "sampleField", }, }, }, }, }), }, }, }), }); runBenchmark(schema).catch(console.error); ================================================ FILE: benchmarks/simple/results.txt ================================================ Core i7 2700K @ 3.5GHz Windows 10 x64 100 000 iterations ----- Node.js v10.17 ES2016 build - singleObject: 4831.113ms - nestedObject: 17067.581ms ES2018 build - singleObject: 2196.958ms - nestedObject: 7311.419ms graphql-js - singleObject: 871.424ms - nestedObject: 2206.372ms ----- Node.js v13.2 ES2018 build - singleObject: 1.622s - nestedObject: 4.557s without field resolvers (`graphql-js` implicit ones) - singleObject: 1.465s - nestedObject: 3.086s graphql-js - singleObject: 1.003s - nestedObject: 2.422s ================================================ FILE: benchmarks/simple/run.ts ================================================ import { type ExecutionResult, type GraphQLSchema, execute } from "graphql"; import { gql } from "graphql-tag"; const BENCHMARK_ITERATIONS = 100000; export async function runBenchmark(schema: GraphQLSchema) { const singleObjectQuery = gql` query { singleObject { sampleField } } `; console.time("singleObject"); for (let i = 0; i < BENCHMARK_ITERATIONS; i += 1) { // eslint-disable-next-line no-await-in-loop const result: ExecutionResult = await execute({ schema, document: singleObjectQuery }); console.assert(result.data !== undefined, "result data is undefined"); console.assert(result.data?.singleObject !== undefined, "data singleObject is undefined"); } console.timeEnd("singleObject"); const nestedObjectQuery = gql` query { nestedObject { sampleField nestedField { sampleField nestedField { sampleField nestedField { sampleField nestedField { sampleField } } } } } } `; console.time("nestedObject"); for (let i = 0; i < BENCHMARK_ITERATIONS; i += 1) { // eslint-disable-next-line no-await-in-loop const result: ExecutionResult = await execute({ schema, document: nestedObjectQuery }); console.assert(result.data !== undefined, "result data is undefined"); console.assert( result.data.nestedObject.nestedField.nestedField.nestedField.nestedField.sampleField !== undefined, "data nestedField are incorrect", ); } console.timeEnd("nestedObject"); } ================================================ FILE: benchmarks/simple/type-graphql.ts ================================================ import "reflect-metadata"; import { Field, ObjectType, Query, Resolver, buildSchema } from "type-graphql"; import { runBenchmark } from "./run"; @ObjectType() class SampleObject { @Field() sampleField!: string; @Field() nestedField?: SampleObject; } @Resolver() class SampleResolver { @Query() singleObject(): SampleObject { return { sampleField: "sampleField" }; } @Query() nestedObject(): SampleObject { return { sampleField: "sampleField", nestedField: { sampleField: "sampleField", nestedField: { sampleField: "sampleField", nestedField: { sampleField: "sampleField", nestedField: { sampleField: "sampleField", }, }, }, }, }; } } async function main() { const schema = await buildSchema({ resolvers: [SampleResolver], }); await runBenchmark(schema); } main().catch(console.error); ================================================ FILE: benchmarks/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "./build" }, "include": [".", "../src"] } ================================================ FILE: cspell.json ================================================ { "version": "0.2", "language": "en", "useGitignore": true, "import": [ "@cspell/dict-node/cspell-ext.json", "@cspell/dict-npm/cspell-ext.json", "@cspell/dict-shell/cspell-ext.json", "@cspell/dict-typescript/cspell-ext.json" ], "ignorePaths": [ "**/build/", "**/dist/", "**/coverage/", "**/node_modules/", "**/package.json", "./images/**/*.svg", "./images/**/*.avif", // FIXME: Remove "website/", "examples" ], "words": [ "amet", "argumented", "asynciterable", "authed", "awad", "betwinner", "buzzoid", "buzzv", "casinodeps", "cdpath", "codecov", "codegen", "codeql", "Famety", "Fansoria", "fbpostlikes", "gamstop", "graming", "graphiql", "graphqlgen", "graphqlid", "graphqlisodate", "graphqlisodatetime", "Insfollowpro", "instanceof", "instinctools", "inversifyjs", "isodate", "joiful", "Kasyno", "kaszinomagyar", "Leofame", "lifex", "lovd", "lytek", "michał", "middlewares", "mikroorm", "moonkasyno", "nestjs", "netrc", "nongamstopbets", "Nove", "opencollective", "overfetching", "paramtypes", "payid", "pookies", "postbuild", "realpath", "returntype", "shellcheck", "sidesmedia", "sindresorhus", "Socialift", "socialmention", "socialwick", "sponsorkit", "supergraph", "tiktok", "todos", "Tokmax", "Twicsy", "typedefs", "typedi", "typegql", "typegraphql", "typeof", "typeorm", "underfetching", "webp", "wordhint", "Zahranicni", "zahranicnicasino" ] } ================================================ FILE: docs/README.md ================================================ # Documentation **The documentation has been prepared to be viewed on [TypeGraphQL website](https://typegraphql.com), please read it there.** ================================================ FILE: docs/authorization.md ================================================ --- title: Authorization --- Authorization is a core feature used in almost all APIs. Sometimes we want to restrict data access or actions for a specific group of users. In express.js (and other Node.js frameworks) we use middleware for this, like `passport.js` or the custom ones. However, in GraphQL's resolver architecture we don't have middleware so we have to imperatively call the auth checking function and manually pass context data to each resolver, which might be a bit tedious. That's why authorization is a first-class feature in `TypeGraphQL`! ## Declaration First, we need to use the `@Authorized` decorator as a guard on a field, query or mutation. Example object type field guards: ```ts @ObjectType() class MyObject { @Field() publicField: string; @Authorized() @Field() authorizedField: string; @Authorized("ADMIN") @Field() adminField: string; @Authorized(["ADMIN", "MODERATOR"]) @Field({ nullable: true }) hiddenField?: string; } ``` We can leave the `@Authorized` decorator brackets empty or we can specify the role/roles that the user needs to possess in order to get access to the field, query or mutation. By default the roles are of type `string` but they can easily be changed as the decorator is generic - `@Authorized(1, 7, 22)`. Thus, authorized users (regardless of their roles) can only read the `publicField` or the `authorizedField` from the `MyObject` object. They will receive `null` when accessing the `hiddenField` field and will receive an error (that will propagate through the whole query tree looking for a nullable field) for the `adminField` when they don't satisfy the role constraints. Sample query and mutation guards: ```ts @Resolver() class MyResolver { @Query() publicQuery(): MyObject { return { publicField: "Some public data", authorizedField: "Data for logged users only", adminField: "Top secret info for admin", }; } @Authorized() @Query() authedQuery(): string { return "Authorized users only!"; } @Authorized("ADMIN", "MODERATOR") @Mutation() adminMutation(): string { return "You are an admin/moderator, you can safely drop the database ;)"; } } ``` Authorized users (regardless of their roles) will be able to read data from the `publicQuery` and the `authedQuery` queries, but will receive an error when trying to perform the `adminMutation` when their roles don't include `ADMIN` or `MODERATOR`. However, declaring `@Authorized()` on all the resolver's class methods would be not only a tedious task but also an error-prone one, as it's easy to forget to put it on some newly added method, etc. Hence, TypeGraphQL support declaring `@Authorized()` or the resolver class level. This way you can declare it once per resolver's class but you can still overwrite the defaults and narrows the authorization rules: ```ts @Authorized() @Resolver() class MyResolver { // this will inherit the auth guard defined on the class level @Query() authedQuery(): string { return "Authorized users only!"; } // this one overwrites the resolver's one // and registers roles required for this mutation @Authorized("ADMIN", "MODERATOR") @Mutation() adminMutation(): string { return "You are an admin/moderator, you can safely drop the database ;)"; } } ``` ## Runtime checks Having all the metadata for authorization set, we need to create our auth checker function. Its implementation may depend on our business logic: ```ts export const customAuthChecker: AuthChecker = ( { root, args, context, info }, roles, ) => { // Read user from context // and check the user's permission against the `roles` argument // that comes from the '@Authorized' decorator, eg. ["ADMIN", "MODERATOR"] return true; // or 'false' if access is denied }; ``` The second argument of the `AuthChecker` generic type is `RoleType` - used together with the `@Authorized` decorator generic type. Auth checker can be also defined as a class - this way we can leverage the dependency injection mechanism: ```ts export class CustomAuthChecker implements AuthCheckerInterface { constructor( // Dependency injection private readonly userRepository: Repository, ) {} check({ root, args, context, info }: ResolverData, roles: string[]) { const userId = getUserIdFromToken(context.token); // Use injected service const user = this.userRepository.getById(userId); // Custom logic, e.g.: return user % 2 === 0; } } ``` The last step is to register the function or class while building the schema: ```ts import { customAuthChecker } from "../auth/custom-auth-checker.ts"; const schema = await buildSchema({ resolvers: [MyResolver], // Register the auth checking function // or defining it inline authChecker: customAuthChecker, }); ``` And it's done! 😉 If we need silent auth guards and don't want to return authorization errors to users, we can set the `authMode` property of the `buildSchema` config object to `"null"`: ```ts const schema = await buildSchema({ resolvers: ["./**/*.resolver.ts"], authChecker: customAuthChecker, authMode: "null", }); ``` It will then return `null` instead of throwing an authorization error. ## Recipes We can also use `TypeGraphQL` with JWT authentication. Here's an example using `@apollo/server`: ```ts import { ApolloServer } from "@apollo/server"; import { expressMiddleware } from "@apollo/server/express4"; import express from "express"; import jwt from "express-jwt"; import bodyParser from "body-parser"; import { schema } from "./graphql/schema"; import { User } from "./User.type"; // GraphQL path const GRAPHQL_PATH = "/graphql"; // GraphQL context type Context = { user?: User; }; // Express const app = express(); // Apollo server const server = new ApolloServer({ schema }); await server.start(); // Mount a JWT or other authentication middleware that is run before the GraphQL execution app.use( GRAPHQL_PATH, jwt({ secret: "TypeGraphQL", credentialsRequired: false, }), ); // Apply GraphQL server middleware app.use( GRAPHQL_PATH, bodyParser.json(), expressMiddleware(server, { // Build context // 'req.user' comes from 'express-jwt' context: async ({ req }) => ({ user: req.user }), }), ); // Start server await new Promise(resolve => app.listen({ port: 4000 }, resolve)); console.log(`GraphQL server ready at http://localhost:4000/${GRAPHQL_PATH}`); ``` Then we can use standard, token based authorization in the HTTP header like in classic REST APIs and take advantage of the `TypeGraphQL` authorization mechanism. ## Example See how this works in the [simple real life example](https://github.com/MichalLytek/type-graphql/tree/master/examples/authorization). ================================================ FILE: docs/aws-lambda.md ================================================ --- title: AWS Lambda integration --- ## Using TypeGraphQL in AWS Lambda environment AWS Lambda environment is a bit different than a standard Node.js server deployment. However, the only tricky part with the setup is that we need to "cache" the built schema, to save some computing time by avoiding rebuilding the schema on every request to our lambda. So all we need to do is to assign the built schema to the local variable using the `??=` conditional assignment operator. We can do the same thing for `ApolloServer`. Below you you can find the full snippet for the AWS Lambda integration: ```ts import { APIGatewayProxyHandlerV2 } from "aws-lambda"; import { ApolloServer } from "apollo-server-lambda"; let cachedSchema: GraphQLSchema | null = null; let cachedServer: ApolloServer | null = null; export const handler: APIGatewayProxyHandlerV2 = async (event, context, callback) => { // build TypeGraphQL executable schema only once, then read it from local "cached" variable cachedSchema ??= await buildSchema({ resolvers: [RecipeResolver], }); // create the GraphQL server only once cachedServer ??= new ApolloServer({ schema: cachedSchema }); // make a handler for `aws-lambda` return cachedServer.createHandler({})(event, context, callback); }; ``` ================================================ FILE: docs/azure-functions.md ================================================ --- title: Azure Functions Integration --- ## Using TypeGraphQL in Microsoft Azure Functions Integrating TypeGraphQL with Azure Functions involves the following key steps: 1. Generate GraphQL schema based on your resolvers 2. Notify Apollo Server about your schema Below is how you can implement the azure function entry point (with explanations in-line): ```ts // index.ts import "reflect-metadata"; import path from "path"; import { ApolloServer } from "@apollo/server"; import { startServerAndCreateHandler } from "@as-integrations/azure-functions"; import { buildSchemaSync } from "type-graphql"; import { Container } from "typedi"; import { GraphQLFormattedError } from "graphql"; import { UserResolver } from "YOUR_IMPORT_PATH"; // TypeGraphQL Resolver import { AccountResolver } from "YOUR_IMPORT_PATH"; // TypeGraphQL Resolver // Bundle resolvers to build the schema const schema = buildSchemaSync({ // Include resolvers you'd like to expose to the API // Deployment to Azure functions might fail if // you include too much resolvers (means your app is too big) resolvers: [ UserResolver, AccountResolver, // your other resolvers ], // Only build the GraphQL schema locally // The resulting schema.graphql will be generated to the following path: // Path: /YOUR_PROJECT/src/schema.graphql emitSchemaFile: process.env.NODE_ENV === "local" ? path.resolve("./src/schema.graphql") : false, container: Container, validate: true, }); // Add schema into Apollo Server const server = new ApolloServer({ // include your schema schema, // only allow introspection in non-prod environments introspection: process.env.NODE_ENV !== "production", // you can handle errors in your own styles formatError: (err: GraphQLFormattedError) => err, }); // Start the server(less handler/function) export default startServerAndCreateHandler(server); ``` Each Azure Function needs to have an equivalent configuration file called `function.json`, here's how you can configure it: ```json // function.json { "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "route": "graphql", "methods": ["get", "post", "options"] }, { "type": "http", "direction": "out", "name": "$return" } ], "scriptFile": "../dist/handler-graphql/index.js" } ``` For better maintainability of your codebase, we recommend separate your Azure Functions into its own folders, away from the actual GraphQL Resolvers. Here's an example: ```text /YOUR_PROJECT /handlers /handler-graphql index.ts function.json /handler-SOME-OTHER-FUNCTION-1 index.ts function.json /handler-SOME-OTHER-FUNCTION-2 index.ts function.json /src /resolvers user.resolver.ts account.resolver.ts /services user.service.ts account.service.ts package.json host.json .eslintrc.js .prettierrc .eslintignore .prettierignore etc etc etc... ``` ================================================ FILE: docs/bootstrap.md ================================================ --- title: Bootstrapping --- After creating our resolvers, type classes, and other business-related code, we need to make our app run. First we have to build the schema, then we can expose it with an HTTP server, WebSockets or even MQTT. ## Create Executable Schema To create an executable schema from type and resolver definitions, we need to use the `buildSchema` function. It takes a configuration object as a parameter and returns a promise of a `GraphQLSchema` object. In the configuration object we must provide a `resolvers` property, which is supposed to be an array of resolver classes: ```ts import { FirstResolver, SecondResolver } from "./resolvers"; // ... const schema = await buildSchema({ resolvers: [FirstResolver, SecondResolver], }); ``` Be aware that only operations (queries, mutation, etc.) defined in the resolvers classes (and types directly connected to them) will be emitted in schema. So if we have defined some object types (that implements an interface type [with disabled auto registering](./interfaces.md#registering-in-schema)) but are not directly used in other types definition (like a part of an union, a type of a field or a return type of an operation), we need to provide them manually in `orphanedTypes` options of `buildSchema`: ```ts import { FirstResolver, SecondResolver } from "../app/src/resolvers"; import { FirstObject } from "../app/src/types"; // ... const schema = await buildSchema({ resolvers: [FirstResolver, SecondResolver], // Provide all the types that are missing in schema orphanedTypes: [FirstObject], }); ``` In case of defining the resolvers array somewhere else (not inline in the `buildSchema`), we need to use the `as const` syntax to inform the TS compiler and satisfy the `NonEmptyArray` constraints: ```ts // resolvers.ts export const resolvers = [FirstResolver, SecondResolver] as const; // schema.ts import { resolvers } from "./resolvers"; const schema = await buildSchema({ resolvers }); ``` There are also other options related to advanced features like [authorization](./authorization.md) or [validation](./validation.md) - you can read about them in docs. To make `await` work, we need to declare it as an async function. Example of `main.ts` file: ```ts import { buildSchema } from "type-graphql"; async function bootstrap() { const schema = await buildSchema({ resolvers: [ // ... Resolvers classes ], }); // ... } bootstrap(); // Actually run the async function ``` ## Create an HTTP GraphQL endpoint In most cases, the GraphQL app is served by an HTTP server. After building the schema we can create the GraphQL endpoint with a variety of tools such as [`graphql-yoga`](https://github.com/dotansimha/graphql-yoga) or [`@apollo/server`](https://github.com/apollographql/apollo-server). Below is an example using [`@apollo/server`](https://github.com/apollographql/apollo-server): ```ts import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; const PORT = process.env.PORT || 4000; async function bootstrap() { // ... Build GraphQL schema // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap(); ``` Remember to install the `@apollo/server` package from npm - it's not bundled with TypeGraphQL. Of course you can use the `express-graphql` middleware, `graphql-yoga` or whatever you want 😉 ## Create typeDefs and resolvers map TypeGraphQL provides a second way to generate the GraphQL schema - the `buildTypeDefsAndResolvers` function. It accepts the same `BuildSchemaOptions` as the `buildSchema` function but instead of an executable `GraphQLSchema`, it creates a typeDefs and resolversMap pair that you can use e.g. with [@graphql-tools/\*`](https://the-guild.dev/graphql/tools): ```ts import { makeExecutableSchema } from "@graphql-tools/schema"; const { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ resolvers: [FirstResolver, SecondResolver], }); const schema = makeExecutableSchema({ typeDefs, resolvers }); ``` Or even with other libraries that expect the schema info in that shape, like [`apollo-link-state`](https://github.com/apollographql/apollo-link-state): ```ts import { withClientState } from "apollo-link-state"; const { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ resolvers: [FirstResolver, SecondResolver], }); const stateLink = withClientState({ // ... Other options like `cache` typeDefs, resolvers, }); // ... Rest of `ApolloClient` initialization code ``` There's also a `sync` version of it - `buildTypeDefsAndResolversSync`: ```ts const { typeDefs, resolvers } = buildTypeDefsAndResolversSync({ resolvers: [FirstResolver, SecondResolver], }); ``` However, be aware that some of the TypeGraphQL features (i.a. [query complexity](./complexity.md)) might not work with the `buildTypeDefsAndResolvers` approach because they use some low-level `graphql-js` features. ================================================ FILE: docs/browser-usage.md ================================================ --- title: Browser usage --- ## Using classes in a client app Sometimes we might want to use the classes we've created and annotated with TypeGraphQL decorators, in our client app that works in the browser. For example, reusing the args or input classes with `class-validator` decorators or the object type classes with some helpful custom methods. Since TypeGraphQL is a Node.js framework, it doesn't work in a browser environment, so we may quickly get an error, e.g. `ERROR in ./node_modules/fs.realpath/index.js` or `utils1_promisify is not a function`, while trying to build our app e.g. with Webpack. To correct this, we have to configure bundler or compiler to use the decorator shim instead of the normal module. The steps to accomplish this are different, depending on the framework, bundler or compiler we use. However, in all cases, using shim makes our bundle much lighter as we don't need to embed the whole TypeGraphQL library code in our app. ## CRA and similar We simply add this plugin code to our webpack config: ```js module.exports = { // ... Rest of Webpack configuration plugins: [ // ... Other existing plugins new webpack.NormalModuleReplacementPlugin(/type-graphql$/, resource => { resource.request = resource.request.replace(/type-graphql/, "type-graphql/shim"); }), ]; } ``` In case of cypress, we can adapt the same webpack config trick just by applying the [cypress-webpack-preprocessor](https://github.com/cypress-io/cypress-webpack-preprocessor) plugin. ## Angular and similar In some TypeScript projects, like the ones using Angular, which AoT compiler requires that a full `*.ts` file is provided instead of just a `*.js` and `*.d.ts` files, to use this shim we have to simply set up our TypeScript configuration in `tsconfig.json` to use this file instead of a normal TypeGraphQL module: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "type-graphql": ["node_modules/type-graphql/build/typings/shim.ts"] } } } ``` ## Next.js and similar When using the shim with Next.js as a dedicated frontend server be aware that Next has pre-renders on the server. This means that in development mode the `webpack: {}` config in `next.config.js` is skipped and full `type-graphql` is bundled. But we still need to handle some webpack rewiring for the client bundling which still happens with webpack both in development and in production mode. The easiest way is to accomplish this is also done in `tsconfig.json` - add the same keys like in the example before to `compilerOptions`: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "type-graphql": ["node_modules/type-graphql/build/typings/shim.ts"] } } } ``` Then, `npm install -D tsconfig-paths` and enable it with `NODE_OPTIONS="-r tsconfig-paths/register"` in our environment variables setup. ================================================ FILE: docs/complexity.md ================================================ --- title: Query complexity --- A single GraphQL query can potentially generate a huge workload for a server, like thousands of database operations which can be used to cause DDoS attacks. In order to limit and keep track of what each GraphQL operation can do, `TypeGraphQL` provides the option of integrating with Query Complexity tools like [graphql-query-complexity](https://github.com/ivome/graphql-query-complexity). This cost analysis-based solution is very promising, since we can define a “cost” per field and then analyze the AST to estimate the total cost of the GraphQL query. Of course all the analysis is handled by `graphql-query-complexity`. All we must do is define our complexity cost for the fields, mutations or subscriptions in `TypeGraphQL` and implement `graphql-query-complexity` in whatever GraphQL server that is being used. ## How to use First, we need to pass `complexity` as an option to the decorator on a field, query or mutation. Example of complexity ```ts @ObjectType() class MyObject { @Field({ complexity: 2 }) publicField: string; @Field({ complexity: ({ args, childComplexity }) => childComplexity + 1 }) complexField: string; } ``` The `complexity` option may be omitted if the complexity value is 1. Complexity can be passed as an option to any `@Field`, `@FieldResolver`, `@Mutation` or `@Subscription` decorator. If both `@FieldResolver` and `@Field` decorators of the same property have complexity defined, then the complexity passed to the field resolver decorator takes precedence. In the next step, we will integrate `graphql-query-complexity` with the server that expose our GraphQL schema over HTTP. You can use it with `express-graphql` like [in the lib examples](https://github.com/slicknode/graphql-query-complexity/blob/b6a000c0984f7391f3b4e886e3df6a7ed1093b07/README.md#usage-with-express-graphql), however we will use Apollo Server like in our other examples: ```ts async function bootstrap() { // ... Build GraphQL schema // Create GraphQL server const server = new ApolloServer({ schema, // Create a plugin to allow query complexity calculation for every request plugins: [ { requestDidStart: async () => ({ async didResolveOperation({ request, document }) { /** * Provides GraphQL query analysis to be able to react on complex queries to the GraphQL server * It can be used to protect the GraphQL server against resource exhaustion and DoS attacks * More documentation can be found at https://github.com/ivome/graphql-query-complexity */ const complexity = getComplexity({ // GraphQL schema schema, // To calculate query complexity properly, // check only the requested operation // not the whole document that may contains multiple operations operationName: request.operationName, // GraphQL query document query: document, // GraphQL query variables variables: request.variables, // Add any number of estimators. The estimators are invoked in order, the first // numeric value that is being returned by an estimator is used as the field complexity // If no estimator returns a value, an exception is raised estimators: [ // Using fieldExtensionsEstimator is mandatory to make it work with type-graphql fieldExtensionsEstimator(), // Add more estimators here... // This will assign each field a complexity of 1 // if no other estimator returned a value simpleEstimator({ defaultComplexity: 1 }), ], }); // React to the calculated complexity, // like compare it with max and throw error when the threshold is reached if (complexity > MAX_COMPLEXITY) { throw new Error( `Sorry, too complicated query! ${complexity} exceeded the maximum allowed complexity of ${MAX_COMPLEXITY}`, ); } console.log("Used query complexity points:", complexity); }, }), }, ], }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } ``` And it's done! 😉 For more info about how query complexity is computed, please visit [graphql-query-complexity](https://github.com/ivome/graphql-query-complexity). ## Example See how this works in the [simple query complexity example](https://github.com/MichalLytek/type-graphql/tree/master/examples/query-complexity). ================================================ FILE: docs/custom-decorators.md ================================================ --- title: Custom decorators --- Custom decorators are a great way to reduce the boilerplate and reuse some common logic between different resolvers. TypeGraphQL supports three kinds of custom decorators - method, resolver class and parameter. ## Method decorators Using [middlewares](./middlewares.md) allows to reuse some code between resolvers. To further reduce the boilerplate and have a nicer API, we can create our own custom method decorators. They work in the same way as the [reusable middleware function](./middlewares.md#reusable-middleware), however, in this case we need to call `createMethodMiddlewareDecorator` helper function with our middleware logic and return its value: ```ts export function ValidateArgs(schema: JoiSchema) { return createMethodMiddlewareDecorator(async ({ args }, next) => { // Middleware code that uses custom decorator arguments // e.g. Validation logic based on schema using 'joi' await joiValidate(schema, args); return next(); }); } ``` The usage is then very simple, as we have a custom, descriptive decorator - we just place it above the resolver/field and pass the required arguments to it: ```ts @Resolver() export class RecipeResolver { @ValidateArgs(MyArgsSchema) // Custom decorator @UseMiddleware(ResolveTime) // Explicit middleware @Query() randomValue(@Args() { scale }: MyArgs): number { return Math.random() * scale; } } ``` ## Resolver class decorators Similar to method decorators, we can create our own custom resolver class decorators. In this case we need to call `createResolverClassMiddlewareDecorator` helper function, just like we did for `createMethodMiddlewareDecorator`: ```ts export function ValidateArgs(schema: JoiSchema) { return createResolverClassMiddlewareDecorator(async ({ args }, next) => { // Middleware code that uses custom decorator arguments // e.g. Validation logic based on schema using 'joi' await joiValidate(schema, args); return next(); }); } ``` The usage is then analogue - we just place it above the resolver class and pass the required arguments to it: ```ts @ValidateArgs(MyArgsSchema) // Custom decorator @UseMiddleware(ResolveTime) // Explicit middleware @Resolver() export class RecipeResolver { @Query() randomValue(@Args() { scale }: MyArgs): number { return Math.random() * scale; } } ``` This way, we just need to put it once in the code and our custom decorator will be applied to all the resolver's queries or mutations. As simple as that! ## Parameter decorators Parameter decorators are just like the custom method decorators or middlewares but with an ability to return some value that will be injected to the method as a parameter. Thanks to this, it reduces the pollution in `context` which was used as a workaround for the communication between reusable middlewares and resolvers. They might be just a simple data extractor function, that makes our resolver more unit test friendly: ```ts function CurrentUser() { return createParameterDecorator(({ context }) => context.currentUser); } ``` Or might be a more advanced one that performs some calculations and encapsulates some logic. Compared to middlewares, they allow for a more granular control on executing the code, like calculating fields map based on GraphQL info only when it's really needed (requested by using the `@Fields()` decorator): ```ts function Fields(level = 1): ParameterDecorator { return createParameterDecorator(async ({ info }) => { const fieldsMap: FieldsMap = {}; // Calculate an object with info about requested fields // based on GraphQL 'info' parameter of the resolver and the level parameter // or even call some async service, as it can be a regular async function and we can just 'await' return fieldsMap; }); } ``` > Be aware, that `async` function as a custom param decorators logic can make the GraphQL resolver execution slower, so try to avoid them, if possible. Then we can use our custom param decorators in the resolvers just like the built-in decorators: ```ts @Resolver() export class RecipeResolver { constructor(private readonly recipesRepository: Repository) {} @Authorized() @Mutation(returns => Recipe) async addRecipe( @Args() recipeData: AddRecipeInput, // Custom decorator just like the built-in one @CurrentUser() currentUser: User, ) { const recipe: Recipe = { ...recipeData, // and use the data returned from custom decorator in the resolver code author: currentUser, }; await this.recipesRepository.save(recipe); return recipe; } @Query(returns => Recipe, { nullable: true }) async recipe( @Arg("id") id: string, // Custom decorator that parses the fields from GraphQL query info @Fields() fields: FieldsMap, ) { return await this.recipesRepository.find(id, { // use the fields map as a select projection to optimize db queries select: fields, }); } } ``` ### Custom `@Arg` decorator In some cases we might want to create a custom decorator that will also register/expose an argument in the GraphQL schema. Calling both `Arg()` and `createParameterDecorator()` inside a custom decorator does not play well with the internals of TypeGraphQL. Hence, the `createParameterDecorator()` function supports second argument, `CustomParameterOptions` which allows to set decorator metadata for `@Arg` under the `arg` key: ```ts function RandomIdArg(argName = "id") { return createParameterDecorator( // here we do the logic of getting provided argument or generating a random one ({ args }) => args[argName] ?? Math.round(Math.random() * MAX_ID_VALUE), { // here we provide the metadata to register the parameter as a GraphQL argument arg: { name: argName, typeFunc: () => Int, options: { nullable: true, description: "Accepts provided id or generates a random one.", }, }, }, ); } ``` The usage of that custom decorator is very similar to the previous one and `@Arg` decorator itself: ```ts @Resolver() export class RecipeResolver { constructor(private readonly recipesRepository: Repository) {} @Query(returns => Recipe, { nullable: true }) async recipe( // custom decorator that will expose an arg in the schema @RandomIdArg("id") id: number, ) { return await this.recipesRepository.findById(id); } } ``` ## Example See how different kinds of custom decorators work in the [custom decorators and middlewares example](https://github.com/MichalLytek/type-graphql/tree/master/examples/middlewares-custom-decorators). ================================================ FILE: docs/dependency-injection.md ================================================ --- title: Dependency injection --- Dependency injection is a really useful pattern that helps in decoupling parts of the app. TypeGraphQL supports this technique by allowing users to provide their IoC container that will be used by the framework. ## Basic usage The usage of this feature is very simple - all you need to do is register a 3rd party container. Example using TypeDI: ```ts import { buildSchema } from "type-graphql"; // IOC container import { Container } from "typedi"; import { SampleResolver } from "./resolvers"; // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [SampleResolver], // Registry 3rd party IOC container container: Container, }); ``` Resolvers will then be able to declare their dependencies and TypeGraphQL will use the container to solve them: ```ts import { Service } from "typedi"; @Service() @Resolver(of => Recipe) export class RecipeResolver { constructor( // Dependency injection private readonly recipeService: RecipeService, ) {} @Query(returns => Recipe, { nullable: true }) async recipe(@Arg("recipeId") recipeId: string) { // Usage of the injected service return this.recipeService.getOne(recipeId); } } ``` A sample recipe service implementation may look like this: ```ts import { Service, Inject } from "typedi"; @Service() export class RecipeService { @Inject("SAMPLE_RECIPES") private readonly items: Recipe[], async getAll() { return this.items; } async getOne(id: string) { return this.items.find(item => item.id === id); } } ``` > Be aware than when you use [InversifyJS](https://github.com/inversify/InversifyJS), you have to bind the resolver class with the [self-binding of concrete types](https://github.com/inversify/InversifyJS/blob/master/wiki/classes_as_id.md#self-binding-of-concrete-types), e.g.: > > ```ts > container.bind(SampleResolver).to(SampleResolver).inSingletonScope(); > ``` ## Scoped containers Dependency injection is a really powerful pattern, but some advanced users may encounter the need for creating fresh instances of some services or resolvers for every request. Since `v0.13.0`, **TypeGraphQL** supports this feature, that is extremely useful for tracking logs by individual requests or managing stateful services. To register a scoped container, we need to make some changes in the server bootstrapping config code. First we need to provide a container resolver function. It takes the resolver data (like context) as an argument and should return an instance of the container scoped to the request. For simple container libraries we may define it inline, e.g. using `TypeDI`: ```ts await buildSchema({ container: (({ context }: ResolverData) => Container.of(context.requestId)); }; ``` The tricky part is where the `context.requestId` comes from. Unfortunately, we need to provide it manually using hooks that are exposed by HTTP GraphQL middleware like `express-graphql`, `@apollo/server` or `graphql-yoga`. For some other advanced libraries, we might need to create an instance of the container, place it in the context object and then retrieve it in the `container` getter function: ```ts await buildSchema({ container: (({ context }: ResolverData) => context.container); }; ``` Example using `TypeDI` and `@apollo/server` with the `context` creation method: ```ts import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Container } from "typedi"; // Create GraphQL server const server = new ApolloServer({ // GraphQL schema schema, }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, // Provide unique context with 'requestId' for each request context: async () => { const requestId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); // uuid-like const container = Container.of(requestId.toString()); // Get scoped container const context = { requestId, container }; // Create context container.set("context", context); // Set context or other data in container return context; }, }); console.log(`GraphQL server ready at ${url}`); ``` We also have to dispose the container after the request has been handled and the response is ready. Otherwise, there would be a huge memory leak as the new instances of services and resolvers have been created for each request but they haven't been cleaned up. Apollo Server has a [plugins](https://www.apollographql.com/docs/apollo-server/integrations/plugins) feature that supports [`willSendResponse`](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#willsendresponse) lifecycle event. We can leverage it to clean up the container after handling the request. Example using `TypeDI` and `@apollo/server` with plugins approach: ```ts import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Container } from "typedi"; const server = new ApolloServer({ // GraphQL schema schema, // Create a plugin to allow for disposing the scoped container created for every request plugins: [ { requestDidStart: async () => ({ async willSendResponse(requestContext) { // Dispose the scoped container to prevent memory leaks Container.reset(requestContext.contextValue.requestId.toString()); // For developers curiosity purpose, here is the logging of current scoped container instances // Make multiple parallel requests to see in console how this works const instancesIds = ((Container as any).instances as ContainerInstance[]).map( instance => instance.id, ); console.log("Instances left in memory: ", instancesIds); }, }), }, ], }); ``` And basically that's it! The configuration of the container is done and TypeGraphQL will be able to use different instances of resolvers for each request. The only thing that's left is the container configuration - we need to check out the docs for our container library (`InversifyJS`, `injection-js`, `TypeDI` or other) to get know how to setup the lifetime of the injectable objects (transient, scoped or singleton). > Be aware that some libraries (like `TypeDI`) by default create new instances for every scoped container, so you might experience a **significant increase in memory usage** and some slowing down in query resolving speed, so please be careful with using this feature! ## Example You can see how this fits together in the [simple example](https://github.com/MichalLytek/type-graphql/tree/master/examples/using-container). For a more advanced usage example with scoped containers, check out [advanced example with scoped containers](https://github.com/MichalLytek/type-graphql/tree/master/examples/using-scoped-container). Integration with [TSyringe](https://github.com/MichalLytek/type-graphql/tree/master/examples/tsyringe). ================================================ FILE: docs/directives.md ================================================ --- title: Directives --- > A directive is an identifier preceded by a `@` character, optionally followed by a list of named arguments, which can appear after almost any form of syntax in the GraphQL query or schema languages. Though the [GraphQL directives](https://www.apollographql.com/docs/graphql-tools/schema-directives) syntax is similar to TS decorators, they are purely an SDL (Schema Definition Language) feature that allows you to add metadata to a selected type or its field: ```graphql type Foo @auth(requires: USER) { field: String! } type Bar { field: String! @auth(requires: USER) } ``` That metadata can be read at runtime to modify the structure and behavior of a GraphQL schema to support reusable code and tasks like authentication, permission, formatting, and plenty more. They are also really useful for some external services like [Apollo Cache Control](https://www.apollographql.com/docs/apollo-server/performance/caching/#adding-cache-hints-statically-in-your-schema) or [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/#federated-schema-example). **TypeGraphQL** of course provides some basic support for using the schema directives via the `@Directive` decorator. ## Usage ### Declaring in schema Basically, we declare the usage of directives just like in SDL, with the `@` syntax: ```ts @Directive('@deprecated(reason: "Use newField")') ``` Currently, you can use the directives only on object types, input types, interface types and their fields or fields resolvers, args type fields, as well as queries, mutations and subscriptions and the inline arguments. Other locations like scalars, enums or unions are not yet supported. So the `@Directive` decorator can be placed over the class property/method or over the type class itself, depending on the needs and the placements supported by the implementation: ```ts @Directive("@auth(requires: USER)") @ObjectType() class Foo { @Field() field: string; } @ObjectType() class Bar { @Directive("@auth(requires: USER)") @Field() field: string; } @ArgsType() class FooBarArgs { @Directive('@deprecated(reason: "Not used anymore")') @Field({ nullable: true }) baz?: string; } @Resolver(of => Foo) class FooBarResolver { @Directive("@auth(requires: ANY)") @Query() foobar(@Args() { baz }: FooBarArgs): string { return "foobar"; } @Directive("@auth(requires: ADMIN)") @FieldResolver() bar(): string { return "foobar"; } } ``` In case of inline args using `@Arg` decorator, directives can be placed over the parameter of the class method: ```ts @Resolver(of => Foo) class FooBarResolver { @Query() foo( @Directive('@deprecated(reason: "Not used anymore")') @Arg("foobar", { defaultValue: "foobar" }) foobar: string, ) { return "foo"; } @FieldResolver() bar( @Directive('@deprecated(reason: "Not used anymore")') @Arg("foobar", { defaultValue: "foobar" }) foobar: string, ) { return "bar"; } } ``` > Note that even as directives are a purely SDL thing, they won't appear in the generated schema definition file. Current implementation of directives in TypeGraphQL is using some crazy workarounds because [`graphql-js` doesn't support setting them by code](https://github.com/graphql/graphql-js/issues/1343) and the built-in `printSchema` utility omits the directives while printing. See [emit schema with custom directives](./emit-schema.md#emit-schema-with-custom-directives) for more info. Also please note that `@Directive` can only contain a single GraphQL directive name or declaration. If you need to have multiple directives declared, just place multiple decorators: ```ts @ObjectType() class Foo { @Directive("@lowercase") @Directive('@deprecated(reason: "Use `newField`")') @Directive("@hasRole(role: Manager)") @Field() bar: string; } ``` ### Providing the implementation Besides declaring the usage of directives, you also have to register the runtime part of the used directives. > Be aware that TypeGraphQL doesn't have any special way for implementing schema directives. You should use some [3rd party libraries](https://the-guild.dev/graphql/tools/docs/schema-directives#implementing-schema-directives) depending on the tool set you use in your project, e.g. `@graphql-tools/*` or `ApolloServer`. If you write your custom GraphQL directive or import a package that exports a `GraphQLDirective` instance, you need to register the directives definitions in the `buildSchema` options: ```ts // Build TypeGraphQL executable schema const tempSchema = await buildSchema({ resolvers: [SampleResolver], // Register the directives definitions directives: [myDirective], }); ``` Then you need to apply the schema transformer for your directive, that implements the desired logic of your directive: ```ts // Transform and obtain the final schema const schema = myDirectiveTransformer(tempSchema); ``` If the directive package used by you exports a string-based `typeDefs`, you need to add those typedefs to the schema and then apply directive transformer. Here is an example using the [`@graphql-tools/*`](https://the-guild.dev/graphql/tools): ```ts import { mergeSchemas } from "@graphql-tools/schema"; import { renameDirective } from "fake-rename-directive-package"; // Build TypeGraphQL executable schema const schemaSimple = await buildSchema({ resolvers: [SampleResolver], }); // Merge schema with sample directive type definitions const schemaMerged = mergeSchemas({ schemas: [schemaSimple], // Register the directives definitions typeDefs: [renameDirective.typeDefs], }); // Transform and obtain the final schema const schema = renameDirective.transformer(schemaMerged); ``` ================================================ FILE: docs/emit-schema.md ================================================ --- title: Emitting the schema SDL --- TypeGraphQL's main feature is creating the schema using only TypeScript classes and decorators. However, there might be a need for the schema to be printed into a `schema.graphql` file and there are plenty of reasons for that. Mainly, the schema SDL file is needed for GraphQL ecosystem tools that perform client-side queries autocompletion and validation. Some developers also may want to use it as a kind of snapshot for detecting schema regression or they just prefer to read the SDL file to explore the API instead of reading the complicated TypeGraphQL-based app code, navigating through the GraphiQL or GraphQL Playground. To accomplish this demand, TypeGraphQL allows you to create a schema definition file in two ways. The first one is to generate it automatically on every build of the schema - just pass `emitSchemaFile: true` to the `buildSchema` options in order to emit the `schema.graphql` in the root of the project's working directory. You can also manually specify the path and the file name where the schema definition should be written or even specify `PrintSchemaOptions` to configure the look and format of the schema definition. ```ts const schema = await buildSchema({ resolvers: [ExampleResolver], // Automatically create `schema.graphql` file with schema definition in project's working directory emitSchemaFile: true, // Or create the file with schema in selected path emitSchemaFile: path.resolve(__dirname, "__snapshots__/schema/schema.graphql"), // Or pass a config object emitSchemaFile: { path: __dirname + "/schema.graphql", sortedSchema: false, // By default the printed schema is sorted alphabetically }, }); ``` The second way to emit the schema definition file is by doing it programmatically. We would use the `emitSchemaDefinitionFile` function (or it's sync version `emitSchemaDefinitionFileSync`) and pass in the path, along with the schema object. We can use this among others as part of a testing script that checks if the snapshot of the schema definition is correct or to automatically generate it on every file change during local development. ```ts import { emitSchemaDefinitionFile } from "type-graphql"; // ... hypotheticalFileWatcher.watch("./src/**/*.{resolver,type,input,arg}.ts", async () => { const schema = getSchemaNotFromBuildSchemaFunction(); await emitSchemaDefinitionFile("/path/to/folder/schema.graphql", schema); }); ``` ## Emit schema with custom directives Currently TypeGraphQL does not directly support emitting the schema with custom directives due to `printSchema` function limitations from `graphql-js`. If we want the custom directives to appear in the generated schema definition file we have to create a custom function that use a third-party `printSchema` function. Below there is an example that uses the `printSchemaWithDirectives` function from [`@graphql-tools/utils`](https://www.graphql-tools.com/docs/api/modules/utils): ```ts import { GraphQLSchema, lexicographicSortSchema } from "graphql"; import { printSchemaWithDirectives } from "@graphql-tools/utils"; import fs from "node:fs/promises"; export async function emitSchemaDefinitionWithDirectivesFile( schemaFilePath: string, schema: GraphQLSchema, ): Promise { const schemaFileContent = printSchemaWithDirectives(lexicographicSortSchema(schema)); await fs.writeFile(schemaFilePath, schemaFileContent); } ``` The usage of `emitSchemaDefinitionWithDirectivesFile` function is the same as with standard `emitSchemaDefinitionFile`: ```ts const schema = await buildSchema(/*...*/); await emitSchemaDefinitionWithDirectivesFile("/path/to/folder/schema.graphql", schema); ``` ================================================ FILE: docs/enums.md ================================================ --- title: Enums --- Nowadays almost all typed languages have support for enumerated types, including TypeScript. Enums limit the range of a variable's values to a set of predefined constants, which makes it easier to document intent. GraphQL also has enum type support, so TypeGraphQL allows us to use TypeScript enums in our GraphQL schema. ## Creating enum Let's create a TypeScript enum. It can be a numeric or string enum - the internal values of enums are taken from the enum definition values and the public names taken from the enum keys: ```ts // Implicit value 0, 1, 2, 3 enum Direction { UP, DOWN, LEFT, RIGHT, } // Or explicit values enum Direction { UP = "UP", DOWN = "DOWN", LEFT = "LEFT", RIGHT = "RIGHT", } ``` To tell TypeGraphQL about our enum, we would ideally mark the enums with the `@EnumType()` decorator. However, TypeScript decorators only work with classes, so we need to make TypeGraphQL aware of the enums manually by calling the `registerEnumType` function and providing the enum name for GraphQL: ```ts import { registerEnumType } from "type-graphql"; registerEnumType(Direction, { name: "Direction", // Mandatory description: "The basic directions", // Optional }); ``` In case we need to provide additional GraphQL-related config for values, like description or deprecation reason, we can use `valuesConfig` property and put the data inside it, e.g.: ```ts enum Direction { UP = "UP", DOWN = "DOWN", LEFT = "LEFT", RIGHT = "RIGHT", SIDEWAYS = "SIDEWAYS", } registerEnumType(Direction, { name: "Direction", description: "The basic directions", valuesConfig: { SIDEWAYS: { deprecationReason: "Replaced with Left or Right", }, RIGHT: { description: "The other left", }, }, }); ``` This way, the additional info will be emitted in the GraphQL schema: ```graphql enum Direction { UP DOWN LEFT """ The other left """ RIGHT SIDEWAYS @deprecated(reason: "Replaced with Left or Right") } ``` ## Using enum The last step is very important: TypeScript has limited reflection ability, so this is a case where we have to explicitly provide the enum type for object type fields, input type fields, args, and the return type of queries and mutations: ```ts @InputType() class JourneyInput { @Field(type => Direction) // Mandatory direction: Direction; } ``` Without this annotation, the generated GQL type would be `String` or `Float` (depending on the enum type), rather than the `ENUM` we are aiming for. With all that in place, we can use our enum directly in our code 😉 ```ts @Resolver() class SpriteResolver { private sprite = getMarioSprite(); @Mutation() move(@Arg("direction", type => Direction) direction: Direction): boolean { switch (direction) { case Direction.Up: this.sprite.position.y++; break; case Direction.Down: this.sprite.position.y--; break; case Direction.Left: this.sprite.position.x--; break; case Direction.Right: this.sprite.position.x++; break; default: // Never reached return false; } return true; } } ``` ## Interoperability Enums in TypeGraphQL are designed with server side in mind - the runtime will map the string value from input into a corresponding enum value, like `"UP"` into `0`. While this is very handy e.g. for mapping database values into GraphQL API enum names, it makes it unusable on the query side because `Direction.UP` will put `0` in the query which is an invalid value (should be `UP`). So if we would like to share the types definition and use the enum on the client side app or use the enums directly on the server app e.g. in tests, we have to use the direct mapping of the enum member names with values, e.g.: ```ts enum Direction { UP = "UP", DOWN = "DOWN", LEFT = "LEFT", RIGHT = "RIGHT", } ``` ================================================ FILE: docs/esm.md ================================================ --- title: ECMAScript Modules --- Since `v2.0.0` release, TypeGraphQL is compatible with ECMAScript modules. Thanks to this, we can `import` the `type-graphql` package in the ESM projects without any hassle. ## TypeScript configuration It's important to properly configure the project, so that it uses ESM correctly: - the `module` option should be set to `NodeNext` - the `moduleResolution` option should be set to `"NodeNext"` All in all, the `tsconfig.json` file should looks like this: ```json title="tsconfig.json" { "compilerOptions": { "target": "es2021", "module": "NodeNext", "moduleResolution": "NodeNext", "experimentalDecorators": true, "emitDecoratorMetadata": true } } ``` ## Package.json configuration It is also important to set `type` option to `"module"` in your `package.json` file: ```json title="package.json" { "type": "module" } ``` ## Imports Apart from using `import` syntax, your local imports have to use the `.js` suffix, e.g.: ```ts import { MyResolver } from "./resolvers/MyResolver.js"; ``` ================================================ FILE: docs/examples.md ================================================ --- title: Examples sidebar_label: List of examples --- On the [GitHub repository](https://github.com/MichalLytek/type-graphql) there are a few simple [`examples`](https://github.com/MichalLytek/type-graphql/tree/master/examples) of how to use different `TypeGraphQL` features and how well they integrate with 3rd party libraries. To run an example, simply go to the subdirectory (e.g. `cd ./simple-usage`), and then start the server (`npx ts-node ./index.ts`). Each subdirectory contains a `examples.graphql` file with predefined GraphQL queries/mutations/subscriptions that you can use in Apollo Studio () and play with them by modifying their shape and data. ## Basics - [Simple usage of fields, basic types and resolvers](https://github.com/MichalLytek/type-graphql/tree/master/examples/simple-usage) ## Advanced - [Enums and unions](https://github.com/MichalLytek/type-graphql/tree/master/examples/enums-and-unions) - [Subscriptions (simple)](https://github.com/MichalLytek/type-graphql/tree/master/examples/simple-subscriptions) - [Subscriptions (using Redis) \*\*](https://github.com/MichalLytek/type-graphql/tree/master/examples/redis-subscriptions) - [Interfaces](https://github.com/MichalLytek/type-graphql/tree/master/examples/interfaces-inheritance) - [Extensions (metadata)](https://github.com/MichalLytek/type-graphql/tree/master/examples/extensions) ## Features usage - [Dependency injection (IoC container)](https://github.com/MichalLytek/type-graphql/tree/master/examples/using-container) - [Scoped containers](https://github.com/MichalLytek/type-graphql/tree/master/examples/using-scoped-container) - [Authorization](https://github.com/MichalLytek/type-graphql/tree/master/examples/authorization) - [Validation](https://github.com/MichalLytek/type-graphql/tree/master/examples/automatic-validation) - [Custom validation](https://github.com/MichalLytek/type-graphql/tree/master/examples/custom-validation) - [Types inheritance](https://github.com/MichalLytek/type-graphql/tree/master/examples/interfaces-inheritance) - [Resolvers inheritance](https://github.com/MichalLytek/type-graphql/tree/master/examples/resolvers-inheritance) - [Generic types](https://github.com/MichalLytek/type-graphql/tree/master/examples/generic-types) - [Mixin classes](https://github.com/MichalLytek/type-graphql/tree/master/examples/mixin-classes) - [Middlewares and Custom Decorators](https://github.com/MichalLytek/type-graphql/tree/master/examples/middlewares-custom-decorators) - [Query complexity](https://github.com/MichalLytek/type-graphql/tree/master/examples/query-complexity) ## 3rd party libs integration - [TypeORM (manual, synchronous) \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/typeorm-basic-usage) - [TypeORM (automatic, lazy relations) \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/typeorm-lazy-relations) - [MikroORM \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/mikro-orm) - [Typegoose \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/typegoose) - [Apollo Federation](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-federation) - [Apollo Federation 2](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-federation-2) - [Apollo Cache Control](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-cache) - [GraphQL Scalars](https://github.com/MichalLytek/type-graphql/tree/master/examples/graphql-scalars) - [TSyringe](https://github.com/MichalLytek/type-graphql/tree/master/examples/tsyringe) _\* Note that we need to provide the environment variable `DATABASE_URL` with connection parameters to your local database_ \ _\*\* Note that we need to provide the environment variable `REDIS_URL` with connection parameters to your local Redis instance_ ================================================ FILE: docs/extensions.md ================================================ --- title: Extensions --- The `graphql-js` library allows for putting arbitrary data into GraphQL types config inside the `extensions` property. Annotating schema types or fields with a custom metadata, that can be then used at runtime by middlewares or resolvers, is a really powerful and useful feature. For such use cases, **TypeGraphQL** provides the `@Extensions` decorator, which adds the data we defined to the `extensions` property of the executable schema for the decorated classes, methods or properties. > Be aware that this is a low-level decorator and you generally have to provide your own logic to make use of the `extensions` metadata. ## Using the `@Extensions` decorator Adding extensions to the schema type is as simple as using the `@Extensions` decorator and passing it an object of the custom data we want: ```ts @Extensions({ complexity: 2 }) ``` We can pass several fields to the decorator: ```ts @Extensions({ logMessage: "Restricted access", logLevel: 1 }) ``` And we can also decorate a type several times. The snippet below shows that this attaches the exact same extensions data to the schema type as the snippet above: ```ts @Extensions({ logMessage: "Restricted access" }) @Extensions({ logLevel: 1 }) ``` If we decorate the same type several times with the same extensions key, the one defined at the bottom takes precedence: ```ts @Extensions({ logMessage: "Restricted access" }) @Extensions({ logMessage: "Another message" }) ``` The above usage results in your GraphQL type having a `logMessage: "Another message"` property in its extensions. TypeGraphQL classes with the following decorators can be annotated with `@Extensions` decorator: - `@ObjectType` - `@InputType` - `@Field` - `@Query` - `@Mutation` - `@FieldResolver` So the `@Extensions` decorator can be placed over the class property/method or over the type class itself, and multiple times if necessary, depending on what we want to do with the extensions data: ```ts @Extensions({ roles: ["USER"] }) @ObjectType() class Foo { @Field() field: string; } @ObjectType() class Bar { @Extensions({ roles: ["USER"] }) @Field() field: string; } @ObjectType() class Bar { @Extensions({ roles: ["USER"] }) @Extensions({ visible: false, logMessage: "User accessed restricted field" }) @Field() field: string; } @Resolver(of => Foo) class FooBarResolver { @Extensions({ roles: ["USER"] }) @Query() foobar(@Arg("baz") baz: string): string { return "foobar"; } @Extensions({ roles: ["ADMIN"] }) @FieldResolver() bar(): string { return "foobar"; } } ``` ## Using the extensions data in runtime Once we have decorated the necessary types with extensions, the executable schema will contain the extensions data, and we can make use of it in any way we choose. The most common use will be to read it at runtime in resolvers or middlewares and perform some custom logic there. Here is a simple example of a global middleware that will be logging a message on field resolver execution whenever the field is decorated appropriately with `@Extensions`: ```ts export class LoggerMiddleware implements MiddlewareInterface { constructor(private readonly logger: Logger) {} use({ info }: ResolverData, next: NextFn) { // extract `extensions` object from GraphQLResolveInfo object to get the `logMessage` value const { logMessage } = info.parentType.getFields()[info.fieldName].extensions || {}; if (logMessage) { this.logger.log(logMessage); } return next(); } } ``` ## Examples You can see [more detailed examples of usage here](https://github.com/MichalLytek/type-graphql/tree/master/examples/extensions). ================================================ FILE: docs/faq.md ================================================ --- title: Frequently Asked Questions --- ## Resolvers ### Should I implement a field resolver as an object type getter, a method or a resolver class method? This depends on various factors: - if the resolver only needs access to the root/object value - use a getter - if the field has arguments - and must perform side effects e.g. a database call - use a resolver class method and leverage the dependency injection mechanism - otherwise, use object type methods (pure functions, calculations based on object values and arguments) - if the business logic must be separated from the type definition - use a resolver class method ### Are there any global error handlers to catch errors from resolvers or services? Use middleware for this purpose - just wrap `await next()` in a try-catch block then register it as the first global middleware. ### Why did I receive this error? `GraphQLError: Expected value of type "MyType" but got: [object Object]` This error occurs when the resolver (query, mutation, field) type is an interface/union and a plain object is returned from it. In this case, what should be returned is an instance of the selected object type class in the resolver. Otherwise, `graphql-js` will not be able to correctly detect the underlying GraphQL type. ## Bootstrapping ### How do I fix this error? `Cannot use GraphQLSchema "[object Object]" from another module or realm` This error occurs mostly when there are more than one version of the `graphql-js` module in the project. In most cases it means that one of our dependencies has a dependency on a different version of `graphql-js`, e.g. we, or TypeGraphQL use `v14.0.2` but `apollo-server-express` depends on `v0.13.2`. We can print the dependency tree by running `npm ls graphql` (or the yarn equivalent) to find the faulty dependencies. Then we should update or downgrade them until they all match the semver on `graphql`, e.g. `^14.0.0`. Dependencies may also need to be flattened, so that they all share a single instance of the `graphql` module in the `node_modules` directory - to achieve this, just run `npm dedupe` (or the yarn equivalent). The same rule applies to this error: `node_modules/type-graphql/node_modules/@types/graphql/type/schema").GraphQLSchema' is not assignable to type 'import("node_modules/@types/graphql/type/schema").GraphQLSchema'`. In this case we repeat the same checks but for the `@types/graphql` module in our dependencies. ## Types ### Is `@InputType()` different from `@ArgsType()`? Of course! `@InputType` will generate a real `GraphQLInputType` type and should be used when we need a nested object in the args: ```graphql updateItem(data: UpdateItemInput!): Item! ``` `@ArgsType` is virtual and it will be flattened in schema: ```graphql updateItem(id: Int!, userId: Int!): Item! ``` ### When should I use the `() => [ItemType]` syntax? We should use the `[ItemType]` syntax any time the field type or the return type is an array from a query or mutation. Even though technically the array notation can be omitted (when the base type is not `Promise`) and only provide the type of array item (e.g. `@Field(() => ItemType) field: ItemType[]`) - it's better to be consistent with other annotations by explicitly defining the type. ### How can I define a tuple? Unfortunately, [GraphQL spec doesn't support tuples](https://github.com/graphql/graphql-spec/issues/423), so you can't just use `data: [Int, Float]` as a GraphQL type. Instead, you have to create a transient object (or input) type that fits your data, e.g.: ```graphql type DataPoint { x: Int y: Float } ``` and then use it in the list type as your GraphQL type: ```graphql data: [DataPoint] ``` ### Situations frequently arise where InputType and ObjectType have exactly the same shape. How can I share the definitions? In GraphQL, input objects have a separate type in the system because object types can contain fields that express circular references or references to interfaces and unions, neither of which are appropriate for use as input arguments. However, if there are only simple fields in the class definition, reuse the code between the InputType and the ObjectType by decorating the ObjectType class with `@InputType`. Remember to set a new name of the type in the decorator parameter: ```ts @ObjectType() // Name inferred as 'Person' from class name @InputType("PersonInput") export class Person {} ``` ================================================ FILE: docs/generic-types.md ================================================ --- title: Generic Types --- [Type Inheritance](./inheritance.md) is a great way to reduce code duplication by extracting common fields to the base class. But in some cases, the strict set of fields is not enough because we might need to declare the types of some fields in a more flexible way, like a type parameter (e.g. `items: T[]` in case of a pagination). Hence TypeGraphQL also has support for describing generic GraphQL types. ## How to? Unfortunately, the limited reflection capabilities of TypeScript don't allow for combining decorators with standard generic classes. To achieve behavior like that of generic types, we use the same class-creator pattern like the one described in the [Resolvers Inheritance](./inheritance.md) docs. ### Basic usage Start by defining a `PaginatedResponse` function that creates and returns an abstract `PaginatedResponseClass`: ```ts export default function PaginatedResponse() { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; } ``` To achieve generic-like behavior, the function has to be generic and take some runtime argument related to the type parameter: ```ts export default function PaginatedResponse(TItemClass: ClassType) { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; } ``` Then, add proper decorators to the class which might be `@ObjectType`, `@InterfaceType` or `@InputType`: ```ts export default function PaginatedResponse(TItemClass: ClassType) { @ObjectType() abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; } ``` After that, add fields like in a normal class but using the generic type and parameters: ```ts export default function PaginatedResponse(TItemClass: ClassType) { @ObjectType() abstract class PaginatedResponseClass { // Runtime argument @Field(type => [TItemClass]) // Generic type items: TItem[]; @Field(type => Int) total: number; @Field() hasMore: boolean; } return PaginatedResponseClass; } ``` Finally, use the generic function factory to create a dedicated type class: ```ts @ObjectType() class PaginatedUserResponse extends PaginatedResponse(User) { // Add more fields or overwrite the existing one's types @Field(type => [String]) otherInfo: string[]; } ``` And then use it in our resolvers: ```ts @Resolver() class UserResolver { @Query() users(): PaginatedUserResponse { // Custom business logic, // depending on underlying data source and libraries return { items, total, hasMore, otherInfo, }; } } ``` ### Complex generic type values When we need to provide something different than a class (object type) for the field type, we need to enhance the parameter type signature and provide the needed types. Basically, the parameter that the `PaginatedResponse` function accepts is the value we can provide to `@Field` decorator. So if we want to return an array of strings as the `items` field, we need to add proper types to the function signature, like `GraphQLScalarType` or `String`: ```ts export default function PaginatedResponse( itemsFieldValue: ClassType | GraphQLScalarType | String | Number | Boolean, ) { @ObjectType() abstract class PaginatedResponseClass { @Field(type => [itemsFieldValue]) items: TItemsFieldValue[]; // ... Other fields } return PaginatedResponseClass; } ``` And then provide a proper runtime value (like `String`) while creating a proper subtype of generic `PaginatedResponse` object type: ```ts @ObjectType() class PaginatedStringsResponse extends PaginatedResponse(String) { // ... } ``` ### Types factory We can also create a generic class without using the `abstract` keyword. But with this approach, types created with this kind of factory will be registered in the schema, so this way is not recommended to extend the types for adding fields. To avoid generating schema errors of duplicated `PaginatedResponseClass` type names, we must provide our own unique, generated type name: ```ts export default function PaginatedResponse(TItemClass: ClassType) { // Provide a unique type name used in schema @ObjectType(`Paginated${TItemClass.name}Response`) class PaginatedResponseClass { // ... } return PaginatedResponseClass; } ``` Then, we can store the generated class in a variable and in order to use it both as a runtime object and as a type, we must also create a type for this new class: ```ts const PaginatedUserResponse = PaginatedResponse(User); type PaginatedUserResponse = InstanceType; @Resolver() class UserResolver { // Provide a runtime type argument to the decorator @Query(returns => PaginatedUserResponse) users(): PaginatedUserResponse { // Same implementation as in the earlier code snippet } } ``` ## Examples A more advanced usage example of the generic types feature can be found in [this examples folder](https://github.com/MichalLytek/type-graphql/tree/master/examples/generic-types). ================================================ FILE: docs/getting-started.md ================================================ --- title: Getting started --- > Make sure you've completed all the steps described in the [installation instructions](./installation.md). To explore all of the powerful capabilities of TypeGraphQL, we will create a sample GraphQL API for cooking recipes. Let's start with the `Recipe` type, which is the foundation of our API. ## Types Our goal is to get the equivalent of this type described in SDL: ```graphql type Recipe { id: ID! title: String! description: String creationDate: Date! ingredients: [String!]! } ``` So we create the `Recipe` class with all its properties and types: ```ts class Recipe { id: string; title: string; description?: string; creationDate: Date; ingredients: string[]; } ``` Then we decorate the class and its properties with decorators: ```ts @ObjectType() class Recipe { @Field(type => ID) id: string; @Field() title: string; @Field({ nullable: true }) description?: string; @Field() creationDate: Date; @Field(type => [String]) ingredients: string[]; } ``` The detailed rules of when to use `nullable`, `array` and others are described in the [fields and types docs](./types-and-fields.md). ## Resolvers After that we want to create typical crud queries and mutations. To do so, we create the resolver (controller) class that will have injected the `RecipeService` in the constructor: ```ts @Resolver(Recipe) class RecipeResolver { constructor(private recipeService: RecipeService) {} @Query(returns => Recipe) async recipe(@Arg("id") id: string) { const recipe = await this.recipeService.findById(id); if (recipe === undefined) { throw new RecipeNotFoundError(id); } return recipe; } @Query(returns => [Recipe]) recipes(@Args() { skip, take }: RecipesArgs) { return this.recipeService.findAll({ skip, take }); } @Mutation(returns => Recipe) @Authorized() addRecipe( @Arg("newRecipeData") newRecipeData: NewRecipeInput, @Ctx("user") user: User, ): Promise { return this.recipeService.addNew({ data: newRecipeData, user }); } @Mutation(returns => Boolean) @Authorized(Roles.Admin) async removeRecipe(@Arg("id") id: string) { try { await this.recipeService.removeById(id); return true; } catch { return false; } } } ``` We use the `@Authorized()` decorator to restrict access to authorized users only or the users that fulfil the roles requirements. The detailed rules for when and why we declare `returns => Recipe` functions and others are described in [resolvers docs](./resolvers.md). ## Inputs and Arguments Ok, but what are `NewRecipeInput` and `RecipesArgs`? They are, of course, classes: ```ts @InputType() class NewRecipeInput { @Field() @MaxLength(30) title: string; @Field({ nullable: true }) @Length(30, 255) description?: string; @Field(type => [String]) @ArrayMaxSize(30) ingredients: string[]; } @ArgsType() class RecipesArgs { @Field(type => Int) @Min(0) skip: number = 0; @Field(type => Int) @Min(1) @Max(50) take: number = 25; } ``` `@Length`, `@Min` and `@ArrayMaxSize` are decorators from [`class-validator`](https://github.com/typestack/class-validator) that automatically perform field validation in TypeGraphQL. ## Building schema The last step that needs to be done is to actually build the schema from the TypeGraphQL definition. We use the `buildSchema` function for this: ```ts const schema = await buildSchema({ resolvers: [RecipeResolver], }); // ... Server ``` Et voilà! Now we have fully functional GraphQL schema! If we print it, this is how it would look: ```graphql type Recipe { id: ID! title: String! description: String creationDate: Date! ingredients: [String!]! } input NewRecipeInput { title: String! description: String ingredients: [String!]! } type Query { recipe(id: ID!): Recipe recipes(skip: Int = 0, take: Int = 25): [Recipe!]! } type Mutation { addRecipe(newRecipeData: NewRecipeInput!): Recipe! removeRecipe(id: ID!): Boolean! } ``` ## Want more? That was only the tip of the iceberg - a very simple example with basic GraphQL types. Do you use interfaces, enums, unions and custom scalars? That's great because TypeGraphQL fully supports them too! There are also more advanced concepts like the authorization checker, inheritance support and field resolvers. A lot of these topics are covered in [Ben Awad](https://github.com/benawad)'s [TypeGraphQL video series](https://www.youtube.com/playlist?list=PLN3n1USn4xlma1bBu3Tloe4NyYn9Ko8Gs) on YouTube. For more complicated cases, go to the [Examples section](./examples.md) where you can discover e.g. how well TypeGraphQL integrates with TypeORM. ================================================ FILE: docs/inheritance.md ================================================ --- title: Inheritance --- The main idea of TypeGraphQL is to create GraphQL types based on TypeScript classes. In object-oriented programming it is common to compose classes using inheritance. Hence, TypeGraphQL supports composing type definitions by extending classes. ## Types inheritance One of the most known principles of software development is DRY - Don't Repeat Yourself - which is about avoiding code redundancy. While creating a GraphQL API, it's a common pattern to have pagination args in resolvers, like `skip` and `take`. So instead of repeating ourselves, we declare it once: ```ts @ArgsType() class PaginationArgs { @Field(type => Int) skip: number = 0; @Field(type => Int) take: number = 25; } ``` and then reuse it everywhere: ```ts @ArgsType() class GetTodosArgs extends PaginationArgs { @Field() onlyCompleted: boolean = false; } ``` This technique also works with input type classes, as well as with object type classes: ```ts @ObjectType() class Person { @Field() age: number; } @ObjectType() class Student extends Person { @Field() universityName: string; } ``` Note that both the subclass and the parent class must be decorated with the same type of decorator, like `@ObjectType()` in the example `Person -> Student` above. Mixing decorator types across parent and child classes is prohibited and might result in a schema building error, e.g. we can't decorate the subclass with `@ObjectType()` and the parent with `@InputType()`. ## Resolver Inheritance A special kind of inheritance in TypeGraphQL is resolver class inheritance. This pattern allows us e.g. to create a base CRUD resolver class for our resource/entity, so we don't have to repeat common boilerplate code. Since we need to generate unique query/mutation names, we have to create a factory function for our base class: ```ts function createBaseResolver() { abstract class BaseResolver {} return BaseResolver; } ``` Be aware that with some `tsconfig.json` settings (like `declarations: true`) we might receive a `[ts] Return type of exported function has or is using private name 'BaseResolver'` error - in this case we might need to use `any` as the return type or create a separate class/interface describing the class methods and properties. This factory should take a parameter that we can use to generate the query/mutation names, as well as the type that we would return from the resolvers: ```ts function createBaseResolver(suffix: string, objectTypeCls: T) { abstract class BaseResolver {} return BaseResolver; } ``` It's very important to mark the `BaseResolver` class using the `@Resolver` decorator: ```ts function createBaseResolver(suffix: string, objectTypeCls: T) { @Resolver() abstract class BaseResolver {} return BaseResolver; } ``` We can then implement the resolver methods as usual. The only difference is that we can use the `name` decorator option for `@Query`, `@Mutation` and `@Subscription` decorators to overwrite the name that will be emitted in schema: ```ts function createBaseResolver(suffix: string, objectTypeCls: T) { @Resolver() abstract class BaseResolver { protected items: T[] = []; @Query(type => [objectTypeCls], { name: `getAll${suffix}` }) async getAll(@Arg("first", type => Int) first: number): Promise { return this.items.slice(0, first); } } return BaseResolver; } ``` Now we can create a specific resolver class that will extend the base resolver class: ```ts const PersonBaseResolver = createBaseResolver("person", Person); @Resolver(of => Person) export class PersonResolver extends PersonBaseResolver { // ... } ``` We can also add specific queries and mutations in our resolver class, as always: ```ts const PersonBaseResolver = createBaseResolver("person", Person); @Resolver(of => Person) export class PersonResolver extends PersonBaseResolver { @Mutation() addPerson(@Arg("input") personInput: PersonInput): Person { this.items.push(personInput); return personInput; } } ``` And that's it! We just need to normally register `PersonResolver` in `buildSchema` and the extended resolver will work correctly. We must be aware that if we want to overwrite the query/mutation/subscription from the parent resolver class, we need to generate the same schema name (using the `name` decorator option or the class method name). It will overwrite the implementation along with the GraphQL args and return types. If we only provide a different implementation of the inherited method like `getOne`, it won't work. ## Examples More advanced usage examples of type inheritance (and interfaces) can be found in [the example folder](https://github.com/MichalLytek/type-graphql/tree/master/examples/interfaces-inheritance). For a more advanced resolver inheritance example, please go to [this example folder](https://github.com/MichalLytek/type-graphql/tree/master/examples/resolvers-inheritance). ================================================ FILE: docs/installation.md ================================================ --- title: Installation --- Before getting started with TypeGraphQL we need to install some additional dependencies and properly configure the TypeScript configuration for our project. > **Prerequisites** > > Before we begin, we must make sure our development environment includes Node.js and npm. ## Packages installation First, we have to install the main package, as well as [`graphql-js`](https://github.com/graphql/graphql-js) and [`graphql-scalars`](https://github.com/urigo/graphql-scalars) which are peer dependencies of TypeGraphQL: ```sh npm install graphql graphql-scalars type-graphql ``` Also, the `Reflect.metadata()` shim is required to make the type reflection work: ```sh npm install reflect-metadata # or npm install core-js ``` We must ensure that it is imported at the top of our entry file (before we use/import `type-graphql` or our resolvers): ```ts import "reflect-metadata"; // or import "core-js/features/reflect"; ``` ## TypeScript configuration It's important to set these options in the `tsconfig.json` file of our project: ```json { "emitDecoratorMetadata": true, "experimentalDecorators": true } ``` `TypeGraphQL` is designed to work with Node.js LTS and the latest stable releases. It uses features from ES2021 so we should set our `tsconfig.json` file appropriately: ```js { "target": "es2021" // Or newer if Node.js version supports it } ``` All in all, the minimal `tsconfig.json` file example looks like this: ```json { "compilerOptions": { "target": "es2021", "module": "commonjs", "experimentalDecorators": true, "emitDecoratorMetadata": true } } ``` ================================================ FILE: docs/interfaces.md ================================================ --- title: Interfaces --- The main idea of TypeGraphQL is to create GraphQL types based on TypeScript classes. In object-oriented programming it is common to create interfaces which describe the contract that classes implementing them must adhere to. Hence, TypeGraphQL supports defining GraphQL interfaces. Read more about the GraphQL Interface Type in the [official GraphQL docs](https://graphql.org/learn/schema/#interfaces). ## Abstract classes TypeScript has first class support for interfaces. Unfortunately, they only exist at compile-time, so we can't use them to build GraphQL schema at runtime by using decorators. Luckily, we can use an abstract class for this purpose. It behaves almost like an interface as it can't be instantiated but it can be implemented by another class. The only difference is that it just won't prevent developers from implementing a method or initializing a field. So, as long as we treat the abstract class like an interface, we can safely use it. ## Defining interface type How do we create a GraphQL interface definition? We create an abstract class and decorate it with the `@InterfaceType()` decorator. The rest is exactly the same as with object types: we use the `@Field` decorator to declare the shape of the type: ```ts @InterfaceType() abstract class IPerson { @Field(type => ID) id: string; @Field() name: string; @Field(type => Int) age: number; } ``` We can then use this interface type class like an interface in the object type class definition: ```ts @ObjectType({ implements: IPerson }) class Person implements IPerson { id: string; name: string; age: number; } ``` The only difference is that we have to let TypeGraphQL know that this `ObjectType` is implementing the `InterfaceType`. We do this by passing the param `({ implements: IPerson })` to the decorator. If we implement multiple interfaces, we pass an array of interfaces like so: `({ implements: [IPerson, IAnimal, IMachine] })`. It is also allowed to omit the decorators since the GraphQL types will be copied from the interface definition - this way we won't have to maintain two definitions and solely rely on TypeScript type checking for correct interface implementation. We can also extend the base interface type abstract class as well because all the fields are inherited and emitted in schema: ```ts @ObjectType({ implements: IPerson }) class Person extends IPerson { @Field() hasKids: boolean; } ``` ## Implementing other interfaces Since `graphql-js` version `15.0`, it's also possible for interface type to [implement other interface types](https://github.com/graphql/graphql-js/pull/2084). To accomplish this, we can just use the same syntax that we utilize for object types - the `implements` decorator option: ```ts @InterfaceType() class Node { @Field(type => ID) id: string; } @InterfaceType({ implements: Node }) class Person extends Node { @Field() name: string; @Field(type => Int) age: number; } ``` Also, when we implement the interface that already implements other interface, there's no need to put them all in `implements` array in `@ObjectType` decorator option - only the closest one in the inheritance chain is required, e.g.: ```ts @ObjectType({ implements: [Person] }) class Student extends Person { @Field() universityName: string; } ``` This example produces following representation in GraphQL SDL: ```graphql interface Node { id: ID! } interface Person implements Node { id: ID! name: String! age: Int! } type Student implements Node & Person { id: ID! name: String! age: Int! universityName: String! } ``` ## Resolvers and arguments What's more, we can define resolvers for the interface fields, using the same syntax we would use when defining one for our object type: ```ts @InterfaceType() abstract class IPerson { @Field() firstName: string; @Field() lastName: string; @Field() fullName(): string { return `${this.firstName} ${this.lastName}`; } } ``` They're inherited by all the object types that implements this interface type but does not provide their own resolver implementation for those fields. Additionally, if we want to declare that the interface accepts some arguments, e.g.: ```graphql interface IPerson { avatar(size: Int!): String! } ``` We can just use `@Arg` or `@Args` decorators as usual: ```ts @InterfaceType() abstract class IPerson { @Field() avatar(@Arg("size") size: number): string { return `http://i.pravatar.cc/${size}`; } } ``` Unfortunately, TypeScript doesn't allow using decorators on abstract methods. So if we don't want to provide implementation for that field resolver, only to enforce some signature (args and return type), we have to throw an error inside the body: ```ts @InterfaceType() abstract class IPerson { @Field() avatar(@Arg("size") size: number): string { throw new Error("Method not implemented!"); } } ``` And then we need to extend the interface class and override the method by providing its body - it is required for all object types that implements that interface type: ```ts @ObjectType({ implements: IPerson }) class Person extends IPerson { avatar(size: number): string { return `http://i.pravatar.cc/${size}`; } } ``` In order to extend the signature by providing additional arguments (like `format`), we need to redeclare the whole field signature: ```ts @ObjectType({ implements: IPerson }) class Person implements IPerson { @Field() avatar(@Arg("size") size: number, @Arg("format") format: string): string { return `http://i.pravatar.cc/${size}.${format}`; } } ``` Resolvers for interface type fields can be also defined on resolvers classes level, by using the `@FieldResolver` decorator: ```ts @Resolver(of => IPerson) class IPersonResolver { @FieldResolver() avatar(@Root() person: IPerson, @Arg("size") size: number): string { return `http://typegraphql.com/${person.id}/${size}`; } } ``` ## Registering in schema By default, if the interface type is explicitly used in schema definition (used as a return type of a query/mutation or as some field type), all object types that implement that interface will be emitted in schema, so we don't need to do anything. However, in some cases like the `Node` interface that is used in Relay-based systems, this behavior might be not intended when exposing multiple, separates schemas (like a public and the private ones). In this situation, we can provide an `{ autoRegisterImplementations: false }` option to the `@InterfaceType` decorator to prevent emitting all this object types in the schema: ```ts @InterfaceType({ autoRegisterImplementations: false }) abstract class Node { @Field(type => ID) id: string; } ``` Then we need to add all the object types (that implement this interface type and which we want to expose in selected schema) to the `orphanedTypes` array option in `buildSchema`: ```ts const schema = await buildSchema({ resolvers, // Provide orphaned object types orphanedTypes: [Person, Animal, Recipe], }); ``` Be aware that if the object type class is explicitly used as the GraphQL type (like `Recipe` type as the return type of `addRecipe` mutation), it will be emitted regardless the `orphanedTypes` setting. ## Resolving Type Be aware that when our object type is implementing a GraphQL interface type, **we have to return an instance of the type class** in our resolvers. Otherwise, `graphql-js` will not be able to detect the underlying GraphQL type correctly. We can also provide our own `resolveType` function implementation to the `@InterfaceType` options. This way we can return plain objects in resolvers and then determine the returned object type by checking the shape of the data object, the same ways [like in unions](./unions.md), e.g.: ```ts @InterfaceType({ resolveType: value => { if ("grades" in value) { return "Student"; // Schema name of type string } return Person; // Or object type class }, }) abstract class IPerson { // ... } ``` However in case of interfaces, it might be a little bit more tricky than with unions, as we might not remember all the object types that implements this particular interface. ## Examples For more advanced usage examples of interfaces (and type inheritance), e.g. with query returning an interface type, go to [this examples folder](https://github.com/MichalLytek/type-graphql/tree/master/examples/interfaces-inheritance). ================================================ FILE: docs/introduction.md ================================================ --- title: Introduction sidebar_label: What & Why --- We all love GraphQL! It's really great and solves many problems that we have with REST APIs, such as overfetching and underfetching. But developing a GraphQL API in Node.js with TypeScript is sometimes a bit of a pain. ## What? **TypeGraphQL** is a library that makes this process enjoyable by defining the schema using only classes and a bit of decorator magic. Example object type: ```ts @ObjectType() class Recipe { @Field() title: string; @Field(type => [Rate]) ratings: Rate[]; @Field({ nullable: true }) averageRating?: number; } ``` It also has a set of useful features, like validation, authorization and dependency injection, which helps develop GraphQL APIs quickly & easily! ## Why? As mentioned, developing a GraphQL API in Node.js with TypeScript is sometimes a bit of a pain. Why? Let's take a look at the steps we usually have to take. First, we create all the schema types in SDL. We also create our data models using [ORM classes](https://github.com/typeorm/typeorm), which represent our database entities. Then we start to write resolvers for our queries, mutations and fields. This forces us, however, to begin with creating TypeScript interfaces for all arguments and inputs and/or object types. After that, we can actually implement the resolvers, using weird generic signatures, e.g.: ```ts export const getRecipesResolver: GraphQLFieldResolver = async ( _, args, ctx, ) => { // Common tasks repeatable for almost every resolver const auth = Container.get(AuthService); if (!auth.check(ctx.user)) { throw new NotAuthorizedError(); } await joi.validate(getRecipesSchema, args); const repository = TypeORM.getRepository(Recipe); // Business logic, e.g.: return repository.find({ skip: args.offset, take: args.limit }); }; ``` The biggest problem is code redundancy which makes it difficult to keep things in sync. To add a new field to our entity, we have to jump through all the files: modify the entity class, then modify the schema, and finally update the interface. The same goes with inputs or arguments: it's easy to forget to update one of them or make a mistake with a type. Also, what if we've made a typo in a field name? The rename feature (F2) won't work correctly. **TypeGraphQL** comes to address these issues, based on experience from a few years of developing GraphQL APIs in TypeScript. The main idea is to have only one source of truth by defining the schema using classes and a bit of decorator help. Additional features like dependency injection, validation and auth guards help with common tasks that would normally have to be handled by ourselves. ================================================ FILE: docs/middlewares.md ================================================ --- title: Middleware and guards --- Middleware are pieces of reusable code that can be easily attached to resolvers and fields. By using middleware we can extract the commonly used code from our resolvers and then declaratively attach it using a decorator or even registering it globally. ## Creating Middleware ### What is Middleware? Middleware is a very powerful but somewhat complicated feature. Basically, middleware is a function that takes 2 arguments: - resolver data - the same as resolvers (`root`, `args`, `context`, `info`) - the `next` function - used to control the execution of the next middleware and the resolver to which it is attached We may be familiar with how middleware works in [`express.js`](https://expressjs.com/en/guide/writing-middleware.html) but TypeGraphQL middleware is inspired by [`koa.js`](http://koajs.com/#application). The difference is that the `next` function returns a promise of the value of subsequent middleware and resolver execution from the stack. This makes it easy to perform actions before or after resolver execution. So things like measuring execution time are simple to implement: ```ts export const ResolveTime: MiddlewareFn = async ({ info }, next) => { const start = Date.now(); await next(); const resolveTime = Date.now() - start; console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`); }; ``` ### Intercepting the execution result Middleware also has the ability to intercept the result of a resolver's execution. It's not only able to e.g. create a log but also replace the result with a new value: ```ts export const CompetitorInterceptor: MiddlewareFn = async (_, next) => { const result = await next(); if (result === "typegql") { return "type-graphql"; } return result; }; ``` It might not seem very useful from the perspective of this library's users but this feature was mainly introduced for plugin systems and 3rd-party library integration. Thanks to this, it's possible to e.g. wrap the returned object with a lazy-relation wrapper that automatically fetches relations from a database on demand under the hood. ### Simple Middleware If we only want to do something before an action, like log the access to the resolver, we can just place the `return next()` statement at the end of our middleware: ```ts const LogAccess: MiddlewareFn = ({ context, info }, next) => { const username: string = context.username || "guest"; console.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`); return next(); }; ``` ### Guards Middleware can also break the middleware stack by not calling the `next` function. This way, the result returned from the middleware will be used instead of calling the resolver and returning it's result. We can also throw an error in the middleware if the execution must be terminated and an error returned to the user, e.g. when resolver arguments are incorrect. This way we can create a guard that blocks access to the resolver and prevents execution or any data return. ```ts export const CompetitorDetector: MiddlewareFn = async ({ args }, next) => { if (args.frameworkName === "type-graphql") { return "TypeGraphQL"; } if (args.frameworkName === "typegql") { throw new Error("Competitive framework detected!"); } return next(); }; ``` ### Reusable Middleware Sometimes middleware has to be configurable, just like we pass a `roles` array to the [`@Authorized()` decorator](./authorization.md). In this case, we should create a simple middleware factory - a function that takes our configuration as a parameter and returns a middleware that uses the provided value. ```ts export function NumberInterceptor(minValue: number): MiddlewareFn { return async (_, next) => { const result = await next(); // Hide values below minValue if (typeof result === "number" && result < minValue) { return null; } return result; }; } ``` Remember to call this middleware with an argument, e.g. `NumberInterceptor(3.0)`, when attaching it to a resolver! ### Error Interceptors Middleware can also catch errors that were thrown during execution. This way, they can easily be logged and even filtered for info that can't be returned to the user: ```ts export const ErrorInterceptor: MiddlewareFn = async ({ context, info }, next) => { try { return await next(); } catch (err) { // Write error to file log fileLog.write(err, context, info); // Hide errors from db like printing sql query if (someCondition(err)) { throw new Error("Unknown error occurred!"); } // Rethrow the error throw err; } }; ``` ### Class-based Middleware Sometimes our middleware logic can be a bit complicated - it may communicate with a database, write logs to file, etc., so we might want to test it. In that case we create class middleware that is able to benefit from [dependency injection](./dependency-injection.md) and easily mock a file logger or a database repository. To accomplish this, we implement a `MiddlewareInterface`. Our class must have the `use` method that conforms with the `MiddlewareFn` signature. Below we can see how the previously defined `LogAccess` middleware looks after the transformation: ```ts export class LogAccess implements MiddlewareInterface { constructor(private readonly logger: Logger) {} async use({ context, info }: ResolverData, next: NextFn) { const username: string = context.username || "guest"; this.logger.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`); return next(); } } ``` ## How to use ### Attaching Middleware To attach middleware to a resolver method, place the `@UseMiddleware()` decorator above the method declaration. It accepts an array of middleware that will be called in the provided order. We can also pass them without an array as it supports [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters): ```ts @Resolver() export class RecipeResolver { @Query() @UseMiddleware(ResolveTime, LogAccess) randomValue(): number { return Math.random(); } } ``` If we want to apply the middlewares to all the resolver's class methods, we can put the decorator on top of the class declaration: ```ts @UseMiddleware(ResolveTime, LogAccess) @Resolver() export class RecipeResolver { @Query() randomValue(): number { return Math.random(); } @Query() constantValue(): number { return 21.37; } } ``` > Be aware that resolver's class middlewares are executed first, before the method's ones. We can also attach the middleware to the `ObjectType` fields, the same way as with the [`@Authorized()` decorator](./authorization.md). ```ts @ObjectType() export class Recipe { @Field() title: string; @Field(type => [Int]) @UseMiddleware(LogAccess) ratings: number[]; } ``` ### Global Middleware However, for common middlewares like measuring resolve time or catching errors, it might be annoying to place a `@UseMiddleware(ResolveTime)` decorator on every field, method or resolver class. Hence, in TypeGraphQL we can also register a global middleware that will be called for each query, mutation, subscription and a field. For this, we use the `globalMiddlewares` property of the `buildSchema` configuration object: ```ts const schema = await buildSchema({ resolvers: [RecipeResolver], globalMiddlewares: [ErrorInterceptor, ResolveTime], }); ``` ### Custom Decorators If we want to use middlewares with a more descriptive and declarative API, we can also create a custom method decorators. See how to do this in [custom decorators docs](./custom-decorators.md#method-decorators). ## Example See how different kinds of middlewares work in the [middlewares and custom decorators example](https://github.com/MichalLytek/type-graphql/tree/master/examples/middlewares-custom-decorators). ================================================ FILE: docs/migration-guide.md ================================================ --- title: Migration Guide sidebar_label: v1.x -> v2.0 --- > This chapter contains migration guide, that will help you upgrade your codebase from using old Typegraphql `v1.x` into the newest `v2.0` release. > > If you just started using TypeGraphQL and you have `v2.0` installed, you can skip this chapter and go straight into the "Advanced guides" section. ## New `DateTimeISO` scalar name in schema One of the breaking change released in `v2.0` is using `Date` scalars from `graphql-scalars` package, instead of custom ones that were built-in in TypegraphQL. This means that the exported `GraphQLISODateTime` scalar is registered in schema under a changed name - `DateTimeISO`. If you don't plan to use other `DateTime` scalar in your project and you need to restore the existing scalar name for an easy upgrade to the latest TypeGraphQL version (without rewriting your GraphQL queries), here's a simple snippet for you to use. First, you need to create an alias for the `GraphQLDateTimeISO` scalar: ```ts import { GraphQLDateTimeISO } from "graphql-scalars"; import { GraphQLScalarType } from "graphql"; const AliasedGraphQLDateTimeISO = new GraphQLScalarType({ ...GraphQLDateTimeISO.toConfig(), name: "DateTime", // use old name }); ``` And then register the scalars mapping in the schema you build, in order to overwrite the default date scalar: ```ts import { buildSchema } from "type-graphql"; const schema = await buildSchema({ resolvers, scalarsMap: [{ type: Date, scalar: AliasedGraphQLDateTimeISO }], }); ``` An alternative solution would be to just search for `DateTime` via CTRL+F in your codebase and replace with `DateTimeISO` in your queries, if you don't need the backward compatibility for existing released client apps. ## Subscriptions The new `v2.0` release contains a bunch of breaking changes related to the GraphQL subscriptions feature. In previous releases, this feature was build upon the [`graphql-subscriptions`](https://github.com/apollographql/graphql-subscriptions) package and it's `PubSub` system. However, it's become unmaintained in the last years and some alternatives has been developed in the meantime. So since `v2.0`, TypeGraphQL relies on the new [`@graphql-yoga/subscriptions`](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions) package which is built on top of latest ECMAScript features. It also has own `PubSub` implementation which works in a similar fashion, but has a slightly different API. We did out best to hide under the hood all the differences between the APIs of those packages, but some breaking changes had to occurred in the TypeGraphQL API. ### The `pubSub` option of `buildSchema` It is now required to pass the `PubSub` instance as the config option of `buildSchema` function. Previously, you could omit it and rely on the default one created by TypeGraphQL. The reason for this change is that `@graphql-yoga/subscriptions` package allows to create a type-safe `PubSub` instance via the [generic `createPubSub` function](https://the-guild.dev/graphql/yoga-server/v2/features/subscriptions#topics), so you can add type info about the topics and params required while using `.publish()` method. Simple example of the new API: ```ts import { buildSchema } from "type-graphql"; import { createPubSub } from "@graphql-yoga/subscriptions"; export const pubSub = createPubSub<{ NOTIFICATIONS: [NotificationPayload]; DYNAMIC_ID_TOPIC: [number, NotificationPayload]; }>(); const schema = await buildSchema({ resolver, pubSub, }); ``` Be aware that you can use any `PubSub` system you want, not only the `graphql-yoga` one. The only requirement is to comply with the exported `PubSub` interface - having proper `.subscribe()` and `.publish()` methods. ### No `@PubSub` decorator The consequence of not having automatically created, default `PubSub` instance, is that you don't need access to the internally-created `PubSub` instance. Hence, the `@PubSub` decorator was removed - please use dependency injection system if you don't want to have a hardcoded import. The corresponding `Publisher` type was also removed as it was not needed anymore. ### Renamed and removed types There was some inconsistency in naming of the decorator option functions argument types, which was unified in the `v2.0` release. If you reference those types in your code (`filter` or `subscribe` decorator option functions), make sure you update your type annotation and imports to the new name. - `ResolverFilterData` -> `SubscriptionHandlerData` - `ResolverTopicData` -> `SubscribeResolverData` Also, apart from the `Publisher` type mentioned above, the `PubSubEngine` type has been removed and is no longer exported from the package. ### Topic with Dynamic ID As TypeGraphQL uses `@graphql-yoga/subscriptions` under the hood, it also aims to use its features. And one of the extension to the old `PubSub` system used in `v1.x` is ability to not only use dynamic topics but a topic with a dynamic id. You can read more about this new feature in [subscription docs](./subscriptions.md#topic-with-dynamic-id). ================================================ FILE: docs/nestjs.md ================================================ --- title: NestJS Integration sidebar_label: NestJS --- TypeGraphQL provides some basic integration with NestJS by the [`typegraphql-nestjs` package](https://www.npmjs.com/package/typegraphql-nestjs). It allows to use TypeGraphQL features while integrating with NestJS modules system and its dependency injector. ## Overview The usage is similar to the official `@nestjs/graphql` package. First you need to register your resolver classes in `providers` of the `@Module` : ```ts @Module({ providers: [RecipeResolver, RecipeService], }) export default class RecipeModule {} ``` Then you need to register the TypeGraphQL module in your root module - you can pass there all standard `buildSchema` options: ```ts @Module({ imports: [ TypeGraphQLModule.forRoot({ emitSchemaFile: true, authChecker, context: ({ req }) => ({ currentUser: req.user }), }), RecipeModule, ], }) export default class AppModule {} ``` And your `AppModule` is ready to use like with a standard NestJS approach. ### Caveats For now, this basic integration doesn't support other NestJS features like guards, interceptors, filters and pipes. To achieve the same goals, you can use standard TypeGraphQL equivalents - middlewares, custom decorators, built-in authorization and validation. ## Documentation and examples You can find some examples and more detailed info about the installation and the usage [in the separate GitHub repository](https://github.com/MichalLytek/typegraphql-nestjs). ================================================ FILE: docs/performance.md ================================================ --- title: Performance --- **TypeGraphQL** is basically an abstraction layer built on top of the reference GraphQL implementation for Javascript - [`graphql-js`](https://github.com/graphql/graphql-js). It not only allows for building a GraphQL schema using classes and decorators but also gives a set of tools that focus on the developer experience and allows for making common tasks easily - authorization, validation, custom middlewares and others. While this enable easy and convenient development, it's sometimes a tradeoff in a performance. ## Benchmarks To measure the overhead of the abstraction, a few demo examples were made to compare the usage of TypeGraphQL against the implementations using "bare metal" - raw `graphql-js` library. The benchmarks are located in a [folder on the GitHub repo](../benchmarks). The most demanding cases like returning an array of 25 000 nested objects showed that in some cases it might be about 5 times slower. | | 25 000 array items | Deeply nested object | | -------------------- | :----------------: | :------------------: | | Standard TypeGraphQL | 1253.28 ms | 45.57 μs | | `graphql-js` | 265.52 ms | 24.22 μs | In real apps (e.g. with complex database queries) it's usually a much lower factor but still not negligible. That's why TypeGraphQL has some built-in performance optimization options. ## Optimizations Promises in JS have a quite big performance overhead. In the same example of returning an array with 25 000 items, if we change the Object Type field resolvers to an asynchronous one that return a promise, the execution slows down by a half even in "raw" `graphql-js`. | `graphql-js` | 25 000 array items | | --------------- | :----------------: | | sync resolvers | 265.52 ms | | async resolvers | 512.61 ms | TypeGraphQL tries to avoid the async execution path when it's possible, e.g. if the query/mutation/field resolver doesn't use the auth feature, doesn't use args (or has args validation disabled) and if doesn't return a promise. So if you find a bottleneck in your app, try to investigate your resolvers, disable not used features and maybe remove some unnecessary async/await usage. Also, using middlewares implicitly turns on the async execution path (for global middlewares the middlewares stack is created even for every implicit field resolver!), so be careful when using this feature if you care about the performance very much (and maybe then use the "simple resolvers" tweak described below). The whole middleware stack will be soon redesigned with a performance in mind and with a new API that will also allow fine-grained scoping of global middlewares. Stay tuned! ## Further performance tweaks When we have a query that returns a huge amount of JSON-like data and we don't need any field-level access control or other custom middlewares, we can turn off the whole authorization and middlewares stack for selected field resolver using a `{ simple: true }` decorator option, e.g.: ```ts @ObjectType() class SampleObject { @Field() sampleField: string; @Field({ simple: true }) publicFrequentlyQueriedField: SomeType; } ``` Moreover, we can also apply this behavior for all the fields of the object type by using a `{ simpleResolvers: true }` decorator option, e.g.: ```ts @ObjectType({ simpleResolvers: true }) class Post { @Field() title: string; @Field() createdAt: Date; @Field() isPublished: boolean; } ``` This simple trick can speed up the execution up to 76%! The benchmarks show that using simple resolvers allows for as fast execution as with bare `graphql-js` - the measured overhead is only about ~13%, which is a much more reasonable value than 500%. Below you can see [the benchmarks results](../benchmarks): | | 25 000 array items | | ------------------------------------------------------------------------ | :----------------: | | `graphql-js` | 265.52 ms | | Standard TypeGraphQL | 310.36 ms | | TypeGraphQL with a global middleware | 1253.28 ms | | **TypeGraphQL with "simpleResolvers" applied (and a global middleware)** | **299.61 ms** | > This optimization **is not turned on by default** mostly because of the global middlewares and authorization feature. By using "simple resolvers" we are turning them off, so we have to be aware of the consequences - `@Authorized` guard on fields won't work for that fields so they will be publicly available, as well as global middlewares won't be executed for that fields, so we might lost, for example, performance metrics or access logs. That's why we should **be really careful with using this tweak**. The rule of thumb is to use "simple resolvers" only when it's really needed, like returning huge array of nested objects. ================================================ FILE: docs/prisma.md ================================================ --- title: Prisma Integration sidebar_label: Prisma --- TypeGraphQL provides an integration with Prisma by the [`typegraphql-prisma` package](https://www.npmjs.com/package/typegraphql-prisma). It generates the type classes and CRUD resolvers based on the Prisma schema, so we can execute complex queries or mutations that corresponds to the Prisma actions, without having to write any code for that. ## Overview To make use of the prisma integration, first we need to add a new generator to the `schema.prisma` file: ```sh generator typegraphql { provider = "typegraphql-prisma" } ``` Then, after running `prisma generate` we can import the generated resolvers classes and use them to build our schema: ```ts import { resolvers } from "@generated/type-graphql"; const schema = await buildSchema({ resolvers, validate: false, }); ``` So we will be able to execute a complex query, that talks with the real database, in just a few minutes! ```graphql query GetSomeUsers { users(where: { email: { contains: "prisma" } }, orderBy: { name: desc }) { id name email posts(take: 10, orderBy: { updatedAt: desc }) { published title content } } } ``` ## Documentation and examples To read about all the `typegraphql-prisma` features, like exposing selected Prisma actions or changing exposed model type name, as well as how to write a custom query or how to add some fields to model type, please check the docs [on the dedicated website](https://prisma.typegraphql.com). There also can be found the links to some examples and more detailed info about the installation and the configuration. ================================================ FILE: docs/resolvers.md ================================================ --- title: Resolvers --- Besides [declaring GraphQL's object types](./types-and-fields.md), TypeGraphQL allows us to easily create queries, mutations and field resolvers - like normal class methods, similar to REST controllers in frameworks like Java `Spring`, .NET `Web API` or TypeScript [`routing-controllers`](https://github.com/typestack/routing-controllers). ## Queries and Mutations ### Resolver classes First we create the resolver class and annotate it with the `@Resolver()` decorator. This class will behave like a controller from classic REST frameworks: ```ts @Resolver() class RecipeResolver {} ``` We can use a DI framework (as described in the [dependency injection docs](./dependency-injection.md)) to inject class dependencies (like services or repositories) or to store data inside the resolver class - it's guaranteed to be a single instance per app. ```ts @Resolver() class RecipeResolver { private recipesCollection: Recipe[] = []; } ``` Then we can create class methods which will handle queries and mutations. For example, let's add the `recipes` query to return a collection of all recipes: ```ts @Resolver() class RecipeResolver { private recipesCollection: Recipe[] = []; async recipes() { // Fake async return await this.recipesCollection; } } ``` We also need to do two things. The first is to add the `@Query` decorator, which marks the class method as a GraphQL query. The second is to provide the return type. Since the method is async, the reflection metadata system shows the return type as a `Promise`, so we have to add the decorator's parameter as `returns => [Recipe]` to declare it resolves to an array of `Recipe` object types. ```ts @Resolver() class RecipeResolver { private recipesCollection: Recipe[] = []; @Query(returns => [Recipe]) async recipes() { return await this.recipesCollection; } } ``` ### Arguments Usually, queries have some arguments - it might be the id of a resource, a search phrase or pagination settings. TypeGraphQL allows you to define arguments in two ways. First is the inline method using the `@Arg()` decorator. The drawback is the need to repeating the argument name (due to a limitation of the reflection system) in the decorator parameter. As we can see below, we can also pass a `defaultValue` option that will be reflected in the GraphQL schema. ```ts @Resolver() class RecipeResolver { // ... @Query(returns => [Recipe]) async recipes( @Arg("servings", { defaultValue: 2 }) servings: number, @Arg("title", { nullable: true }) title?: string, ): Promise { // ... } } ``` This works well when there are 2 - 3 args. But when you have many more, the resolver's method definitions become bloated. In this case we can use a class definition to describe the arguments. It looks like the object type class but it has the `@ArgsType()` decorator on top. ```ts @ArgsType() class GetRecipesArgs { @Field(type => Int, { nullable: true }) skip?: number; @Field(type => Int, { nullable: true }) take?: number; @Field({ nullable: true }) title?: string; } ``` We can define default values for optional fields in the `@Field()` decorator using the `defaultValue` option or by using a property initializer - in both cases TypeGraphQL will reflect this in the schema by setting the default value, so users will be able to omit those args while sending a query. > Be aware that `defaultValue` works only for input args and fields, like `@Arg`, `@ArgsType` and `@InputType`. > Setting `defaultValue` does not affect `@ObjectType` or `@InterfaceType` fields as they are for output purposes only. Also, this way of declaring arguments allows you to perform validation. You can find more details about this feature in the [validation docs](./validation.md). We can also define helper fields and methods for our args or input classes. But be aware that **defining constructors is strictly forbidden** and we shouldn't use them there, as TypeGraphQL creates instances of args and input classes under the hood by itself. ```ts import { Min, Max } from "class-validator"; @ArgsType() class GetRecipesArgs { @Field(type => Int, { defaultValue: 0 }) @Min(0) skip: number; @Field(type => Int) @Min(1) @Max(50) take = 25; @Field({ nullable: true }) title?: string; // Helpers - index calculations get startIndex(): number { return this.skip; } get endIndex(): number { return this.skip + this.take; } } ``` Then all that is left to do is use the args class as the type of the method parameter. We can use the destructuring syntax to gain access to single arguments as variables, instead of the reference to the whole args object. ```ts @Resolver() class RecipeResolver { // ... @Query(returns => [Recipe]) async recipes(@Args() { title, startIndex, endIndex }: GetRecipesArgs) { // Example implementation let recipes = this.recipesCollection; if (title) { recipes = recipes.filter(recipe => recipe.title === title); } return recipes.slice(startIndex, endIndex); } } ``` This declaration will result in the following part of the schema in SDL: ```graphql type Query { recipes(skip: Int = 0, take: Int = 25, title: String): [Recipe!] } ``` ### Input types GraphQL mutations can be similarly created: Declare the class method, use the `@Mutation` decorator, create arguments, provide a return type (if needed) etc. But for mutations we usually use `input` types, hence TypeGraphQL allows us to create inputs in the same way as [object types](./types-and-fields.md) but by using the `@InputType()` decorator: ```ts @InputType() class AddRecipeInput {} ``` To ensure we don't accidentally change the property type we leverage the TypeScript type checking system by implementing the `Partial` type: ```ts @InputType() class AddRecipeInput implements Partial {} ``` We then declare any input fields we need, using the `@Field()` decorator: ```ts @InputType({ description: "New recipe data" }) class AddRecipeInput implements Partial { @Field() title: string; @Field({ nullable: true }) description?: string; } ``` After that we can use the `AddRecipeInput` type in our mutation. We can do this inline (using the `@Arg()` decorator) or as a field of the args class like in the query example above. We may also need access to the context. To achieve this we use the `@Ctx()` decorator with the optional user-defined `Context` interface: ```ts @Resolver() class RecipeResolver { // ... @Mutation() addRecipe(@Arg("data") newRecipeData: AddRecipeInput, @Ctx() ctx: Context): Recipe { // Example implementation const recipe = RecipesUtils.create(newRecipeData, ctx.user); this.recipesCollection.push(recipe); return recipe; } } ``` Because our method is synchronous and explicitly returns `Recipe`, we can omit the `@Mutation()` type annotation. This declaration will result in the following part of the schema in SDL: ```graphql input AddRecipeInput { title: String! description: String } ``` ```graphql type Mutation { addRecipe(data: AddRecipeInput!): Recipe! } ``` By using parameter decorators, we can get rid of unnecessary parameters (like `root`) that bloat our method definition and have to be ignored by prefixing the parameter name with `_`. Also, we can achieve a clean separation between GraphQL and our business code by using decorators, so our resolvers and their methods behave just like services which can be easily unit-tested. ## Field resolvers Queries and mutations are not the only type of resolvers. We often create object type field resolvers (e.g. when a `user` type has a `posts` field) which we have to resolve by fetching relational data from the database. Field resolvers in TypeGraphQL are very similar to queries and mutations - we create them as a method on the resolver class but with a few modifications. First we declare which object type fields we are resolving by providing the type to the `@Resolver` decorator: ```ts @Resolver(of => Recipe) class RecipeResolver { // Queries and mutations } ``` Then we create a class method that will become the field resolver. In our example we have the `averageRating` field in the `Recipe` object type that should calculate the average from the `ratings` array. ```ts @Resolver(of => Recipe) class RecipeResolver { // Queries and mutations averageRating(recipe: Recipe) { // ... } } ``` We then mark the method as a field resolver with the `@FieldResolver()` decorator. Since we've already defined the field type in the `Recipe` class definition, there's no need to redefine it. We also decorate the method parameters with the `@Root` decorator in order to inject the recipe object. ```ts @Resolver(of => Recipe) class RecipeResolver { // Queries and mutations @FieldResolver() averageRating(@Root() recipe: Recipe) { // ... } } ``` For enhanced type safety we can implement the `ResolverInterface` interface. It's a small helper that checks if the return type of the field resolver methods, like `averageRating(...)`, matches the `averageRating` property of the `Recipe` class and whether the first parameter of the method is the actual object type (`Recipe` class). ```ts @Resolver(of => Recipe) class RecipeResolver implements ResolverInterface { // Queries and mutations @FieldResolver() averageRating(@Root() recipe: Recipe) { // ... } } ``` Here is the full implementation of the sample `averageRating` field resolver: ```ts @Resolver(of => Recipe) class RecipeResolver implements ResolverInterface { // Queries and mutations @FieldResolver() averageRating(@Root() recipe: Recipe) { const ratingsSum = recipe.ratings.reduce((a, b) => a + b, 0); return recipe.ratings.length ? ratingsSum / recipe.ratings.length : null; } } ``` For simple resolvers like `averageRating` or deprecated fields that behave like aliases, you can create field resolvers inline in the object type class definition: ```ts @ObjectType() class Recipe { @Field() title: string; @Field({ deprecationReason: "Use `title` instead" }) get name(): string { return this.title; } @Field(type => [Rate]) ratings: Rate[]; @Field(type => Float, { nullable: true }) averageRating(@Arg("since") sinceDate: Date): number | null { const ratings = this.ratings.filter(rate => rate.date > sinceDate); if (!ratings.length) return null; const ratingsSum = ratings.reduce((a, b) => a + b, 0); return ratingsSum / ratings.length; } } ``` However, if the code is more complicated and has side effects (i.e. api calls, fetching data from a databases), a resolver class method should be used instead. This way we can leverage the dependency injection mechanism, which is really helpful in testing. For example: ```ts import { Repository } from "typeorm"; @Resolver(of => Recipe) class RecipeResolver implements ResolverInterface { constructor( // Dependency injection private readonly userRepository: Repository, ) {} @FieldResolver() async author(@Root() recipe: Recipe) { const author = await this.userRepository.findById(recipe.userId); if (!author) throw new SomethingWentWrongError(); return author; } } ``` Note that if a field name of a field resolver doesn't exist in the resolver object type, it will create a field in the schema with this name. This feature is useful when the field is purely calculable (eg. `averageRating` from `ratings` array) and to avoid polluting the class signature. ## Resolver Inheritance Resolver class `inheritance` is an advanced topic covered in the [resolver inheritance docs](./inheritance.md#resolvers-inheritance). ## Examples These code samples are just made up for tutorial purposes. You can find more advanced, real examples in the [examples folder on the repository](https://github.com/MichalLytek/type-graphql/tree/master/examples). ================================================ FILE: docs/scalars.md ================================================ --- title: Scalars --- ## Aliases TypeGraphQL provides aliases for 3 basic scalars: - Int --> GraphQLInt; - Float --> GraphQLFloat; - ID --> GraphQLID; This shorthand allows you to save keystrokes when declaring field types: ```ts // Import the aliases import { ID, Float, Int } from "type-graphql"; @ObjectType() class MysteryObject { @Field(type => ID) readonly id: string; @Field(type => Int) notificationsCount: number; @Field(type => Float) probability: number; } ``` In the last case you can omit the `type => Float` since JavaScript `Number` will become `GraphQLFloat` in the schema automatically. Other scalars - i.e. `GraphQLString` and `GraphQLBoolean` - do not need aliases. When possible, they will be reflected automatically: ```ts @ObjectType() class User { @Field() name: string; @Field() isOld: boolean; } ``` However in some cases we must explicitly declare the string/bool scalar type. Use JS constructor functions (`String`, `Boolean`) then: ```ts @ObjectType() class SampleObject { @Field(type => String, { nullable: true }) // TS reflected type is `Object` :( get optionalInfo(): string | undefined { if (Math.random() > 0.5) { return "Gotcha!"; } } } ``` ## Custom Scalars TypeGraphQL also supports custom scalar types! First of all, we need to create our own `GraphQLScalarType` instance or import a scalar type from a 3rd-party npm library. For example, Mongo's ObjectId: ```ts import { GraphQLScalarType, Kind } from "graphql"; import { ObjectId } from "mongodb"; export const ObjectIdScalar = new GraphQLScalarType({ name: "ObjectId", description: "Mongo object id scalar type", serialize(value: unknown): string { // Check type of value if (!(value instanceof ObjectId)) { throw new Error("ObjectIdScalar can only serialize ObjectId values"); } return value.toHexString(); // Value sent to client }, parseValue(value: unknown): ObjectId { // Check type of value if (typeof value !== "string") { throw new Error("ObjectIdScalar can only parse string values"); } return new ObjectId(value); // Value from client input variables }, parseLiteral(ast): ObjectId { // Check type of value if (ast.kind !== Kind.STRING) { throw new Error("ObjectIdScalar can only parse string values"); } return new ObjectId(ast.value); // Value from client query }, }); ``` Then we can just use it in our field decorators: ```ts // Import earlier created const import { ObjectIdScalar } from "../my-scalars/ObjectId"; @ObjectType() class User { @Field(type => ObjectIdScalar) // Explicitly use it readonly id: ObjectId; @Field() name: string; @Field() isOld: boolean; } ``` Optionally, we can declare the association between the reflected property type and our scalars to automatically map them (no need for explicit type annotation!): ```ts @ObjectType() class User { @Field() // Magic goes here - no type annotation for custom scalar readonly id: ObjectId; } ``` All we need to do is register the association map in the `buildSchema` options: ```ts import { ObjectId } from "mongodb"; import { ObjectIdScalar } from "../my-scalars/ObjectId"; import { buildSchema } from "type-graphql"; const schema = await buildSchema({ resolvers, scalarsMap: [{ type: ObjectId, scalar: ObjectIdScalar }], }); ``` However, we must be aware that this will only work when the TypeScript reflection mechanism can handle it. So our class property type must be a `class`, not an enum, union or interface. ## Date Scalars TypeGraphQL provides built-in scalars for the `Date` type. There are two versions of this scalar: - ISO-formatted string: `"2023-05-19T21:04:39.573Z"` - timestamp-based number: `1518037458374` They are exported from the `type-graphql` package as `GraphQLISODateTime` and `GraphQLTimestamp` but comes from `graphql-scalars` npm package. By default, TypeGraphQL uses the ISO date format, however we can change it to timestamp format using the mentioned above `scalarsMap` option of `buildSchema` configuration: ```ts import { buildSchema, GraphQLTimestamp } from "type-graphql"; const schema = await buildSchema({ resolvers, scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }], }); ``` There's no need then to explicitly declare the field type: ```ts @ObjectType() class User { @Field() registrationDate: Date; } ``` We can of course use any other `Date` scalar from `graphql-scalars` or any other npm package. ================================================ FILE: docs/subscriptions.md ================================================ --- title: Subscriptions --- GraphQL can be used to perform reads with queries and writes with mutations. However, oftentimes clients want to get updates pushed to them from the server when data they care about changes. To support that, GraphQL has a third operation: subscription. TypeGraphQL of course has great support for subscription, using the [`@graphql-yoga/subscription`](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions) package created by [`The Guild`](https://the-guild.dev/). ## Creating Subscriptions Subscription resolvers are similar to [queries and mutation resolvers](./resolvers.md) but slightly more complicated. First we create a normal class method as always, but this time annotated with the `@Subscription()` decorator. ```ts class SampleResolver { // ... @Subscription() newNotification(): Notification { // ... } } ``` Then we have to provide the topics we wish to subscribe to. This can be a single topic string, an array of topics or a function to dynamically create a topic based on subscription arguments passed to the query. We can also use TypeScript enums for enhanced type safety. ```ts class SampleResolver { // ... @Subscription({ topics: "NOTIFICATIONS", // Single topic topics: ["NOTIFICATIONS", "ERRORS"] // Or topics array topics: ({ args, context }) => args.topic // Or dynamic topic function }) newNotification(): Notification { // ... } } ``` We can also provide the `filter` option to decide which topic events should trigger our subscription. This function should return a `boolean` or `Promise` type. ```ts class SampleResolver { // ... @Subscription({ topics: "NOTIFICATIONS", filter: ({ payload, args }) => args.priorities.includes(payload.priority), }) newNotification(): Notification { // ... } } ``` We can also provide a custom subscription logic which might be useful, e.g. if we want to use the Prisma subscription functionality or something similar. All we need to do is to use the `subscribe` option which should be a function that returns an `AsyncIterable` or a `Promise`. Example using Prisma 1 subscription feature: ```ts class SampleResolver { // ... @Subscription({ subscribe: ({ root, args, context, info }) => { return context.prisma.$subscribe.users({ mutation_in: [args.mutationType] }); }, }) newNotification(): Notification { // ... } } ``` > Be aware that we can't mix the `subscribe` option with the `topics` and `filter` options. If the filtering is still needed, we can use the [`filter` and `map` helpers](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions#filter-and-map-values) from the `@graphql-yoga/subscription` package. Now we can implement the subscription resolver. It will receive the payload from a triggered topic of the pubsub system using the `@Root()` decorator. There, we can transform it to the returned shape. ```ts class SampleResolver { // ... @Subscription({ topics: "NOTIFICATIONS", filter: ({ payload, args }) => args.priorities.includes(payload.priority), }) newNotification( @Root() notificationPayload: NotificationPayload, @Args() args: NewNotificationsArgs, ): Notification { return { ...notificationPayload, date: new Date(), }; } } ``` ## Triggering subscription topics Ok, we've created subscriptions, but what is the `pubsub` system and how do we trigger topics? They might be triggered from external sources like a database but also in mutations, e.g. when we modify some resource that clients want to receive notifications about when it changes. So, let us assume we have this mutation for adding a new comment: ```ts class SampleResolver { // ... @Mutation(returns => Boolean) async addNewComment(@Arg("comment") input: CommentInput) { const comment = this.commentsService.createNew(input); await this.commentsRepository.save(comment); return true; } } ``` First, we need to create the `PubSub` instance. In most cases, we call `createPubSub()` function from `@graphql-yoga/subscription` package. Optionally, we can define the used topics and payload type using the type argument, e.g.: ```ts import { createPubSub } from "@graphql-yoga/subscription"; export const pubSub = createPubSub<{ NOTIFICATIONS: [NotificationPayload]; DYNAMIC_ID_TOPIC: [number, NotificationPayload]; }>(); ``` Then, we need to register the `PubSub` instance in the `buildSchema()` function options: ```ts import { buildSchema } from "type-graphql"; import { pubSub } from "./pubsub"; const schema = await buildSchema({ resolver, pubSub, }); ``` Finally, we can use the created `PubSub` instance to trigger the topics and send the payload to all topic subscribers: ```ts import { pubSub } from "./pubsub"; class SampleResolver { // ... @Mutation(returns => Boolean) async addNewComment(@Arg("comment") input: CommentInput) { const comment = this.commentsService.createNew(input); await this.commentsRepository.save(comment); // Trigger subscriptions topics const payload: NotificationPayload = { message: input.content }; pubSub.publish("NOTIFICATIONS", payload); return true; } } ``` And that's it! Now all subscriptions attached to the `NOTIFICATIONS` topic will be triggered when performing the `addNewComment` mutation. ## Topic with dynamic ID The idea of this feature is taken from the `@graphql-yoga/subscription` that is used under the hood. Basically, sometimes you only want to emit and listen for events for a specific entity (e.g. user or product). Dynamic topic ID lets you declare topics scoped to a special identifier, e.g.: ```ts @Resolver() class NotificationResolver { @Subscription({ topics: "NOTIFICATIONS", topicId: ({ context }) => context.userId, }) newNotification(@Root() { message }: NotificationPayload): Notification { return { message, date: new Date() }; } } ``` Then in your mutation or services, you need to pass the topic id as the second parameter: ```ts pubSub.publish("NOTIFICATIONS", userId, { id, message }); ``` > Be aware that this feature must be supported by the pubsub system of your choice. > If you decide to use something different than `createPubSub()` from `@graphql-yoga/subscription`, the second argument might be treated as a payload, not dynamic topic id. ## Using a custom PubSub system While TypeGraphQL uses the `@graphql-yoga/subscription` package under the hood to handle subscription, there's no requirement to use that implementation of `PubSub`. In fact, you can use any pubsub system you want, not only the `graphql-yoga` one. The only requirement is to comply with the exported `PubSub` interface - having proper `.subscribe()` and `.publish()` methods. This is especially helpful for production usage, where we can't rely on the in-memory event emitter, so that we [use distributed pubsub](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions#distributed-pubsub-for-production). ## Creating a Subscription Server The [bootstrap guide](./bootstrap.md) and all the earlier examples used [`apollo-server`](https://github.com/apollographql/apollo-server) to create an HTTP endpoint for our GraphQL API. However, beginning in Apollo Server 3, subscriptions are not supported by the "batteries-included" apollo-server package. To enable subscriptions, you need to follow the guide on their docs page: ## Examples See how subscriptions work in a [simple example](https://github.com/MichalLytek/type-graphql/tree/master/examples/simple-subscriptions). You can see there, how simple is setting up GraphQL subscriptions using `graphql-yoga` package. For production usage, it's better to use something more scalable like a Redis-based pubsub system - [a working example is also available](https://github.com/MichalLytek/type-graphql/tree/master/examples/redis-subscriptions). However, to launch this example you need to have a running instance of Redis and you might have to modify the example code to provide your connection parameters. ================================================ FILE: docs/types-and-fields.md ================================================ --- title: Types and Fields --- The main idea of TypeGraphQL is to automatically create GraphQL schema definitions from TypeScript classes. To avoid the need for schema definition files and interfaces describing the schema, we use decorators and a bit of reflection magic. Let's start by defining our example TypeScript class which represents our `Recipe` model with fields for storing the recipe data: ```ts class Recipe { id: string; title: string; ratings: Rate[]; averageRating?: number; } ``` The first thing we must do is decorate the class with the `@ObjectType` decorator. It marks the class as the `type` known from the GraphQL SDL or `GraphQLObjectType` from `graphql-js`: ```ts @ObjectType() class Recipe { id: string; title: string; ratings: Rate[]; averageRating: number; } ``` Then we declare which class properties should be mapped to the GraphQL fields. To do this, we use the `@Field` decorator, which is also used to collect metadata from the TypeScript reflection system: ```ts @ObjectType() class Recipe { @Field() id: string; @Field() title: string; @Field() ratings: Rate[]; @Field() averageRating: number; } ``` For simple types (like `string` or `boolean`) this is all that's needed but due to a limitation in TypeScript's reflection, we need to provide info about generic types (like `Array` or `Promise`). So to declare the `Rate[]` type, we have to use the explicit `[ ]` syntax for array types - `@Field(type => [Rate])`. For nested arrays, we just use the explicit `[ ]` notation to determine the depth of the array, e.g. `@Field(type => [[Int]])` would tell the compiler we expect an integer array of depth 2. Why use function syntax and not a simple `{ type: Rate }` config object? Because, by using function syntax we solve the problem of circular dependencies (e.g. Post <--> User), so it was adopted as a convention. You can use the shorthand syntax `@Field(() => Rate)` if you want to save some keystrokes but it might be less readable for others. By default, all fields are non nullable, just like properties in TypeScript. However, you can change that behavior by providing `nullableByDefault: true` option in `buildSchema` settings, described in [bootstrap guide](./bootstrap.md). So for nullable properties like `averageRating` which might not be defined when a recipe has no ratings yet, we mark the class property as optional with a `?:` operator and also have to pass the `{ nullable: true }` decorator parameter. We should be aware that when we declare our type as a nullable union (e.g. `string | null`), we need to explicitly provide the type to the `@Field` decorator. In the case of lists, we may also need to define their nullability in a more detailed form. The basic `{ nullable: true | false }` setting only applies to the whole list (`[Item!]` or `[Item!]!`), so if we need a sparse array, we can control the list items' nullability via `nullable: "items"` (for `[Item]!`) or `nullable: "itemsAndList"` (for the `[Item]`) option. Be aware that setting `nullableByDefault: true` option will also apply to lists, so it will produce `[Item]` type, just like with `nullable: "itemsAndList"`. For nested lists, those options apply to the whole depth of the array: `@Field(() => [[Item]]` would by default produce `[[Item!]!]!`, setting `nullable: "itemsAndList"` would produce `[[Item]]` while `nullable: "items"` would produce `[[Item]]!` In the config object we can also provide the `description` and `deprecationReason` properties for GraphQL schema purposes. So after these changes our example class would look like this: ```ts @ObjectType({ description: "The recipe model" }) class Recipe { @Field(type => ID) id: string; @Field({ description: "The title of the recipe" }) title: string; @Field(type => [Rate]) ratings: Rate[]; @Field({ nullable: true }) averageRating?: number; } ``` Which will result in generating the following part of the GraphQL schema in SDL: ```graphql type Recipe { id: ID! title: String! ratings: [Rate!]! averageRating: Float } ``` Similarly, the `Rate` type class would look like this: ```ts @ObjectType() class Rate { @Field(type => Int) value: number; @Field() date: Date; user: User; } ``` which results in this equivalent of the GraphQL SDL: ```graphql type Rate { value: Int! date: Date! } ``` As we can see, for the `id` property of `Recipe` we passed `type => ID` and for the `value` field of `Rate` we passed `type => Int`. This way we can overwrite the inferred type from the reflection metadata. We can read more about the ID and Int scalars in [the scalars docs](./scalars.md). There is also a section about the built-in `Date` scalar. Also the `user` property doesn't have a `@Field()` decorator - this way we can hide some properties of our data model. In this case, we need to store the `user` field of the `Rate` object to the database in order to prevent multiple rates, but we don't want to make it publicly accessible. Note that if a field of an object type is purely calculable (e.g. `averageRating` from `ratings` array) and we don't want to pollute the class signature, we can omit it and just implement the field resolver (described in [resolvers doc](./resolvers.md)). Be aware that **defining constructors is strictly forbidden** and we shouldn't use them there, as TypeGraphQL creates instances of object type classes under the hood by itself. In some case we may want to expose our classes or properties under a different types or fields name. To accomplish this, we can use the `name` parameter or `name` property of decorator's options, e.g.: ```ts @ObjectType("ExternalTypeName") class InternalClassName { @Field({ name: "externalFieldName" }) internalPropertyName: string; } ``` However, be aware that renaming fields works only for output types like object type or interface type. It's due to a fact that input fields has no resolvers that could translate one field value into another property value. ================================================ FILE: docs/unions.md ================================================ --- title: Unions --- Sometimes our API has to be flexible and return a type that is not specific but one from a range of possible types. An example might be a movie site's search functionality: using the provided phrase we search the database for movies but also actors. So the query has to return a list of `Movie` or `Actor` types. Read more about the GraphQL Union Type in the [official GraphQL docs](http://graphql.org/learn/schema/#union-types). ## Usage Let's start by creating the object types from the example above: ```ts @ObjectType() class Movie { @Field() name: string; @Field() rating: number; } ``` ```ts @ObjectType() class Actor { @Field() name: string; @Field(type => Int) age: number; } ``` Now let's create an union type from the object types above - the rarely seen `[ ] as const` syntax is to inform TypeScript compiler that it's a tuple, which allows for better TS union type inference: ```ts import { createUnionType } from "type-graphql"; const SearchResultUnion = createUnionType({ name: "SearchResult", // Name of the GraphQL union types: () => [Movie, Actor] as const, // function that returns tuple of object types classes }); ``` Then we can use the union type in the query by providing the `SearchResultUnion` value in the `@Query` decorator return type annotation. Notice, that we have to explicitly use the decorator return type annotation due to TypeScript's reflection limitations. For TypeScript compile-time type safety we can also use `typeof SearchResultUnion` which is equal to type `Movie | Actor`. ```ts @Resolver() class SearchResolver { @Query(returns => [SearchResultUnion]) async search(@Arg("phrase") phrase: string): Promise> { const movies = await Movies.findAll(phrase); const actors = await Actors.findAll(phrase); return [...movies, ...actors]; } } ``` ## Resolving Type Be aware that when the query/mutation return type (or field type) is a union, we have to return a specific instance of the object type class. Otherwise, `graphql-js` will not be able to detect the underlying GraphQL type correctly when we use plain JS objects. However, we can also provide our own `resolveType` function implementation to the `createUnionType` options. This way we can return plain objects in resolvers and then determine the returned object type by checking the shape of the data object, e.g.: ```ts const SearchResultUnion = createUnionType({ name: "SearchResult", types: () => [Movie, Actor] as const, // Implementation of detecting returned object type resolveType: value => { if ("rating" in value) { return Movie; // Return object type class (the one with `@ObjectType()`) } if ("age" in value) { return "Actor"; // Or the schema name of the type as a string } return undefined; }, }); ``` **Et Voilà!** We can now build the schema and make the example query 😉 ```graphql query { search(phrase: "Holmes") { ... on Actor { # Maybe Katie Holmes? name age } ... on Movie { # For sure Sherlock Holmes! name rating } } } ``` ## Examples More advanced usage examples of unions (and enums) are located in [this examples folder](https://github.com/MichalLytek/type-graphql/tree/master/examples/enums-and-unions). ================================================ FILE: docs/validation.md ================================================ --- title: Argument and Input validation sidebar_label: Validation --- ## Scalars The standard way to ensure that inputs and arguments are correct, such as an `email` field that really contains a proper e-mail address, is to use [custom scalars](./scalars.md) e.g. `GraphQLEmail` from [`graphql-custom-types`](https://github.com/stylesuxx/graphql-custom-types). However, creating scalars for all single cases of data types (credit card number, base64, IP, URL) might be cumbersome. That's why TypeGraphQL has built-in support for argument and input validation. By default, we can use the [`class-validator`](https://github.com/typestack/class-validator) library and easily declare the requirements for incoming data (e.g. a number is in the range 0-255 or a password that is longer than 8 characters) thanks to the awesomeness of decorators. We can also use other libraries or our own custom solution, as described in [custom validators](#custom-validator) section. ## `class-validator` ### How to use First, we need to install the `class-validator` package: ```sh npm install class-validator ``` Then we decorate the input/arguments class with the appropriate decorators from `class-validator`. So we take this: ```ts @InputType() export class RecipeInput { @Field() title: string; @Field({ nullable: true }) description?: string; } ``` ...and turn it into this: ```ts import { MaxLength, Length } from "class-validator"; @InputType() export class RecipeInput { @Field() @MaxLength(30) title: string; @Field({ nullable: true }) @Length(30, 255) description?: string; } ``` Then we need to enable the auto-validate feature (as it's disabled by default) by simply setting `validate: true` in `buildSchema` options, e.g.: ```ts const schema = await buildSchema({ resolvers: [RecipeResolver], validate: true, // Enable 'class-validator' integration }); ``` And that's it! 😉 TypeGraphQL will automatically validate our inputs and arguments based on the definitions: ```ts @Resolver(of => Recipe) export class RecipeResolver { @Mutation(returns => Recipe) async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise { // 100% sure that the input is correct console.assert(recipeInput.title.length <= 30); console.assert(recipeInput.description.length >= 30); console.assert(recipeInput.description.length <= 255); } } ``` Of course, [there are many more decorators](https://github.com/typestack/class-validator#validation-decorators) we have access to, not just the simple `@Length` decorator used in the example above, so take a look at the `class-validator` documentation. This feature is enabled by default. However, we can disable it if we must: ```ts const schema = await buildSchema({ resolvers: [RecipeResolver], validate: false, // Disable automatic validation or pass the default config object }); ``` And we can still enable it per resolver's argument if we need to: ```ts class RecipeResolver { @Mutation(returns => Recipe) async addRecipe(@Arg("input", { validate: true }) recipeInput: RecipeInput) { // ... } } ``` The `ValidatorOptions` object used for setting features like [validation groups](https://github.com/typestack/class-validator#validation-groups) can also be passed: ```ts class RecipeResolver { @Mutation(returns => Recipe) async addRecipe( @Arg("input", { validate: { groups: ["admin"] } }) recipeInput: RecipeInput, ) { // ... } } ``` Note that by default, the `skipMissingProperties` setting of the `class-validator` is set to `true` because GraphQL will independently check whether the params/fields exist or not. Same goes to `forbidUnknownValues` setting which is set to `false` because the GraphQL runtime checks for additional data, not described in schema. GraphQL will also check whether the fields have correct types (String, Int, Float, Boolean, etc.) so we don't have to use the `@IsOptional`, `@Allow`, `@IsString` or the `@IsInt` decorators at all! However, when using nested input or arrays, we always have to use [`@ValidateNested()` decorator](https://github.com/typestack/class-validator#validating-nested-objects) or [`{ each: true }` option](https://github.com/typestack/class-validator#validating-arrays) to make nested validation work properly. ### Response to the Client When a client sends incorrect data to the server: ```graphql mutation ValidationMutation { addRecipe( input: { # Too long! title: "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet" } ) { title creationDate } } ``` the [`ArgumentValidationError`](https://github.com/MichalLytek/type-graphql/blob/master/src/errors/ArgumentValidationError.ts) will be thrown. By default, the `apollo-server` package from the [bootstrap guide](./bootstrap.md) will format the error to match the `GraphQLFormattedError` interface. So when the `ArgumentValidationError` occurs, the client will receive this JSON with a nice `validationErrors` property inside of `extensions.exception`: ```json { "errors": [ { "message": "Argument Validation Error", "locations": [ { "line": 2, "column": 3 } ], "path": ["addRecipe"], "extensions": { "code": "INTERNAL_SERVER_ERROR", "exception": { "validationErrors": [ { "target": { "title": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet" }, "value": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet", "property": "title", "children": [], "constraints": { "maxLength": "title must be shorter than or equal to 30 characters" } } ], "stacktrace": [ "Error: Argument Validation Error", " at Object. (/type-graphql/src/resolvers/validate-arg.ts:29:11)", " at Generator.throw ()", " at rejected (/type-graphql/node_modules/tslib/tslib.js:105:69)", " at processTicksAndRejections (internal/process/next_tick.js:81:5)" ] } } } ], "data": null } ``` Of course we can also create our own custom implementation of the `formatError` function provided in the `ApolloServer` config options which will transform the `GraphQLError` with a `ValidationError` array in the desired output format (e.g. `extensions.code = "ARGUMENT_VALIDATION_ERROR"`). ### Automatic Validation Example To see how this works, check out the [simple real life example](https://github.com/MichalLytek/type-graphql/tree/master/examples/automatic-validation). ### Caveats Even if we don't use the validation feature (and we have provided `{ validate: false }` option to `buildSchema`), we still need to have `class-validator` installed as a dev dependency in order to compile our app without errors using `tsc`. An alternative solution that allows to completely get rid off big `class-validator` from our project's `node_modules` folder is to suppress the `error TS2307: Cannot find module 'class-validator'` TS error by providing `"skipLibCheck": true` setting in `tsconfig.json`. ## Custom validator We can also use other libraries than `class-validator` together with TypeGraphQL. To integrate it, all we need to do is to provide a custom function. It receives three parameters: - `argValue` which is the injected value of `@Arg()` or `@Args()` - `argType` which is a runtime type information (e.g. `String` or `RecipeInput`) - `resolverData` which holds the resolver execution context, described as generic type `ResolverData` This function can be an async function and should return nothing (`void`) when validation passes, or throw an error when validation fails. So be aware of this while trying to wrap another library in `validateFn` function for TypeGraphQL. Then we provide this function as a `validateFn` option in `buildSchema`. Example using [decorators library for Joi validators (`joiful`)](https://github.com/joiful-ts/joiful): ```ts const schema = await buildSchema({ // ... validateFn: argValue => { // Call joiful validate const { error } = joiful.validate(argValue); if (error) { // Throw error on failed validation throw error; } }, }); ``` The `validateFn` option is also supported as a `@Arg()` or `@Args()` decorator option, e.g.: ```ts @Resolver() class SampleResolver { @Query() sampleQuery( @Arg("sampleArg", { validateFn: (argValue, argType) => { // Do something here with arg value and type... }, }) sampleArg: string, ): string { // ... } } ``` > Be aware that when using custom validator, the error won't be wrapped with `ArgumentValidationError` like for the built-in `class-validator` validation. ### Custom Validation Example To see how this works, check out the [simple custom validation integration example](https://github.com/MichalLytek/type-graphql/tree/master/examples/custom-validation). ================================================ FILE: examples/.eslintrc ================================================ { "rules": { "no-console": "off", "class-methods-use-this": "off", "import/no-cycle": "off", "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] } } ================================================ FILE: examples/README.md ================================================ # Examples This folder consists of simple examples showing how to use different `TypeGraphQL` features and how well it integrates with 3rd party libraries. To run an example, simply go to the subdirectory (e.g. `cd ./simple-usage`), and then start the server (`npx ts-node ./index.ts`). Each subdirectory contains a `examples.graphql` file with predefined GraphQL queries/mutations/subscriptions that you can use in Apollo Studio () and play with them by modifying their shape and data. > **Note**: Be aware that the examples on master branch are designed to work with latest codebase that might not be released yet. > So if you are looking for examples that are compatible with the version you use, just browse the files by the git tag, e.g. [`tree/v0.16.0` for `0.16.0` release](https://github.com/MichalLytek/type-graphql/tree/v0.16.0/examples). ## Basics - [Simple usage of fields, basic types and resolvers](./simple-usage) ## Advanced - [Enums and unions](./enums-and-unions) - [Subscriptions (simple)](./simple-subscriptions) - [Subscriptions (using Redis) \*\*](./redis-subscriptions) - [Interfaces](./interfaces-inheritance) - [Extensions (metadata)](./extensions) ## Features usage - [Dependency injection (IoC container)](./using-container) - [Scoped containers](./using-scoped-container) - [Authorization](./authorization) - [Validation](./automatic-validation) - [Custom validation](./custom-validation) - [Types inheritance](./interfaces-inheritance) - [Resolvers inheritance](./resolvers-inheritance) - [Generic types](./generic-types) - [Mixin classes](./mixin-classes) - [Middlewares and custom decorators](./middlewares-custom-decorators) - [Query complexity](./query-complexity) ## 3rd party libs integration - [TypeORM (manual, synchronous) \*](./typeorm-basic-usage) - [TypeORM (automatic, lazy relations) \*](./typeorm-lazy-relations) - [MikroORM \*](./mikro-orm) - [Typegoose \*](./typegoose) - [Apollo Federation](./apollo-federation) - [Apollo Federation 2](./apollo-federation-2) - [Apollo Cache Control](./apollo-cache) - [GraphQL Scalars](./graphql-scalars) - [TSyringe](./tsyringe) _\* Note that we need to provide the environment variable `DATABASE_URL` with connection parameters to your local database_ \ _\*\* Note that we need to provide the environment variable `REDIS_URL` with connection parameters to your local Redis instance_ ================================================ FILE: examples/apollo-cache/cache-control.ts ================================================ import { type CacheHint } from "@apollo/cache-control-types"; import { Directive } from "type-graphql"; import { type RequireAtLeastOne } from "./helpers/RequireAtLeastOne"; export function CacheControl({ maxAge, scope }: RequireAtLeastOne) { if (maxAge === undefined && scope === undefined) { throw new Error("Missing maxAge or scope param for @CacheControl"); } let sdl = "@cacheControl("; if (maxAge !== undefined) { sdl += `maxAge: ${maxAge}`; } if (scope) { sdl += ` scope: ${scope}`; } sdl += ")"; return Directive(sdl); } ================================================ FILE: examples/apollo-cache/examples.graphql ================================================ query CachedQuery { cachedRecipe(title: "Recipe 1") { title description cachedAverageRating } } query NotCachedQuery { recipe(title: "Recipe 1") { title description averageRating } } ================================================ FILE: examples/apollo-cache/helpers/RequireAtLeastOne.d.ts ================================================ // https://stackoverflow.com/a/49725198/6676781 export type RequireAtLeastOne = Pick> & { [K in Keys]-?: Required> & Partial>>; }[Keys]; ================================================ FILE: examples/apollo-cache/helpers/getTime.ts ================================================ export function getTime(date: Date = new Date()) { return date.toTimeString().slice(0, 8); } ================================================ FILE: examples/apollo-cache/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { ApolloServerPluginCacheControl } from "@apollo/server/plugin/cacheControl"; import { startStandaloneServer } from "@apollo/server/standalone"; import responseCachePlugin from "@apollo/server-plugin-response-cache"; import { buildSchema } from "type-graphql"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema, plugins: [ // Cache headers ApolloServerPluginCacheControl(), // In-memory cache responseCachePlugin(), ], }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/apollo-cache/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial) { return Object.assign(new Recipe(), recipeData); } export function createRecipeSamples() { return [ createRecipe({ title: "Recipe 1", description: "Desc 1", ratings: [0, 3, 1], creationDate: new Date("2018-04-11"), }), createRecipe({ title: "Recipe 2", description: "Desc 2", ratings: [4, 2, 3, 1], creationDate: new Date("2018-04-15"), }), createRecipe({ title: "Recipe 3", description: "Desc 3", ratings: [5, 4], creationDate: new Date(), }), ]; } ================================================ FILE: examples/apollo-cache/recipe.resolver.ts ================================================ import { Arg, Query, Resolver } from "type-graphql"; import { CacheControl } from "./cache-control"; import { getTime } from "./helpers/getTime"; import { createRecipeSamples } from "./recipe.data"; import { Recipe } from "./recipe.type"; @Resolver(_of => Recipe) export class RecipeResolver { private readonly items: Recipe[] = createRecipeSamples(); @Query(_returns => Recipe, { nullable: true }) async recipe(@Arg("title") title: string): Promise { console.log(`Called 'recipe' with title '${title}' on ${getTime()}`); return this.items.find(recipe => recipe.title === title); } @Query(_returns => Recipe, { nullable: true }) // Cache query for 60s @CacheControl({ maxAge: 60 }) async cachedRecipe(@Arg("title") title: string): Promise { console.log(`Called 'cachedRecipe' with title '${title}' on ${getTime()}`); return this.items.find(recipe => recipe.title === title); } @Query(_returns => [Recipe]) async recipes(): Promise { return this.items; } } ================================================ FILE: examples/apollo-cache/recipe.type.ts ================================================ import { Field, Float, Int, ObjectType } from "type-graphql"; import { CacheControl } from "./cache-control"; import { getTime } from "./helpers/getTime"; @ObjectType() export class Recipe { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [Int]) ratings!: number[]; @Field() creationDate!: Date; @Field(_type => Float, { nullable: true }) // Invalidate 'cachedRecipe' cache with maxAge of 60 to 10 (if requested) @CacheControl({ maxAge: 10 }) get cachedAverageRating() { console.log(`Called 'cachedAverageRating' for recipe '${this.title}' on ${getTime()}`); return this.averageRating; } @Field(_type => Float, { nullable: true }) get averageRating(): number | null { console.log(`Called 'averageRating' for recipe '${this.title}' on ${getTime()}`); const ratingsCount = this.ratings.length; if (ratingsCount === 0) { return null; } const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); return ratingsSum / ratingsCount; } } ================================================ FILE: examples/apollo-cache/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Query { cachedRecipe(title: String!): Recipe recipe(title: String!): Recipe recipes: [Recipe!]! } type Recipe { averageRating: Float cachedAverageRating: Float creationDate: DateTimeISO! description: String ratings: [Int!]! title: String! } ================================================ FILE: examples/apollo-federation/accounts/data.ts ================================================ import { User } from "./user"; function createUser(userData: Partial) { return Object.assign(new User(), userData); } export const users: User[] = [ createUser({ id: "1", name: "Ada Lovelace", birthDate: "1815-12-10", username: "@ada", }), createUser({ id: "2", name: "Alan Turing", birthDate: "1912-06-23", username: "@complete", }), ]; ================================================ FILE: examples/apollo-federation/accounts/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { AccountsResolver } from "./resolver"; import { User } from "./user"; import { resolveUserReference } from "./user.reference"; import { buildFederatedSchema } from "../helpers"; export async function listen(port: number): Promise { // Build TypeGraphQL executable schema const schema = await buildFederatedSchema( { resolvers: [AccountsResolver], orphanedTypes: [User], }, { User: { __resolveReference: resolveUserReference }, }, ); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Accounts service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation/accounts/resolver.ts ================================================ import { Query, Resolver } from "type-graphql"; import { users } from "./data"; import { User } from "./user"; @Resolver(_of => User) export class AccountsResolver { @Query(_returns => User) me(): User { return users[0]; } } ================================================ FILE: examples/apollo-federation/accounts/user.reference.ts ================================================ import { users } from "./data"; import { type User } from "./user"; export async function resolveUserReference(reference: Pick): Promise { return users.find(u => u.id === reference.id)!; } ================================================ FILE: examples/apollo-federation/accounts/user.ts ================================================ import { Directive, Field, ID, ObjectType } from "type-graphql"; @Directive(`@key(fields: "id")`) @ObjectType() export class User { @Field(_type => ID) id!: string; @Field() username!: string; @Field() name!: string; @Field() birthDate!: string; } ================================================ FILE: examples/apollo-federation/examples.graphql ================================================ query { topProducts { name price shippingEstimate inStock reviews { body author { name birthDate reviews { product { name } body } } } } } ================================================ FILE: examples/apollo-federation/helpers/buildFederatedSchema.ts ================================================ import { buildSubgraphSchema } from "@apollo/subgraph"; import { type IResolvers, printSchemaWithDirectives } from "@graphql-tools/utils"; import gql from "graphql-tag"; import deepMerge from "lodash.merge"; import { type BuildSchemaOptions, buildSchema, createResolversMap } from "type-graphql"; export async function buildFederatedSchema( options: Omit, referenceResolvers?: IResolvers, ) { // Build TypeGraphQL executable schema const schema = await buildSchema({ ...options, // Disable check to allow schemas without query, etc... skipCheck: true, }); // Build Apollo Subgraph schema const federatedSchema = buildSubgraphSchema({ typeDefs: gql(printSchemaWithDirectives(schema)), // Merge schema's resolvers with reference resolvers resolvers: deepMerge(createResolversMap(schema) as any, referenceResolvers), }); return federatedSchema; } ================================================ FILE: examples/apollo-federation/helpers/index.ts ================================================ export * from "./buildFederatedSchema"; ================================================ FILE: examples/apollo-federation/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloGateway, IntrospectAndCompose } from "@apollo/gateway"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { emitSchemaDefinitionFile } from "type-graphql"; import * as accounts from "./accounts"; import * as inventory from "./inventory"; import * as products from "./products"; import * as reviews from "./reviews"; async function bootstrap() { const gateway = new ApolloGateway({ supergraphSdl: new IntrospectAndCompose({ subgraphs: [ { name: "accounts", url: await accounts.listen(4001) }, { name: "reviews", url: await reviews.listen(4002) }, { name: "products", url: await products.listen(4003) }, { name: "inventory", url: await inventory.listen(4004) }, ], }), }); // Create GraphQL server const server = new ApolloServer({ gateway }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`Apollo Gateway ready at ${url}`); // Create 'schema.graphql' file with schema definition in current directory await emitSchemaDefinitionFile(path.resolve(__dirname, "schema.graphql"), gateway.schema!); } bootstrap().catch(console.error); ================================================ FILE: examples/apollo-federation/inventory/data.ts ================================================ export interface Inventory { upc: string; inStock: boolean; } export const inventory: Inventory[] = [ { upc: "1", inStock: true }, { upc: "2", inStock: false }, { upc: "3", inStock: true }, ]; ================================================ FILE: examples/apollo-federation/inventory/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Product } from "./product"; import { resolveProductReference } from "./product.reference"; import { InventoryResolver } from "./resolver"; import { buildFederatedSchema } from "../helpers"; export async function listen(port: number): Promise { // Build TypeGraphQL executable schema const schema = await buildFederatedSchema( { resolvers: [InventoryResolver], orphanedTypes: [Product], }, { Product: { __resolveReference: resolveProductReference }, }, ); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Inventory service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation/inventory/product.reference.ts ================================================ import { inventory } from "./data"; import { Product } from "./product"; export async function resolveProductReference( reference: Pick, ): Promise { const found = inventory.find(i => i.upc === reference.upc); if (!found) { return; } // eslint-disable-next-line consistent-return return Object.assign(new Product(), { ...reference, ...found, }); } ================================================ FILE: examples/apollo-federation/inventory/product.ts ================================================ import { Directive, Field, ObjectType } from "type-graphql"; @ObjectType() @Directive("@extends") @Directive(`@key(fields: "upc")`) export class Product { @Field() @Directive("@external") upc!: string; @Field() @Directive("@external") weight!: number; @Field() @Directive("@external") price!: number; @Field() inStock!: boolean; } ================================================ FILE: examples/apollo-federation/inventory/resolver.ts ================================================ import { Directive, FieldResolver, Resolver, Root } from "type-graphql"; import { Product } from "./product"; @Resolver(_of => Product) export class InventoryResolver { @Directive(`@requires(fields: "price weight")`) @FieldResolver(_returns => Number) async shippingEstimate(@Root() product: Product): Promise { // Free for expensive items if (product.price > 1000) { return 0; } // Estimate is based on weight return product.weight * 0.5; } } ================================================ FILE: examples/apollo-federation/products/data.ts ================================================ import { Product } from "./product"; function createProduct(productData: Partial) { return Object.assign(new Product(), productData); } export const products: Product[] = [ createProduct({ upc: "1", name: "Table", price: 899, weight: 100, }), createProduct({ upc: "2", name: "Couch", price: 1299, weight: 1000, }), createProduct({ upc: "3", name: "Chair", price: 54, weight: 50, }), ]; ================================================ FILE: examples/apollo-federation/products/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Product } from "./product"; import { resolveProductReference } from "./product.reference"; import { ProductsResolver } from "./resolver"; import { buildFederatedSchema } from "../helpers"; export async function listen(port: number): Promise { // Build TypeGraphQL executable schema const schema = await buildFederatedSchema( { resolvers: [ProductsResolver], orphanedTypes: [Product], }, { Product: { __resolveReference: resolveProductReference }, }, ); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Products service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation/products/product.reference.ts ================================================ import { products } from "./data"; import { type Product } from "./product"; export async function resolveProductReference( reference: Pick, ): Promise { return products.find(p => p.upc === reference.upc); } ================================================ FILE: examples/apollo-federation/products/product.ts ================================================ import { Directive, Field, ObjectType } from "type-graphql"; @Directive(`@key(fields: "upc")`) @ObjectType() export class Product { @Field() upc!: string; @Field() name!: string; @Field() price!: number; @Field() weight!: number; } ================================================ FILE: examples/apollo-federation/products/resolver.ts ================================================ import { Arg, Query, Resolver } from "type-graphql"; import { products } from "./data"; import { Product } from "./product"; @Resolver(_of => Product) export class ProductsResolver { @Query(_returns => [Product]) async topProducts( @Arg("first", { defaultValue: 5 }) first: number, ): Promise { return products.slice(0, first); } } ================================================ FILE: examples/apollo-federation/reviews/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Product, ProductReviewsResolver } from "./product"; import { Review, ReviewsResolver } from "./review"; import { User, UserReviewsResolver } from "./user"; import { buildFederatedSchema } from "../helpers"; export async function listen(port: number): Promise { // Build TypeGraphQL executable schema const schema = await buildFederatedSchema({ resolvers: [ReviewsResolver, ProductReviewsResolver, UserReviewsResolver], orphanedTypes: [User, Review, Product], }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Reviews service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation/reviews/product/index.ts ================================================ export * from "./product"; export * from "./resolver"; ================================================ FILE: examples/apollo-federation/reviews/product/product.ts ================================================ import { Directive, Field, ObjectType } from "type-graphql"; @Directive("@extends") @Directive(`@key(fields: "upc")`) @ObjectType() export class Product { @Directive("@external") @Field() upc!: string; } ================================================ FILE: examples/apollo-federation/reviews/product/resolver.ts ================================================ import { FieldResolver, Resolver, Root } from "type-graphql"; import { Product } from "./product"; import { Review, reviews } from "../review"; @Resolver(_of => Product) export class ProductReviewsResolver { @FieldResolver(() => [Review]) async reviews(@Root() product: Product): Promise { return reviews.filter(review => review.product.upc === product.upc); } } ================================================ FILE: examples/apollo-federation/reviews/review/data.ts ================================================ import { Review } from "./review"; import { Product } from "../product"; import { User } from "../user"; function createReview(reviewData: Partial) { return Object.assign(new Review(), reviewData); } function createUser(userData: Partial) { return Object.assign(new User(), userData); } function createProduct(productData: Partial) { return Object.assign(new Product(), productData); } export const reviews: Review[] = [ createReview({ id: "1", author: createUser({ id: "1", username: "@ada", }), product: createProduct({ upc: "1", }), body: "Love it!", }), createReview({ id: "2", author: createUser({ id: "1", username: "@ada", }), product: createProduct({ upc: "2", }), body: "Too expensive.", }), createReview({ id: "3", author: createUser({ id: "2", username: "@complete", }), product: createProduct({ upc: "3", }), body: "Could be better.", }), createReview({ id: "4", author: createUser({ id: "2", username: "@complete", }), product: createProduct({ upc: "1", }), body: "Prefer something else.", }), ]; ================================================ FILE: examples/apollo-federation/reviews/review/index.ts ================================================ export * from "./data"; export * from "./resolver"; export * from "./review"; ================================================ FILE: examples/apollo-federation/reviews/review/resolver.ts ================================================ import { FieldResolver, Resolver } from "type-graphql"; import { reviews } from "./data"; import { Review } from "./review"; @Resolver(_of => Review) export class ReviewsResolver { @FieldResolver(_returns => [Review]) async reviews(): Promise { return reviews; } } ================================================ FILE: examples/apollo-federation/reviews/review/review.ts ================================================ import { Directive, Field, ID, ObjectType } from "type-graphql"; import { Product } from "../product"; import { User } from "../user"; @Directive(`@key(fields: "id")`) @ObjectType() export class Review { @Field(_type => ID) id!: string; @Field() body!: string; @Directive(`@provides(fields: "username")`) @Field() author!: User; @Field() product!: Product; } ================================================ FILE: examples/apollo-federation/reviews/user/index.ts ================================================ export * from "./resolver"; export * from "./user"; ================================================ FILE: examples/apollo-federation/reviews/user/resolver.ts ================================================ import { FieldResolver, Resolver, Root } from "type-graphql"; import { User } from "./user"; import { Review, reviews } from "../review"; @Resolver(_of => User) export class UserReviewsResolver { @FieldResolver(_returns => [Review]) async reviews(@Root() user: User): Promise { return reviews.filter(review => review.author.id === user.id); } } ================================================ FILE: examples/apollo-federation/reviews/user/user.ts ================================================ import { Directive, Field, ID, ObjectType } from "type-graphql"; @Directive("@extends") @Directive(`@key(fields: "id")`) @ObjectType() export class User { @Directive("@external") @Field(_type => ID) id!: string; @Directive("@external") @Field() username!: string; } ================================================ FILE: examples/apollo-federation/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Product { inStock: Boolean! name: String! price: Float! reviews: [Review!]! shippingEstimate: Float! upc: String! weight: Float! } type Query { me: User! topProducts(first: Float! = 5): [Product!]! } type Review { author: User! body: String! id: ID! product: Product! reviews: [Review!]! } type User { birthDate: String! id: ID! name: String! reviews: [Review!]! username: String! } ================================================ FILE: examples/apollo-federation-2/accounts/data.ts ================================================ import { User } from "./user"; function createUser(userData: Partial) { return Object.assign(new User(), userData); } export const users: User[] = [ createUser({ id: "1", name: "Ada Lovelace", birthDate: "1815-12-10", username: "@ada", }), createUser({ id: "2", name: "Alan Turing", birthDate: "1912-06-23", username: "@complete", }), ]; ================================================ FILE: examples/apollo-federation-2/accounts/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { AccountsResolver } from "./resolver"; import { User } from "./user"; import { resolveUserReference } from "./user.reference"; import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; export async function listen(port: number): Promise { const schema = await buildFederatedSchema( { resolvers: [AccountsResolver], orphanedTypes: [User], }, { User: { __resolveReference: resolveUserReference }, }, ); const server = new ApolloServer({ schema }); const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Accounts service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation-2/accounts/resolver.ts ================================================ import { Query, Resolver } from "type-graphql"; import { users } from "./data"; import { User } from "./user"; @Resolver(_of => User) export class AccountsResolver { @Query(_returns => User) me(): User { return users[0]; } } ================================================ FILE: examples/apollo-federation-2/accounts/user.reference.ts ================================================ import { users } from "./data"; import { type User } from "./user"; export async function resolveUserReference(reference: Pick): Promise { return users.find(u => u.id === reference.id)!; } ================================================ FILE: examples/apollo-federation-2/accounts/user.ts ================================================ import { Directive, Field, ID, ObjectType } from "type-graphql"; @Directive(`@key(fields: "id")`) @ObjectType() export class User { @Field(_type => ID) id!: string; @Directive("@shareable") @Field() username!: string; @Field() name!: string; @Field() birthDate!: string; } ================================================ FILE: examples/apollo-federation-2/examples.graphql ================================================ query { topProducts { __typename name price shippingEstimate inStock reviews { body author { name birthDate reviews { product { __typename name } body } } } } } ================================================ FILE: examples/apollo-federation-2/helpers/buildFederatedSchema.ts ================================================ import { buildSubgraphSchema } from "@apollo/subgraph"; import { type IResolvers, printSchemaWithDirectives } from "@graphql-tools/utils"; import gql from "graphql-tag"; import deepMerge from "lodash.merge"; import { type BuildSchemaOptions, buildSchema, createResolversMap } from "type-graphql"; export async function buildFederatedSchema( options: Omit, referenceResolvers?: IResolvers, ) { // build TypeGraphQL schema const schema = await buildSchema({ ...options, skipCheck: true, // disable check to allow schemas without query, etc. }); // build Apollo Subgraph schema const federatedSchema = buildSubgraphSchema({ typeDefs: gql` extend schema @link( url: "https://specs.apollo.dev/federation/v2.3" import: [ "@key" "@shareable" "@provides" "@extends" "@requires" "@external" "@interfaceObject" ] ) ${printSchemaWithDirectives(schema)} `, // merge schema's resolvers with reference resolvers resolvers: deepMerge(createResolversMap(schema) as any, referenceResolvers), }); return federatedSchema; } ================================================ FILE: examples/apollo-federation-2/index.ts ================================================ import "reflect-metadata"; import path from "path"; import { ApolloGateway, IntrospectAndCompose } from "@apollo/gateway"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { emitSchemaDefinitionFile } from "type-graphql"; import * as accounts from "./accounts"; import * as inventory from "./inventory"; import * as products from "./products"; import * as reviews from "./reviews"; const startGraph = async (name: string, urlOrPromise: string | Promise) => { const url = await urlOrPromise; return { name, url }; }; async function bootstrap() { const subgraphs = await Promise.all([ startGraph("accounts", accounts.listen(4001)), startGraph("reviews", reviews.listen(4002)), startGraph("products", products.listen(4003)), startGraph("inventory", inventory.listen(4004)), ]); const schemaGateway = new ApolloGateway({ supergraphSdl: new IntrospectAndCompose({ subgraphs, }), }); const { schema } = await schemaGateway.load(); await emitSchemaDefinitionFile(path.resolve(__dirname, "schema.graphql"), schema); await schemaGateway.stop(); const gateway = new ApolloGateway({ supergraphSdl: new IntrospectAndCompose({ subgraphs, }), }); const server = new ApolloServer({ gateway }); const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`Apollo Gateway ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/apollo-federation-2/inventory/data.ts ================================================ export interface Inventory { upc: string; inStock: boolean; } export const inventory: Inventory[] = [ { upc: "1", inStock: true }, { upc: "2", inStock: false }, { upc: "3", inStock: true }, ]; ================================================ FILE: examples/apollo-federation-2/inventory/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Product } from "./product"; import { resolveProductReference } from "./product.reference"; import { InventoryResolver } from "./resolver"; import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; export async function listen(port: number): Promise { const schema = await buildFederatedSchema( { resolvers: [InventoryResolver], orphanedTypes: [Product], }, { Product: { __resolveReference: resolveProductReference }, }, ); const server = new ApolloServer({ schema }); const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Inventory service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation-2/inventory/product.reference.ts ================================================ import { inventory } from "./data"; import { Product } from "./product"; export async function resolveProductReference( reference: Pick, ): Promise { const found = inventory.find(i => i.upc === reference.upc); if (!found) { return undefined; } return Object.assign(new Product(), { ...reference, ...found, }); } ================================================ FILE: examples/apollo-federation-2/inventory/product.ts ================================================ import { Directive, Field, ObjectType } from "type-graphql"; @ObjectType() @Directive("@extends") @Directive("@interfaceObject") @Directive(`@key(fields: "upc")`) export class Product { @Field() @Directive("@external") upc!: string; @Field() @Directive("@external") weight!: number; @Field() @Directive("@external") price!: number; @Field() inStock!: boolean; } ================================================ FILE: examples/apollo-federation-2/inventory/resolver.ts ================================================ import { Directive, FieldResolver, Resolver, Root } from "type-graphql"; import { Product } from "./product"; @Resolver(_of => Product) export class InventoryResolver { @Directive(`@requires(fields: "price weight")`) @FieldResolver(_returns => Number) async shippingEstimate(@Root() product: Product): Promise { // free for expensive items if (product.price > 1000) { return 0; } // estimate is based on weight return product.weight * 0.5; } } ================================================ FILE: examples/apollo-federation-2/products/data.ts ================================================ import { Dining } from "./dining"; import { type Product } from "./product"; import { Seating } from "./seating"; export const products: Product[] = [ Object.assign(new Dining(), { upc: "1", name: "Table", price: 899, weight: 100, height: "3ft", }), Object.assign(new Seating(), { upc: "2", name: "Couch", price: 1299, weight: 1000, seats: 2, }), Object.assign(new Seating(), { upc: "3", name: "Chair", price: 54, weight: 50, seats: 1, }), ]; ================================================ FILE: examples/apollo-federation-2/products/dining.ts ================================================ import { Directive, Field, ObjectType } from "type-graphql"; import { Product } from "./product"; @Directive(`@key(fields: "upc")`) @ObjectType({ implements: Product }) export class Dining extends Product { @Field() height!: string; } ================================================ FILE: examples/apollo-federation-2/products/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Product } from "./product"; import { resolveProductReference } from "./product.reference"; import { ProductsResolver } from "./resolver"; import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; export async function listen(port: number): Promise { const schema = await buildFederatedSchema( { resolvers: [ProductsResolver], orphanedTypes: [Product], }, { Product: { __resolveReference: resolveProductReference }, }, ); const server = new ApolloServer({ schema }); const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Products service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation-2/products/product.reference.ts ================================================ import { products } from "./data"; import { type Product } from "./product"; export async function resolveProductReference( reference: Pick, ): Promise { return products.find(p => p.upc === reference.upc); } ================================================ FILE: examples/apollo-federation-2/products/product.ts ================================================ import { Directive, Field, InterfaceType } from "type-graphql"; @Directive(`@key(fields: "upc")`) @InterfaceType() export abstract class Product { @Field() upc!: string; @Field() name!: string; @Field() price!: number; @Field() weight!: number; } ================================================ FILE: examples/apollo-federation-2/products/resolver.ts ================================================ import { Arg, Query, Resolver } from "type-graphql"; import { products } from "./data"; import { Product } from "./product"; @Resolver(_of => Product) export class ProductsResolver { @Query(_returns => [Product]) async topProducts( @Arg("first", { defaultValue: 5 }) first: number, ): Promise { return products.slice(0, first); } } ================================================ FILE: examples/apollo-federation-2/products/seating.ts ================================================ import { Directive, Field, ObjectType } from "type-graphql"; import { Product } from "./product"; @Directive(`@key(fields: "upc")`) @ObjectType({ implements: Product }) export class Seating extends Product { @Field() seats!: number; } ================================================ FILE: examples/apollo-federation-2/reviews/index.ts ================================================ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Product } from "./product/product"; import { ProductReviewsResolver } from "./product/resolver"; import { ReviewsResolver } from "./review/resolver"; import { Review } from "./review/review"; import { UserReviewsResolver } from "./user/resolver"; import { User } from "./user/user"; import { buildFederatedSchema } from "../helpers/buildFederatedSchema"; export async function listen(port: number): Promise { const schema = await buildFederatedSchema({ resolvers: [ReviewsResolver, ProductReviewsResolver, UserReviewsResolver], orphanedTypes: [User, Review, Product], }); const server = new ApolloServer({ schema }); const { url } = await startStandaloneServer(server, { listen: { port } }); console.log(`Reviews service ready at ${url}`); return url; } ================================================ FILE: examples/apollo-federation-2/reviews/product/product.ts ================================================ import { Directive, Field, ObjectType } from "type-graphql"; @Directive("@extends") @Directive(`@key(fields: "upc")`) @Directive("@interfaceObject") @ObjectType() export class Product { @Directive("@external") @Field() upc!: string; } ================================================ FILE: examples/apollo-federation-2/reviews/product/resolver.ts ================================================ import { FieldResolver, Resolver, Root } from "type-graphql"; import { Product } from "./product"; import { reviews } from "../review/data"; import { Review } from "../review/review"; @Resolver(_of => Product) export class ProductReviewsResolver { @FieldResolver(() => [Review]) async reviews(@Root() product: Product): Promise { return reviews.filter(review => review.product.upc === product.upc); } } ================================================ FILE: examples/apollo-federation-2/reviews/review/data.ts ================================================ import { Review } from "./review"; import { Product } from "../product/product"; import { User } from "../user/user"; function createReview(reviewData: Partial) { return Object.assign(new Review(), reviewData); } function createUser(userData: Partial) { return Object.assign(new User(), userData); } function createProduct(productData: Partial) { return Object.assign(new Product(), productData); } export const reviews: Review[] = [ createReview({ id: "1", author: createUser({ id: "1", username: "@ada", }), product: createProduct({ upc: "1", }), body: "Love it!", }), createReview({ id: "2", author: createUser({ id: "1", username: "@ada", }), product: createProduct({ upc: "2", }), body: "Too expensive.", }), createReview({ id: "3", author: createUser({ id: "2", username: "@complete", }), product: createProduct({ upc: "3", }), body: "Could be better.", }), createReview({ id: "4", author: createUser({ id: "2", username: "@complete", }), product: createProduct({ upc: "1", }), body: "Prefer something else.", }), ]; ================================================ FILE: examples/apollo-federation-2/reviews/review/resolver.ts ================================================ import { FieldResolver, Resolver } from "type-graphql"; import { reviews } from "./data"; import { Review } from "./review"; @Resolver(_of => Review) export class ReviewsResolver { @FieldResolver(_returns => [Review]) async reviews(): Promise { return reviews; } } ================================================ FILE: examples/apollo-federation-2/reviews/review/review.ts ================================================ import { Directive, Field, ID, ObjectType } from "type-graphql"; import { Product } from "../product/product"; import { User } from "../user/user"; @Directive(`@key(fields: "id")`) @ObjectType() export class Review { @Field(_type => ID) id!: string; @Field() body!: string; @Directive(`@provides(fields: "username")`) @Field() author!: User; @Field() product!: Product; } ================================================ FILE: examples/apollo-federation-2/reviews/user/resolver.ts ================================================ import { FieldResolver, Resolver, Root } from "type-graphql"; import { User } from "./user"; import { reviews } from "../review/data"; import { Review } from "../review/review"; @Resolver(_of => User) export class UserReviewsResolver { @FieldResolver(_returns => [Review]) async reviews(@Root() user: User): Promise { return reviews.filter(review => review.author.id === user.id); } } ================================================ FILE: examples/apollo-federation-2/reviews/user/user.ts ================================================ import { Directive, Field, ID, ObjectType } from "type-graphql"; @Directive(`@key(fields: "id")`) @ObjectType() export class User { @Field(_type => ID) id!: string; @Directive("@external") @Field() username!: string; } ================================================ FILE: examples/apollo-federation-2/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Dining implements Product { height: String! inStock: Boolean! name: String! price: Float! reviews: [Review!]! shippingEstimate: Float! upc: String! weight: Float! } interface Product { inStock: Boolean! name: String! price: Float! reviews: [Review!]! shippingEstimate: Float! upc: String! weight: Float! } type Query { me: User! topProducts(first: Float! = 5): [Product!]! } type Review { author: User! body: String! id: ID! product: Product! reviews: [Review!]! } type Seating implements Product { inStock: Boolean! name: String! price: Float! reviews: [Review!]! seats: Float! shippingEstimate: Float! upc: String! weight: Float! } type User { birthDate: String! id: ID! name: String! reviews: [Review!]! username: String! } ================================================ FILE: examples/authorization/auth-checker.ts ================================================ import { type AuthChecker } from "type-graphql"; import { type Context } from "./context.type"; // Auth checker function export const authChecker: AuthChecker = ({ context: { user } }, roles) => { // Check user if (!user) { // No user, restrict access return false; } // Check '@Authorized()' if (roles.length === 0) { // Only authentication required return true; } // Check '@Authorized(...)' roles overlap return user.roles.some(role => roles.includes(role)); }; ================================================ FILE: examples/authorization/context.type.ts ================================================ import { type User } from "./user.type"; export interface Context { user?: User; } ================================================ FILE: examples/authorization/examples.graphql ================================================ query GetPublicRecipes { recipes { title description averageRating } } query GetRecipesForAuthorizedUser { recipes { title description ingredients averageRating } } query GetRecipesForAdmin { recipes { title description ingredients averageRating ratings } } mutation AddRecipeByAuthorizedUser { addRecipe(title: "Sample Recipe") { averageRating } } mutation DeleteRecipeByAdmin { deleteRecipe(title: "Recipe 1") } ================================================ FILE: examples/authorization/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { authChecker } from "./auth-checker"; import { type Context } from "./context.type"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Register auth checker function authChecker, // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, // Provide context for each request context: async () => ({ // Create mocked user in context // In real app you would be mapping user from 'req.user' or sth user: { id: 1, name: "Sample user", roles: ["REGULAR"], }, }), }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/authorization/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } export const sampleRecipes = [ createRecipe({ title: "Recipe 1", description: "Desc 1", ingredients: ["one", "two", "three"], ratings: [3, 4, 5, 5, 5], }), createRecipe({ title: "Recipe 2", description: "Desc 2", ingredients: ["four", "five", "six"], ratings: [3, 4, 5, 3, 2], }), createRecipe({ title: "Recipe 3", ingredients: ["seven", "eight", "nine"], ratings: [4, 4, 5, 5, 4], }), ]; ================================================ FILE: examples/authorization/recipe.resolver.ts ================================================ import { Arg, Authorized, Mutation, Query, Resolver } from "type-graphql"; import { sampleRecipes } from "./recipe.data"; import { Recipe } from "./recipe.type"; @Resolver() export class RecipeResolver { private recipesData: Recipe[] = sampleRecipes.slice(); // Anyone can read recipes collection @Query(_returns => [Recipe]) async recipes(): Promise { return this.recipesData; } @Authorized() // Only authenticated users can add new recipe @Mutation() addRecipe( @Arg("title") title: string, @Arg("description", { nullable: true }) description?: string, ): Recipe { const newRecipe = Object.assign(new Recipe(), { title, description, ratings: [], }); this.recipesData.push(newRecipe); return newRecipe; } @Authorized("ADMIN") // Only 'ADMIN' users can remove published recipe @Mutation() deleteRecipe(@Arg("title") title: string): boolean { const foundRecipeIndex = this.recipesData.findIndex(it => it.title === title); if (!foundRecipeIndex) { return false; } this.recipesData.splice(foundRecipeIndex, 1); return true; } } ================================================ FILE: examples/authorization/recipe.type.ts ================================================ import { Authorized, Field, Float, Int, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { @Field() title!: string; @Field({ nullable: true }) description?: string; @Authorized() // Restrict access only for authenticated users @Field(_type => [String]) ingredients!: string[]; @Authorized("ADMIN") // Restrict access only for 'ADMIN' users @Field(_type => [Int]) ratings!: number[]; @Field(_type => Float, { nullable: true }) get averageRating(): number | null { if (!this.ratings.length) { return null; } return this.ratings.reduce((a, b) => a + b, 0) / this.ratings.length; } } ================================================ FILE: examples/authorization/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { addRecipe(description: String, title: String!): Recipe! deleteRecipe(title: String!): Boolean! } type Query { recipes: [Recipe!]! } type Recipe { averageRating: Float description: String ingredients: [String!]! ratings: [Int!]! title: String! } ================================================ FILE: examples/authorization/user.type.ts ================================================ export interface User { id: number; name: string; roles: string[]; } ================================================ FILE: examples/automatic-validation/examples.graphql ================================================ query GetRecipes { recipes { title description creationDate } } mutation CorrectAddRecipe { addRecipe( input: { title: "Correct title" description: "Very very very very very very very very long description" } ) { creationDate } } mutation AddRecipeWithoutDesc { addRecipe(input: { title: "Correct title" }) { creationDate } } mutation IncorrectAddRecipe { addRecipe(input: { title: "Correct title", description: "Too short description" }) { creationDate } } ================================================ FILE: examples/automatic-validation/helpers.ts ================================================ import { type Recipe } from "./recipe.type"; export function generateRecipes(count: number): Recipe[] { return new Array(count).fill(null).map( (_, i): Recipe => ({ title: `Recipe #${i + 1}`, description: `Description #${i + 1}`, creationDate: new Date(), }), ); } ================================================ FILE: examples/automatic-validation/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // remember to turn on validation! validate: true, }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/automatic-validation/recipe.input.ts ================================================ import { Length, MaxLength } from "class-validator"; import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial { @Field() @MaxLength(30) title!: string; @Field({ nullable: true }) @Length(30, 255) description?: string; } ================================================ FILE: examples/automatic-validation/recipe.resolver.ts ================================================ import { Arg, Args, Mutation, Query, Resolver } from "type-graphql"; import { generateRecipes } from "./helpers"; import { RecipeInput } from "./recipe.input"; import { Recipe } from "./recipe.type"; import { RecipesArguments } from "./recipes.arguments"; @Resolver(_of => Recipe) export class RecipeResolver { private readonly items: Recipe[] = generateRecipes(100); @Query(_returns => [Recipe]) async recipes(@Args() options: RecipesArguments): Promise { const start: number = options.skip; const end: number = options.skip + options.take; return this.items.slice(start, end); } @Mutation(_returns => Recipe) async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise { const recipe = new Recipe(); recipe.description = recipeInput.description; recipe.title = recipeInput.title; recipe.creationDate = new Date(); await this.items.push(recipe); return recipe; } } ================================================ FILE: examples/automatic-validation/recipe.type.ts ================================================ import { Field, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field() creationDate!: Date; } ================================================ FILE: examples/automatic-validation/recipes.arguments.ts ================================================ import { Max, Min } from "class-validator"; import { ArgsType, Field, Int } from "type-graphql"; @ArgsType() export class RecipesArguments { @Field(_type => Int) @Min(0) skip = 0; @Field(_type => Int) @Min(1) @Max(50) take = 10; } ================================================ FILE: examples/automatic-validation/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { addRecipe(input: RecipeInput!): Recipe! } type Query { recipes(skip: Int! = 0, take: Int! = 10): [Recipe!]! } type Recipe { creationDate: DateTimeISO! description: String title: String! } input RecipeInput { description: String title: String! } ================================================ FILE: examples/custom-validation/examples.graphql ================================================ query GetRecipes { recipes { title description creationDate } } mutation CorrectAddRecipe { addRecipe( input: { title: "Correct title" description: "Very very very very very very very very long description" } ) { creationDate } } mutation AddRecipeWithoutDesc { addRecipe(input: { title: "Correct title" }) { creationDate } } mutation IncorrectAddRecipe { addRecipe(input: { title: "Correct title", description: "Too short description" }) { creationDate } } ================================================ FILE: examples/custom-validation/helpers.ts ================================================ import { type Recipe } from "./recipe.type"; export function generateRecipes(count: number): Recipe[] { return new Array(count).fill(null).map( (_, i): Recipe => ({ title: `Recipe #${i + 1}`, description: `Description #${i + 1}`, creationDate: new Date(), }), ); } ================================================ FILE: examples/custom-validation/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import joiful from "joiful"; import { buildSchema } from "type-graphql"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // Custom validate function validateFn: (argValue, _argType) => { // Call joiful validate const { error } = joiful.validate(argValue); if (error) { // Throw error if validation failed throw error; } }, }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/custom-validation/recipe.input.ts ================================================ import Joiful from "joiful"; import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial { @Field() // Joi decorator @(Joiful.string().required().max(30)) title!: string; @Field({ nullable: true }) // Joi decorator @(Joiful.string().min(30).max(255)) description?: string; } ================================================ FILE: examples/custom-validation/recipe.resolver.ts ================================================ import { Arg, Args, Mutation, Query, Resolver } from "type-graphql"; import { generateRecipes } from "./helpers"; import { RecipeInput } from "./recipe.input"; import { Recipe } from "./recipe.type"; import { RecipesArguments } from "./recipes.arguments"; @Resolver(_of => Recipe) export class RecipeResolver { private readonly items: Recipe[] = generateRecipes(100); @Query(_returns => [Recipe]) async recipes(@Args() options: RecipesArguments): Promise { const start: number = options.skip; const end: number = options.skip + options.take; return this.items.slice(start, end); } @Mutation(_returns => Recipe) async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise { const recipe = new Recipe(); recipe.description = recipeInput.description; recipe.title = recipeInput.title; recipe.creationDate = new Date(); await this.items.push(recipe); return recipe; } } ================================================ FILE: examples/custom-validation/recipe.type.ts ================================================ import { Field, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field() creationDate!: Date; } ================================================ FILE: examples/custom-validation/recipes.arguments.ts ================================================ import Joiful from "joiful"; import { ArgsType, Field, Int } from "type-graphql"; @ArgsType() export class RecipesArguments { @Field(_type => Int) // use decorators for Joi @(Joiful.number().min(0)) skip = 0; @Field(_type => Int) // use decorators for Joi @(Joiful.number().min(1).max(50)) take = 10; } ================================================ FILE: examples/custom-validation/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ The javascript `Date` as string. Type represents date and time as the ISO Date string. """ scalar DateTime type Mutation { addRecipe(input: RecipeInput!): Recipe! } type Query { recipes(skip: Int! = 0, take: Int! = 10): [Recipe!]! } type Recipe { creationDate: DateTime! description: String title: String! } input RecipeInput { description: String title: String! } ================================================ FILE: examples/enums-and-unions/cook.data.ts ================================================ import { Cook } from "./cook.type"; function createCook(cookData: Partial): Cook { return Object.assign(new Cook(), cookData); } export const sampleCooks = [ createCook({ name: "Gordon Ramsay", yearsOfExperience: 21, }), createCook({ name: "Marilyn Monroe", yearsOfExperience: 1, }), ]; ================================================ FILE: examples/enums-and-unions/cook.type.ts ================================================ import { Field, Int, ObjectType } from "type-graphql"; @ObjectType() export class Cook { @Field() name!: string; @Field(_type => Int) yearsOfExperience!: number; } ================================================ FILE: examples/enums-and-unions/difficulty.enum.ts ================================================ import { registerEnumType } from "type-graphql"; export enum Difficulty { Beginner, Easy, Medium, Hard, MasterChef, } registerEnumType(Difficulty, { name: "Difficulty", description: "All possible preparation difficulty levels", }); ================================================ FILE: examples/enums-and-unions/examples.graphql ================================================ query AllRecipes { recipes { title description preparationDifficulty cook { name } } } query EasyRecipes { recipes(difficulty: Easy) { title description ingredients cook { name } } } query SearchByCookName { search(cookName: "Gordon") { __typename ... on Recipe { title preparationDifficulty cook { name } } ... on Cook { name yearsOfExperience } } } ================================================ FILE: examples/enums-and-unions/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { ExampleResolver } from "./resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [ExampleResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/enums-and-unions/recipe.data.ts ================================================ import { sampleCooks } from "./cook.data"; import { Difficulty } from "./difficulty.enum"; import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } export const sampleRecipes = [ createRecipe({ title: "Recipe 1", description: "Desc 1", preparationDifficulty: Difficulty.Easy, ingredients: ["one", "two", "three"], cook: sampleCooks[1], }), createRecipe({ title: "Recipe 2", description: "Desc 2", preparationDifficulty: Difficulty.Easy, ingredients: ["four", "five", "six"], cook: sampleCooks[0], }), createRecipe({ title: "Recipe 3", preparationDifficulty: Difficulty.Beginner, ingredients: ["seven", "eight", "nine"], cook: sampleCooks[1], }), createRecipe({ title: "Recipe 4", description: "Desc 4", preparationDifficulty: Difficulty.MasterChef, ingredients: ["ten", "eleven", "twelve"], cook: sampleCooks[0], }), createRecipe({ title: "Recipe 5", preparationDifficulty: Difficulty.Hard, ingredients: ["thirteen", "fourteen", "fifteen"], cook: sampleCooks[0], }), ]; ================================================ FILE: examples/enums-and-unions/recipe.type.ts ================================================ import { Field, ObjectType } from "type-graphql"; import { Cook } from "./cook.type"; import { Difficulty } from "./difficulty.enum"; @ObjectType() export class Recipe { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [String]) ingredients!: string[]; @Field(_type => Difficulty) preparationDifficulty!: Difficulty; @Field() cook!: Cook; } ================================================ FILE: examples/enums-and-unions/resolver.ts ================================================ import { Arg, Query, Resolver } from "type-graphql"; import { sampleCooks } from "./cook.data"; import { type Cook } from "./cook.type"; import { Difficulty } from "./difficulty.enum"; import { sampleRecipes } from "./recipe.data"; import { Recipe } from "./recipe.type"; import { SearchResult } from "./search-result.union"; @Resolver() export class ExampleResolver { private recipesData: Recipe[] = sampleRecipes; private cooks: Cook[] = sampleCooks; @Query(_returns => [Recipe]) async recipes( @Arg("difficulty", _type => Difficulty, { nullable: true }) difficulty?: Difficulty, ): Promise { if (!difficulty) { return this.recipesData; } return this.recipesData.filter(recipe => recipe.preparationDifficulty === difficulty); } @Query(_returns => [SearchResult]) async search(@Arg("cookName") cookName: string): Promise> { const recipes = this.recipesData.filter(recipe => recipe.cook.name.match(cookName)); const cooks = this.cooks.filter(cook => cook.name.match(cookName)); return [...recipes, ...cooks]; } } ================================================ FILE: examples/enums-and-unions/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Cook { name: String! yearsOfExperience: Int! } """ All possible preparation difficulty levels """ enum Difficulty { Beginner Easy Hard MasterChef Medium } type Query { recipes(difficulty: Difficulty): [Recipe!]! search(cookName: String!): [SearchResult!]! } type Recipe { cook: Cook! description: String ingredients: [String!]! preparationDifficulty: Difficulty! title: String! } union SearchResult = Cook | Recipe ================================================ FILE: examples/enums-and-unions/search-result.union.ts ================================================ import { createUnionType } from "type-graphql"; import { Cook } from "./cook.type"; import { Recipe } from "./recipe.type"; export const SearchResult = createUnionType({ name: "SearchResult", types: () => [Recipe, Cook] as const, }); ================================================ FILE: examples/extensions/context.type.ts ================================================ import { type User } from "./user.type"; export interface Context { user?: User; } ================================================ FILE: examples/extensions/examples.graphql ================================================ query GetRecipes { recipes { title description ingredients averageRating ratings } } mutation AddRecipe { addRecipe(title: "Sample Recipe") { averageRating } } mutation DeleteRecipe { deleteRecipe(title: "Recipe 1") } ================================================ FILE: examples/extensions/helpers/config.extractors.ts ================================================ import { type GraphQLFieldConfig, type GraphQLObjectTypeConfig, type GraphQLResolveInfo, } from "graphql"; export const extractFieldConfig = (info: GraphQLResolveInfo): GraphQLFieldConfig => { const { type, extensions, description, deprecationReason } = info.parentType.getFields()[info.fieldName]; return { type, description, extensions, deprecationReason, }; }; export const extractParentTypeConfig = ( info: GraphQLResolveInfo, ): GraphQLObjectTypeConfig => info.parentType.toConfig(); ================================================ FILE: examples/extensions/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { Container } from "typedi"; import { type Context } from "./context.type"; import { LoggerMiddleware } from "./logger.middleware"; import { RecipeResolver } from "./resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // IOC container container: Container, // Global middleware globalMiddlewares: [LoggerMiddleware], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema, }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, // Provide context context: async () => ({ // Example user user: { id: 123, name: "Sample user", }, }), }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/extensions/log-message.decorator.ts ================================================ import { Extensions } from "type-graphql"; interface LogOptions { message: string; level?: number; } export function LogMessage(messageOrOptions: string | LogOptions) { // Parse the parameters of the custom decorator const log: LogOptions = typeof messageOrOptions === "string" ? { level: 4, message: messageOrOptions, } : messageOrOptions; // Return the '@Extensions' decorator with a prepared property return Extensions({ log }); } ================================================ FILE: examples/extensions/logger.middleware.ts ================================================ import { type GraphQLFieldConfig, type GraphQLObjectTypeConfig, type GraphQLResolveInfo, } from "graphql"; import { type MiddlewareInterface, type NextFn, type ResolverData } from "type-graphql"; import { Service } from "typedi"; import { type Context } from "./context.type"; import { extractFieldConfig, extractParentTypeConfig } from "./helpers/config.extractors"; import { Logger } from "./logger.service"; interface LoggerConfig { message?: string; level?: number; } const extractLoggerExtensionsFromConfig = ( config: GraphQLObjectTypeConfig | GraphQLFieldConfig, ): LoggerConfig => (config.extensions && (config.extensions.log as LoggerConfig)) || {}; const getLoggerExtensions = (info: GraphQLResolveInfo) => { const fieldConfig = extractFieldConfig(info); const fieldLoggerExtensions = extractLoggerExtensionsFromConfig(fieldConfig); const parentConfig = extractParentTypeConfig(info); const parentLoggerExtensions = extractLoggerExtensionsFromConfig(parentConfig); return { ...parentLoggerExtensions, ...fieldLoggerExtensions, }; }; @Service() export class LoggerMiddleware implements MiddlewareInterface { constructor(private readonly logger: Logger) {} use({ context: { user }, info }: ResolverData, next: NextFn) { const { message, level = 0 } = getLoggerExtensions(info); if (message) { this.logger.log(level, `${user ? ` (user: ${user.id})` : ""}`, message); } return next(); } } ================================================ FILE: examples/extensions/logger.service.ts ================================================ import { Service } from "typedi"; @Service() export class Logger { log(...args: unknown[]) { console.log(...args); } } ================================================ FILE: examples/extensions/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } export const sampleRecipes = [ createRecipe({ title: "Recipe 1", description: "Desc 1", ingredients: ["one", "two", "three"], ratings: [3, 4, 5, 5, 5], }), createRecipe({ title: "Recipe 2", description: "Desc 2", ingredients: ["four", "five", "six"], ratings: [3, 4, 5, 3, 2], }), createRecipe({ title: "Recipe 3", ingredients: ["seven", "eight", "nine"], ratings: [4, 4, 5, 5, 4], }), ]; ================================================ FILE: examples/extensions/recipe.type.ts ================================================ import { Extensions, Field, Float, Int, ObjectType } from "type-graphql"; import { LogMessage } from "./log-message.decorator"; @ObjectType() // Log a message when any Recipe field is accessed @LogMessage("Recipe field accessed") export class Recipe { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [String]) // Use raw 'Extensions' decorator @Extensions({ log: { message: "Ingredients field accessed", level: 0 } }) ingredients!: string[]; // Override the object type log message @LogMessage("Ratings accessed") @Field(_type => [Int]) ratings!: number[]; @Field(_type => Float, { nullable: true }) get averageRating(): number | null { if (!this.ratings.length) { return null; } return this.ratings.reduce((a, b) => a + b, 0) / this.ratings.length; } } ================================================ FILE: examples/extensions/resolver.ts ================================================ import { Arg, Mutation, Query, Resolver } from "type-graphql"; import { Service } from "typedi"; import { LogMessage } from "./log-message.decorator"; import { sampleRecipes } from "./recipe.data"; import { Recipe } from "./recipe.type"; @Service() @Resolver() export class RecipeResolver { private recipesData: Recipe[] = sampleRecipes.slice(); @Query(_returns => [Recipe]) async recipes(): Promise { return this.recipesData; } @Mutation() addRecipe( @Arg("title") title: string, @Arg("description", { nullable: true }) description?: string, ): Recipe { const newRecipe = Object.assign(new Recipe(), { title, description, ratings: [], }); this.recipesData.push(newRecipe); return newRecipe; } @LogMessage("Recipe deletion requested") @Mutation() deleteRecipe(@Arg("title") title: string): boolean { const foundRecipeIndex = this.recipesData.findIndex(it => it.title === title); if (!foundRecipeIndex) { return false; } this.recipesData.splice(foundRecipeIndex, 1); return true; } } ================================================ FILE: examples/extensions/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { addRecipe(description: String, title: String!): Recipe! deleteRecipe(title: String!): Boolean! } type Query { recipes: [Recipe!]! } type Recipe { averageRating: Float description: String ingredients: [String!]! ratings: [Int!]! title: String! } ================================================ FILE: examples/extensions/user.type.ts ================================================ export interface User { id: number; name: string; } ================================================ FILE: examples/generic-types/examples.graphql ================================================ query GetRecipes { recipes(first: 3) { items { title ratings } total hasMore } } mutation AddRecipe { addSampleRecipe { title } } ================================================ FILE: examples/generic-types/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/generic-types/paginated-response.type.ts ================================================ import { type ClassType, Field, Int, ObjectType } from "type-graphql"; export function PaginatedResponse( itemsFieldValue: ClassType | string | number | boolean, ) { @ObjectType() abstract class PaginatedResponseClass { @Field(_type => [itemsFieldValue]) items!: TItemsFieldValue[]; @Field(_type => Int) total!: number; @Field() hasMore!: boolean; } return PaginatedResponseClass; } ================================================ FILE: examples/generic-types/recipe.data.ts ================================================ import { type Recipe } from "./recipe.type"; export function createSampleRecipes(): Recipe[] { return [ { description: "Desc 1", title: "Recipe 1", ratings: [0, 3, 1], }, { description: "Desc 2", title: "Recipe 2", ratings: [4, 2, 3, 1], }, { description: "Desc 3", title: "Recipe 3", ratings: [5, 4], }, ]; } ================================================ FILE: examples/generic-types/recipe.resolver.ts ================================================ /* eslint-disable max-classes-per-file */ import { Arg, Int, Mutation, ObjectType, Query, Resolver } from "type-graphql"; import { PaginatedResponse } from "./paginated-response.type"; import { createSampleRecipes } from "./recipe.data"; import { Recipe } from "./recipe.type"; // Create a temporary class for the abstract generic class 'instance' @ObjectType() class RecipesResponse extends PaginatedResponse(Recipe) { // Add more fields here if needed } @Resolver() export class RecipeResolver { private readonly recipes = createSampleRecipes(); @Query({ name: "recipes" }) getRecipes( @Arg("first", _type => Int, { nullable: true, defaultValue: 10 }) first: number, ): RecipesResponse { const total = this.recipes.length; return { items: this.recipes.slice(0, first), hasMore: total > first, total, }; } @Mutation() addSampleRecipe(): Recipe { const recipe: Recipe = { title: "Sample recipe", description: "Sample description", ratings: [1, 2, 3, 4], }; this.recipes.push(recipe); return recipe; } } ================================================ FILE: examples/generic-types/recipe.type.ts ================================================ import { Field, Int, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [Int]) ratings!: number[]; } ================================================ FILE: examples/generic-types/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { addSampleRecipe: Recipe! } type Query { recipes(first: Int = 10): RecipesResponse! } type Recipe { description: String ratings: [Int!]! title: String! } type RecipesResponse { hasMore: Boolean! items: [Recipe!]! total: Int! } ================================================ FILE: examples/graphql-scalars/examples.graphql ================================================ query GetRecipe1 { recipe(title: "Recipe 1") { title description ratings creationDate ratingsCount(minRate: 2) averageRating } } query GetRecipes { recipes { title description creationDate averageRating } } mutation AddRecipe { addRecipe(recipe: { title: "New recipe", description: "Simple description" }) { creationDate } } ================================================ FILE: examples/graphql-scalars/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/graphql-scalars/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial) { return Object.assign(new Recipe(), recipeData); } export function createRecipeSamples() { return [ createRecipe({ description: "Desc 1", title: "Recipe 1", ratings: [0, 3, 1], creationDate: new Date("2018-04-11"), }), createRecipe({ description: "Desc 2", title: "Recipe 2", ratings: [4, 2, 3, 1], creationDate: new Date("2018-04-15"), }), createRecipe({ description: "Desc 3", title: "Recipe 3", ratings: [5, 4], creationDate: new Date(), }), ]; } ================================================ FILE: examples/graphql-scalars/recipe.input.ts ================================================ import { GraphQLNonEmptyString } from "graphql-scalars"; import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial { @Field(_type => GraphQLNonEmptyString) title!: string; @Field(_type => GraphQLNonEmptyString, { nullable: true }) description?: string; } ================================================ FILE: examples/graphql-scalars/recipe.resolver.ts ================================================ import { GraphQLNonNegativeInt } from "graphql-scalars"; import { Arg, FieldResolver, Int, Mutation, Query, Resolver, type ResolverInterface, Root, } from "type-graphql"; import { createRecipeSamples } from "./recipe.data"; import { RecipeInput } from "./recipe.input"; import { Recipe } from "./recipe.type"; @Resolver(_of => Recipe) export class RecipeResolver implements ResolverInterface { private readonly items: Recipe[] = createRecipeSamples(); @Query(_returns => Recipe, { nullable: true }) async recipe(@Arg("title") title: string): Promise { return this.items.find(recipe => recipe.title === title); } @Query(_returns => [Recipe], { description: "Get all the recipes from around the world " }) async recipes(): Promise { return this.items; } @Mutation(_returns => Recipe) async addRecipe(@Arg("recipe") recipeInput: RecipeInput): Promise { const recipe = Object.assign(new Recipe(), { description: recipeInput.description, title: recipeInput.title, ratings: [], creationDate: new Date(), }); await this.items.push(recipe); return recipe; } @FieldResolver(_returns => GraphQLNonNegativeInt) ratingsCount( @Root() recipe: Recipe, @Arg("minRate", _type => Int, { defaultValue: 0 }) minRate: number, ): number { return recipe.ratings.filter(rating => rating >= minRate).length; } } ================================================ FILE: examples/graphql-scalars/recipe.type.ts ================================================ import { GraphQLNonEmptyString, GraphQLNonNegativeFloat, GraphQLNonNegativeInt, GraphQLTimestamp, } from "graphql-scalars"; import { Field, ObjectType } from "type-graphql"; @ObjectType({ description: "Object representing cooking recipe" }) export class Recipe { @Field(_type => GraphQLNonEmptyString) title!: string; @Field(_type => GraphQLNonEmptyString, { nullable: true, deprecationReason: "Use 'description' field instead", }) get specification(): string | undefined { return this.description; } @Field(_type => GraphQLNonEmptyString, { nullable: true, description: "The recipe description with preparation info", }) description?: string; @Field(_type => [GraphQLNonNegativeInt]) ratings!: number[]; @Field(_type => GraphQLTimestamp) creationDate!: Date; @Field(_type => GraphQLNonNegativeInt) ratingsCount!: number; @Field(_type => GraphQLNonNegativeFloat, { nullable: true }) get averageRating(): number | null { const ratingsCount = this.ratings.length; if (ratingsCount === 0) { return null; } const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); return ratingsSum / ratingsCount; } } ================================================ FILE: examples/graphql-scalars/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { addRecipe(recipe: RecipeInput!): Recipe! } """ A string that cannot be passed as an empty value """ scalar NonEmptyString """ Floats that will have a value of 0 or more. """ scalar NonNegativeFloat """ Integers that will have a value of 0 or more. """ scalar NonNegativeInt type Query { recipe(title: String!): Recipe """ Get all the recipes from around the world """ recipes: [Recipe!]! } """ Object representing cooking recipe """ type Recipe { averageRating: NonNegativeFloat creationDate: Timestamp! """ The recipe description with preparation info """ description: NonEmptyString ratings: [NonNegativeInt!]! ratingsCount(minRate: Int! = 0): NonNegativeInt! specification: NonEmptyString @deprecated(reason: "Use 'description' field instead") title: NonEmptyString! } input RecipeInput { description: NonEmptyString title: NonEmptyString! } """ The javascript `Date` as integer. Type represents date and time as number of milliseconds from start of UNIX epoch. """ scalar Timestamp ================================================ FILE: examples/interfaces-inheritance/employee/employee.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { PersonInput } from "../person"; @InputType() export class EmployeeInput extends PersonInput { @Field() companyName!: string; } ================================================ FILE: examples/interfaces-inheritance/employee/employee.type.ts ================================================ import { Field, ObjectType } from "type-graphql"; import { Person } from "../person"; @ObjectType() export class Employee extends Person { @Field() companyName!: string; } ================================================ FILE: examples/interfaces-inheritance/employee/index.ts ================================================ export * from "./employee.input"; export * from "./employee.type"; ================================================ FILE: examples/interfaces-inheritance/examples.graphql ================================================ query GetPersons { persons { __typename id name age ... on Student { universityName } ... on Employee { companyName } } } mutation AddStudent { addStudent( input: { name: "Student 1", dateOfBirth: "1991-11-30T00:00:00.000Z", universityName: "Uni 1" } ) { id age } } mutation AddEmployee { addEmployee( input: { name: "Employee 1", dateOfBirth: "1995-07-23T00:00:00.000Z", companyName: "Company 1" } ) { id age } } ================================================ FILE: examples/interfaces-inheritance/helpers.ts ================================================ import crypto from "node:crypto"; export function getId(): string { const randomNumber = Math.random(); const hash = crypto.createHash("sha256"); hash.update(randomNumber.toString()); return hash.digest("hex"); } export function calculateAge(birthday: Date) { const ageDiffMs = Date.now() - birthday.getTime(); const ageDate = new Date(ageDiffMs); // Milliseconds from epoch return Math.abs(ageDate.getUTCFullYear() - 1970); } ================================================ FILE: examples/interfaces-inheritance/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { Person } from "./person"; import { MultiResolver } from "./resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [MultiResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // Provide the type that implements an interface but it is not directly used in schema orphanedTypes: [Person], }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/interfaces-inheritance/person/index.ts ================================================ export * from "./person.input"; export * from "./person.interface"; export * from "./person.type"; ================================================ FILE: examples/interfaces-inheritance/person/person.input.ts ================================================ import { Field, InputType } from "type-graphql"; @InputType() export class PersonInput { @Field() name!: string; @Field() dateOfBirth!: Date; } ================================================ FILE: examples/interfaces-inheritance/person/person.interface.ts ================================================ import { Arg, Field, ID, Int, InterfaceType } from "type-graphql"; import { type IResource } from "../resource"; @InterfaceType({ // Workaround issue #373 (https://github.com/MichalLytek/type-graphql/issues/373) resolveType: value => value.constructor.name, }) export abstract class IPerson implements IResource { @Field(_type => ID) id!: string; @Field() name!: string; @Field(_type => Int) age!: number; @Field() avatar(@Arg("size") _size: number): string { throw new Error("Method not implemented."); } } ================================================ FILE: examples/interfaces-inheritance/person/person.type.ts ================================================ import { Arg, Field, ObjectType } from "type-graphql"; import { IPerson } from "./person.interface"; @ObjectType({ implements: IPerson }) export class Person implements IPerson { id!: string; name!: string; age!: number; @Field() avatar(@Arg("size") size: number): string { return `http://i.pravatar.cc/${size}`; } } ================================================ FILE: examples/interfaces-inheritance/resolver.ts ================================================ import { Arg, Mutation, Query, Resolver } from "type-graphql"; import { Employee, EmployeeInput } from "./employee"; import { calculateAge, getId } from "./helpers"; import { IPerson } from "./person"; import { Student, StudentInput } from "./student"; @Resolver() export class MultiResolver { private readonly personsRegistry: IPerson[] = []; @Query(_returns => [IPerson]) persons(): IPerson[] { // This one returns interfaces, // GraphQL has to be able to resolve type of the item return this.personsRegistry; } @Mutation() addStudent(@Arg("input") input: StudentInput): Student { // Be sure to create real instances of classes const student = Object.assign(new Student(), { id: getId(), name: input.name, universityName: input.universityName, age: calculateAge(input.dateOfBirth), }); this.personsRegistry.push(student); return student; } @Mutation() addEmployee(@Arg("input") input: EmployeeInput): Employee { const employee = Object.assign(new Employee(), { id: getId(), name: input.name, companyName: input.companyName, age: calculateAge(input.dateOfBirth), }); this.personsRegistry.push(employee); return employee; } } ================================================ FILE: examples/interfaces-inheritance/resource/index.ts ================================================ export * from "./resource.interface"; ================================================ FILE: examples/interfaces-inheritance/resource/resource.interface.ts ================================================ import { Field, ID, InterfaceType } from "type-graphql"; @InterfaceType() export abstract class IResource { @Field(_type => ID) id!: string; } ================================================ FILE: examples/interfaces-inheritance/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Employee implements IPerson { age: Int! avatar(size: Float!): String! companyName: String! id: ID! name: String! } input EmployeeInput { companyName: String! dateOfBirth: DateTimeISO! name: String! } interface IPerson { age: Int! avatar(size: Float!): String! id: ID! name: String! } type Mutation { addEmployee(input: EmployeeInput!): Employee! addStudent(input: StudentInput!): Student! } type Person implements IPerson { age: Int! avatar(size: Float!): String! id: ID! name: String! } type Query { persons: [IPerson!]! } type Student implements IPerson { age: Int! avatar(size: Float!): String! id: ID! name: String! universityName: String! } input StudentInput { dateOfBirth: DateTimeISO! name: String! universityName: String! } ================================================ FILE: examples/interfaces-inheritance/student/index.ts ================================================ export * from "./student.input"; export * from "./student.type"; ================================================ FILE: examples/interfaces-inheritance/student/student.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { PersonInput } from "../person"; @InputType() export class StudentInput extends PersonInput { @Field() universityName!: string; } ================================================ FILE: examples/interfaces-inheritance/student/student.type.ts ================================================ import { Field, ObjectType } from "type-graphql"; import { Person } from "../person"; @ObjectType() export class Student extends Person { @Field() universityName!: string; } ================================================ FILE: examples/middlewares-custom-decorators/context.type.ts ================================================ import { type User } from "./user.type"; export interface Context { currentUser: User; } ================================================ FILE: examples/middlewares-custom-decorators/decorators/current-user.ts ================================================ import { createParameterDecorator } from "type-graphql"; import { type Context } from "../context.type"; export function CurrentUser() { return createParameterDecorator(({ context }) => context.currentUser); } ================================================ FILE: examples/middlewares-custom-decorators/decorators/index.ts ================================================ export * from "./current-user"; export * from "./validate-args"; ================================================ FILE: examples/middlewares-custom-decorators/decorators/random-id-arg.ts ================================================ import { Int, createParameterDecorator } from "type-graphql"; const MAX_ID_VALUE = 3; // Number.MAX_SAFE_INTEGER export function RandomIdArg(argName = "id") { return createParameterDecorator( ({ args }) => args[argName] ?? Math.round(Math.random() * MAX_ID_VALUE), { arg: { name: argName, typeFunc: () => Int, options: { nullable: true, description: "Accepts provided id or generates a random one.", validateFn: (value: number): void => { if (value < 0 || value > MAX_ID_VALUE) { throw new Error(`Invalid value for ${argName}`); } }, }, }, }, ); } ================================================ FILE: examples/middlewares-custom-decorators/decorators/validate-args.ts ================================================ import { validate } from "class-validator"; import { ArgumentValidationError, type ClassType, createMethodMiddlewareDecorator, } from "type-graphql"; // Sample implementation of custom validation decorator // This example use 'class-validator' however you can plug-in 'joi' or any other validation library export function ValidateArgs(Type: ClassType) { return createMethodMiddlewareDecorator(async ({ args }, next) => { const instance = Object.assign(new Type(), args); const validationErrors = await validate(instance); if (validationErrors.length > 0) { throw new ArgumentValidationError(validationErrors); } return next(); }); } ================================================ FILE: examples/middlewares-custom-decorators/examples.graphql ================================================ query InvalidArgs { recipes(take: -1) { title description } } query LoggingQuery { recipes { title description ratings } } query InterceptorsQuery { recipes(skip: 1, take: 2) { title ratings averageRating } } query RandomIdQuery { recipe { id title averageRating description } } query SelectedIdQuery { recipe(id: 2) { id title averageRating description } } ================================================ FILE: examples/middlewares-custom-decorators/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import Container from "typedi"; import { type Context } from "./context.type"; import { ErrorLoggerMiddleware } from "./middlewares"; import { RecipeResolver } from "./recipe"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Array of global middlewares globalMiddlewares: [ErrorLoggerMiddleware], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // Registry 3rd party IOC container container: Container, }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, // Provide context for each request context: async (): Promise => ({ // Create mocked user in context currentUser: { id: 123, name: "Sample user", }, }), }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/middlewares-custom-decorators/logger.ts ================================================ import { Service } from "typedi"; @Service() export class Logger { log(...args: any[]) { // Replace with a more sophisticated solution console.log(...args); } } ================================================ FILE: examples/middlewares-custom-decorators/middlewares/error-logger.ts ================================================ import { ArgumentValidationError, type MiddlewareInterface, type NextFn, type ResolverData, } from "type-graphql"; import { Service } from "typedi"; import { type Context } from "../context.type"; import { Logger } from "../logger"; @Service() export class ErrorLoggerMiddleware implements MiddlewareInterface { constructor(private readonly logger: Logger) {} async use({ context, info }: ResolverData, next: NextFn) { try { return await next(); } catch (err) { this.logger.log({ message: (err as Error).message, operation: info.operation.operation, fieldName: info.fieldName, userName: context.currentUser.name, }); if (!(err instanceof ArgumentValidationError)) { // Hide errors from db like printing sql query throw new Error("Unknown error occurred. Try again later!"); } throw err; } } } ================================================ FILE: examples/middlewares-custom-decorators/middlewares/index.ts ================================================ export * from "./error-logger"; export * from "./log-access"; export * from "./number-interceptor"; export * from "./resolve-time"; ================================================ FILE: examples/middlewares-custom-decorators/middlewares/log-access.ts ================================================ import { type MiddlewareInterface, type NextFn, type ResolverData } from "type-graphql"; import { Service } from "typedi"; import { type Context } from "../context.type"; import { Logger } from "../logger"; @Service() export class LogAccessMiddleware implements MiddlewareInterface { constructor(private readonly logger: Logger) {} async use({ context, info }: ResolverData, next: NextFn) { this.logger.log( `Logging access: ${context.currentUser.name} -> ${info.parentType.name}.${info.fieldName}`, ); return next(); } } ================================================ FILE: examples/middlewares-custom-decorators/middlewares/number-interceptor.ts ================================================ import { type MiddlewareFn } from "type-graphql"; export function NumberInterceptor(minValue: number): MiddlewareFn { return async (_, next) => { const result = await next(); // Hide ratings below minValue if (typeof result === "number" && result < minValue) { return null; } return result; }; } ================================================ FILE: examples/middlewares-custom-decorators/middlewares/resolve-time.ts ================================================ import { type MiddlewareFn } from "type-graphql"; export const ResolveTimeMiddleware: MiddlewareFn = async ({ info }, next) => { const start = Date.now(); await next(); const resolveTime = Date.now() - start; console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`); }; ================================================ FILE: examples/middlewares-custom-decorators/recipe/index.ts ================================================ export * from "./recipe.args"; export * from "./recipe.resolver"; export * from "./recipe.data"; export * from "./recipe.type"; ================================================ FILE: examples/middlewares-custom-decorators/recipe/recipe.args.ts ================================================ import { Max, Min } from "class-validator"; import { ArgsType, Field, Int } from "type-graphql"; @ArgsType() export class RecipesArgs { @Field(_type => Int) @Min(0) skip = 0; @Field(_type => Int) @Min(1) @Max(50) take = 10; } ================================================ FILE: examples/middlewares-custom-decorators/recipe/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; let lastRecipeId = 0; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), { // eslint-disable-next-line no-plusplus id: lastRecipeId++, ...recipeData, }); } export const recipes = [ createRecipe({ description: "Desc 1", title: "Recipe 1", ratings: [0, 3, 1], }), createRecipe({ description: "Desc 2", title: "Recipe 2", ratings: [4, 2, 3, 1], }), createRecipe({ description: "Desc 3", title: "Recipe 3", ratings: [4, 5, 3, 1, 5], }), ]; ================================================ FILE: examples/middlewares-custom-decorators/recipe/recipe.resolver.ts ================================================ import { Args, Query, Resolver, UseMiddleware } from "type-graphql"; import { Service } from "typedi"; import { RecipesArgs } from "./recipe.args"; import { recipes as recipesData } from "./recipe.data"; import { Recipe } from "./recipe.type"; import { CurrentUser, ValidateArgs } from "../decorators"; import { RandomIdArg } from "../decorators/random-id-arg"; import { ResolveTimeMiddleware } from "../middlewares"; import { User } from "../user.type"; @Service() @UseMiddleware(ResolveTimeMiddleware) @Resolver(_of => Recipe) export class RecipeResolver { private readonly items: Recipe[] = recipesData; @Query(_returns => Recipe, { nullable: true }) async recipe(@RandomIdArg("id") id: number) { console.log(`Queried for recipe with id: ${id}`); return this.items.find(item => item.id === id); } @Query(_returns => [Recipe]) @ValidateArgs(RecipesArgs) async recipes( @Args({ validate: false }) // Disable built-in validation here options: RecipesArgs, @CurrentUser() currentUser: User, ): Promise { console.log(`User "${currentUser.name}" queried for recipes!`); const start = options.skip; const end = options.skip + options.take; return this.items.slice(start, end); } } ================================================ FILE: examples/middlewares-custom-decorators/recipe/recipe.type.ts ================================================ import { Field, Float, Int, ObjectType, UseMiddleware } from "type-graphql"; import { LogAccessMiddleware, NumberInterceptor } from "../middlewares"; @ObjectType() export class Recipe { @Field(_type => Int) id!: number; @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [Int]) @UseMiddleware(LogAccessMiddleware) ratings!: number[]; @Field(_type => Float, { nullable: true }) @UseMiddleware(NumberInterceptor(3)) get averageRating(): number | null { const ratingsCount = this.ratings.length; if (ratingsCount === 0) { return null; } const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); return ratingsSum / ratingsCount; } } ================================================ FILE: examples/middlewares-custom-decorators/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Query { recipe( """ Accepts provided id or generates a random one. """ id: Int ): Recipe recipes(skip: Int! = 0, take: Int! = 10): [Recipe!]! } type Recipe { averageRating: Float description: String id: Int! ratings: [Int!]! title: String! } ================================================ FILE: examples/middlewares-custom-decorators/user.type.ts ================================================ export interface User { id: number; name: string; } ================================================ FILE: examples/mikro-orm/context.type.ts ================================================ import { type EntityManager } from "@mikro-orm/core"; import { type User } from "./entities"; export interface Context { entityManager: EntityManager; user: User; } ================================================ FILE: examples/mikro-orm/entities/index.ts ================================================ export * from "./rating"; export * from "./recipe"; export * from "./user"; ================================================ FILE: examples/mikro-orm/entities/rating.ts ================================================ import { Entity, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; import { Field, Int, ObjectType } from "type-graphql"; import { Recipe } from "./recipe"; import { User } from "./user"; @Entity() @ObjectType() export class Rating { @PrimaryKey() readonly id!: number; @Field(_type => Int) @Property({ type: "smallint" }) value!: number; @Field(_type => User) @ManyToOne(_type => User) user!: User; @Field() @Property({ onCreate: () => new Date() }) date!: Date; @ManyToOne(_type => Recipe) recipe!: Recipe; [OptionalProps]?: "date"; } ================================================ FILE: examples/mikro-orm/entities/recipe.ts ================================================ import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from "@mikro-orm/core"; import { Field, ID, ObjectType } from "type-graphql"; import { Rating } from "./rating"; import { User } from "./user"; @Entity() @ObjectType() export class Recipe { @Field(_type => ID) @PrimaryKey() readonly id!: number; @Field() @Property() title!: string; @Field({ nullable: true }) @Property({ nullable: true }) description?: string; @Field(_type => [Rating]) @OneToMany(_type => Rating, rating => rating.recipe) ratings = new Collection(this); @Field(_type => User) @ManyToOne(_type => User) author!: User; } ================================================ FILE: examples/mikro-orm/entities/user.ts ================================================ import { Entity, PrimaryKey, Property } from "@mikro-orm/core"; import { Field, ID, ObjectType } from "type-graphql"; @ObjectType() @Entity() export class User { @Field(_type => ID) @PrimaryKey() readonly id!: number; @Field() @Property() email!: string; @Field({ nullable: true }) @Property({ nullable: true }) nickname?: string; @Property() password!: string; } ================================================ FILE: examples/mikro-orm/examples.graphql ================================================ query GetRecipes { recipes { id title author { email } ratings { value } } } query GetRecipe { recipe(recipeId: 1) { id title ratings { value user { nickname } date } author { id nickname email } } } mutation AddRecipe { addRecipe(recipe: { title: "New Recipe" }) { id ratings { value } author { nickname } } } mutation RatingRecipe { rating(rating: { recipeId: 3, value: 4 }) { id ratings { value user { email } } } } ================================================ FILE: examples/mikro-orm/helpers.ts ================================================ import { type EntityManager } from "@mikro-orm/core"; import { Rating, Recipe, User } from "./entities"; export async function seedDatabase(em: EntityManager) { const defaultUser = em.create(User, { email: "admin@github.com", nickname: "administrator", password: "s3cr3tp4ssw0rd", }); em.persist(defaultUser); const recipe1 = em.create(Recipe, { title: "Recipe 1", description: "Desc 1", author: defaultUser, }); recipe1.ratings.add( em.create(Rating, { value: 2, user: defaultUser, recipe: recipe1 }), em.create(Rating, { value: 4, user: defaultUser, recipe: recipe1 }), em.create(Rating, { value: 5, user: defaultUser, recipe: recipe1 }), em.create(Rating, { value: 3, user: defaultUser, recipe: recipe1 }), em.create(Rating, { value: 4, user: defaultUser, recipe: recipe1 }), ); em.persist(recipe1); const recipe2 = em.create(Recipe, { title: "Recipe 2", author: defaultUser, }); recipe2.ratings.add( em.create(Rating, { value: 2, user: defaultUser, recipe: recipe2 }), em.create(Rating, { value: 4, user: defaultUser, recipe: recipe2 }), ); em.persist(recipe2); await em.flush(); return { defaultUser }; } ================================================ FILE: examples/mikro-orm/index.ts ================================================ import "reflect-metadata"; import "dotenv/config"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { MikroORM, ReflectMetadataProvider } from "@mikro-orm/core"; import { PostgreSqlDriver } from "@mikro-orm/postgresql"; import { buildSchema } from "type-graphql"; import { type Context } from "./context.type"; import { Rating, Recipe, User } from "./entities"; import { seedDatabase } from "./helpers"; import { RatingResolver, RecipeResolver } from "./resolvers"; async function bootstrap() { // Initialize MikroORM const orm = await MikroORM.init({ driver: PostgreSqlDriver, clientUrl: process.env.DATABASE_URL, entities: [Rating, Recipe, User], metadataProvider: ReflectMetadataProvider, metadataCache: { enabled: false }, }); const generator = orm.getSchemaGenerator(); await generator.dropSchema(); await generator.createSchema(); await generator.updateSchema(); // Seed database with some data const { defaultUser } = await seedDatabase(orm.em.fork()); // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver, RatingResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), validate: false, }); // Create GraphQL server const server = new ApolloServer({ schema, }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async () => ({ user: defaultUser, // Create fresh instance of entity manager per request entityManager: orm.em.fork(), }) satisfies Context, }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/mikro-orm/resolvers/index.ts ================================================ export * from "./rating.resolver"; export * from "./recipe.resolver"; ================================================ FILE: examples/mikro-orm/resolvers/rating.resolver.ts ================================================ import { Ctx, FieldResolver, Resolver, Root } from "type-graphql"; import { Context } from "../context.type"; import { Rating, User } from "../entities"; @Resolver(_of => Rating) export class RatingResolver { @FieldResolver() async user(@Root() rating: Rating, @Ctx() { entityManager }: Context): Promise { return entityManager.findOneOrFail(User, rating.user.id); } } ================================================ FILE: examples/mikro-orm/resolvers/recipe.resolver.ts ================================================ import { Arg, Ctx, FieldResolver, Int, Mutation, Query, Resolver, Root } from "type-graphql"; import { RatingInput, RecipeInput } from "./types"; import { Context } from "../context.type"; import { Rating, Recipe, User } from "../entities"; @Resolver(_of => Recipe) export class RecipeResolver { @Query(_returns => Recipe, { nullable: true }) recipe(@Arg("recipeId", _type => Int) recipeId: number, @Ctx() { entityManager }: Context) { return entityManager.findOne(Recipe, recipeId); } @Query(_returns => [Recipe]) recipes(@Ctx() { entityManager }: Context): Promise { return entityManager.find(Recipe, {}); } @Mutation(_returns => Recipe) async addRecipe( @Arg("recipe") recipeInput: RecipeInput, @Ctx() { user, entityManager }: Context, ): Promise { const recipe = entityManager.create(Recipe, { title: recipeInput.title, description: recipeInput.description, author: entityManager.getReference(User, user.id), }); await entityManager.persistAndFlush(recipe); return recipe; } @Mutation(_returns => Recipe) async rate( @Arg("rate") rateInput: RatingInput, @Ctx() { user, entityManager }: Context, ): Promise { // Find the recipe const recipe = await entityManager.findOne(Recipe, rateInput.recipeId, { populate: ["ratings"], }); if (!recipe) { throw new Error("Invalid recipe ID"); } // Set the new recipe rating const newRating = entityManager.create(Rating, { recipe, value: rateInput.value, user: entityManager.getReference(User, user.id), }); recipe.ratings.add(newRating); // Update the recipe await entityManager.persistAndFlush(recipe); return recipe; } @FieldResolver() ratings(@Root() recipe: Recipe, @Ctx() { entityManager }: Context) { return entityManager.find(Rating, { recipe: { id: recipe.id } }); } @FieldResolver() async author(@Root() recipe: Recipe, @Ctx() { entityManager }: Context): Promise { return entityManager.findOneOrFail(User, recipe.author.id); } } ================================================ FILE: examples/mikro-orm/resolvers/types/index.ts ================================================ export * from "./rating.input"; export * from "./recipe.input"; ================================================ FILE: examples/mikro-orm/resolvers/types/rating.input.ts ================================================ import { Field, InputType, Int } from "type-graphql"; @InputType() export class RatingInput { @Field(_type => Int) recipeId!: number; @Field(_type => Int) value!: number; } ================================================ FILE: examples/mikro-orm/resolvers/types/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "../../entities"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description?: string; } ================================================ FILE: examples/mikro-orm/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { addRecipe(recipe: RecipeInput!): Recipe! rate(rate: RatingInput!): Recipe! } type Query { recipe(recipeId: Int!): Recipe recipes: [Recipe!]! } type Rating { date: DateTimeISO! user: User! value: Int! } input RatingInput { recipeId: Int! value: Int! } type Recipe { author: User! description: String id: ID! ratings: [Rating!]! title: String! } input RecipeInput { description: String title: String! } type User { email: String! id: ID! nickname: String } ================================================ FILE: examples/mixin-classes/examples.graphql ================================================ query GetUsers { users { ...UserInfo } } mutation CreateUser { createUser( input: { forename: "Sample 1" dateOfBirth: "2000-09-21T14:54:17.369Z" email: "test@test.test" password: "qwerty123" } ) { ...UserInfo } } mutation AmendUser { amendUser( input: { id: 1 forename: "Sample Amend 1" dateOfBirth: "2000-09-21T14:54:17.369Z" email: "test2@test.test" } ) { ...UserInfo } } fragment UserInfo on User { id forename surname dateOfBirth email } ================================================ FILE: examples/mixin-classes/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { UserResolver } from "./resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [UserResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), skipCheck: true, validate: true, }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/mixin-classes/inputs/amend.user.input.ts ================================================ import { InputType } from "type-graphql"; import { withId } from "../mixins"; import { UserDetails } from "../types"; // 'AmendUser' is like the full 'User' class without the password @InputType() export class AmendUserInput extends withId(UserDetails) {} ================================================ FILE: examples/mixin-classes/inputs/create.user.input.ts ================================================ import { InputType } from "type-graphql"; import { withPassword } from "../mixins"; import { UserDetails } from "../types"; // 'CreateUser' is like the full 'User' class without the id @InputType() export class CreateUserInput extends withPassword(UserDetails) {} ================================================ FILE: examples/mixin-classes/inputs/index.ts ================================================ export * from "./amend.user.input"; export * from "./create.user.input"; ================================================ FILE: examples/mixin-classes/mixins/index.ts ================================================ export * from "./with.id"; export * from "./with.password"; ================================================ FILE: examples/mixin-classes/mixins/with.id.ts ================================================ import { type ClassType, Field, InputType, Int, ObjectType } from "type-graphql"; // Adds 'id' property to the base, extended class export function withId(BaseClass: TClassType) { @ObjectType() @InputType() class IDTrait extends BaseClass { @Field(_type => Int) id!: number; } return IDTrait; } ================================================ FILE: examples/mixin-classes/mixins/with.password.ts ================================================ import { MinLength } from "class-validator"; import { type ClassType, Field, InputType, ObjectType } from "type-graphql"; // Adds 'password' property with validation to the base, extended class export function withPassword(BaseClass: TClassType) { @ObjectType() @InputType() class PasswordTrait extends BaseClass { @MinLength(8) @Field() password!: string; } return PasswordTrait; } ================================================ FILE: examples/mixin-classes/resolver.ts ================================================ import { Arg, Mutation, Query, Resolver } from "type-graphql"; import { AmendUserInput, CreateUserInput } from "./inputs"; import { User } from "./types"; @Resolver() export class UserResolver { private autoIncrementId = 0; private readonly usersData: User[] = []; @Query(_returns => [User]) async users(): Promise { return this.usersData; } @Mutation(_returns => User) async createUser(@Arg("input") userData: CreateUserInput): Promise { // Generate the ID and store the password this.autoIncrementId += 1; const user: User = { ...userData, id: this.autoIncrementId }; this.usersData.push(user); return user; } @Mutation(_returns => User) async amendUser(@Arg("input") { id, ...userData }: AmendUserInput): Promise { // Receive the ID but can't change the password const user = this.usersData.find(it => it.id === id); if (!user) { throw new Error(`Invalid ID: ${id}`); } Object.assign(user, userData); return user; } } ================================================ FILE: examples/mixin-classes/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- input AmendUserInput { dateOfBirth: DateTimeISO! email: String! forename: String! id: Int! surname: String } input CreateUserInput { dateOfBirth: DateTimeISO! email: String! forename: String! password: String! surname: String } """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { amendUser(input: AmendUserInput!): User! createUser(input: CreateUserInput!): User! } type Query { users: [User!]! } type User { dateOfBirth: DateTimeISO! email: String! forename: String! id: Int! surname: String } ================================================ FILE: examples/mixin-classes/types/index.ts ================================================ export * from "./user"; export * from "./user.details"; ================================================ FILE: examples/mixin-classes/types/user.details.ts ================================================ import { IsEmail } from "class-validator"; import { Field, InputType, ObjectType } from "type-graphql"; // 'UserDetails' stores base common user properties @ObjectType() @InputType("UserDetailsInput") export class UserDetails { @Field() forename!: string; @Field({ nullable: true }) surname?: string; @Field(_type => Date) dateOfBirth!: Date; @IsEmail() @Field() email!: string; } ================================================ FILE: examples/mixin-classes/types/user.ts ================================================ import { ObjectType } from "type-graphql"; import { UserDetails } from "./user.details"; import { withId } from "../mixins/with.id"; // 'User' is a full object with 'id' and hidden 'password' @ObjectType() export class User extends withId(UserDetails) { // No TypeGraphQL decorator, hidden in schema password!: string; } ================================================ FILE: examples/query-complexity/examples.graphql ================================================ query GetRecipesWithComplexityError { recipes(count: 3) { title averageRating } } query GetRecipesWithoutComplexityError { recipes(count: 2) { title ratingsCount } } ================================================ FILE: examples/query-complexity/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from "graphql-query-complexity"; import { buildSchema } from "type-graphql"; import { RecipeResolver } from "./recipe.resolver"; // Maximum allowed complexity const MAX_COMPLEXITY = 20; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema, // Create a plugin to allow query complexity calculation for every request plugins: [ { requestDidStart: async () => ({ async didResolveOperation({ request, document }) { /** * Provides GraphQL query analysis to be able to react on complex queries to the GraphQL server * It can be used to protect the GraphQL server against resource exhaustion and DoS attacks * More documentation can be found at https://github.com/ivome/graphql-query-complexity */ const complexity = getComplexity({ // GraphQL schema schema, // To calculate query complexity properly, // check only the requested operation // not the whole document that may contains multiple operations operationName: request.operationName, // GraphQL query document query: document, // GraphQL query variables variables: request.variables, // Add any number of estimators. The estimators are invoked in order, the first // numeric value that is being returned by an estimator is used as the field complexity // If no estimator returns a value, an exception is raised estimators: [ // Using fieldExtensionsEstimator is mandatory to make it work with type-graphql fieldExtensionsEstimator(), // Add more estimators here... // This will assign each field a complexity of 1 // if no other estimator returned a value simpleEstimator({ defaultComplexity: 1 }), ], }); // React to the calculated complexity, // like compare it with max and throw error when the threshold is reached if (complexity > MAX_COMPLEXITY) { throw new Error( `Sorry, too complicated query! ${complexity} exceeded the maximum allowed complexity of ${MAX_COMPLEXITY}`, ); } console.log("Used query complexity points:", complexity); }, }), }, ], }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/query-complexity/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } export function createRecipeSamples() { return [ createRecipe({ title: "Recipe 1", ratings: [0, 3, 1], }), createRecipe({ title: "Recipe 2", ratings: [4, 2, 3, 1], }), createRecipe({ title: "Recipe 3", ratings: [5, 4], }), ]; } ================================================ FILE: examples/query-complexity/recipe.resolver.ts ================================================ import { Arg, FieldResolver, Query, Resolver, type ResolverInterface, Root } from "type-graphql"; import { createRecipeSamples } from "./recipe.data"; import { Recipe } from "./recipe.type"; @Resolver(_of => Recipe) export class RecipeResolver implements ResolverInterface { private readonly items: Recipe[] = createRecipeSamples(); @Query(_returns => [Recipe], { /* Pass also a calculation function in the complexity option to determine a custom complexity. This function provide the complexity of the child nodes as well as the field input arguments. That way a more realistic estimation of individual field complexity values is made, e.g. by multiplying childComplexity by the number of items in array */ complexity: ({ childComplexity, args }) => args.count * childComplexity, }) async recipes(@Arg("count") count: number): Promise { return this.items.slice(0, count); } /* Complexity in field resolver overrides complexity of equivalent field type */ @FieldResolver({ complexity: 5 }) ratingsCount(@Root() recipe: Recipe): number { return recipe.ratings.length; } } ================================================ FILE: examples/query-complexity/recipe.type.ts ================================================ import { Field, Float, Int, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { /* By default, every field gets a complexity of 1 */ @Field() title!: string; /* Which can be customized by passing the complexity parameter */ @Field(_type => Int, { complexity: 2 }) ratingsCount!: number; @Field(_type => Float, { nullable: true, complexity: 10, }) get averageRating(): number | null { const ratingsCount = this.ratings.length; if (ratingsCount === 0) { return null; } const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); return ratingsSum / ratingsCount; } // Internal property, not exposed in schema ratings!: number[]; } ================================================ FILE: examples/query-complexity/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Query { recipes(count: Float!): [Recipe!]! } type Recipe { averageRating: Float ratingsCount: Int! title: String! } ================================================ FILE: examples/redis-subscriptions/comment.input.ts ================================================ import { Field, ID, InputType } from "type-graphql"; import { type Comment } from "./comment.type"; @InputType() export class CommentInput implements Partial { @Field(_type => ID) recipeId!: string; @Field({ nullable: true }) nickname?: string; @Field() content!: string; } ================================================ FILE: examples/redis-subscriptions/comment.type.ts ================================================ import { Field, ObjectType } from "type-graphql"; @ObjectType() export class Comment { @Field({ nullable: true }) nickname?: string; @Field() content!: string; @Field() date!: Date; } export interface NewCommentPayload { recipeId: string; dateString: string; // Limitation of Redis payload serialization content: string; nickname?: string; } ================================================ FILE: examples/redis-subscriptions/examples.graphql ================================================ query FirstRecipe { recipe(id: "1") { title description comments { nickname content date } } } mutation AddCommentToRecipe1 { addNewComment(comment: { recipeId: "1", nickname: "MichalLytek", content: "Nice one!" }) } mutation AddCommentToRecipe2 { addNewComment(comment: { recipeId: "2", nickname: "MichalLytek", content: "Nice two!" }) } subscription NewCommentsForRecipe2 { newComments(recipeId: "2") { nickname content date } } ================================================ FILE: examples/redis-subscriptions/index.ts ================================================ import "reflect-metadata"; import "dotenv/config"; import http from "node:http"; import path from "node:path"; import { createYoga } from "graphql-yoga"; import { buildSchema } from "type-graphql"; import { pubSub } from "./pubsub"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // Publish/Subscribe pubSub, validate: false, }); // Create GraphQL server const yoga = createYoga({ schema, graphqlEndpoint: "/graphql", }); // Create server const httpServer = http.createServer(yoga); // Start server httpServer.listen(4000, () => { console.log(`GraphQL server ready at http://localhost:4000/graphql`); }); } bootstrap().catch(console.error); ================================================ FILE: examples/redis-subscriptions/pubsub.ts ================================================ import { createRedisEventTarget } from "@graphql-yoga/redis-event-target"; import { createPubSub } from "@graphql-yoga/subscription"; import { Redis } from "ioredis"; import { type NewCommentPayload } from "./comment.type"; const redisUrl = process.env.REDIS_URL; if (!redisUrl) { throw new Error("REDIS_URL env variable is not defined"); } export const enum Topic { NEW_COMMENT = "NEW_COMMENT", } export const pubSub = createPubSub<{ [Topic.NEW_COMMENT]: [NewCommentPayload]; }>({ eventTarget: createRedisEventTarget({ publishClient: new Redis(redisUrl, { retryStrategy: times => Math.max(times * 100, 3000), }), subscribeClient: new Redis(redisUrl, { retryStrategy: times => Math.max(times * 100, 3000), }), }), }); ================================================ FILE: examples/redis-subscriptions/recipe.data.ts ================================================ import { Comment } from "./comment.type"; import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } function createComment(commentData: Partial): Comment { return Object.assign(new Comment(), commentData); } export const sampleRecipes = [ createRecipe({ id: "1", title: "Recipe 1", description: "Desc 1", comments: [ createComment({ date: new Date("2018-03-21"), content: "Very tasty!", nickname: "Anonymous", }), createComment({ date: new Date("2018-01-12"), content: "Not so tasty!", nickname: "Anonymous again", }), ], }), createRecipe({ id: "2", title: "Recipe 2", description: "Desc 2", comments: [ createComment({ date: new Date(), content: "Very good, very cheap!", nickname: "Master of cooking", }), ], }), createRecipe({ id: "3", title: "Recipe 3", comments: [], }), ]; ================================================ FILE: examples/redis-subscriptions/recipe.resolver.args.ts ================================================ import { ArgsType, Field, ID } from "type-graphql"; @ArgsType() export class NewCommentsArgs { @Field(_type => ID) recipeId!: string; } ================================================ FILE: examples/redis-subscriptions/recipe.resolver.ts ================================================ import { Arg, Args, ID, Mutation, Query, Resolver, Root, Subscription, type SubscriptionHandlerData, } from "type-graphql"; import { CommentInput } from "./comment.input"; import { Comment, NewCommentPayload } from "./comment.type"; import { Topic, pubSub } from "./pubsub"; import { sampleRecipes } from "./recipe.data"; import { NewCommentsArgs } from "./recipe.resolver.args"; import { Recipe } from "./recipe.type"; @Resolver() export class RecipeResolver { private readonly recipes: Recipe[] = sampleRecipes.slice(); @Query(_returns => Recipe, { nullable: true }) async recipe(@Arg("id", _type => ID) id: string) { return this.recipes.find(recipe => recipe.id === id); } @Mutation(_returns => Boolean) async addNewComment(@Arg("comment") input: CommentInput): Promise { const recipe = this.recipes.find(r => r.id === input.recipeId); if (!recipe) { return false; } const comment: Comment = { content: input.content, nickname: input.nickname, date: new Date(), }; recipe.comments.push(comment); pubSub.publish(Topic.NEW_COMMENT, { content: comment.content, nickname: comment.nickname, dateString: comment.date.toISOString(), recipeId: input.recipeId, }); return true; } @Subscription(_returns => Comment, { topics: Topic.NEW_COMMENT, filter: ({ payload, args }: SubscriptionHandlerData) => payload.recipeId === args.recipeId, }) newComments(@Root() newComment: NewCommentPayload, @Args() _args: NewCommentsArgs): Comment { return { content: newComment.content, date: new Date(newComment.dateString), // Limitation of Redis payload serialization nickname: newComment.nickname, } satisfies Comment; } } ================================================ FILE: examples/redis-subscriptions/recipe.type.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; import { Comment } from "./comment.type"; @ObjectType() export class Recipe { @Field(_type => ID) id!: string; @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [Comment]) comments!: Comment[]; } ================================================ FILE: examples/redis-subscriptions/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Comment { content: String! date: DateTimeISO! nickname: String } input CommentInput { content: String! nickname: String recipeId: ID! } """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { addNewComment(comment: CommentInput!): Boolean! } type Query { recipe(id: ID!): Recipe } type Recipe { comments: [Comment!]! description: String id: ID! title: String! } type Subscription { newComments(recipeId: ID!): Comment! } ================================================ FILE: examples/resolvers-inheritance/examples.graphql ================================================ query AllPersons { persons { id name age role } } query OneRecipe { recipe(id: 1) { uuid title ratings averageRating } } mutation PromotePersonOne { promote(personId: 1) } ================================================ FILE: examples/resolvers-inheritance/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { Container } from "typedi"; import { PersonResolver } from "./person"; import { RecipeResolver } from "./recipe"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver, PersonResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // Registry 3rd party IOC container container: Container, }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/resolvers-inheritance/person/index.ts ================================================ export * from "./person.resolver"; export * from "./person.role"; export * from "./person.type"; ================================================ FILE: examples/resolvers-inheritance/person/person.resolver.ts ================================================ import { Arg, Int, Mutation, Resolver } from "type-graphql"; import { Service } from "typedi"; import { PersonRole } from "./person.role"; import { Person } from "./person.type"; import { ResourceResolver } from "../resource"; const persons: Person[] = [ { id: 1, name: "Person 1", age: 23, role: PersonRole.Normal, }, { id: 2, name: "Person 2", age: 48, role: PersonRole.Admin, }, ]; @Resolver() @Service() export class PersonResolver extends ResourceResolver(Person, persons) { // Here you can add resource-specific operations @Mutation() promote(@Arg("personId", _type => Int) personId: number): boolean { // Full access to base resolver class fields and methods const person = this.resourceService.getOne(personId); if (!person) { throw new Error("Person not found!"); } if (person.role === PersonRole.Normal) { person.role = PersonRole.Pro; return true; } return false; } } ================================================ FILE: examples/resolvers-inheritance/person/person.role.ts ================================================ import { registerEnumType } from "type-graphql"; export enum PersonRole { Normal, Pro, Admin, } registerEnumType(PersonRole, { name: "PersonRole" }); ================================================ FILE: examples/resolvers-inheritance/person/person.type.ts ================================================ import { Field, Int, ObjectType } from "type-graphql"; import { PersonRole } from "./person.role"; import { type Resource } from "../resource"; @ObjectType() export class Person implements Resource { @Field() id!: number; @Field() name!: string; @Field(_type => Int) age!: number; @Field(_type => PersonRole) role!: PersonRole; } ================================================ FILE: examples/resolvers-inheritance/recipe/index.ts ================================================ export * from "./recipe.resolver"; export * from "./recipe.type"; ================================================ FILE: examples/resolvers-inheritance/recipe/recipe.resolver.ts ================================================ import { FieldResolver, Resolver, Root } from "type-graphql"; import { Service } from "typedi"; import { Recipe } from "./recipe.type"; import { ResourceResolver } from "../resource"; const recipes: Recipe[] = [ { id: 1, title: "Recipe 1", ratings: [1, 3, 4], }, ]; @Resolver(_of => Recipe) @Service() export class RecipeResolver extends ResourceResolver(Recipe, recipes) { // Here you can add resource-specific operations @FieldResolver() averageRating(@Root() recipe: Recipe): number { return recipe.ratings.reduce((a, b) => a + b, 0) / recipe.ratings.length; } } ================================================ FILE: examples/resolvers-inheritance/recipe/recipe.type.ts ================================================ import { Field, Int, ObjectType } from "type-graphql"; import { type Resource } from "../resource"; @ObjectType() export class Recipe implements Resource { @Field() id!: number; @Field() title!: string; @Field(_type => [Int]) ratings!: number[]; } ================================================ FILE: examples/resolvers-inheritance/resource/index.ts ================================================ export * from "./resource.args"; export * from "./resource"; export * from "./resource.resolver"; export * from "./resource.service"; export * from "./resource.service.factory"; ================================================ FILE: examples/resolvers-inheritance/resource/resource.args.ts ================================================ import { ArgsType, Field, Int } from "type-graphql"; @ArgsType() export class GetAllArgs { @Field(_type => Int) skip = 0; @Field(_type => Int) take = 10; } ================================================ FILE: examples/resolvers-inheritance/resource/resource.resolver.ts ================================================ import { Arg, Args, type ClassType, FieldResolver, Int, Query, Resolver, Root } from "type-graphql"; import { Service } from "typedi"; import { Resource } from "./resource"; import { GetAllArgs } from "./resource.args"; import { type ResourceService } from "./resource.service"; import { ResourceServiceFactory } from "./resource.service.factory"; export function ResourceResolver( ResourceCls: ClassType, resources: TResource[], ) { const resourceName = ResourceCls.name.toLocaleLowerCase(); @Resolver(_of => ResourceCls) @Service() abstract class ResourceResolverClass { protected resourceService: ResourceService; constructor(factory: ResourceServiceFactory) { this.resourceService = factory.create(resources); } @Query(_returns => ResourceCls, { name: `${resourceName}` }) protected async getOne(@Arg("id", _type => Int) id: number) { return this.resourceService.getOne(id); } @Query(_returns => [ResourceCls], { name: `${resourceName}s` }) protected async getAll(@Args() { skip, take }: GetAllArgs) { const all = this.resourceService.getAll(skip, take); return all; } // Dynamically created field with resolver for all child resource classes @FieldResolver({ name: "uuid" }) protected getUuid(@Root() resource: Resource): string { return `${resourceName}_${resource.id}`; } } return ResourceResolverClass; } ================================================ FILE: examples/resolvers-inheritance/resource/resource.service.factory.ts ================================================ import { Service } from "typedi"; import { type Resource } from "./resource"; import { ResourceService } from "./resource.service"; // Use factory to separate instance of service for each generic @Service() export class ResourceServiceFactory { create(resources?: TResource[]) { return new ResourceService(resources); } } ================================================ FILE: examples/resolvers-inheritance/resource/resource.service.ts ================================================ import { type Resource } from "./resource"; export class ResourceService { constructor(protected resources: TResource[] = []) {} getOne(id: number): TResource | undefined { return this.resources.find(res => res.id === id); } getAll(skip: number, take: number): TResource[] { const start: number = skip; const end: number = skip + take; return this.resources.slice(start, end); } } ================================================ FILE: examples/resolvers-inheritance/resource/resource.ts ================================================ export interface Resource { id: number; } ================================================ FILE: examples/resolvers-inheritance/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { promote(personId: Int!): Boolean! } type Person { age: Int! id: Float! name: String! role: PersonRole! uuid: String! } enum PersonRole { Admin Normal Pro } type Query { person(id: Int!): Person! persons(skip: Int! = 0, take: Int! = 10): [Person!]! recipe(id: Int!): Recipe! recipes(skip: Int! = 0, take: Int! = 10): [Recipe!]! } type Recipe { averageRating: Float! id: Float! ratings: [Int!]! title: String! uuid: String! } ================================================ FILE: examples/simple-subscriptions/examples.graphql ================================================ subscription AllNotifications { normalSubscription { id message date } } subscription EvenNotifications { subscriptionWithFilter { id message date } } mutation PublishMessage { pubSubMutation(message: "Hello") } # Dynamic topics subscription DynamicTopic { subscribeToTopicFromArg(topic: "FOO_MESSAGES") { id message } } mutation PublishMessageToDynamicTopic { publishToDynamicTopic(topic: "FOO_MESSAGES", message: "Hi Foo!") } # Dynamic topic id subscription DynamicTopicId { subscribeToTopicIdFromArg(topicId: 2137) { id message } } mutation PublishMessageToDynamicTopicId { publishWithDynamicTopicId(topicId: 2137, message: "Hi Foo!") } ================================================ FILE: examples/simple-subscriptions/index.ts ================================================ import "reflect-metadata"; import http from "node:http"; import path from "node:path"; import { createYoga } from "graphql-yoga"; import { buildSchema } from "type-graphql"; import { NotificationResolver } from "./notification.resolver"; import { pubSub } from "./pubsub"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [NotificationResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // Publish/Subscribe pubSub, }); // Create GraphQL server const yoga = createYoga({ schema, graphqlEndpoint: "/graphql", }); // Create server const httpServer = http.createServer(yoga); // Start server httpServer.listen(4000, () => { console.log(`GraphQL server ready at http://localhost:4000/graphql`); }); } bootstrap().catch(console.error); ================================================ FILE: examples/simple-subscriptions/notification.resolver.ts ================================================ import { Arg, Int, Mutation, Query, Resolver, Root, type SubscribeResolverData, Subscription, type SubscriptionHandlerData, } from "type-graphql"; import { Notification, NotificationPayload } from "./notification.type"; import { Topic, pubSub } from "./pubsub"; @Resolver() export class NotificationResolver { private id: number = 0; @Query(_returns => Date) currentDate() { return new Date(); } @Mutation(_returns => Boolean) async pubSubMutation(@Arg("message", { nullable: true }) message?: string): Promise { this.id += 1; const payload: NotificationPayload = { id: this.id, message }; pubSub.publish(Topic.NOTIFICATIONS, payload); return true; } @Subscription({ topics: Topic.NOTIFICATIONS }) normalSubscription(@Root() { id, message }: NotificationPayload): Notification { return { id, message, date: new Date() }; } @Subscription(_returns => Notification, { topics: Topic.NOTIFICATIONS, filter: ({ payload }: SubscriptionHandlerData) => payload.id % 2 === 0, }) subscriptionWithFilter(@Root() { id, message }: NotificationPayload) { const newNotification: Notification = { id, message, date: new Date() }; return newNotification; } // Multiple topics @Subscription(_returns => Notification, { topics: [Topic.NOTIFICATIONS, "NOTIFICATIONS_2"], filter: ({ payload }: SubscriptionHandlerData) => payload.id % 2 === 0, }) subscriptionWithMultipleTopics(@Root() { id, message }: NotificationPayload) { const newNotification: Notification = { id, message, date: new Date() }; return newNotification; } // Dynamic topic @Mutation(() => Boolean) async publishToDynamicTopic( @Arg("topic") topic: string, @Arg("message", { nullable: true }) message?: string, ): Promise { this.id += 1; const payload: NotificationPayload = { id: this.id, message }; pubSub.publish(topic, payload); return true; } @Subscription({ topics: ({ args }) => args.topic, }) subscribeToTopicFromArg( @Arg("topic") _topic: string, @Root() { id, message }: NotificationPayload, ): Notification { return { id, message, date: new Date() }; } // Dynamic topic id @Mutation(() => Boolean) async publishWithDynamicTopicId( @Arg("topicId", () => Int) topicId: number, @Arg("message", { nullable: true }) message?: string, ): Promise { this.id += 1; const payload: NotificationPayload = { id: this.id, message }; pubSub.publish(Topic.DYNAMIC_ID_TOPIC, topicId, payload); return true; } @Subscription({ topics: Topic.DYNAMIC_ID_TOPIC, topicId: ({ args }: SubscribeResolverData) => args.topicId, }) subscribeToTopicIdFromArg( @Arg("topicId", () => Int) _topicId: number, @Root() { id, message }: NotificationPayload, ): Notification { return { id, message, date: new Date() }; } } ================================================ FILE: examples/simple-subscriptions/notification.type.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; @ObjectType() export class Notification { @Field(_type => ID) id!: number; @Field({ nullable: true }) message?: string; @Field(_type => Date) date!: Date; } export interface NotificationPayload { id: number; message?: string; } ================================================ FILE: examples/simple-subscriptions/pubsub.ts ================================================ import { createPubSub } from "@graphql-yoga/subscription"; import { type NotificationPayload } from "./notification.type"; export const enum Topic { NOTIFICATIONS = "NOTIFICATIONS", DYNAMIC_ID_TOPIC = "DYNAMIC_ID_TOPIC", } export const pubSub = createPubSub< { [Topic.NOTIFICATIONS]: [NotificationPayload]; [Topic.DYNAMIC_ID_TOPIC]: [number, NotificationPayload]; } & Record // Fallback for dynamic topics >(); ================================================ FILE: examples/simple-subscriptions/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { pubSubMutation(message: String): Boolean! publishToDynamicTopic(message: String, topic: String!): Boolean! publishWithDynamicTopicId(message: String, topicId: Int!): Boolean! } type Notification { date: DateTimeISO! id: ID! message: String } type Query { currentDate: DateTimeISO! } type Subscription { normalSubscription: Notification! subscribeToTopicFromArg(topic: String!): Notification! subscribeToTopicIdFromArg(topicId: Int!): Notification! subscriptionWithFilter: Notification! subscriptionWithMultipleTopics: Notification! } ================================================ FILE: examples/simple-usage/examples.graphql ================================================ query GetRecipe1 { recipe(title: "Recipe 1") { title description ratings creationDate ratingsCount(minRate: 2) averageRating } } query GetRecipes { recipes { title description creationDate averageRating } } mutation AddRecipe { addRecipe(recipe: { title: "New recipe", description: "Simple description" }) { creationDate } } ================================================ FILE: examples/simple-usage/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { RecipeResolver } from "./recipe.resolver"; async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/simple-usage/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial) { return Object.assign(new Recipe(), recipeData); } export function createRecipeSamples() { return [ createRecipe({ description: "Desc 1", title: "Recipe 1", ratings: [0, 3, 1], creationDate: new Date("2018-04-11"), }), createRecipe({ description: "Desc 2", title: "Recipe 2", ratings: [4, 2, 3, 1], creationDate: new Date("2018-04-15"), }), createRecipe({ description: "Desc 3", title: "Recipe 3", ratings: [5, 4], creationDate: new Date(), }), ]; } ================================================ FILE: examples/simple-usage/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description?: string; } ================================================ FILE: examples/simple-usage/recipe.resolver.ts ================================================ import { Arg, FieldResolver, Int, Mutation, Query, Resolver, type ResolverInterface, Root, } from "type-graphql"; import { createRecipeSamples } from "./recipe.data"; import { RecipeInput } from "./recipe.input"; import { Recipe } from "./recipe.type"; @Resolver(_of => Recipe) export class RecipeResolver implements ResolverInterface { private readonly items: Recipe[] = createRecipeSamples(); @Query(_returns => Recipe, { nullable: true }) async recipe(@Arg("title") title: string): Promise { return this.items.find(recipe => recipe.title === title); } @Query(_returns => [Recipe], { description: "Get all the recipes from around the world " }) async recipes(): Promise { return this.items; } @Mutation(_returns => Recipe) async addRecipe(@Arg("recipe") recipeInput: RecipeInput): Promise { const recipe = Object.assign(new Recipe(), { description: recipeInput.description, title: recipeInput.title, ratings: [], creationDate: new Date(), }); await this.items.push(recipe); return recipe; } @FieldResolver() ratingsCount( @Root() recipe: Recipe, @Arg("minRate", _type => Int, { defaultValue: 0 }) minRate: number, ): number { return recipe.ratings.filter(rating => rating >= minRate).length; } } ================================================ FILE: examples/simple-usage/recipe.type.ts ================================================ import { Field, Float, Int, ObjectType } from "type-graphql"; @ObjectType({ description: "Object representing cooking recipe" }) export class Recipe { @Field() title!: string; @Field(_type => String, { nullable: true, deprecationReason: "Use 'description' field instead" }) get specification(): string | undefined { return this.description; } @Field({ nullable: true, description: "The recipe description with preparation info" }) description?: string; @Field(_type => [Int]) ratings!: number[]; @Field() creationDate!: Date; @Field(_type => Int) ratingsCount!: number; @Field(_type => Float, { nullable: true }) get averageRating(): number | null { const ratingsCount = this.ratings.length; if (ratingsCount === 0) { return null; } const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); return ratingsSum / ratingsCount; } } ================================================ FILE: examples/simple-usage/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { addRecipe(recipe: RecipeInput!): Recipe! } type Query { recipe(title: String!): Recipe """ Get all the recipes from around the world """ recipes: [Recipe!]! } """ Object representing cooking recipe """ type Recipe { averageRating: Float creationDate: DateTimeISO! """ The recipe description with preparation info """ description: String ratings: [Int!]! ratingsCount(minRate: Int! = 0): Int! specification: String @deprecated(reason: "Use 'description' field instead") title: String! } input RecipeInput { description: String title: String! } ================================================ FILE: examples/tsconfig.json ================================================ { "extends": "../tsconfig.cjs.json", "include": ["../src", "./"], "compilerOptions": { "emitDecoratorMetadata": true } } ================================================ FILE: examples/tsyringe/examples.graphql ================================================ query GetRecipe1 { recipe(recipeId: "1") { title description ingredients numberInCollection } } query GetRecipes { recipes { title description ingredientsLength numberInCollection } } mutation AddRecipe { addRecipe(recipe: { title: "New recipe", ingredients: ["One", "Two", "Three"] }) { id numberInCollection } } ================================================ FILE: examples/tsyringe/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { container } from "tsyringe"; import { buildSchema } from "type-graphql"; import { sampleRecipes } from "./recipe.data"; import { RecipeResolver } from "./recipe.resolver"; // Add sample recipes in container container.register("SAMPLE_RECIPES", { useValue: sampleRecipes.slice() }); async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Registry 3rd party IOC container container: { get: cls => container.resolve(cls) }, // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/tsyringe/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } export const sampleRecipes = [ createRecipe({ id: "1", title: "Recipe 1", description: "Desc 1", ingredients: ["one", "two", "three"], }), createRecipe({ id: "2", title: "Recipe 2", description: "Desc 2", ingredients: ["four", "five", "six"], }), createRecipe({ id: "3", title: "Recipe 3", ingredients: ["seven", "eight", "nine"], }), ]; ================================================ FILE: examples/tsyringe/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [String]) ingredients!: string[]; } ================================================ FILE: examples/tsyringe/recipe.resolver.ts ================================================ import { inject, injectable } from "tsyringe"; import { Arg, FieldResolver, Mutation, Query, Resolver, Root } from "type-graphql"; import { RecipeInput } from "./recipe.input"; import { RecipeService } from "./recipe.service"; import { Recipe } from "./recipe.type"; @injectable() @Resolver(_of => Recipe) export class RecipeResolver { constructor( // Inject service @inject(RecipeService) private readonly recipeService: RecipeService, ) {} @Query(_returns => Recipe, { nullable: true }) async recipe(@Arg("recipeId") recipeId: string) { return this.recipeService.getOne(recipeId); } @Query(_returns => [Recipe]) async recipes(): Promise { return this.recipeService.getAll(); } @Mutation(_returns => Recipe) async addRecipe(@Arg("recipe") recipe: RecipeInput): Promise { return this.recipeService.add(recipe); } @FieldResolver() async numberInCollection(@Root() recipe: Recipe): Promise { const index = await this.recipeService.findIndex(recipe); return index + 1; } } ================================================ FILE: examples/tsyringe/recipe.service.ts ================================================ import { inject } from "tsyringe"; import { type RecipeInput } from "./recipe.input"; import { Recipe } from "./recipe.type"; export class RecipeService { private autoIncrementValue: number; constructor( @inject("SAMPLE_RECIPES") private readonly items: Recipe[], ) { this.autoIncrementValue = this.items.length; } async getAll() { return this.items; } async getOne(id: string) { return this.items.find(it => it.id === id); } async add(data: RecipeInput) { const recipe = this.createRecipe(data); this.items.push(recipe); return recipe; } async findIndex(recipe: Recipe) { return this.items.findIndex(it => it.id === recipe.id); } private createRecipe(recipeData: Partial): Recipe { const recipe = Object.assign(new Recipe(), recipeData); recipe.id = this.getId(); return recipe; } private getId(): string { this.autoIncrementValue += 1; return this.autoIncrementValue.toString(); } } ================================================ FILE: examples/tsyringe/recipe.type.ts ================================================ import { Field, ID, Int, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { @Field(_type => ID) id!: string; @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [String]) ingredients!: string[]; @Field(_type => Int) protected numberInCollection!: number; @Field(_type => Int) protected get ingredientsLength(): number { return this.ingredients.length; } } ================================================ FILE: examples/tsyringe/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { addRecipe(recipe: RecipeInput!): Recipe! } type Query { recipe(recipeId: String!): Recipe recipes: [Recipe!]! } type Recipe { description: String id: ID! ingredients: [String!]! ingredientsLength: Int! numberInCollection: Int! title: String! } input RecipeInput { description: String ingredients: [String!]! title: String! } ================================================ FILE: examples/typegoose/.eslintrc ================================================ { "rules": { "no-underscore-dangle": "off" } } ================================================ FILE: examples/typegoose/context.type.ts ================================================ import { type User } from "./entities"; export interface Context { user: User; } ================================================ FILE: examples/typegoose/entities/index.ts ================================================ export * from "./rating"; export * from "./recipe"; export * from "./user"; ================================================ FILE: examples/typegoose/entities/rating.ts ================================================ import { prop as Property } from "@typegoose/typegoose"; import { Field, Int, ObjectType } from "type-graphql"; import { User } from "./user"; import { Ref } from "../types"; @ObjectType() export class Rating { @Field(_type => Int) @Property({ required: true }) value!: number; @Field() @Property({ default: new Date(), required: true }) date!: Date; @Field(_type => User) @Property({ ref: User, required: true }) user!: Ref; } ================================================ FILE: examples/typegoose/entities/recipe.ts ================================================ import { prop as Property, getModelForClass } from "@typegoose/typegoose"; import { Types } from "mongoose"; import { Field, ObjectType } from "type-graphql"; import { Rating } from "./rating"; import { User } from "./user"; import { Ref } from "../types"; @ObjectType() export class Recipe { @Field() readonly _id!: Types.ObjectId; @Field() @Property({ required: true }) title!: string; @Field({ nullable: true }) @Property() description?: string; @Field(_type => [Rating]) @Property({ type: () => Rating, default: [] }) ratings!: Rating[]; @Field(_type => User) @Property({ ref: User, required: true }) author!: Ref; } export const RecipeModel = getModelForClass(Recipe); ================================================ FILE: examples/typegoose/entities/user.ts ================================================ import { prop as Property, getModelForClass } from "@typegoose/typegoose"; import { Types } from "mongoose"; import { Field, ObjectType } from "type-graphql"; @ObjectType() export class User { @Field() readonly _id!: Types.ObjectId; @Field() @Property({ required: true }) email!: string; @Field({ nullable: true }) @Property() nickname?: string; @Property({ required: true }) password!: string; } export const UserModel = getModelForClass(User); ================================================ FILE: examples/typegoose/examples.graphql ================================================ query GetRecipes { recipes { _id title author { email } ratings { value } } } query GetRecipe { # Fill with correct ObjectId recipe(recipeId: "64551384aac388414b391778") { _id title ratings { value user { nickname } date } author { _id nickname email } } } mutation AddRecipe { addRecipe(recipe: { title: "New Recipe" }) { _id ratings { value } author { nickname } } } mutation RatingRecipe { # Fill with correct ObjectId rating(rating: { recipeId: "64557478aac388414b391799", value: 4 }) { _id ratings { value user { email } } } } ================================================ FILE: examples/typegoose/helpers.ts ================================================ import { RecipeModel, type User, UserModel } from "./entities"; export async function seedDatabase() { const defaultUser = new UserModel({ email: "admin@github.com", nickname: "administrator", password: "s3cr3tp4ssw0rd", } as User); await defaultUser.save(); await RecipeModel.create([ { title: "Recipe 1", description: "Desc 1", author: defaultUser._id, ratings: [ { value: 2, user: defaultUser._id }, { value: 4, user: defaultUser._id }, { value: 5, user: defaultUser._id }, { value: 3, user: defaultUser._id }, { value: 4, user: defaultUser._id }, ], }, { title: "Recipe 2", author: defaultUser._id, ratings: [ { value: 2, user: defaultUser._id }, { value: 4, user: defaultUser._id }, ], }, ]); return { defaultUser }; } ================================================ FILE: examples/typegoose/index.ts ================================================ import "reflect-metadata"; import "dotenv/config"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { Types, connect } from "mongoose"; import { buildSchema } from "type-graphql"; import { type Context } from "./context.type"; import { seedDatabase } from "./helpers"; import { ObjectIdScalar } from "./object-id.scalar"; import { RatingResolver, RecipeResolver } from "./resolvers"; import { TypegooseMiddleware } from "./typegoose.middleware"; async function bootstrap() { // Create mongoose connection const mongoose = await connect(process.env.DATABASE_URL!); // Clean database await mongoose.connection.db?.dropDatabase(); // Seed database with some data const { defaultUser } = await seedDatabase(); // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver, RatingResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), // Use document converting middleware globalMiddlewares: [TypegooseMiddleware], // Use ObjectId scalar mapping scalarsMap: [{ type: Types.ObjectId, scalar: ObjectIdScalar }], validate: false, }); // Create mocked context const context: Context = { user: defaultUser }; // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async () => context, }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/typegoose/object-id.scalar.ts ================================================ import { GraphQLScalarType, Kind } from "graphql"; import { Types } from "mongoose"; export const ObjectIdScalar = new GraphQLScalarType({ name: "ObjectId", description: "Mongo object id scalar type", serialize(value: unknown): string { if (!(value instanceof Types.ObjectId)) { throw new Error("ObjectIdScalar can only serialize ObjectId values"); } return value.toHexString(); }, parseValue(value: unknown): Types.ObjectId { if (typeof value !== "string") { throw new Error("ObjectIdScalar can only parse string values"); } return new Types.ObjectId(value); }, parseLiteral(ast): Types.ObjectId { if (ast.kind !== Kind.STRING) { throw new Error("ObjectIdScalar can only parse string values"); } return new Types.ObjectId(ast.value); }, }); ================================================ FILE: examples/typegoose/resolvers/index.ts ================================================ export * from "./rating.resolver"; export * from "./recipe.resolver"; ================================================ FILE: examples/typegoose/resolvers/rating.resolver.ts ================================================ import { FieldResolver, Resolver, Root } from "type-graphql"; import { Rating, type User, UserModel } from "../entities"; @Resolver(_of => Rating) export class RatingResolver { @FieldResolver() async user(@Root() rating: Rating): Promise { return (await UserModel.findById(rating.user))!; } } ================================================ FILE: examples/typegoose/resolvers/recipe.resolver.ts ================================================ import { Types } from "mongoose"; import { Arg, Ctx, FieldResolver, Mutation, Query, Resolver, Root } from "type-graphql"; import { RatingInput, RecipeInput } from "./types"; import { Context } from "../context.type"; import { type Rating, Recipe, RecipeModel, type User, UserModel } from "../entities"; import { ObjectIdScalar } from "../object-id.scalar"; @Resolver(_of => Recipe) export class RecipeResolver { @Query(_returns => Recipe, { nullable: true }) recipe(@Arg("recipeId", () => ObjectIdScalar) recipeId: Types.ObjectId) { return RecipeModel.findById(recipeId); } @Query(_returns => [Recipe]) async recipes(): Promise { return RecipeModel.find({}); } @Mutation(_returns => Recipe) async addRecipe( @Arg("recipe") recipeInput: RecipeInput, @Ctx() { user }: Context, ): Promise { const recipe = new RecipeModel({ ...recipeInput, author: user._id, }); await recipe.save(); return recipe; } @Mutation(_returns => Recipe) async rating(@Arg("rating") ratingInput: RatingInput, @Ctx() { user }: Context): Promise { // Find the recipe const recipe = await RecipeModel.findById(ratingInput.recipeId); if (!recipe) { throw new Error("Invalid recipe ID"); } // Set the new recipe rating const newRating: Rating = { value: ratingInput.value, user: user._id, date: new Date(), }; // Add and update the new recipe recipe.ratings.push(newRating); await recipe.save(); return recipe; } @FieldResolver() async author(@Root() recipe: Recipe): Promise { return (await UserModel.findById(recipe.author))!; } } ================================================ FILE: examples/typegoose/resolvers/types/index.ts ================================================ export * from "./rating.input"; export * from "./recipe.input"; ================================================ FILE: examples/typegoose/resolvers/types/rating.input.ts ================================================ import { Types } from "mongoose"; import { Field, InputType, Int } from "type-graphql"; @InputType() export class RatingInput { @Field() recipeId!: Types.ObjectId; @Field(_type => Int) value!: number; } ================================================ FILE: examples/typegoose/resolvers/types/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "../../entities"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description?: string; } ================================================ FILE: examples/typegoose/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { addRecipe(recipe: RecipeInput!): Recipe! rating(rating: RatingInput!): Recipe! } """ Mongo object id scalar type """ scalar ObjectId type Query { recipe(recipeId: ObjectId!): Recipe recipes: [Recipe!]! } type Rating { date: DateTimeISO! user: User! value: Int! } input RatingInput { recipeId: ObjectId! value: Int! } type Recipe { author: User! description: String id: ObjectId! ratings: [Rating!]! title: String! } input RecipeInput { description: String title: String! } type User { email: String! id: ObjectId! nickname: String } ================================================ FILE: examples/typegoose/typegoose.middleware.ts ================================================ import { getClass } from "@typegoose/typegoose"; import { type Document, Model } from "mongoose"; import { type MiddlewareFn } from "type-graphql"; function convertDocument(doc: Document) { const convertedDocument = doc.toObject(); const DocumentClass = getClass(doc)!; Object.setPrototypeOf(convertedDocument, DocumentClass.prototype); return convertedDocument; } export const TypegooseMiddleware: MiddlewareFn = async (_, next) => { const result = await next(); if (Array.isArray(result)) { return result.map(item => (item instanceof Model ? convertDocument(item) : item)); } if (result instanceof Model) { return convertDocument(result); } return result; }; ================================================ FILE: examples/typegoose/types.ts ================================================ import { type Types } from "mongoose"; export type Ref = T | Types.ObjectId; ================================================ FILE: examples/typeorm-basic-usage/context.type.ts ================================================ import { type User } from "./entities"; export interface Context { user: User; } ================================================ FILE: examples/typeorm-basic-usage/datasource.ts ================================================ import * as TypeORM from "typeorm"; import { Rating, Recipe, User } from "./entities"; // Create TypeORM dataSource export const dataSource = new TypeORM.DataSource({ type: "postgres", url: process.env.DATABASE_URL, synchronize: true, dropSchema: true, cache: true, logging: "all", entities: [Rating, Recipe, User], logger: "advanced-console", }); ================================================ FILE: examples/typeorm-basic-usage/entities/index.ts ================================================ export * from "./rating"; export * from "./recipe"; export * from "./user"; ================================================ FILE: examples/typeorm-basic-usage/entities/rating.ts ================================================ import { Field, Int, ObjectType } from "type-graphql"; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId, } from "typeorm"; import { Recipe } from "./recipe"; import { User } from "./user"; @Entity() @ObjectType() export class Rating { @PrimaryGeneratedColumn() readonly id!: number; @Field(_type => Int) @Column({ type: "int" }) value!: number; @Field(_type => User) @ManyToOne(_type => User) user!: User; @RelationId((rating: Rating) => rating.user) userId!: number; @Field() @CreateDateColumn() date!: Date; @ManyToOne(_type => Recipe) recipe!: Recipe; @RelationId((rating: Rating) => rating.recipe) recipeId!: number; } ================================================ FILE: examples/typeorm-basic-usage/entities/recipe.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, RelationId } from "typeorm"; import { Rating } from "./rating"; import { User } from "./user"; @Entity() @ObjectType() export class Recipe { @Field(_type => ID) @PrimaryGeneratedColumn() readonly id!: number; @Field() @Column() title!: string; @Field({ nullable: true }) @Column({ nullable: true }) description?: string; @Field(_type => [Rating]) @OneToMany(_type => Rating, rating => rating.recipe, { cascade: ["insert"] }) ratings!: Rating[]; @Field(_type => User) @ManyToOne(_type => User) author!: User; @RelationId((recipe: Recipe) => recipe.author) authorId!: number; } ================================================ FILE: examples/typeorm-basic-usage/entities/user.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; @ObjectType() @Entity() export class User { @Field(_type => ID) @PrimaryGeneratedColumn() readonly id!: number; @Field() @Column() email!: string; @Field({ nullable: true }) @Column({ nullable: true }) nickname?: string; @Column() password!: string; } ================================================ FILE: examples/typeorm-basic-usage/examples.graphql ================================================ query GetRecipes { recipes { id title author { email } ratings { value } } } query GetRecipe { recipe(recipeId: 1) { id title ratings { value user { nickname } date } author { id nickname email } } } mutation AddRecipe { addRecipe(recipe: { title: "New Recipe" }) { id ratings { value } author { nickname } } } mutation RatingRecipe { rating(rating: { recipeId: 3, value: 4 }) { id ratings { value user { email } } } } ================================================ FILE: examples/typeorm-basic-usage/helpers.ts ================================================ import { dataSource } from "./datasource"; import { Rating, Recipe, User } from "./entities"; export async function seedDatabase() { const recipeRepository = dataSource.getRepository(Recipe); const ratingRepository = dataSource.getRepository(Rating); const userRepository = dataSource.getRepository(User); const defaultUser = userRepository.create({ email: "admin@github.com", nickname: "administrator", password: "s3cr3tp4ssw0rd", }); await userRepository.save(defaultUser); const recipes = recipeRepository.create([ { title: "Recipe 1", description: "Desc 1", author: defaultUser, ratings: ratingRepository.create([ { value: 2, user: defaultUser }, { value: 4, user: defaultUser }, { value: 5, user: defaultUser }, { value: 3, user: defaultUser }, { value: 4, user: defaultUser }, ]), }, { title: "Recipe 2", author: defaultUser, ratings: ratingRepository.create([ { value: 2, user: defaultUser }, { value: 4, user: defaultUser }, ]), }, ]); await recipeRepository.save(recipes); return { defaultUser, }; } ================================================ FILE: examples/typeorm-basic-usage/index.ts ================================================ import "reflect-metadata"; import "dotenv/config"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { type Context } from "./context.type"; import { dataSource } from "./datasource"; import { seedDatabase } from "./helpers"; import { RatingResolver, RecipeResolver } from "./resolvers"; async function bootstrap() { // Create TypeORM connection await dataSource.initialize(); // Seed database with some data const { defaultUser } = await seedDatabase(); // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver, RatingResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create mocked context const context: Context = { user: defaultUser }; // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async () => context, }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/typeorm-basic-usage/resolvers/index.ts ================================================ export * from "./rating.resolver"; export * from "./recipe.resolver"; ================================================ FILE: examples/typeorm-basic-usage/resolvers/rating.resolver.ts ================================================ import { FieldResolver, Resolver, Root } from "type-graphql"; import { type Repository } from "typeorm"; import { dataSource } from "../datasource"; import { Rating, User } from "../entities"; @Resolver(_of => Rating) export class RatingResolver { private readonly userRepository: Repository; constructor() { this.userRepository = dataSource.getRepository(User); } @FieldResolver() async user(@Root() rating: Rating): Promise { return (await this.userRepository.findOne({ where: { id: rating.userId }, cache: 1000 }))!; } } ================================================ FILE: examples/typeorm-basic-usage/resolvers/recipe.resolver.ts ================================================ import { Arg, Ctx, FieldResolver, Int, Mutation, Query, Resolver, Root } from "type-graphql"; import { type Repository } from "typeorm"; import { RatingInput, RecipeInput } from "./types"; import { Context } from "../context.type"; import { dataSource } from "../datasource"; import { Rating, Recipe, User } from "../entities"; @Resolver(_of => Recipe) export class RecipeResolver { private readonly ratingRepository: Repository; private readonly recipeRepository: Repository; private readonly userRepository: Repository; constructor() { this.ratingRepository = dataSource.getRepository(Rating); this.recipeRepository = dataSource.getRepository(Recipe); this.userRepository = dataSource.getRepository(User); } @Query(_returns => Recipe, { nullable: true }) recipe(@Arg("recipeId", _type => Int) recipeId: number) { return this.recipeRepository.findOneBy({ id: recipeId }); } @Query(_returns => [Recipe]) recipes(): Promise { return this.recipeRepository.find(); } @Mutation(_returns => Recipe) async addRecipe( @Arg("recipe") recipeInput: RecipeInput, @Ctx() { user }: Context, ): Promise { const recipe = this.recipeRepository.create({ ...recipeInput, authorId: user.id, }); return this.recipeRepository.save(recipe); } @Mutation(_returns => Recipe) async rating(@Arg("rating") ratingInput: RatingInput, @Ctx() { user }: Context): Promise { // Find the recipe const recipe = await this.recipeRepository.findOne({ where: { id: ratingInput.recipeId }, relations: ["ratings"], }); if (!recipe) { throw new Error("Invalid recipe ID"); } // Set the new recipe rating const newRating = this.ratingRepository.create({ recipe, value: ratingInput.value, user, }); recipe.ratings.push(newRating); // Update and return recipe return this.recipeRepository.save(recipe); } @FieldResolver() ratings(@Root() recipe: Recipe) { return this.ratingRepository.find({ cache: 1000, where: { recipe: { id: recipe.id } }, }); } @FieldResolver() async author(@Root() recipe: Recipe): Promise { return (await this.userRepository.findOne({ where: { id: recipe.authorId }, cache: 1000 }))!; } } ================================================ FILE: examples/typeorm-basic-usage/resolvers/types/index.ts ================================================ export * from "./rating.input"; export * from "./recipe.input"; ================================================ FILE: examples/typeorm-basic-usage/resolvers/types/rating.input.ts ================================================ import { Field, InputType, Int } from "type-graphql"; @InputType() export class RatingInput { @Field(_type => Int) recipeId!: number; @Field(_type => Int) value!: number; } ================================================ FILE: examples/typeorm-basic-usage/resolvers/types/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "../../entities"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description?: string; } ================================================ FILE: examples/typeorm-basic-usage/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { addRecipe(recipe: RecipeInput!): Recipe! rating(rating: RatingInput!): Recipe! } type Query { recipe(recipeId: Int!): Recipe recipes: [Recipe!]! } type Rating { date: DateTimeISO! user: User! value: Int! } input RatingInput { recipeId: Int! value: Int! } type Recipe { author: User! description: String id: ID! ratings: [Rating!]! title: String! } input RecipeInput { description: String title: String! } type User { email: String! id: ID! nickname: String } ================================================ FILE: examples/typeorm-lazy-relations/context.type.ts ================================================ import { type User } from "./entities"; export interface Context { user: User; } ================================================ FILE: examples/typeorm-lazy-relations/datasource.ts ================================================ import * as TypeORM from "typeorm"; import { Rating, Recipe, User } from "./entities"; // Create TypeORM dataSource export const dataSource = new TypeORM.DataSource({ type: "postgres", url: process.env.DATABASE_URL, synchronize: true, dropSchema: true, cache: true, logging: "all", entities: [Rating, Recipe, User], logger: "advanced-console", }); ================================================ FILE: examples/typeorm-lazy-relations/entities/index.ts ================================================ export * from "./rating"; export * from "./recipe"; export * from "./user"; ================================================ FILE: examples/typeorm-lazy-relations/entities/rating.ts ================================================ import { Field, Int, ObjectType } from "type-graphql"; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import { Recipe } from "./recipe"; import { User } from "./user"; @Entity() @ObjectType() export class Rating { @PrimaryGeneratedColumn() readonly id!: number; @Field(_type => Int) @Column({ type: "int" }) value!: number; @Field(_type => User) @ManyToOne(_type => User, { lazy: true }) user!: User | Promise; @Field() @CreateDateColumn() date!: Date; @ManyToOne(_type => Recipe, { lazy: true }) recipe!: Recipe | Promise; } ================================================ FILE: examples/typeorm-lazy-relations/entities/recipe.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Rating } from "./rating"; import { User } from "./user"; @Entity() @ObjectType() export class Recipe { @Field(_type => ID) @PrimaryGeneratedColumn() readonly id!: number; @Field() @Column() title!: string; @Field({ nullable: true }) @Column({ nullable: true }) description?: string; @Field(_type => [Rating]) @OneToMany(_type => Rating, rating => rating.recipe, { lazy: true, cascade: ["insert"] }) ratings!: Rating[] | Promise; @Field(_type => User) @ManyToOne(_type => User, { lazy: true }) author!: User | Promise; } ================================================ FILE: examples/typeorm-lazy-relations/entities/user.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Recipe } from "./recipe"; @ObjectType() @Entity() export class User { @Field(_type => ID) @PrimaryGeneratedColumn() readonly id!: number; @Field() @Column() email!: string; @Field({ nullable: true }) @Column({ nullable: true }) nickname?: string; @Column() password!: string; @OneToMany(_type => Recipe, recipe => recipe.author, { lazy: true }) @Field(_type => [Recipe]) recipes!: Recipe[] | Promise; } ================================================ FILE: examples/typeorm-lazy-relations/examples.graphql ================================================ query GetRecipes { recipes { id title author { id email nickname } ratings { value } } } query GetRecipe { recipe(recipeId: 1) { id title ratings { value user { nickname } date } author { nickname recipes { title } } } } mutation AddRecipe { addRecipe(recipe: { title: "New Recipe" }) { id ratings { value } author { nickname } } } mutation RatingRecipe { rating(rating: { recipeId: 3, value: 4 }) { id ratings { value } } } ================================================ FILE: examples/typeorm-lazy-relations/helpers.ts ================================================ import { dataSource } from "./datasource"; import { Rating, Recipe, User } from "./entities"; export async function seedDatabase() { const recipeRepository = dataSource.getRepository(Recipe); const ratingRepository = dataSource.getRepository(Rating); const userRepository = dataSource.getRepository(User); const defaultUser = userRepository.create({ email: "admin@github.com", nickname: "administrator", password: "s3cr3tp4ssw0rd", }); await userRepository.save(defaultUser); const [recipe1, recipe2] = recipeRepository.create([ { title: "Recipe 1", description: "Desc 1", author: defaultUser, }, { title: "Recipe 2", author: defaultUser, }, ]); await recipeRepository.save([recipe1, recipe2]); const ratings = ratingRepository.create([ { value: 2, user: defaultUser, recipe: recipe1 }, { value: 4, user: defaultUser, recipe: recipe1 }, { value: 5, user: defaultUser, recipe: recipe1 }, { value: 3, user: defaultUser, recipe: recipe1 }, { value: 4, user: defaultUser, recipe: recipe1 }, { value: 2, user: defaultUser, recipe: recipe2 }, { value: 4, user: defaultUser, recipe: recipe2 }, ]); await ratingRepository.save(ratings); return { defaultUser, }; } ================================================ FILE: examples/typeorm-lazy-relations/index.ts ================================================ import "reflect-metadata"; import "dotenv/config"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { type Context } from "./context.type"; import { dataSource } from "./datasource"; import { seedDatabase } from "./helpers"; import { RecipeResolver } from "./resolvers"; async function bootstrap() { // Create TypeORM connection await dataSource.initialize(); // Seed database with some data const { defaultUser } = await seedDatabase(); // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create mocked context const context: Context = { user: defaultUser }; // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async () => context, }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/typeorm-lazy-relations/resolvers/index.ts ================================================ export * from "./recipe.resolver"; ================================================ FILE: examples/typeorm-lazy-relations/resolvers/recipe.resolver.ts ================================================ import { Arg, Ctx, Int, Mutation, Query, Resolver } from "type-graphql"; import { type Repository } from "typeorm"; import { RatingInput, RecipeInput } from "./types"; import { Context } from "../context.type"; import { dataSource } from "../datasource"; import { Rating, Recipe } from "../entities"; @Resolver(Recipe) export class RecipeResolver { private readonly ratingRepository: Repository; private readonly recipeRepository: Repository; constructor() { this.ratingRepository = dataSource.getRepository(Rating); this.recipeRepository = dataSource.getRepository(Recipe); } @Query(_returns => Recipe, { nullable: true }) recipe(@Arg("recipeId", _type => Int) recipeId: number) { return this.recipeRepository.findOneBy({ id: recipeId }); } @Query(_returns => [Recipe]) recipes(): Promise { return this.recipeRepository.find(); } @Mutation(_returns => Recipe) addRecipe(@Arg("recipe") recipeInput: RecipeInput, @Ctx() { user }: Context): Promise { const recipe = this.recipeRepository.create({ ...recipeInput, author: user, }); return this.recipeRepository.save(recipe); } @Mutation(_returns => Recipe) async rating(@Ctx() { user }: Context, @Arg("rating") rateInput: RatingInput): Promise { // Find the recipe const recipe = await this.recipeRepository.findOne({ where: { id: rateInput.recipeId }, relations: ["ratings"], }); if (!recipe) { throw new Error("Invalid recipe ID"); } // Set the new recipe rating const newRating = this.ratingRepository.create({ recipe, user, value: rateInput.value, }); // Add the new recipe rating (await recipe.ratings).push(newRating); // Update and return recipe return this.recipeRepository.save(recipe); } } ================================================ FILE: examples/typeorm-lazy-relations/resolvers/types/index.ts ================================================ export * from "./rating.input"; export * from "./recipe.input"; ================================================ FILE: examples/typeorm-lazy-relations/resolvers/types/rating.input.ts ================================================ import { Field, InputType, Int } from "type-graphql"; @InputType() export class RatingInput { @Field(_type => Int) recipeId!: number; @Field(_type => Int) value!: number; } ================================================ FILE: examples/typeorm-lazy-relations/resolvers/types/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "../../entities"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description!: string; } ================================================ FILE: examples/typeorm-lazy-relations/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ scalar DateTimeISO type Mutation { addRecipe(recipe: RecipeInput!): Recipe! rating(rating: RatingInput!): Recipe! } type Query { recipe(recipeId: Int!): Recipe recipes: [Recipe!]! } type Rating { date: DateTimeISO! user: User! value: Int! } input RatingInput { recipeId: Int! value: Int! } type Recipe { author: User! description: String id: ID! ratings: [Rating!]! title: String! } input RecipeInput { description: String title: String! } type User { email: String! id: ID! nickname: String recipes: [Recipe!]! } ================================================ FILE: examples/using-container/examples.graphql ================================================ query GetRecipe1 { recipe(recipeId: "1") { title description ingredients numberInCollection } } query GetRecipes { recipes { title description ingredientsLength numberInCollection } } mutation AddRecipe { addRecipe(recipe: { title: "New recipe", ingredients: ["One", "Two", "Three"] }) { id numberInCollection } } ================================================ FILE: examples/using-container/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { buildSchema } from "type-graphql"; import { Container } from "typedi"; import { sampleRecipes } from "./recipe.data"; import { RecipeResolver } from "./recipe.resolver"; // Add sample recipes in container Container.set({ id: "SAMPLE_RECIPES", factory: () => sampleRecipes.slice() }); async function bootstrap() { // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Registry 3rd party IOC container container: Container, // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/using-container/recipe.data.ts ================================================ import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } export const sampleRecipes = [ createRecipe({ id: "1", title: "Recipe 1", description: "Desc 1", ingredients: ["one", "two", "three"], }), createRecipe({ id: "2", title: "Recipe 2", description: "Desc 2", ingredients: ["four", "five", "six"], }), createRecipe({ id: "3", title: "Recipe 3", ingredients: ["seven", "eight", "nine"], }), ]; ================================================ FILE: examples/using-container/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [String]) ingredients!: string[]; } ================================================ FILE: examples/using-container/recipe.resolver.ts ================================================ import { Arg, FieldResolver, Mutation, Query, Resolver, Root } from "type-graphql"; import { Inject, Service } from "typedi"; import { RecipeInput } from "./recipe.input"; import { RecipeService } from "./recipe.service"; import { Recipe } from "./recipe.type"; @Service() @Resolver(_of => Recipe) export class RecipeResolver { constructor( // Inject service @Inject() private readonly recipeService: RecipeService, ) {} @Query(_returns => Recipe, { nullable: true }) async recipe(@Arg("recipeId") recipeId: string) { return this.recipeService.getOne(recipeId); } @Query(_returns => [Recipe]) async recipes(): Promise { return this.recipeService.getAll(); } @Mutation(_returns => Recipe) async addRecipe(@Arg("recipe") recipe: RecipeInput): Promise { return this.recipeService.add(recipe); } @FieldResolver() async numberInCollection(@Root() recipe: Recipe): Promise { const index = await this.recipeService.findIndex(recipe); return index + 1; } } ================================================ FILE: examples/using-container/recipe.service.ts ================================================ import { Inject, Service } from "typedi"; import { type RecipeInput } from "./recipe.input"; import { Recipe } from "./recipe.type"; @Service() export class RecipeService { private autoIncrementValue: number; constructor( @Inject("SAMPLE_RECIPES") private readonly items: Recipe[], ) { this.autoIncrementValue = this.items.length; } async getAll() { return this.items; } async getOne(id: string) { return this.items.find(it => it.id === id); } async add(data: RecipeInput) { const recipe = this.createRecipe(data); this.items.push(recipe); return recipe; } async findIndex(recipe: Recipe) { return this.items.findIndex(it => it.id === recipe.id); } private createRecipe(recipeData: Partial): Recipe { const recipe = Object.assign(new Recipe(), recipeData); recipe.id = this.getId(); return recipe; } private getId(): string { this.autoIncrementValue += 1; return this.autoIncrementValue.toString(); } } ================================================ FILE: examples/using-container/recipe.type.ts ================================================ import { Field, ID, Int, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { @Field(_type => ID) id!: string; @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [String]) ingredients!: string[]; @Field(_type => Int) protected numberInCollection!: number; @Field(_type => Int) protected get ingredientsLength(): number { return this.ingredients.length; } } ================================================ FILE: examples/using-container/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { addRecipe(recipe: RecipeInput!): Recipe! } type Query { recipe(recipeId: String!): Recipe recipes: [Recipe!]! } type Recipe { description: String id: ID! ingredients: [String!]! ingredientsLength: Int! numberInCollection: Int! title: String! } input RecipeInput { description: String ingredients: [String!]! title: String! } ================================================ FILE: examples/using-scoped-container/context.type.ts ================================================ import { type ContainerInstance } from "typedi"; export interface Context { requestId: number; container: ContainerInstance; } ================================================ FILE: examples/using-scoped-container/examples.graphql ================================================ query GetRecipe1 { recipe(recipeId: "1") { title description } } query GetNotExistingRecipe10 { recipe(recipeId: "10") { title description } } query GetRecipes { recipes { title description } } mutation AddRecipe { addRecipe(recipe: { title: "New recipe", ingredients: ["One", "Two", "Three"] }) { id title } } ================================================ FILE: examples/using-scoped-container/index.ts ================================================ import "reflect-metadata"; import path from "node:path"; import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { type ResolverData, buildSchema } from "type-graphql"; import { Container, type ContainerInstance } from "typedi"; import { type Context } from "./context.type"; import { setSamplesInContainer } from "./recipe/recipe.data"; import { RecipeResolver } from "./recipe/recipe.resolver"; async function bootstrap() { setSamplesInContainer(); // Build TypeGraphQL executable schema const schema = await buildSchema({ // Array of resolvers resolvers: [RecipeResolver], // Registry custom, scoped IOC container from resolver data function container: ({ context }: ResolverData) => context.container, // Create 'schema.graphql' file with schema definition in current directory emitSchemaFile: path.resolve(__dirname, "schema.graphql"), }); // Create GraphQL server const server = new ApolloServer({ schema, // Create a plugin to allow for disposing the scoped container created for every request plugins: [ { requestDidStart: async () => ({ async willSendResponse(requestContext) { // Dispose the scoped container to prevent memory leaks Container.reset(requestContext.contextValue.requestId.toString()); // For developers curiosity purpose, here is the logging of current scoped container instances // Make multiple parallel requests to see in console how this works const instancesIds = ((Container as any).instances as ContainerInstance[]).map( instance => instance.id, ); console.log("Instances left in memory: ", instancesIds); }, }), }, ], }); // Start server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, // Provide unique context with 'requestId' for each request context: async () => { const requestId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); // uuid-like const container = Container.of(requestId.toString()); // Get scoped container const context = { requestId, container }; // Create context container.set("context", context); // Set context or other data in container return context; }, }); console.log(`GraphQL server ready at ${url}`); } bootstrap().catch(console.error); ================================================ FILE: examples/using-scoped-container/logger.ts ================================================ import { Inject, Service } from "typedi"; import { Context } from "./context.type"; // Service is recreated for each request (scoped) @Service() export class Logger { constructor(@Inject("context") private readonly context: Context) { console.log("Logger created!"); } log(...messages: any[]) { console.log(`(ID ${this.context.requestId}):`, ...messages); } } ================================================ FILE: examples/using-scoped-container/recipe/recipe.data.ts ================================================ import Container from "typedi"; import { Recipe } from "./recipe.type"; function createRecipe(recipeData: Partial): Recipe { return Object.assign(new Recipe(), recipeData); } export const sampleRecipes = [ createRecipe({ id: "1", title: "Recipe 1", description: "Desc 1", ingredients: ["one", "two", "three"], }), createRecipe({ id: "2", title: "Recipe 2", description: "Desc 2", ingredients: ["four", "five", "six"], }), createRecipe({ id: "3", title: "Recipe 3", ingredients: ["seven", "eight", "nine"], }), ]; export function setSamplesInContainer() { // Add sample recipes to container Container.set({ id: "SAMPLE_RECIPES", transient: true, // Create a fresh copy for each 'get' of samples factory: () => { console.log("sampleRecipes copy created!"); return sampleRecipes.slice(); }, }); } ================================================ FILE: examples/using-scoped-container/recipe/recipe.input.ts ================================================ import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial { @Field() title!: string; @Field({ nullable: true }) description!: string; @Field(_type => [String]) ingredients!: string[]; } ================================================ FILE: examples/using-scoped-container/recipe/recipe.resolver.ts ================================================ import { Arg, Ctx, Mutation, Query, Resolver } from "type-graphql"; import { Inject, Service } from "typedi"; import { RecipeInput } from "./recipe.input"; import { RecipeService } from "./recipe.service"; import { Recipe } from "./recipe.type"; import { Context } from "../context.type"; import { Logger } from "../logger"; const delay = (time: number) => new Promise(resolve => { setTimeout(resolve, time); }); // Resolver is recreated for each request (scoped) @Service() @Resolver(_of => Recipe) export class RecipeResolver { constructor( @Inject() private readonly recipeService: RecipeService, @Inject() private readonly logger: Logger, ) { console.log("RecipeResolver created!"); } @Query(_returns => Recipe, { nullable: true }) async recipe(@Arg("recipeId") recipeId: string, @Ctx() { requestId }: Context) { const recipe = await this.recipeService.getOne(recipeId); if (!recipe) { console.log("request ID:", requestId); // Same requestId of logger this.logger.log(`Recipe ${recipeId} not found!`); } return recipe; } @Query(_returns => [Recipe]) async recipes(): Promise { await delay(5000); // Simulate delay to allow for manual concurrent requests return this.recipeService.getAll(); } @Mutation(_returns => Recipe) async addRecipe(@Arg("recipe") recipe: RecipeInput): Promise { return this.recipeService.add(recipe); } } ================================================ FILE: examples/using-scoped-container/recipe/recipe.service.ts ================================================ import { Inject, Service } from "typedi"; import { type RecipeInput } from "./recipe.input"; import { Recipe } from "./recipe.type"; // Service is global, shared by every request @Service({ global: true }) export class RecipeService { private autoIncrementValue: number; constructor(@Inject("SAMPLE_RECIPES") private readonly items: Recipe[]) { console.log("RecipeService created!"); this.autoIncrementValue = items.length; } async getAll() { return this.items; } async getOne(id: string) { return this.items.find(it => it.id === id); } async add(data: RecipeInput) { const recipe = this.createRecipe(data); this.items.push(recipe); return recipe; } private createRecipe(recipeData: Partial): Recipe { const recipe = Object.assign(new Recipe(), { ...recipeData, id: this.getId(), }); return recipe; } private getId(): string { this.autoIncrementValue += 1; return this.autoIncrementValue.toString(); } } ================================================ FILE: examples/using-scoped-container/recipe/recipe.type.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; @ObjectType() export class Recipe { @Field(_type => ID) id!: string; @Field() title!: string; @Field({ nullable: true }) description?: string; @Field(_type => [String]) ingredients!: string[]; } ================================================ FILE: examples/using-scoped-container/schema.graphql ================================================ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Mutation { addRecipe(recipe: RecipeInput!): Recipe! } type Query { recipe(recipeId: String!): Recipe recipes: [Recipe!]! } type Recipe { description: String id: ID! ingredients: [String!]! title: String! } input RecipeInput { description: String ingredients: [String!]! title: String! } ================================================ FILE: jest.config.cts ================================================ import { pathsToModuleNameMapper, JestConfigWithTsJest } from "ts-jest"; import tsconfig from "./tsconfig.json"; export default { preset: "ts-jest", verbose: false, rootDir: "./", roots: ["/src", "/tests"], testEnvironment: "node", collectCoverage: false, collectCoverageFrom: [ "/src/**/*.ts", "!/src/**/*.d.ts", "!/src/shim.ts", ], moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: "", }), transform: { "^.+\\.tsx?$": ["ts-jest", { tsconfig: "./tests/tsconfig.json" }], }, testMatch: ["**/functional/**/*.ts"], moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], coverageDirectory: "/coverage", } satisfies JestConfigWithTsJest; ================================================ FILE: package.json ================================================ { "name": "type-graphql", "version": "2.0.0-rc.4", "private": false, "description": "Create GraphQL schema and resolvers with TypeScript, using classes and decorators!", "keywords": [ "typescript", "graphql", "schema", "resolvers", "api", "decorators", "controllers", "apollo" ], "homepage": "https://typegraphql.com", "bugs": { "url": "https://github.com/MichalLytek/type-graphql/issues" }, "repository": { "type": "git", "url": "git+https://github.com/MichalLytek/type-graphql.git" }, "funding": [ { "type": "github", "url": "https://github.com/sponsors/TypeGraphQL" }, { "type": "opencollective", "url": "https://opencollective.com/typegraphql" } ], "license": "MIT", "author": "Michał Lytek (https://github.com/MichalLytek)", "exports": { ".": { "require": "./build/cjs/index.js", "import": "./build/esm/index.js", "types": "./build/typings/index.d.ts" }, "./shim": { "require": "./build/cjs/shim.js", "import": "./build/esm/shim.js", "types": "./build/typings/shim.ts" } }, "main": "./build/cjs/index.js", "module": "./build/esm/index.js", "browser": "./build/cjs/shim.js", "types": "./build/typings/index.d.ts", "files": [ "./build" ], "scripts": { "prebuild": "npm run clean:build && npm run check:version", "build": "npx tsc --build ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.typings.json", "postbuild": "npx shx rm ./build/typings/shim.d.ts && npx shx cp ./src/shim.ts ./build/typings && npx ts-node ./scripts/package.json.ts", "prebuild:benchmarks": "npm run clean:build:benchmarks", "build:benchmarks": "npx tsc --build ./benchmarks/tsconfig.json", "check": "npx npm-run-all --npm-path npm \"check:*\"", "check:benchmarks": "npx tsc --project ./benchmarks/tsconfig.json --noEmit", "check:examples": "npx tsc --project ./examples/tsconfig.json --noEmit", "check:format": "npx prettier --check .", "check:lint": "npx eslint .", "check:markdown": "npx markdownlint \"**/*.md\"", "check:script": "npx shellcheck ./.husky/pre-commit", "check:scripts": "npx tsc --project ./scripts/tsconfig.json --noEmit", "check:spell": "npx cspell lint --config cspell.json --no-progress --show-context \"**\"", "check:type": "npx npm-run-all --npm-path npm \"check:type:*\"", "check:type:cjs": "npx tsc --project ./tsconfig.cjs.json --noEmit", "check:type:esm": "npx tsc --project ./tsconfig.esm.json --noEmit", "check:type:tests": "npx tsc --project ./tests/tsconfig.json --noEmit", "check:type:typings": "npx tsc --project ./tsconfig.typings.json --noEmit --emitDeclarationOnly false", "check:version": "npx ts-node ./scripts/version.ts", "clean": "npx npm-run-all --npm-path npm \"clean:*\"", "clean:build": "npx shx rm -rf ./build", "clean:build:benchmarks": "npx shx rm -rf ./benchmarks/build", "clean:coverage": "npx shx rm -rf ./coverage", "prepublishOnly": "npm run build && npm run gen:readme", "pretest": "npm run clean:coverage", "test": "npx jest --verbose --coverage", "test:watch": "npx jest --watch", "pretest:ci": "npm run clean:coverage", "test:ci": "npx jest --verbose --coverage --ci --forceExit --runInBand", "release": "npm version --message \"release: %s\"", "version": "npm run --prefix ./website new-release --release=$npm_package_version && git add -A .", "docs": "npm run start --prefix website", "fix": "npx npm-run-all --npm-path npm \"fix:*\"", "fix:format": "npx prettier --write .", "fix:lint": "npx eslint --fix .", "fix:markdown": "npx markdownlint --fix \"**/*.md\"", "gen:docs": "npx ts-node ./scripts/markdown.ts --on docs", "gen:readme": "npx ts-node ./scripts/markdown.ts --on readme", "gen:sponsorkit": "npx sponsorkit --width=320 --dir=./images --name=github-sponsors", "postgen:sponsorkit": "npx shx cp ./images/github-sponsors.svg ./website/static/img/github-sponsors.svg", "prepare": "npx ts-patch install -s && npx husky install" }, "peerDependencies": { "class-validator": ">=0.14.3", "graphql": "^16.12.0", "graphql-scalars": "^1.25.0" }, "peerDependenciesMeta": { "class-validator": { "optional": true } }, "dependencies": { "@graphql-yoga/subscription": "^5.0.5", "@types/node": "*", "@types/semver": "^7.7.1", "graphql-query-complexity": "^1.1.0", "semver": "^7.7.3", "tslib": "^2.8.1" }, "devDependencies": { "@apollo/cache-control-types": "^1.0.3", "@apollo/gateway": "^2.12.2", "@apollo/server": "^5.2.0", "@apollo/server-plugin-response-cache": "^5.0.0", "@apollo/subgraph": "^2.12.2", "@cspell/dict-node": "^5.0.8", "@cspell/dict-npm": "^5.2.27", "@cspell/dict-shell": "^1.1.2", "@cspell/dict-typescript": "^3.2.3", "@cspell/eslint-plugin": "^9.4.0", "@graphql-tools/schema": "^10.0.30", "@graphql-tools/utils": "^10.11.0", "@graphql-yoga/redis-event-target": "^3.0.3", "@mikro-orm/core": "^6.6.2", "@mikro-orm/postgresql": "^6.6.2", "@typegoose/typegoose": "^13.0.0", "@types/jest": "^30.0.0", "@types/lodash.merge": "^4.6.9", "@types/node": "^25.0.3", "@types/shelljs": "^0.10.0", "@types/yargs": "^17.0.35", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "class-validator": "^0.14.3", "cspell": "^9.4.0", "dotenv": "^17.2.3", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.7.0", "eslint-plugin-tsdoc": "^0.3.0", "expect": "^30.2.0", "glob": "^13.0.0", "graphql": "^16.12.0", "graphql-scalars": "^1.25.0", "graphql-tag": "^2.12.6", "graphql-yoga": "^5.18.0", "husky": "^9.1.7", "ioredis": "^5.8.2", "jest": "^30.2.0", "joiful": "^3.0.2", "lint-staged": "^16.2.7", "lodash.merge": "^4.6.2", "markdownlint": "^0.40.0", "markdownlint-cli": "^0.47.0", "mongoose": "^9.0.2", "npm-run-all": "^4.1.5", "pg": "^8.16.3", "prettier": "^3.7.4", "prettier-plugin-sh": "^0.18.0", "reflect-metadata": "0.1.13", "shellcheck": "^4.1.0", "shelljs": "^0.10.0", "shx": "^0.4.0", "sponsorkit": "^17.0.0", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", "ts-patch": "^3.3.0", "tsconfig-paths": "^4.2.0", "tsyringe": "^4.10.0", "typedi": "^0.10.0", "typeorm": "^0.3.28", "typescript": "^5.5.4", "typescript-transform-paths": "^3.5.5", "typescript-transformer-esm": "^1.1.0", "yargs": "^18.0.0" }, "engines": { "node": ">= 20.11.1" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: scripts/.eslintrc ================================================ { "rules": { "import/no-extraneous-dependencies": [ "error", { "devDependencies": true } ] } } ================================================ FILE: scripts/markdown.ts ================================================ #!/usr/bin/env ts-node import fs from "node:fs"; import path from "node:path"; import * as glob from "glob"; import { hideBin } from "yargs/helpers"; import yargs from "yargs/yargs"; function toUrlPath( filePath: string, relativePath: string, rootPath: string, basePath: string, ): string { return path.resolve(path.dirname(filePath), relativePath).replace(rootPath, basePath); } function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const enum Analyze { DOCS = "docs", README = "readme", } const gitHubUrl = `https://github.com/MichalLytek/type-graphql/tree`; const gitHubUrlRaw = `https://raw.githubusercontent.com/MichalLytek/type-graphql`; const rootPath = path.resolve(`${__dirname}/..`); const argv = yargs(hideBin(process.argv)) .strict() .env("TYPE_GRAPHQL") .usage("Markdown\n\nUsage: $0 --ref [options]") .example([ ["$0 --ref v1.2.3", "Use 'v1.2.3' as Git reference"], ["TYPE_GRAPHQL_REF=v1.2.3 $0", "Use 'v1.2.3' as Git reference"], [ `$0 --ref v1.2.3 --on ${Analyze.README}`, `Use 'v1.2.3' as Git reference and analyze '${Analyze.README}'`, ], [ `$0 --ref v1.2.3 --on ${Analyze.README} ${Analyze.DOCS}`, `Use 'v1.2.3' as Git reference and analyze '${Analyze.README}' and '${Analyze.DOCS}'`, ], ]) .option("ref", { type: "string", demandOption: true, description: "Git reference", }) .option("on", { type: "array", default: [] as Analyze[], requiresArg: true, choices: [Analyze.DOCS, Analyze.README], description: "Analysis to be performed", }) .check(({ ref, on }) => { if ( !/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(alpha|beta|rc)\.(0|[1-9][0-9]*))?$/.test( ref, ) ) { throw new Error(`Invalid Git reference '${ref}'`); } if (on.length === 0) { throw new Error(`Empty analysis`); } return true; }) .parseSync(); const gitHubUrlRef = `${gitHubUrl}/${argv.ref}`; const gitHubUrlRawRef = `${gitHubUrlRaw}/${argv.ref}`; // README.md if (argv.on.includes(Analyze.README)) { const readmeFile = path.resolve(`${rootPath}/README.md`); const readme = fs .readFileSync(readmeFile, { encoding: "utf8", flag: "r" }) .replace( /!\[([^\]]*)\]\(((?:\.\/|\.\.\/).*?)\)/gm, // ![altText](relativePath) (_, altText, relativePath) => `![${altText}](${toUrlPath(readmeFile, relativePath, rootPath, gitHubUrlRawRef)})`, ) .replace( /]*)\ssrc="((?:\.\/|\.\.\/)[^">]+)"/gm, // ` `[${linkText}](${toUrlPath(readmeFile, relativePath, rootPath, gitHubUrlRef)})`, ) .replace( /]*)\shref="((?:\.\/|\.\.\/)[^">]+)"/gm, // ` `[${linkText}](${gitHubUrlRef}/${relativePath})`, ) .replace( new RegExp(`]*)\\shref="${gitHubUrlRefMasterEscaped}\\/?([^">]+)"`, "gm"), // ` & { raw: { include: string[] } }; function readTsConfig(fileName: string): TsConfig { const config = typescript.readConfigFile(fileName, typescript.sys.readFile); expect(config.config).toBeDefined(); expect(config.error).toBeUndefined(); const configJson = typescript.parseJsonConfigFileContent( config.config, typescript.sys, path.dirname(fileName), ); expect(configJson.options.outDir).toBeDefined(); expect(configJson.raw.include).toBeDefined(); expect(Array.isArray(configJson.raw.include)).toBeTruthy(); expect(configJson.raw.include.length).toBeGreaterThanOrEqual(1); return configJson as TsConfig; } function writePackageJson(fileName: string, fileContent: string | NodeJS.ArrayBufferView): void { const fileBasename = path.basename(fileName); const fileDirname = path.dirname(fileName); expect(fileBasename).toBe("package.json"); if (!fs.existsSync(fileDirname)) { throw new Error(`Directory '${fileDirname}' does not exists`); } if (fs.existsSync(fileName)) { throw new Error(`File '${fileName}' already exists`); } fs.writeFileSync(fileName, fileContent, { encoding: "utf8", flag: "w", }); } const packageJson = JSON.stringify({ type: "module" }); const tsconfigRoot = readTsConfig(path.resolve(__dirname, "../tsconfig.esm.json")); const packageJsonRoot = path.resolve(`${tsconfigRoot.options.outDir}/package.json`); writePackageJson(packageJsonRoot, packageJson); ================================================ FILE: scripts/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "noEmit": true }, "include": [".", "../src"] } ================================================ FILE: scripts/version.ts ================================================ #!/usr/bin/env ts-node import packageJson from "../package.json"; import { graphQLPeerDependencyVersion } from "../src/utils/graphql-version"; if (graphQLPeerDependencyVersion !== packageJson.peerDependencies.graphql) { throw new Error( `GraphQL peer dependency version (${graphQLPeerDependencyVersion}) != package.json.peerDependencies.graphql (${packageJson.peerDependencies.graphql})`, ); } ================================================ FILE: sponsorkit.config.mts ================================================ import { BadgePreset, defineConfig } from "sponsorkit"; const presets = { past: { avatar: { size: 20, }, boxWidth: 25, boxHeight: 25, container: { sidePadding: 30, }, }, backers: { avatar: { size: 30, }, boxWidth: 35, boxHeight: 35, container: { sidePadding: 30, }, }, members: { avatar: { size: 45, }, boxWidth: 55, boxHeight: 55, container: { sidePadding: 30, }, }, bronze: { avatar: { size: 75, }, boxWidth: 90, boxHeight: 100, container: { sidePadding: 20, }, name: { maxLength: 10, }, }, silver: { avatar: { size: 100, }, boxWidth: 105, boxHeight: 125, container: { sidePadding: 20, }, name: { maxLength: 16, }, }, gold: { avatar: { size: 150, }, boxWidth: 175, boxHeight: 175, container: { sidePadding: 20, }, name: { maxLength: 20, }, }, } satisfies Record; export default defineConfig({ includePastSponsors: true, formats: ["svg"], github: { login: "TypeGraphQL", type: "organization", }, // opencollective: { // type: "collective", // slug: "typegraphql", // }, tiers: [ { title: "Past Sponsors ⏳", monthlyDollars: -1, preset: presets.past, }, { title: "Backers ☕", preset: presets.backers, }, { title: "Members 💪", monthlyDollars: 15, preset: presets.members, }, { title: "Bronze Sponsors 🥉", monthlyDollars: 50, preset: presets.bronze, }, { title: "Silver Sponsors 🥈", monthlyDollars: 100, preset: presets.silver, }, { title: "Gold Sponsors 🏆", monthlyDollars: 300, preset: presets.gold, }, ], }); ================================================ FILE: src/@types/Reflect.d.ts ================================================ // Copied from 'reflect-metadata' (https://github.com/rbuckton/reflect-metadata/blob/master/index.d.ts) /*! ***************************************************************************** Copyright (C) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ declare namespace Reflect { function getMetadata(metadataKey: any, target: object): any; /** * Gets the metadata value for the provided metadata key on the target object or its prototype chain. * @param metadataKey - A key used to store and retrieve metadata. * @param target - The target object on which the metadata is defined. * @param propertyKey - The property key for the target. * @returns The metadata value for the metadata key if found; otherwise, `undefined`. * @example * * class Example \{ * // property declarations are not part of ES6, though they are valid in TypeScript: * // static staticProperty; * // property; * * static staticMethod(p) \{ \} * method(p) \{ \} * \} * * // property (on constructor) * result = Reflect.getMetadata("custom:annotation", Example, "staticProperty"); * * // property (on prototype) * result = Reflect.getMetadata("custom:annotation", Example.prototype, "property"); * * // method (on constructor) * result = Reflect.getMetadata("custom:annotation", Example, "staticMethod"); * * // method (on prototype) * result = Reflect.getMetadata("custom:annotation", Example.prototype, "method"); * */ function getMetadata(metadataKey: any, target: object, propertyKey: string | symbol): any; } ================================================ FILE: src/decorators/Arg.ts ================================================ import { getTypeDecoratorParams } from "@/helpers/decorators"; import { getParamInfo } from "@/helpers/params"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ParameterDecorator } from "@/typings"; import { type DecoratorTypeOptions, type DeprecationOptions, type DescriptionOptions, type ReturnTypeFunc, type ValidateOptions, } from "./types"; export type ArgOptions = DecoratorTypeOptions & DescriptionOptions & ValidateOptions & DeprecationOptions; export function Arg(name: string, options?: ArgOptions): ParameterDecorator; export function Arg( name: string, returnTypeFunc: ReturnTypeFunc, options?: ArgOptions, ): ParameterDecorator; export function Arg( name: string, returnTypeFuncOrOptions?: ReturnTypeFunc | ArgOptions, maybeOptions?: ArgOptions, ): ParameterDecorator { return (prototype, propertyKey, parameterIndex) => { const { options, returnTypeFunc } = getTypeDecoratorParams( returnTypeFuncOrOptions, maybeOptions, ); getMetadataStorage().collectHandlerParamMetadata({ kind: "arg", name, description: options.description, deprecationReason: options.deprecationReason, ...getParamInfo({ prototype, propertyKey, parameterIndex, returnTypeFunc, options, argName: name, }), }); }; } ================================================ FILE: src/decorators/Args.ts ================================================ import { getTypeDecoratorParams } from "@/helpers/decorators"; import { getParamInfo } from "@/helpers/params"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ParameterDecorator } from "@/typings"; import { type ReturnTypeFunc, type ValidateOptions } from "./types"; export function Args(): ParameterDecorator; export function Args(options: ValidateOptions): ParameterDecorator; export function Args( paramTypeFunction: ReturnTypeFunc, options?: ValidateOptions, ): ParameterDecorator; export function Args( paramTypeFnOrOptions?: ReturnTypeFunc | ValidateOptions, maybeOptions?: ValidateOptions, ): ParameterDecorator { const { options, returnTypeFunc } = getTypeDecoratorParams(paramTypeFnOrOptions, maybeOptions); return (prototype, propertyKey, parameterIndex) => { getMetadataStorage().collectHandlerParamMetadata({ kind: "args", ...getParamInfo({ prototype, propertyKey, parameterIndex, returnTypeFunc, options }), }); }; } ================================================ FILE: src/decorators/ArgsType.ts ================================================ import { getMetadataStorage } from "@/metadata/getMetadataStorage"; export function ArgsType(): ClassDecorator { return target => { getMetadataStorage().collectArgsMetadata({ name: target.name, target, }); }; } ================================================ FILE: src/decorators/Authorized.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getArrayFromOverloadedRest } from "@/helpers/decorators"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type MethodPropClassDecorator } from "./types"; export function Authorized(): MethodPropClassDecorator; export function Authorized(roles: readonly RoleType[]): MethodPropClassDecorator; export function Authorized( ...roles: readonly RoleType[] ): MethodPropClassDecorator; export function Authorized( ...rolesOrRolesArray: Array ): MethodPropClassDecorator { const roles = getArrayFromOverloadedRest(rolesOrRolesArray); return ( target: Function | Object, propertyKey?: string | symbol, _descriptor?: TypedPropertyDescriptor, ) => { if (propertyKey == null) { getMetadataStorage().collectAuthorizedResolverMetadata({ target: target as Function, roles, }); return; } if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } getMetadataStorage().collectAuthorizedFieldMetadata({ target: target.constructor, fieldName: propertyKey, roles, }); }; } ================================================ FILE: src/decorators/Ctx.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ParameterDecorator } from "@/typings"; export function Ctx(propertyName?: string): ParameterDecorator { return (prototype, propertyKey, parameterIndex) => { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } getMetadataStorage().collectHandlerParamMetadata({ kind: "context", target: prototype.constructor, methodName: propertyKey, index: parameterIndex, propertyName, }); }; } ================================================ FILE: src/decorators/Directive.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type MethodAndPropDecorator } from "./types"; export function Directive( sdl: string, ): MethodAndPropDecorator & ClassDecorator & ParameterDecorator; export function Directive( nameOrDefinition: string, ): MethodDecorator | PropertyDecorator | ClassDecorator | ParameterDecorator { return ( targetOrPrototype: Object, propertyKey: string | symbol | undefined, parameterIndexOrDescriptor: number | TypedPropertyDescriptor, ) => { const directive = { nameOrDefinition, args: {} }; if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } if (propertyKey) { if (typeof parameterIndexOrDescriptor === "number") { getMetadataStorage().collectDirectiveArgumentMetadata({ target: targetOrPrototype.constructor, fieldName: propertyKey, parameterIndex: parameterIndexOrDescriptor, directive, }); } else { getMetadataStorage().collectDirectiveFieldMetadata({ target: targetOrPrototype.constructor, fieldName: propertyKey, directive, }); } } else { getMetadataStorage().collectDirectiveClassMetadata({ target: targetOrPrototype as Function, directive, }); } }; } ================================================ FILE: src/decorators/Extensions.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { type ExtensionsMetadata } from "@/metadata/definitions"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type MethodAndPropDecorator } from "./types"; export function Extensions(extensions: ExtensionsMetadata): MethodAndPropDecorator & ClassDecorator; export function Extensions( extensions: ExtensionsMetadata, ): MethodDecorator | PropertyDecorator | ClassDecorator { return (targetOrPrototype, propertyKey, _descriptor) => { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } if (propertyKey) { getMetadataStorage().collectExtensionsFieldMetadata({ target: targetOrPrototype.constructor, fieldName: propertyKey, extensions, }); } else { getMetadataStorage().collectExtensionsClassMetadata({ target: targetOrPrototype as Function, extensions, }); } }; } ================================================ FILE: src/decorators/Field.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getTypeDecoratorParams } from "@/helpers/decorators"; import { findType } from "@/helpers/findType"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type AdvancedOptions, type MethodAndPropDecorator, type ReturnTypeFunc } from "./types"; export type FieldOptions = AdvancedOptions & { /** Set to `true` to disable auth and all middlewares stack for this field resolver */ simple?: boolean; }; export function Field(): MethodAndPropDecorator; export function Field(options: FieldOptions): MethodAndPropDecorator; export function Field( returnTypeFunction?: ReturnTypeFunc, options?: FieldOptions, ): MethodAndPropDecorator; export function Field( returnTypeFuncOrOptions?: ReturnTypeFunc | FieldOptions, maybeOptions?: FieldOptions, ): MethodDecorator | PropertyDecorator { return (prototype, propertyKey, descriptor) => { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } const { options, returnTypeFunc } = getTypeDecoratorParams( returnTypeFuncOrOptions, maybeOptions, ); const isResolver = Boolean(descriptor); const isResolverMethod = Boolean(descriptor && descriptor.value); const { getType, typeOptions } = findType({ metadataKey: isResolverMethod ? "design:returntype" : "design:type", prototype, propertyKey, returnTypeFunc, typeOptions: options, }); getMetadataStorage().collectClassFieldMetadata({ name: propertyKey, schemaName: options.name || propertyKey, getType, typeOptions, complexity: options.complexity, target: prototype.constructor, description: options.description, deprecationReason: options.deprecationReason, simple: options.simple, }); if (isResolver) { getMetadataStorage().collectFieldResolverMetadata({ kind: "internal", methodName: propertyKey, schemaName: options.name || propertyKey, target: prototype.constructor, complexity: options.complexity, }); } }; } ================================================ FILE: src/decorators/FieldResolver.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getTypeDecoratorParams } from "@/helpers/decorators"; import { findType } from "@/helpers/findType"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type AdvancedOptions, type ReturnTypeFunc, type TypeOptions, type TypeValueThunk, } from "./types"; export function FieldResolver(): MethodDecorator; export function FieldResolver(options: AdvancedOptions): MethodDecorator; export function FieldResolver( returnTypeFunction?: ReturnTypeFunc, options?: AdvancedOptions, ): MethodDecorator; export function FieldResolver( returnTypeFuncOrOptions?: ReturnTypeFunc | AdvancedOptions, maybeOptions?: AdvancedOptions, ): MethodDecorator { return (prototype, propertyKey) => { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } let getType: TypeValueThunk | undefined; let typeOptions: TypeOptions | undefined; const { options, returnTypeFunc } = getTypeDecoratorParams( returnTypeFuncOrOptions, maybeOptions, ); // try to get return type info try { const typeInfo = findType({ metadataKey: "design:returntype", prototype, propertyKey, returnTypeFunc, typeOptions: options, }); typeOptions = typeInfo.typeOptions; getType = typeInfo.getType; } catch { /* empty */ } getMetadataStorage().collectFieldResolverMetadata({ kind: "external", methodName: propertyKey, schemaName: options.name || propertyKey, target: prototype.constructor, getType, typeOptions, complexity: options.complexity, description: options.description, deprecationReason: options.deprecationReason, }); }; } ================================================ FILE: src/decorators/Info.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ParameterDecorator } from "@/typings"; export function Info(): ParameterDecorator { return (prototype, propertyKey, parameterIndex) => { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } getMetadataStorage().collectHandlerParamMetadata({ kind: "info", target: prototype.constructor, methodName: propertyKey, index: parameterIndex, }); }; } ================================================ FILE: src/decorators/InputType.ts ================================================ import { getNameDecoratorParams } from "@/helpers/decorators"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type DescriptionOptions } from "./types"; export type InputTypeOptions = DescriptionOptions; export function InputType(): ClassDecorator; export function InputType(options: InputTypeOptions): ClassDecorator; export function InputType(name: string, options?: InputTypeOptions): ClassDecorator; export function InputType( nameOrOptions?: string | InputTypeOptions, maybeOptions?: InputTypeOptions, ): ClassDecorator { const { name, options } = getNameDecoratorParams(nameOrOptions, maybeOptions); return target => { getMetadataStorage().collectInputMetadata({ name: name || target.name, target, description: options.description, }); }; } ================================================ FILE: src/decorators/InterfaceType.ts ================================================ import { getNameDecoratorParams } from "@/helpers/decorators"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type DescriptionOptions, type ImplementsClassOptions, type ResolveTypeOptions, } from "./types"; export type InterfaceTypeOptions = DescriptionOptions & ResolveTypeOptions & ImplementsClassOptions & { /** * Set to false to prevent emitting in schema all object types * that implements this interface type. */ autoRegisterImplementations?: boolean; }; export function InterfaceType(): ClassDecorator; export function InterfaceType(options: InterfaceTypeOptions): ClassDecorator; export function InterfaceType(name: string, options?: InterfaceTypeOptions): ClassDecorator; export function InterfaceType( nameOrOptions?: string | InterfaceTypeOptions, maybeOptions?: InterfaceTypeOptions, ): ClassDecorator { const { name, options } = getNameDecoratorParams(nameOrOptions, maybeOptions); const interfaceClasses = options.implements && ([] as Function[]).concat(options.implements); return target => { getMetadataStorage().collectInterfaceMetadata({ name: name || target.name, target, interfaceClasses, autoRegisteringDisabled: options.autoRegisterImplementations === false, ...options, }); }; } ================================================ FILE: src/decorators/Mutation.ts ================================================ import { getTypeDecoratorParams } from "@/helpers/decorators"; import { getResolverMetadata } from "@/helpers/resolver-metadata"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type AdvancedOptions, type ReturnTypeFunc } from "./types"; export function Mutation(): MethodDecorator; export function Mutation(options: AdvancedOptions): MethodDecorator; export function Mutation( returnTypeFunc: ReturnTypeFunc, options?: AdvancedOptions, ): MethodDecorator; export function Mutation( returnTypeFuncOrOptions?: ReturnTypeFunc | AdvancedOptions, maybeOptions?: AdvancedOptions, ): MethodDecorator { const { options, returnTypeFunc } = getTypeDecoratorParams(returnTypeFuncOrOptions, maybeOptions); return (prototype, methodName) => { const metadata = getResolverMetadata(prototype, methodName, returnTypeFunc, options); getMetadataStorage().collectMutationHandlerMetadata(metadata); }; } ================================================ FILE: src/decorators/ObjectType.ts ================================================ import { getNameDecoratorParams } from "@/helpers/decorators"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type DescriptionOptions, type ImplementsClassOptions } from "./types"; export type ObjectTypeOptions = DescriptionOptions & ImplementsClassOptions & { /** Set to `true` to disable auth and all middlewares stack for all this Object Type fields resolvers */ simpleResolvers?: boolean; }; export function ObjectType(): ClassDecorator; export function ObjectType(options: ObjectTypeOptions): ClassDecorator; export function ObjectType(name: string, options?: ObjectTypeOptions): ClassDecorator; export function ObjectType( nameOrOptions?: string | ObjectTypeOptions, maybeOptions?: ObjectTypeOptions, ): ClassDecorator { const { name, options } = getNameDecoratorParams(nameOrOptions, maybeOptions); const interfaceClasses = options.implements && ([] as Function[]).concat(options.implements); return target => { getMetadataStorage().collectObjectMetadata({ name: name || target.name, target, description: options.description, interfaceClasses, simpleResolvers: options.simpleResolvers, }); }; } ================================================ FILE: src/decorators/Query.ts ================================================ import { getTypeDecoratorParams } from "@/helpers/decorators"; import { getResolverMetadata } from "@/helpers/resolver-metadata"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type AdvancedOptions, type ReturnTypeFunc } from "./types"; export function Query(): MethodDecorator; export function Query(options: AdvancedOptions): MethodDecorator; export function Query(returnTypeFunc: ReturnTypeFunc, options?: AdvancedOptions): MethodDecorator; export function Query( returnTypeFuncOrOptions?: ReturnTypeFunc | AdvancedOptions, maybeOptions?: AdvancedOptions, ): MethodDecorator { const { options, returnTypeFunc } = getTypeDecoratorParams(returnTypeFuncOrOptions, maybeOptions); return (prototype, methodName) => { const metadata = getResolverMetadata(prototype, methodName, returnTypeFunc, options); getMetadataStorage().collectQueryHandlerMetadata(metadata); }; } ================================================ FILE: src/decorators/Resolver.ts ================================================ import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ClassType } from "@/typings"; import { type ClassTypeResolver } from "./types"; export function Resolver(): ClassDecorator; export function Resolver(typeFunc: ClassTypeResolver): ClassDecorator; export function Resolver(objectType: ClassType): ClassDecorator; export function Resolver(objectTypeOrTypeFunc?: Function): ClassDecorator { return target => { // eslint-disable-next-line no-nested-ternary const getObjectType = objectTypeOrTypeFunc ? objectTypeOrTypeFunc.prototype ? () => objectTypeOrTypeFunc as ClassType : (objectTypeOrTypeFunc as ClassTypeResolver) : () => { throw new Error( `No provided object type in '@Resolver' decorator for class '${target.name}!'`, ); }; getMetadataStorage().collectResolverClassMetadata({ target, getObjectType, }); }; } ================================================ FILE: src/decorators/Root.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { findType } from "@/helpers/findType"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ParameterDecorator } from "@/typings"; import { type TypeValueThunk } from "./types"; export function Root(propertyName?: string): ParameterDecorator { return (prototype, propertyKey, parameterIndex) => { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } let getType: TypeValueThunk | undefined; try { const typeInfo = findType({ metadataKey: "design:paramtypes", prototype, propertyKey, parameterIndex, }); getType = typeInfo.getType; } catch { /* empty */ } getMetadataStorage().collectHandlerParamMetadata({ kind: "root", target: prototype.constructor, methodName: propertyKey, index: parameterIndex, propertyName, getType, }); }; } ================================================ FILE: src/decorators/Subscription.ts ================================================ import { MissingSubscriptionTopicsError } from "@/errors"; import { getTypeDecoratorParams } from "@/helpers/decorators"; import { getResolverMetadata } from "@/helpers/resolver-metadata"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type MergeExclusive } from "@/typings"; import { type AdvancedOptions, type ReturnTypeFunc, type SubscriptionFilterFunc, type SubscriptionSubscribeFunc, type SubscriptionTopicIdFunc, type SubscriptionTopicsFunc, } from "./types"; interface PubSubOptions { topics: string | string[] | SubscriptionTopicsFunc; topicId?: SubscriptionTopicIdFunc | undefined; filter?: SubscriptionFilterFunc; } interface SubscribeOptions { subscribe: SubscriptionSubscribeFunc; } export type SubscriptionOptions = AdvancedOptions & MergeExclusive; export function Subscription(options: SubscriptionOptions): MethodDecorator; export function Subscription( returnTypeFunc: ReturnTypeFunc, options: SubscriptionOptions, ): MethodDecorator; export function Subscription( returnTypeFuncOrOptions: ReturnTypeFunc | SubscriptionOptions, maybeOptions?: SubscriptionOptions, ): MethodDecorator { const params = getTypeDecoratorParams(returnTypeFuncOrOptions, maybeOptions); const options = params.options as SubscriptionOptions; return (prototype, methodName) => { const metadata = getResolverMetadata(prototype, methodName, params.returnTypeFunc, options); if (Array.isArray(options.topics) && options.topics.length === 0) { throw new MissingSubscriptionTopicsError(metadata.target, metadata.methodName); } getMetadataStorage().collectSubscriptionHandlerMetadata({ ...metadata, topics: options.topics, topicId: options.topicId, filter: options.filter, subscribe: options.subscribe, }); }; } ================================================ FILE: src/decorators/UseMiddleware.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getArrayFromOverloadedRest } from "@/helpers/decorators"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type Middleware } from "@/typings/middleware"; import { type MethodPropClassDecorator } from "./types"; export function UseMiddleware(middlewares: Array>): MethodPropClassDecorator; export function UseMiddleware(...middlewares: Array>): MethodPropClassDecorator; export function UseMiddleware( ...middlewaresOrMiddlewareArray: Array | Array>> ): MethodPropClassDecorator { const middlewares = getArrayFromOverloadedRest(middlewaresOrMiddlewareArray); return ( target: Function | Object, propertyKey?: string | symbol, _descriptor?: TypedPropertyDescriptor, ) => { if (propertyKey == null) { getMetadataStorage().collectResolverMiddlewareMetadata({ target: target as Function, middlewares, }); return; } if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } getMetadataStorage().collectMiddlewareMetadata({ target: target.constructor, fieldName: propertyKey, middlewares, }); }; } ================================================ FILE: src/decorators/createMethodMiddlewareDecorator.ts ================================================ import { type MiddlewareFn } from "@/typings/middleware"; import { UseMiddleware } from "./UseMiddleware"; export function createMethodMiddlewareDecorator( resolver: MiddlewareFn, ): MethodDecorator { return UseMiddleware(resolver); } ================================================ FILE: src/decorators/createParameterDecorator.ts ================================================ import { SymbolKeysNotSupportedError } from "@/errors"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ParameterDecorator, type ResolverData } from "@/typings"; import { type ArgOptions } from "./Arg"; import { type ReturnTypeFunc } from "./types"; import { getParamInfo } from "../helpers/params"; import { type CustomParamOptions } from "../metadata/definitions"; export interface CustomParameterOptions { arg?: { name: string; typeFunc: ReturnTypeFunc; options?: ArgOptions; }; } export type ParameterResolver = ( resolverData: ResolverData, ) => any; export function createParameterDecorator( resolver: ParameterResolver, paramOptions: CustomParameterOptions = {}, ): ParameterDecorator { return (prototype, propertyKey, parameterIndex) => { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } const options: CustomParamOptions = {}; if (paramOptions.arg) { options.arg = { kind: "arg", name: paramOptions.arg.name, description: paramOptions.arg.options?.description, deprecationReason: paramOptions.arg.options?.deprecationReason, ...getParamInfo({ prototype, propertyKey, parameterIndex, returnTypeFunc: paramOptions.arg.typeFunc, options: paramOptions.arg.options, argName: paramOptions.arg.name, }), }; } getMetadataStorage().collectHandlerParamMetadata({ kind: "custom", target: prototype.constructor, methodName: propertyKey, index: parameterIndex, resolver, options, }); }; } ================================================ FILE: src/decorators/createResolverClassMiddlewareDecorator.ts ================================================ import { type MiddlewareFn } from "@/typings"; import { UseMiddleware } from "./UseMiddleware"; export function createResolverClassMiddlewareDecorator( resolver: MiddlewareFn, ): ClassDecorator { return UseMiddleware(resolver); } ================================================ FILE: src/decorators/enums.ts ================================================ import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type EnumConfig } from "./types"; export function registerEnumType( enumObj: TEnum, enumConfig: EnumConfig, ) { getMetadataStorage().collectEnumMetadata({ enumObj, name: enumConfig.name, description: enumConfig.description, valuesConfig: enumConfig.valuesConfig || {}, }); } ================================================ FILE: src/decorators/index.ts ================================================ export type { ArgOptions } from "./Arg"; export { Arg } from "./Arg"; export { Args } from "./Args"; export { ArgsType } from "./ArgsType"; export { Authorized } from "./Authorized"; export { createParameterDecorator } from "./createParameterDecorator"; export { createMethodMiddlewareDecorator } from "./createMethodMiddlewareDecorator"; export { createResolverClassMiddlewareDecorator } from "./createResolverClassMiddlewareDecorator"; export { Ctx } from "./Ctx"; export { Directive } from "./Directive"; export { Extensions } from "./Extensions"; export { registerEnumType } from "./enums"; export type { FieldOptions } from "./Field"; export { Field } from "./Field"; export { FieldResolver } from "./FieldResolver"; export { Info } from "./Info"; export type { InputTypeOptions } from "./InputType"; export { InputType } from "./InputType"; export type { InterfaceTypeOptions } from "./InterfaceType"; export { InterfaceType } from "./InterfaceType"; export { Mutation } from "./Mutation"; export type { ObjectTypeOptions } from "./ObjectType"; export { ObjectType } from "./ObjectType"; export { Query } from "./Query"; export { Resolver } from "./Resolver"; export { Root } from "./Root"; export type { SubscriptionOptions } from "./Subscription"; export { Subscription } from "./Subscription"; export { createUnionType } from "./unions"; export { UseMiddleware } from "./UseMiddleware"; ================================================ FILE: src/decorators/types.ts ================================================ import { type GraphQLScalarType } from "graphql"; import { type ValidateSettings } from "@/schema/build-context"; import { type ClassType, type Complexity, type MaybePromise, type SubscribeResolverData, type SubscriptionHandlerData, type TypeResolver, type ValidatorFn, } from "@/typings"; export type RecursiveArray = Array | TValue>; export type TypeValue = ClassType | GraphQLScalarType | Function | object | symbol; export type ReturnTypeFuncValue = TypeValue | RecursiveArray; export type TypeValueThunk = (type?: void) => TypeValue; export type ClassTypeResolver = (of?: void) => ClassType | Function; export type ReturnTypeFunc = (returns?: void) => ReturnTypeFuncValue; export type SubscriptionFilterFunc = ( handlerData: SubscriptionHandlerData, ) => boolean | Promise; export type SubscriptionTopicsFunc = ( resolverData: SubscribeResolverData, ) => string | string[]; export type SubscriptionSubscribeFunc = ( resolverData: SubscribeResolverData, ) => MaybePromise>; export type SubscriptionTopicIdFunc = (resolverData: SubscribeResolverData) => any; export interface DecoratorTypeOptions { nullable?: boolean | NullableListOptions; defaultValue?: any; } export type NullableListOptions = "items" | "itemsAndList"; export interface TypeOptions extends DecoratorTypeOptions { array?: boolean; arrayDepth?: number; } export interface DescriptionOptions { description?: string; } export interface DeprecationOptions { deprecationReason?: string; } export interface ValidateOptions { validate?: ValidateSettings; validateFn?: ValidatorFn; } export interface ComplexityOptions { complexity?: Complexity; } export interface SchemaNameOptions { name?: string; } export interface ImplementsClassOptions { implements?: Function | Function[]; } export interface ResolveTypeOptions { resolveType?: TypeResolver; } export type BasicOptions = DecoratorTypeOptions & DescriptionOptions; export type AdvancedOptions = BasicOptions & DeprecationOptions & SchemaNameOptions & ComplexityOptions; export interface EnumConfig { name: string; description?: string; valuesConfig?: EnumValuesConfig; } export type EnumValuesConfig = Partial< Record >; export type MethodAndPropDecorator = PropertyDecorator & MethodDecorator; export type MethodPropClassDecorator = PropertyDecorator & MethodDecorator & ClassDecorator; ================================================ FILE: src/decorators/unions.ts ================================================ import { type UnionFromClasses } from "@/helpers/utils"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ClassType } from "@/typings"; import { type ResolveTypeOptions } from "./types"; export type UnionTypeConfig = { name: string; description?: string; types: () => TClassTypes; } & ResolveTypeOptions>; export function createUnionType( config: UnionTypeConfig, ): UnionFromClasses; export function createUnionType({ name, description, types, resolveType, }: UnionTypeConfig): any { const unionMetadataSymbol = getMetadataStorage().collectUnionMetadata({ name, description, getClassTypes: types, resolveType, }); return unionMetadataSymbol; } ================================================ FILE: src/errors/CannotDetermineGraphQLTypeError.ts ================================================ export class CannotDetermineGraphQLTypeError extends Error { constructor( typeKind: "input" | "output", typeName: string, propertyKey: string, parameterIndex?: number, argName?: string, ) { let errorMessage = `Cannot determine GraphQL ${typeKind} type for `; if (argName) { errorMessage += `argument named '${argName}' of `; } else if (parameterIndex !== undefined) { errorMessage += `parameter #${parameterIndex} of `; } errorMessage += `'${propertyKey}' of '${typeName}' class. ` + `Is the value, that is used as its TS type or explicit type, decorated with a proper ` + `decorator or is it a proper ${typeKind} value?`; super(errorMessage); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/ConflictingDefaultValuesError.ts ================================================ export class ConflictingDefaultValuesError extends Error { constructor( typeName: string, fieldName: string, defaultValueFromDecorator: unknown, defaultValueFromInitializer: unknown, ) { super( `The '${fieldName}' field of '${typeName}' has conflicting default values. ` + `Default value from decorator ('${defaultValueFromDecorator}') ` + `is not equal to the property initializer value ('${defaultValueFromInitializer}').`, ); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/GeneratingSchemaError.ts ================================================ import { type GraphQLError } from "graphql"; export class GeneratingSchemaError extends Error { details: readonly GraphQLError[]; constructor(details: readonly GraphQLError[]) { let errorMessage = "Some errors occurred while generating GraphQL schema:\n"; errorMessage += details.map(it => ` ${it.message}\n`); errorMessage += "Please check the `details` property of the error to get more detailed info."; super(errorMessage); Object.setPrototypeOf(this, new.target.prototype); this.details = details; } } ================================================ FILE: src/errors/InterfaceResolveTypeError.ts ================================================ import { type ClassMetadata } from "@/metadata/definitions"; export class InterfaceResolveTypeError extends Error { constructor(interfaceMetadata: ClassMetadata) { super( `Cannot resolve type for interface ${interfaceMetadata.name}! ` + `You need to return instance of object type class, not a plain object!`, ); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/InvalidDirectiveError.ts ================================================ export class InvalidDirectiveError extends Error { constructor(msg: string) { super(msg); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/MissingPubSubError.ts ================================================ export class MissingPubSubError extends Error { constructor() { super( "Looks like you've forgot to provide `pubSub` option in `buildSchema()`. " + "Subscriptions can't work without a proper PubSub system.", ); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/MissingSubscriptionTopicsError.ts ================================================ export class MissingSubscriptionTopicsError extends Error { constructor(target: Function, methodName: string) { super(`${target.name}#${methodName} subscription has no provided topics!`); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/NoExplicitTypeError.ts ================================================ export class NoExplicitTypeError extends Error { constructor(typeName: string, propertyKey: string, parameterIndex?: number, argName?: string) { let errorMessage = `Unable to infer GraphQL type from TypeScript reflection system. ` + `You need to provide explicit type for `; if (argName) { errorMessage += `argument named '${argName}' of `; } else if (parameterIndex !== undefined) { errorMessage += `parameter #${parameterIndex} of `; } errorMessage += `'${propertyKey}' of '${typeName}' class.`; super(errorMessage); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/ReflectMetadataMissingError.ts ================================================ export class ReflectMetadataMissingError extends Error { constructor() { super( "Looks like you've forgot to provide experimental metadata API polyfill. " + "Please read the installation instruction for more details.", ); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/SymbolKeysNotSupportedError.ts ================================================ export class SymbolKeysNotSupportedError extends Error { constructor() { super("Symbol keys are not supported yet!"); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/UnionResolveTypeError.ts ================================================ import { type UnionMetadata } from "@/metadata/definitions"; export class UnionResolveTypeError extends Error { constructor(unionMetadata: UnionMetadata) { super( `Cannot resolve type for union ${unionMetadata.name}! ` + `You need to return instance of object type class, not a plain object!`, ); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/UnmetGraphQLPeerDependencyError.ts ================================================ export class UnmetGraphQLPeerDependencyError extends Error { constructor(graphQLVersion: string, graphQLPeerDependencyVersion: string) { super( `Looks like you use an incorrect version of the 'graphql' package: "${graphQLVersion}". ` + `Please ensure that you have installed a version ` + `that meets TypeGraphQL's requirement: "${graphQLPeerDependencyVersion}".`, ); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/WrongNullableListOptionError.ts ================================================ import { type NullableListOptions } from "@/decorators/types"; export class WrongNullableListOptionError extends Error { constructor( targetName: string, propertyName: string, nullable: boolean | NullableListOptions | undefined, ) { super( `Wrong nullable option set for ${targetName}#${propertyName}. ` + `You cannot combine non-list type with nullable '${nullable}'.`, ); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/graphql/ArgumentValidationError.ts ================================================ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore `class-validator` might not be installed by user import { type ValidationError } from "class-validator"; import { GraphQLError } from "graphql"; export class ArgumentValidationError extends GraphQLError { override readonly extensions!: { code: "BAD_USER_INPUT"; validationErrors: ValidationError[]; [attributeName: string]: unknown; // GraphQLErrorExtensions }; constructor(validationErrors: ValidationError[]) { super("Argument Validation Error", { extensions: { code: "BAD_USER_INPUT", validationErrors, }, }); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/graphql/AuthenticationError.ts ================================================ import { GraphQLError } from "graphql"; export class AuthenticationError extends GraphQLError { override readonly extensions!: { code: "UNAUTHENTICATED"; [attributeName: string]: unknown; // GraphQLErrorExtensions }; constructor(message = "Access denied! You need to be authenticated to perform this action!") { super(message, { extensions: { code: "UNAUTHENTICATED", }, }); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/graphql/AuthorizationError.ts ================================================ import { GraphQLError } from "graphql"; export class AuthorizationError extends GraphQLError { override readonly extensions!: { code: "UNAUTHORIZED"; [attributeName: string]: unknown; // GraphQLErrorExtensions }; constructor(message = "Access denied! You don't have permission for this action!") { super(message, { extensions: { code: "UNAUTHORIZED", }, }); Object.setPrototypeOf(this, new.target.prototype); } } ================================================ FILE: src/errors/graphql/index.ts ================================================ export * from "./AuthenticationError"; export * from "./AuthorizationError"; export * from "./ArgumentValidationError"; ================================================ FILE: src/errors/index.ts ================================================ export * from "./graphql"; export * from "./CannotDetermineGraphQLTypeError"; export * from "./GeneratingSchemaError"; export * from "./ConflictingDefaultValuesError"; export * from "./InterfaceResolveTypeError"; export * from "./InvalidDirectiveError"; export * from "./MissingPubSubError"; export * from "./MissingSubscriptionTopicsError"; export * from "./NoExplicitTypeError"; export * from "./ReflectMetadataMissingError"; export * from "./SymbolKeysNotSupportedError"; export * from "./UnionResolveTypeError"; export * from "./UnmetGraphQLPeerDependencyError"; export * from "./WrongNullableListOptionError"; ================================================ FILE: src/helpers/auth-middleware.ts ================================================ import { AuthenticationError, AuthorizationError } from "@/errors"; import { type AuthChecker, type AuthCheckerFn, type AuthMode } from "@/typings"; import { type MiddlewareFn } from "@/typings/middleware"; import { type IOCContainer } from "@/utils/container"; export function AuthMiddleware( authChecker: AuthChecker, container: IOCContainer, authMode: AuthMode, roles: any[], ): MiddlewareFn { return async (action, next) => { let accessGranted: boolean; if (authChecker.prototype) { const authCheckerInstance = await container.getInstance(authChecker, action); accessGranted = await authCheckerInstance.check(action, roles); } else { accessGranted = await (authChecker as AuthCheckerFn)(action, roles); } if (!accessGranted) { if (authMode === "null") { return null; } if (authMode === "error") { throw roles.length === 0 ? new AuthenticationError() : new AuthorizationError(); } } return next(); }; } ================================================ FILE: src/helpers/decorators.ts ================================================ import { type DescriptionOptions, type ReturnTypeFunc } from "@/decorators/types"; export interface TypeDecoratorParams { options: Partial; returnTypeFunc?: ReturnTypeFunc; } export function getTypeDecoratorParams( returnTypeFuncOrOptions: ReturnTypeFunc | T | undefined, maybeOptions: T | undefined, ): TypeDecoratorParams { if (typeof returnTypeFuncOrOptions === "function") { return { returnTypeFunc: returnTypeFuncOrOptions as ReturnTypeFunc, options: maybeOptions || {}, }; } return { options: returnTypeFuncOrOptions || {}, }; } export function getNameDecoratorParams( nameOrOptions: string | T | undefined, maybeOptions: T | undefined, ) { if (typeof nameOrOptions === "string") { return { name: nameOrOptions, options: maybeOptions || ({} as T), }; } return { options: nameOrOptions || ({} as T), }; } export function getArrayFromOverloadedRest(overloadedArray: Array): T[] { let items: T[]; if (Array.isArray(overloadedArray[0])) { items = overloadedArray[0] as T[]; } else { items = overloadedArray as T[]; } return items; } ================================================ FILE: src/helpers/filesystem.ts ================================================ import fs from "node:fs"; import asyncFs from "node:fs/promises"; import path from "node:path"; export async function outputFile(filePath: string, fileContent: any) { try { await asyncFs.writeFile(filePath, fileContent); } catch (err) { if ((err as NodeJS.ErrnoException).code !== "ENOENT") { throw err; } await asyncFs.mkdir(path.dirname(filePath), { recursive: true }); await asyncFs.writeFile(filePath, fileContent); } } export function outputFileSync(filePath: string, fileContent: any) { try { fs.writeFileSync(filePath, fileContent); } catch (err) { if ((err as NodeJS.ErrnoException).code !== "ENOENT") { throw err; } fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, fileContent); } } ================================================ FILE: src/helpers/findType.ts ================================================ import { type RecursiveArray, type ReturnTypeFunc, type TypeOptions, type TypeValue, type TypeValueThunk, } from "@/decorators/types"; import { NoExplicitTypeError } from "@/errors"; import { ensureReflectMetadataExists } from "@/metadata/utils"; import { bannedTypes } from "./returnTypes"; export type MetadataKey = "design:type" | "design:returntype" | "design:paramtypes"; export interface TypeInfo { getType: TypeValueThunk; typeOptions: TypeOptions; } export interface GetTypeParams { metadataKey: MetadataKey; prototype: Object; propertyKey: string; parameterIndex?: number; argName?: string; returnTypeFunc?: ReturnTypeFunc; typeOptions?: TypeOptions; } function findTypeValueArrayDepth( [typeValueOrArray]: RecursiveArray, innerDepth = 1, ): { depth: number; returnType: TypeValue } { if (!Array.isArray(typeValueOrArray)) { return { depth: innerDepth, returnType: typeValueOrArray }; } return findTypeValueArrayDepth(typeValueOrArray, innerDepth + 1); } export function findType({ metadataKey, prototype, propertyKey, parameterIndex, argName, returnTypeFunc, typeOptions = {}, }: GetTypeParams): TypeInfo { const options: TypeOptions = { ...typeOptions }; let metadataDesignType: Function | undefined; ensureReflectMetadataExists(); const reflectedType: Function[] | Function | undefined = Reflect.getMetadata( metadataKey, prototype, propertyKey, ); if (reflectedType) { if (metadataKey === "design:paramtypes") { metadataDesignType = (reflectedType as Function[])[parameterIndex!]; } else { metadataDesignType = reflectedType as Function; } } if (!returnTypeFunc && (!metadataDesignType || bannedTypes.includes(metadataDesignType))) { throw new NoExplicitTypeError(prototype.constructor.name, propertyKey, parameterIndex, argName); } if (returnTypeFunc) { const getType = () => { const returnTypeFuncReturnValue = returnTypeFunc(); if (Array.isArray(returnTypeFuncReturnValue)) { const { depth, returnType } = findTypeValueArrayDepth(returnTypeFuncReturnValue); options.array = true; options.arrayDepth = depth; return returnType; } return returnTypeFuncReturnValue; }; return { getType, typeOptions: options, }; } if (metadataDesignType) { return { getType: () => metadataDesignType!, typeOptions: options, }; } throw new Error("Ops... this should never happen :)"); } ================================================ FILE: src/helpers/isThrowing.ts ================================================ export function isThrowing(fn: () => void) { try { fn(); return false; } catch { return true; } } ================================================ FILE: src/helpers/params.ts ================================================ import { type ReturnTypeFunc, type TypeOptions, type ValidateOptions } from "@/decorators/types"; import { SymbolKeysNotSupportedError } from "@/errors"; import { type CommonArgMetadata } from "@/metadata/definitions"; import { findType } from "./findType"; export interface ParamInfo { prototype: Object; propertyKey: string | symbol; parameterIndex: number; argName?: string; returnTypeFunc?: ReturnTypeFunc; options?: TypeOptions & ValidateOptions; } export function getParamInfo({ prototype, propertyKey, parameterIndex, argName, returnTypeFunc, options = {}, }: ParamInfo): CommonArgMetadata { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } const { getType, typeOptions } = findType({ metadataKey: "design:paramtypes", prototype, propertyKey, parameterIndex, argName, returnTypeFunc, typeOptions: options, }); return { target: prototype.constructor, methodName: propertyKey, index: parameterIndex, getType, typeOptions, validateSettings: options.validate, validateFn: options.validateFn, }; } ================================================ FILE: src/helpers/resolver-metadata.ts ================================================ import { type AdvancedOptions, type ReturnTypeFunc } from "@/decorators/types"; import { SymbolKeysNotSupportedError } from "@/errors"; import { type ResolverMetadata } from "@/metadata/definitions"; import { findType } from "./findType"; export function getResolverMetadata( prototype: object, propertyKey: string | symbol, returnTypeFunc?: ReturnTypeFunc, options: AdvancedOptions = {}, ): ResolverMetadata { if (typeof propertyKey === "symbol") { throw new SymbolKeysNotSupportedError(); } const { getType, typeOptions } = findType({ metadataKey: "design:returntype", prototype, propertyKey, returnTypeFunc, typeOptions: options, }); const methodName = propertyKey as keyof typeof prototype; return { methodName, schemaName: options.name || methodName, target: prototype.constructor, getReturnType: getType, returnTypeOptions: typeOptions, description: options.description, deprecationReason: options.deprecationReason, complexity: options.complexity, }; } ================================================ FILE: src/helpers/returnTypes.ts ================================================ export const allowedTypes: Function[] = [String, Number, Date, Boolean]; export const bannedTypes: Function[] = [Promise, Array, Object, Function]; ================================================ FILE: src/helpers/types.ts ================================================ import { GraphQLBoolean, GraphQLFloat, GraphQLList, GraphQLNonNull, GraphQLScalarType, GraphQLString, type GraphQLType, } from "graphql"; import { type TypeOptions } from "@/decorators/types"; import { WrongNullableListOptionError } from "@/errors"; import { GraphQLISODateTime } from "@/scalars"; import { BuildContext } from "@/schema/build-context"; function wrapTypeInNestedList( targetType: GraphQLType, depth: number, nullable: boolean, ): GraphQLList { const targetTypeNonNull = nullable ? targetType : new GraphQLNonNull(targetType); if (depth === 0) { return targetType as GraphQLList; } return wrapTypeInNestedList(new GraphQLList(targetTypeNonNull), depth - 1, nullable); } export function convertTypeIfScalar(type: any): GraphQLScalarType | undefined { if (type instanceof GraphQLScalarType) { return type; } const scalarMap = BuildContext.scalarsMaps.find(it => it.type === type); if (scalarMap) { return scalarMap.scalar; } switch (type) { case String: return GraphQLString; case Boolean: return GraphQLBoolean; case Number: return GraphQLFloat; case Date: return GraphQLISODateTime; default: return undefined; } } export function wrapWithTypeOptions( target: Function, propertyName: string, type: T, typeOptions: TypeOptions, nullableByDefault: boolean, ): T { if ( !typeOptions.array && (typeOptions.nullable === "items" || typeOptions.nullable === "itemsAndList") ) { throw new WrongNullableListOptionError(target.name, propertyName, typeOptions.nullable); } let gqlType: GraphQLType = type; if (typeOptions.array) { const isNullableArray = typeOptions.nullable === "items" || typeOptions.nullable === "itemsAndList" || (typeOptions.nullable === undefined && nullableByDefault === true); gqlType = wrapTypeInNestedList(gqlType, typeOptions.arrayDepth!, isNullableArray); } if ( typeOptions.nullable === false || (typeOptions.nullable === undefined && nullableByDefault === false) || typeOptions.nullable === "items" ) { gqlType = new GraphQLNonNull(gqlType); } return gqlType as T; } const simpleTypes: Function[] = [String, Boolean, Number, Date, Array, Promise]; export function convertToType(Target: any, data?: object): object | undefined { // skip converting undefined and null if (data == null) { return data; } // skip converting scalars (object scalar mostly) if (Target instanceof GraphQLScalarType) { return data; } // skip converting simple types if (simpleTypes.includes(data.constructor)) { return data; } // skip converting already converted types if (data instanceof Target) { return data; } // convert array to instances if (Array.isArray(data)) { return data.map(item => convertToType(Target, item)); } // Create instance by calling constructor to initialize instance fields const instance = new (Target as any)(); // Remove undefined properties that weren't provided in the input data // This prevents optional @Field() decorated properties from being enumerable for (const key of Object.keys(instance)) { if (instance[key] === undefined && !(key in data)) { delete instance[key]; } } return Object.assign(instance, data); } export function getEnumValuesMap(enumObject: T) { const enumKeys = Object.keys(enumObject).filter(key => Number.isNaN(parseInt(key, 10))); const enumMap = enumKeys.reduce((map, key) => { // eslint-disable-next-line no-param-reassign map[key] = enumObject[key as keyof T]; return map; }, {}); return enumMap; } ================================================ FILE: src/helpers/utils.ts ================================================ export type ArrayElements = TArray extends ReadonlyArray ? TElement : never; export type UnionFromClasses = InstanceType< ArrayElements >; ================================================ FILE: src/index.ts ================================================ export * from "./decorators"; export * from "./errors"; export * from "./typings"; export * from "./metadata"; export * from "./scalars"; export * from "./utils"; ================================================ FILE: src/metadata/definitions/authorized-metadata.ts ================================================ export interface AuthorizedMetadata { target: Function; fieldName: string; roles: any[]; } export type AuthorizedClassMetadata = Omit; ================================================ FILE: src/metadata/definitions/class-metadata.ts ================================================ import { type DirectiveMetadata } from "./directive-metadata"; import { type ExtensionsMetadata } from "./extensions-metadata"; import { type FieldMetadata } from "./field-metadata"; export interface ClassMetadata { name: string; target: Function; fields?: FieldMetadata[]; description?: string; directives?: DirectiveMetadata[]; extensions?: ExtensionsMetadata; simpleResolvers?: boolean; } ================================================ FILE: src/metadata/definitions/directive-metadata.ts ================================================ export interface DirectiveMetadata { nameOrDefinition: string; args: Record; } export interface DirectiveClassMetadata { target: Function; directive: DirectiveMetadata; } export interface DirectiveFieldMetadata { target: Function; fieldName: string; directive: DirectiveMetadata; } export interface DirectiveArgumentMetadata { target: Function; fieldName: string; parameterIndex: number; directive: DirectiveMetadata; } ================================================ FILE: src/metadata/definitions/enum-metadata.ts ================================================ import { type EnumValuesConfig } from "@/decorators/types"; export interface EnumMetadata { enumObj: object; name: string; description: string | undefined; valuesConfig: EnumValuesConfig; } ================================================ FILE: src/metadata/definitions/extensions-metadata.ts ================================================ export type ExtensionsMetadata = Readonly>; export interface ExtensionsClassMetadata { target: Function; extensions: ExtensionsMetadata; } export interface ExtensionsFieldMetadata { target: Function; fieldName: string; extensions: ExtensionsMetadata; } ================================================ FILE: src/metadata/definitions/field-metadata.ts ================================================ import { type TypeOptions, type TypeValueThunk } from "@/decorators/types"; import { type Complexity } from "@/typings"; import { type Middleware } from "@/typings/middleware"; import { type DirectiveMetadata } from "./directive-metadata"; import { type ExtensionsMetadata } from "./extensions-metadata"; import { type ParamMetadata } from "./param-metadata"; export interface FieldMetadata { target: Function; schemaName: string; name: string; getType: TypeValueThunk; typeOptions: TypeOptions; description: string | undefined; deprecationReason: string | undefined; complexity: Complexity | undefined; params?: ParamMetadata[]; roles?: any[]; middlewares?: Array>; directives?: DirectiveMetadata[]; extensions?: ExtensionsMetadata; simple?: boolean; } ================================================ FILE: src/metadata/definitions/index.ts ================================================ export * from "./authorized-metadata"; export * from "./class-metadata"; export * from "./directive-metadata"; export * from "./enum-metadata"; export * from "./extensions-metadata"; export * from "./field-metadata"; export * from "./middleware-metadata"; export * from "./param-metadata"; export * from "./resolver-metadata"; export * from "./union-metadata"; ================================================ FILE: src/metadata/definitions/interface-class-metadata.ts ================================================ import { type TypeResolver } from "@/typings"; import { type ClassMetadata } from "./class-metadata"; export type InterfaceClassMetadata = { resolveType?: TypeResolver; autoRegisteringDisabled: boolean; interfaceClasses: Function[] | undefined; } & ClassMetadata; ================================================ FILE: src/metadata/definitions/middleware-metadata.ts ================================================ import { type Middleware } from "@/typings/middleware"; export interface MiddlewareMetadata { target: Function; fieldName: string; middlewares: Array>; } export type ResolverMiddlewareMetadata = Omit; ================================================ FILE: src/metadata/definitions/object-class-metadata.ts ================================================ import { type ClassMetadata } from "./class-metadata"; export type ObjectClassMetadata = { interfaceClasses: Function[] | undefined; } & ClassMetadata; ================================================ FILE: src/metadata/definitions/param-metadata.ts ================================================ import { type TypeOptions, type TypeValueThunk } from "@/decorators/types"; import { type ValidateSettings } from "@/schema/build-context"; import { type ResolverData, type ValidatorFn } from "@/typings"; export interface BasicParamMetadata { target: Function; methodName: string; index: number; } export type InfoParamMetadata = { kind: "info"; } & BasicParamMetadata; export type PubSubParamMetadata = { kind: "pubSub"; triggerKey?: string; } & BasicParamMetadata; export type ContextParamMetadata = { kind: "context"; propertyName: string | undefined; } & BasicParamMetadata; export type RootParamMetadata = { kind: "root"; propertyName: string | undefined; getType: TypeValueThunk | undefined; } & BasicParamMetadata; export type CommonArgMetadata = { getType: TypeValueThunk; typeOptions: TypeOptions; validateSettings: ValidateSettings | undefined; validateFn: ValidatorFn | undefined; } & BasicParamMetadata; export type ArgParamMetadata = { kind: "arg"; name: string; description: string | undefined; deprecationReason: string | undefined; } & CommonArgMetadata; export type ArgsParamMetadata = { kind: "args"; } & CommonArgMetadata; export interface CustomParamOptions { arg?: ArgParamMetadata; } export type CustomParamMetadata = { kind: "custom"; resolver: (resolverData: ResolverData) => any; options: CustomParamOptions; } & BasicParamMetadata; export type ParamMetadata = | InfoParamMetadata | PubSubParamMetadata | ContextParamMetadata | RootParamMetadata | ArgParamMetadata | ArgsParamMetadata | CustomParamMetadata; ================================================ FILE: src/metadata/definitions/resolver-metadata.ts ================================================ import { type ClassTypeResolver, type SubscriptionFilterFunc, type SubscriptionSubscribeFunc, type SubscriptionTopicIdFunc, type SubscriptionTopicsFunc, type TypeOptions, type TypeValueThunk, } from "@/decorators/types"; import { type Complexity } from "@/typings"; import { type Middleware } from "@/typings/middleware"; import { type DirectiveMetadata } from "./directive-metadata"; import { type ExtensionsMetadata } from "./extensions-metadata"; import { type ParamMetadata } from "./param-metadata"; export interface BaseResolverMetadata { methodName: string; schemaName: string; target: Function; complexity: Complexity | undefined; resolverClassMetadata?: ResolverClassMetadata; params?: ParamMetadata[]; roles?: any[]; middlewares?: Array>; directives?: DirectiveMetadata[]; extensions?: ExtensionsMetadata; } export type ResolverMetadata = { getReturnType: TypeValueThunk; returnTypeOptions: TypeOptions; description?: string; deprecationReason?: string; } & BaseResolverMetadata; export type FieldResolverMetadata = { kind: "internal" | "external"; description?: string; deprecationReason?: string; getType?: TypeValueThunk; typeOptions?: TypeOptions; getObjectType?: ClassTypeResolver; } & BaseResolverMetadata; export type SubscriptionResolverMetadata = { topics: string | string[] | SubscriptionTopicsFunc | undefined; topicId: SubscriptionTopicIdFunc | undefined; filter: SubscriptionFilterFunc | undefined; subscribe: SubscriptionSubscribeFunc | undefined; } & ResolverMetadata; export interface ResolverClassMetadata { target: Function; getObjectType: ClassTypeResolver; superResolver?: ResolverClassMetadata; } ================================================ FILE: src/metadata/definitions/union-metadata.ts ================================================ import { type ClassType, type TypeResolver } from "@/typings"; export interface UnionMetadata { getClassTypes: () => ClassType[]; name: string; description?: string; resolveType?: TypeResolver; } export type UnionMetadataWithSymbol = { symbol: symbol; } & UnionMetadata; ================================================ FILE: src/metadata/getMetadataStorage.ts ================================================ import { MetadataStorage } from "./metadata-storage"; declare global { // eslint-disable-next-line vars-on-top, no-var var TypeGraphQLMetadataStorage: MetadataStorage; } export function getMetadataStorage(): MetadataStorage { if (!global.TypeGraphQLMetadataStorage) { global.TypeGraphQLMetadataStorage = new MetadataStorage(); } return global.TypeGraphQLMetadataStorage; } ================================================ FILE: src/metadata/index.ts ================================================ export { getMetadataStorage } from "./getMetadataStorage"; ================================================ FILE: src/metadata/metadata-storage.ts ================================================ /* eslint-disable no-param-reassign */ import { NoExplicitTypeError } from "@/errors"; import { type SchemaGeneratorOptions } from "@/schema/schema-generator"; import { type ClassType } from "@/typings"; import { type AuthorizedClassMetadata, type AuthorizedMetadata, type BaseResolverMetadata, type ClassMetadata, type EnumMetadata, type ExtensionsClassMetadata, type ExtensionsFieldMetadata, type ExtensionsMetadata, type FieldMetadata, type FieldResolverMetadata, type MiddlewareMetadata, type ParamMetadata, type ResolverClassMetadata, type ResolverMetadata, type ResolverMiddlewareMetadata, type SubscriptionResolverMetadata, type UnionMetadata, type UnionMetadataWithSymbol, } from "./definitions"; import { type DirectiveArgumentMetadata, type DirectiveClassMetadata, type DirectiveFieldMetadata, } from "./definitions/directive-metadata"; import { type InterfaceClassMetadata } from "./definitions/interface-class-metadata"; import { type ObjectClassMetadata } from "./definitions/object-class-metadata"; import { mapMiddlewareMetadataToArray, mapSuperFieldResolverHandlers, mapSuperResolverHandlers, } from "./utils"; export class MetadataStorage { queries: ResolverMetadata[] = []; mutations: ResolverMetadata[] = []; subscriptions: SubscriptionResolverMetadata[] = []; fieldResolvers: FieldResolverMetadata[] = []; objectTypes: ObjectClassMetadata[] = []; objectTypesCache = new Map(); inputTypes: ClassMetadata[] = []; argumentTypes: ClassMetadata[] = []; interfaceTypes: InterfaceClassMetadata[] = []; interfaceTypesCache = new Map(); authorizedFields: AuthorizedMetadata[] = []; authorizedFieldsByTargetAndFieldCache = new Map>(); authorizedResolver: AuthorizedClassMetadata[] = []; authorizedResolverByTargetCache = new Map(); enums: EnumMetadata[] = []; unions: UnionMetadataWithSymbol[] = []; middlewares: MiddlewareMetadata[] = []; middlewaresByTargetAndFieldCache = new Map>>(); resolverMiddlewares: ResolverMiddlewareMetadata[] = []; resolverMiddlewaresByTargetCache = new Map>(); classDirectives: DirectiveClassMetadata[] = []; classDirectivesByTargetCache = new Map(); fieldDirectives: DirectiveFieldMetadata[] = []; fieldDirectivesByTargetAndFieldCache = new Map>(); argumentDirectives: DirectiveArgumentMetadata[] = []; classExtensions: ExtensionsClassMetadata[] = []; fieldExtensions: ExtensionsFieldMetadata[] = []; resolverClasses: ResolverClassMetadata[] = []; resolverClassesCache = new Map(); fields: FieldMetadata[] = []; fieldsCache = new Map(); params: ParamMetadata[] = []; paramsCache = new Map>(); collectQueryHandlerMetadata(definition: ResolverMetadata) { this.queries.push(definition); } collectMutationHandlerMetadata(definition: ResolverMetadata) { this.mutations.push(definition); } collectSubscriptionHandlerMetadata(definition: SubscriptionResolverMetadata) { this.subscriptions.push(definition); } collectFieldResolverMetadata(definition: FieldResolverMetadata) { this.fieldResolvers.push(definition); } collectObjectMetadata(definition: ObjectClassMetadata) { this.objectTypes.push(definition); } collectInputMetadata(definition: ClassMetadata) { this.inputTypes.push(definition); } collectArgsMetadata(definition: ClassMetadata) { this.argumentTypes.push(definition); } collectInterfaceMetadata(definition: InterfaceClassMetadata) { this.interfaceTypes.push(definition); } collectAuthorizedFieldMetadata(definition: AuthorizedMetadata) { this.authorizedFields.push(definition); } collectAuthorizedResolverMetadata(definition: AuthorizedClassMetadata) { this.authorizedResolver.push(definition); } collectEnumMetadata(definition: EnumMetadata) { this.enums.push(definition); } collectUnionMetadata(definition: UnionMetadata) { const unionSymbol = Symbol(definition.name); this.unions.push({ ...definition, symbol: unionSymbol, }); return unionSymbol; } collectMiddlewareMetadata(definition: MiddlewareMetadata) { this.middlewares.push(definition); } collectResolverMiddlewareMetadata(definition: ResolverMiddlewareMetadata) { this.resolverMiddlewares.push(definition); } collectResolverClassMetadata(definition: ResolverClassMetadata) { this.resolverClasses.push(definition); } collectClassFieldMetadata(definition: FieldMetadata) { this.fields.push(definition); } collectHandlerParamMetadata(definition: ParamMetadata) { this.params.push(definition); } collectDirectiveClassMetadata(definition: DirectiveClassMetadata) { this.classDirectives.push(definition); } collectDirectiveFieldMetadata(definition: DirectiveFieldMetadata) { this.fieldDirectives.push(definition); } collectDirectiveArgumentMetadata(definition: DirectiveArgumentMetadata) { this.argumentDirectives.push(definition); } collectExtensionsClassMetadata(definition: ExtensionsClassMetadata) { this.classExtensions.push(definition); } collectExtensionsFieldMetadata(definition: ExtensionsFieldMetadata) { this.fieldExtensions.push(definition); } initCache() { this.clearMapCaches(); if (this.resolverClasses?.length) { this.resolverClasses.forEach(resolverClass => { if (!this.resolverClassesCache.has(resolverClass.target)) { this.resolverClassesCache.set(resolverClass.target, resolverClass); } }); } if (this.params?.length) { this.params.forEach(param => { if (!this.paramsCache.has(param.target)) { this.paramsCache.set(param.target, new Map()); } if (!this.paramsCache.get(param.target)!.has(param.methodName)) { this.paramsCache.get(param.target)!.set(param.methodName, []); } this.paramsCache.get(param.target)!.get(param.methodName)!.push(param); }); } if (this.middlewares?.length) { this.middlewares.forEach(middleware => { if (!this.middlewaresByTargetAndFieldCache.has(middleware.target)) { this.middlewaresByTargetAndFieldCache.set(middleware.target, new Map()); } if ( !this.middlewaresByTargetAndFieldCache.get(middleware.target)!.has(middleware.fieldName) ) { this.middlewaresByTargetAndFieldCache .get(middleware.target)! .set(middleware.fieldName, new Set()); } if ( !this.middlewaresByTargetAndFieldCache .get(middleware.target)! .get(middleware.fieldName)! .has(middleware) ) { this.middlewaresByTargetAndFieldCache .get(middleware.target)! .get(middleware.fieldName)! .add(middleware); } }); } if (this.resolverMiddlewares?.length) { this.resolverMiddlewares.forEach(middleware => { const key = middleware.target; if (!this.resolverMiddlewaresByTargetCache.has(key)) { this.resolverMiddlewaresByTargetCache.set(key, new Set()); } if (!this.resolverMiddlewaresByTargetCache.get(key)!.has(middleware)) { this.resolverMiddlewaresByTargetCache.get(key)!.add(middleware); } }); } if (this.fieldDirectives?.length) { this.fieldDirectives.forEach(directive => { if (!this.fieldDirectivesByTargetAndFieldCache.has(directive.target)) { this.fieldDirectivesByTargetAndFieldCache.set(directive.target, new Map()); } if ( !this.fieldDirectivesByTargetAndFieldCache.get(directive.target)!.has(directive.fieldName) ) { this.fieldDirectivesByTargetAndFieldCache .get(directive.target)! .set(directive.fieldName, []); } this.fieldDirectivesByTargetAndFieldCache .get(directive.target)! .get(directive.fieldName)! .push(directive); }); } if (this.classDirectives?.length) { this.classDirectives.forEach(directive => { const key = directive.target; if (!this.classDirectivesByTargetCache.has(key)) { this.classDirectivesByTargetCache.set(key, []); } this.classDirectivesByTargetCache.get(key)!.push(directive); }); } if (this.authorizedFields?.length) { this.authorizedFields.forEach(field => { if (!this.authorizedFieldsByTargetAndFieldCache.has(field.target)) { this.authorizedFieldsByTargetAndFieldCache.set(field.target, new Map()); } if (!this.authorizedFieldsByTargetAndFieldCache.get(field.target)!.has(field.fieldName)) { this.authorizedFieldsByTargetAndFieldCache.get(field.target)!.set(field.fieldName, field); } }); } if (this.authorizedResolver?.length) { this.authorizedResolver.forEach(resolver => { const key = resolver.target; if (!this.authorizedResolverByTargetCache.has(key)) { this.authorizedResolverByTargetCache.set(key, resolver); } }); } if (this.fields?.length) { this.fields.forEach(field => { if (!this.fieldsCache.has(field.target)) { this.fieldsCache.set(field.target, []); } this.fieldsCache.get(field.target)!.push(field); }); } if (this.objectTypes?.length) { this.objectTypes.forEach(objType => { this.objectTypesCache.set(objType.target, objType); }); } if (this.interfaceTypes?.length) { this.interfaceTypes.forEach(interfaceType => { this.interfaceTypesCache.set(interfaceType.target, interfaceType); }); } } build(options: SchemaGeneratorOptions) { this.classDirectives.reverse(); this.fieldDirectives.reverse(); this.argumentDirectives.reverse(); this.classExtensions.reverse(); this.fieldExtensions.reverse(); this.initCache(); this.buildClassMetadata(this.objectTypes); this.buildClassMetadata(this.inputTypes); this.buildClassMetadata(this.argumentTypes); this.buildClassMetadata(this.interfaceTypes); this.buildFieldResolverMetadata(this.fieldResolvers, options); this.buildResolversMetadata(this.queries); this.buildResolversMetadata(this.mutations); this.buildResolversMetadata(this.subscriptions); this.buildExtendedResolversMetadata(); } clear() { this.queries = []; this.mutations = []; this.subscriptions = []; this.fieldResolvers = []; this.objectTypes = []; this.inputTypes = []; this.argumentTypes = []; this.interfaceTypes = []; this.authorizedFields = []; this.authorizedResolver = []; this.enums = []; this.unions = []; this.middlewares = []; this.resolverMiddlewares = []; this.classDirectives = []; this.fieldDirectives = []; this.argumentDirectives = []; this.classExtensions = []; this.fieldExtensions = []; this.resolverClasses = []; this.fields = []; this.params = []; this.clearMapCaches(); } clone() { const cloned = new MetadataStorage(); // arrays are cloned to prevent mutation of original metadata storage when building schema cloned.queries = [...this.queries]; cloned.mutations = [...this.mutations]; cloned.subscriptions = [...this.subscriptions]; cloned.fieldResolvers = [...this.fieldResolvers]; cloned.objectTypes = [...this.objectTypes]; cloned.inputTypes = [...this.inputTypes]; cloned.argumentTypes = [...this.argumentTypes]; cloned.interfaceTypes = [...this.interfaceTypes]; cloned.authorizedFields = [...this.authorizedFields]; cloned.authorizedResolver = [...this.authorizedResolver]; cloned.enums = [...this.enums]; cloned.unions = [...this.unions]; cloned.middlewares = [...this.middlewares]; cloned.resolverMiddlewares = [...this.resolverMiddlewares]; cloned.classDirectives = [...this.classDirectives]; cloned.fieldDirectives = [...this.fieldDirectives]; cloned.argumentDirectives = [...this.argumentDirectives]; cloned.classExtensions = [...this.classExtensions]; cloned.fieldExtensions = [...this.fieldExtensions]; cloned.resolverClasses = [...this.resolverClasses]; cloned.fields = [...this.fields]; cloned.params = [...this.params]; return cloned; } private clearMapCaches() { this.fieldsCache = new Map(); this.objectTypesCache = new Map(); this.interfaceTypesCache = new Map(); this.middlewaresByTargetAndFieldCache = new Map(); this.resolverMiddlewaresByTargetCache = new Map(); this.paramsCache = new Map(); this.fieldDirectivesByTargetAndFieldCache = new Map(); this.classDirectivesByTargetCache = new Map(); this.authorizedFieldsByTargetAndFieldCache = new Map(); this.authorizedResolverByTargetCache = new Map(); this.resolverClassesCache = new Map(); } private buildClassMetadata(definitions: ClassMetadata[]) { definitions.forEach(def => { if (!def.fields) { const fields = this.fieldsCache.get(def.target) || []; fields.forEach(field => { field.roles = this.findFieldRoles(field.target, field.name); field.params = this.paramsCache.get(field.target)?.get(field.name) || []; field.middlewares = [ ...mapMiddlewareMetadataToArray([ ...(this.resolverMiddlewaresByTargetCache.get(field.target) || []), ]), ...mapMiddlewareMetadataToArray([ ...(this.middlewaresByTargetAndFieldCache.get(field.target)?.get(field.name) || []), ]), ]; field.directives = ( this.fieldDirectivesByTargetAndFieldCache.get(field.target)?.get(field.name) || [] ).map(it => it.directive); field.extensions = this.findExtensions(field.target, field.name); }); def.fields = fields; } if (!def.directives) { def.directives = (this.classDirectivesByTargetCache.get(def.target) || []).map( it => it.directive, ); } if (!def.extensions) { def.extensions = this.findExtensions(def.target); } }); } private buildResolversMetadata(definitions: BaseResolverMetadata[]) { definitions.forEach(def => { def.resolverClassMetadata = this.resolverClassesCache.get(def.target); def.params = this.paramsCache.get(def.target)?.get(def.methodName) || []; def.roles = this.findFieldRoles(def.target, def.methodName); def.middlewares = [ ...mapMiddlewareMetadataToArray([ ...(this.resolverMiddlewaresByTargetCache.get(def.target) || []), ]), ...mapMiddlewareMetadataToArray([ ...(this.middlewaresByTargetAndFieldCache.get(def.target)?.get(def.methodName) || []), ]), ]; def.directives = ( this.fieldDirectivesByTargetAndFieldCache.get(def.target)?.get(def.methodName) || [] ).map(it => it.directive); def.extensions = this.findExtensions(def.target, def.methodName); }); } private buildFieldResolverMetadata( definitions: FieldResolverMetadata[], options: SchemaGeneratorOptions, ) { this.buildResolversMetadata(definitions); definitions.forEach(def => { def.roles = this.findFieldRoles(def.target, def.methodName); def.directives = ( this.fieldDirectivesByTargetAndFieldCache.get(def.target)?.get(def.methodName) || [] ).map(it => it.directive); def.extensions = this.findExtensions(def.target, def.methodName); def.getObjectType = def.kind === "external" ? this.resolverClassesCache.get(def.target)!.getObjectType : () => def.target as ClassType; if (def.kind === "external") { const typeClass = this.resolverClassesCache.get(def.target)!.getObjectType!(); const typeMetadata = this.objectTypesCache.get(typeClass) || this.interfaceTypesCache.get(typeClass); if (!typeMetadata) { throw new Error( `Unable to find type metadata for input type or object type named '${typeClass.name}'`, ); } const typeField = typeMetadata.fields!.find( fieldDef => fieldDef.schemaName === def.schemaName, )!; if (!typeField) { const shouldCollectFieldMetadata = !options.resolvers || options.resolvers.some( resolverCls => resolverCls === def.target || Object.prototype.isPrototypeOf.call(def.target, resolverCls), ); if (!def.getType || !def.typeOptions) { throw new NoExplicitTypeError(def.target.name, def.methodName); } if (shouldCollectFieldMetadata) { const fieldMetadata: FieldMetadata = { name: def.methodName, schemaName: def.schemaName, getType: def.getType!, target: typeClass, typeOptions: def.typeOptions!, deprecationReason: def.deprecationReason, description: def.description, complexity: def.complexity, roles: def.roles!, middlewares: def.middlewares!, params: def.params!, directives: def.directives, extensions: def.extensions, }; this.collectClassFieldMetadata(fieldMetadata); typeMetadata.fields!.push(fieldMetadata); } } else { typeField.complexity = def.complexity; if (typeField.params!.length === 0) { typeField.params = def.params!; } if (def.roles) { typeField.roles = def.roles; } else if (typeField.roles) { def.roles = typeField.roles; } } } }); } private buildExtendedResolversMetadata() { this.resolverClasses.forEach(def => { let superResolver = Object.getPrototypeOf(def.target); // copy and modify metadata of resolver from parent resolver class while (superResolver.prototype) { const superResolverMetadata = this.resolverClassesCache.get(superResolver); if (superResolverMetadata) { this.queries = mapSuperResolverHandlers(this.queries, superResolver, def); this.mutations = mapSuperResolverHandlers(this.mutations, superResolver, def); this.subscriptions = mapSuperResolverHandlers(this.subscriptions, superResolver, def); this.fieldResolvers = mapSuperFieldResolverHandlers( this.fieldResolvers, superResolver, def, ); } superResolver = Object.getPrototypeOf(superResolver); } }); } private findFieldRoles(target: Function, fieldName: string): any[] | undefined { const authorizedField = this.authorizedFieldsByTargetAndFieldCache.get(target)?.get(fieldName) || this.authorizedResolverByTargetCache.get(target); if (!authorizedField) { return undefined; } return authorizedField.roles; } private findExtensions(target: Function, fieldName?: string): ExtensionsMetadata { const storedExtensions: Array = fieldName ? this.fieldExtensions : this.classExtensions; return storedExtensions .filter( entry => (entry.target === target || Object.prototype.isPrototypeOf.call(entry.target, target)) && (!("fieldName" in entry) || entry.fieldName === fieldName), ) .reduce((extensions, entry) => ({ ...extensions, ...entry.extensions }), {}); } } ================================================ FILE: src/metadata/utils.ts ================================================ import { ReflectMetadataMissingError } from "@/errors"; import { isThrowing } from "@/helpers/isThrowing"; import { type Middleware } from "@/typings/middleware"; import { type BaseResolverMetadata, type FieldResolverMetadata, type ResolverClassMetadata, type ResolverMiddlewareMetadata, } from "./definitions"; export function mapSuperResolverHandlers( definitions: T[], superResolver: Function, resolverMetadata: ResolverClassMetadata, ): T[] { return definitions.map(metadata => metadata.target === superResolver ? { ...metadata, target: resolverMetadata.target, resolverClassMetadata: resolverMetadata, } : metadata, ); } export function mapSuperFieldResolverHandlers( definitions: FieldResolverMetadata[], superResolver: Function, resolverMetadata: ResolverClassMetadata, ) { const superMetadata = mapSuperResolverHandlers(definitions, superResolver, resolverMetadata); return superMetadata.map(metadata => metadata.target === superResolver ? { ...metadata, getObjectType: isThrowing(metadata.getObjectType!) ? resolverMetadata.getObjectType : metadata.getObjectType, } : metadata, ); } export function mapMiddlewareMetadataToArray( metadata: ResolverMiddlewareMetadata[], ): Array> { return metadata .map(m => m.middlewares) .reduce< Array> >((middlewares, resultArray) => resultArray.concat(middlewares), []); } export function ensureReflectMetadataExists() { if (typeof Reflect !== "object" || typeof Reflect.getMetadata !== "function") { throw new ReflectMetadataMissingError(); } } ================================================ FILE: src/resolvers/convert-args.ts ================================================ import { type TypeValue } from "@/decorators/types"; import { convertToType } from "@/helpers/types"; import { type ArgParamMetadata, type ArgsParamMetadata, type ClassMetadata, } from "@/metadata/definitions"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ArgsDictionary, type ClassType } from "@/typings"; interface TransformationTreeField { name: string; target: TypeValue; fields?: TransformationTree; } interface TransformationTree { target: TypeValue; getFields: () => TransformationTreeField[]; } const generatedTrees = new Map(); function getInputType(target: TypeValue): ClassMetadata | undefined { return getMetadataStorage().inputTypes.find(t => t.target === target); } function getArgsType(target: TypeValue): ClassMetadata | undefined { return getMetadataStorage().argumentTypes.find(t => t.target === target); } function generateInstanceTransformationTree(target: TypeValue): TransformationTree | null { if (generatedTrees.has(target)) { return generatedTrees.get(target)!; } const inputType = getInputType(target); if (!inputType) { generatedTrees.set(target, null); return null; } function generateTransformationTree(metadata: ClassMetadata): TransformationTree { let inputFields = metadata.fields!; let superClass = Object.getPrototypeOf(metadata.target); while (superClass.prototype !== undefined) { const superInputType = getInputType(superClass); if (superInputType) { // support overwriting fields of extended types const existingFieldNames = new Set(inputFields.map(field => field.name)); const superFields = superInputType.fields!.filter( field => !existingFieldNames.has(field.name), ); inputFields = [...inputFields, ...superFields]; } superClass = Object.getPrototypeOf(superClass); } const transformationTree: TransformationTree = { target: metadata.target, getFields: () => inputFields.map(field => { const fieldTarget = field.getType(); const fieldInputType = getInputType(fieldTarget); return { name: field.name, target: fieldTarget, fields: fieldTarget === metadata.target ? transformationTree : fieldInputType && generateTransformationTree(fieldInputType), }; }), }; return transformationTree; } const generatedTransformationTree = generateTransformationTree(inputType); generatedTrees.set(target, generatedTransformationTree); return generatedTransformationTree; } function convertToInput(tree: TransformationTree, data: any): any { if (data == null) { // skip converting undefined and null return data; } if (Array.isArray(data)) { // recursively convert nested arrays return data.map(it => convertToInput(tree, it)); } const inputFields = tree.getFields().reduce>((fields, field) => { const siblings = field.fields; const value = data[field.name]; // don't create property for nullable field if (value !== undefined) { if (value === null || !siblings) { // eslint-disable-next-line no-param-reassign fields[field.name] = convertToType(field.target, value); } else if (Array.isArray(value)) { // eslint-disable-next-line no-param-reassign fields[field.name] = value.map(itemValue => convertToInput(siblings, itemValue)); } else { // eslint-disable-next-line no-param-reassign fields[field.name] = convertToInput(siblings, value); } } return fields; }, {}); return convertToType(tree.target, inputFields); } function convertValueToInstance(target: TypeValue, value: any): any { const transformationTree = generateInstanceTransformationTree(target); return transformationTree ? convertToInput(transformationTree, value) : convertToType(target, value); } function convertValuesToInstances(target: TypeValue, value: any): any { // skip converting undefined and null if (value == null) { return value; } if (Array.isArray(value)) { // call function recursively to handle nested arrays case return value.map(itemValue => convertValuesToInstances(target, itemValue)); } return convertValueToInstance(target, value); } export function convertArgsToInstance(argsMetadata: ArgsParamMetadata, args: ArgsDictionary) { const ArgsClass = argsMetadata.getType() as ClassType; const argsType = getArgsType(ArgsClass)!; let argsFields = argsType.fields!; let superClass = Object.getPrototypeOf(argsType.target); while (superClass.prototype !== undefined) { const superArgumentType = getArgsType(superClass); if (superArgumentType) { argsFields = [...argsFields, ...superArgumentType.fields!]; } superClass = Object.getPrototypeOf(superClass); } const transformedFields = argsFields.reduce>((fields, field) => { const fieldValue = args[field.name]; // don't create property for nullable field if (fieldValue !== undefined) { const fieldTarget = field.getType(); // eslint-disable-next-line no-param-reassign fields[field.name] = convertValuesToInstances(fieldTarget, fieldValue); } return fields; }, {}); return convertToType(ArgsClass, transformedFields); } export function convertArgToInstance(argMetadata: ArgParamMetadata, args: ArgsDictionary) { const argValue = args[argMetadata.name]; const argTarget = argMetadata.getType(); return convertValuesToInstances(argTarget, argValue); } ================================================ FILE: src/resolvers/create.ts ================================================ import { type GraphQLFieldResolver } from "graphql"; import { AuthMiddleware } from "@/helpers/auth-middleware"; import { convertToType } from "@/helpers/types"; import { type BaseResolverMetadata, type FieldMetadata, type FieldResolverMetadata, } from "@/metadata/definitions"; import { BuildContext } from "@/schema/build-context"; import { type ResolverData } from "@/typings"; import { type IOCContainer } from "@/utils/container"; import { isPromiseLike } from "@/utils/isPromiseLike"; import { applyAuthChecker, applyMiddlewares, getParams } from "./helpers"; export function createHandlerResolver( resolverMetadata: BaseResolverMetadata, ): GraphQLFieldResolver { const { validate: globalValidate, validateFn, authChecker, authMode, globalMiddlewares, container, } = BuildContext; const middlewares = globalMiddlewares.concat(resolverMetadata.middlewares!); applyAuthChecker(middlewares, authChecker, container, authMode, resolverMetadata.roles); return (root, args, context, info) => { const resolverData: ResolverData = { root, args, context, info }; const targetInstanceOrPromise: Promise | any = container.getInstance( resolverMetadata.target, resolverData, ); if (isPromiseLike(targetInstanceOrPromise)) { return targetInstanceOrPromise.then(targetInstance => applyMiddlewares(container, resolverData, middlewares, () => { const params: Promise | any[] = getParams( resolverMetadata.params!, resolverData, globalValidate, validateFn, ); if (isPromiseLike(params)) { return params.then(resolvedParams => // eslint-disable-next-line prefer-spread targetInstance[resolverMetadata.methodName].apply(targetInstance, resolvedParams), ); } // eslint-disable-next-line prefer-spread return targetInstance[resolverMetadata.methodName].apply(targetInstance, params); }), ); } return applyMiddlewares(container, resolverData, middlewares, () => { const params: Promise | any[] = getParams( resolverMetadata.params!, resolverData, globalValidate, validateFn, ); const targetInstance = targetInstanceOrPromise; if (isPromiseLike(params)) { return params.then(resolvedParams => // eslint-disable-next-line prefer-spread targetInstance[resolverMetadata.methodName].apply(targetInstance, resolvedParams), ); } // eslint-disable-next-line prefer-spread return targetInstance[resolverMetadata.methodName].apply(targetInstance, params); }); }; } export function createAdvancedFieldResolver( fieldResolverMetadata: FieldResolverMetadata, ): GraphQLFieldResolver { if (fieldResolverMetadata.kind === "external") { return createHandlerResolver(fieldResolverMetadata); } const targetType = fieldResolverMetadata.getObjectType!(); const { validate: globalValidate, validateFn, authChecker, authMode, globalMiddlewares, container, } = BuildContext; const middlewares = globalMiddlewares.concat(fieldResolverMetadata.middlewares!); applyAuthChecker(middlewares, authChecker, container, authMode, fieldResolverMetadata.roles); return (root, args, context, info) => { const resolverData: ResolverData = { root, args, context, info }; const targetInstance: any = convertToType(targetType, root); return applyMiddlewares(container, resolverData, middlewares, () => { const handlerOrGetterValue = targetInstance[fieldResolverMetadata.methodName]; if (typeof handlerOrGetterValue !== "function") { // getter return handlerOrGetterValue; } // method const params: Promise | any[] = getParams( fieldResolverMetadata.params!, resolverData, globalValidate, validateFn, ); if (isPromiseLike(params)) { return params.then(resolvedParams => handlerOrGetterValue.apply(targetInstance, resolvedParams), ); } return handlerOrGetterValue.apply(targetInstance, params); }); }; } export function createBasicFieldResolver( fieldMetadata: FieldMetadata, ): GraphQLFieldResolver { const { authChecker, authMode, globalMiddlewares, container } = BuildContext; const middlewares = globalMiddlewares.concat(fieldMetadata.middlewares!); applyAuthChecker(middlewares, authChecker, container, authMode, fieldMetadata.roles); return (root, args, context, info) => { const resolverData: ResolverData = { root, args, context, info }; return applyMiddlewares(container, resolverData, middlewares, () => root[fieldMetadata.name]); }; } export function wrapResolverWithAuthChecker( resolver: GraphQLFieldResolver, container: IOCContainer, roles: any[] | undefined, ): GraphQLFieldResolver { const { authChecker, authMode } = BuildContext; if (!authChecker || !roles) { return resolver; } return (root, args, context, info) => { const resolverData: ResolverData = { root, args, context, info }; return AuthMiddleware( authChecker, container, authMode, roles, )(resolverData, async () => resolver(root, args, context, info)); }; } ================================================ FILE: src/resolvers/helpers.ts ================================================ import { AuthMiddleware } from "@/helpers/auth-middleware"; import { convertToType } from "@/helpers/types"; import { type ParamMetadata } from "@/metadata/definitions"; import { type ValidateSettings } from "@/schema/build-context"; import { type AuthChecker, type AuthMode, type ResolverData, type ValidatorFn } from "@/typings"; import { type Middleware, type MiddlewareClass, type MiddlewareFn } from "@/typings/middleware"; import { type IOCContainer } from "@/utils/container"; import { isPromiseLike } from "@/utils/isPromiseLike"; import { convertArgToInstance, convertArgsToInstance } from "./convert-args"; import { validateArg } from "./validate-arg"; export function getParams( params: ParamMetadata[], resolverData: ResolverData, globalValidate: ValidateSettings, globalValidateFn: ValidatorFn | undefined, ): Promise | any[] { const paramValues = params .sort((a, b) => a.index - b.index) // eslint-disable-next-line array-callback-return, consistent-return .map(paramInfo => { switch (paramInfo.kind) { case "args": return validateArg( convertArgsToInstance(paramInfo, resolverData.args), paramInfo.getType(), resolverData, globalValidate, paramInfo.validateSettings, globalValidateFn, paramInfo.validateFn, ); case "arg": return validateArg( convertArgToInstance(paramInfo, resolverData.args), paramInfo.getType(), resolverData, globalValidate, paramInfo.validateSettings, globalValidateFn, paramInfo.validateFn, ); case "context": if (paramInfo.propertyName) { return resolverData.context[paramInfo.propertyName]; } return resolverData.context; case "root": { const rootValue = paramInfo.propertyName ? resolverData.root[paramInfo.propertyName] : resolverData.root; if (!paramInfo.getType) { return rootValue; } return convertToType(paramInfo.getType(), rootValue); } case "info": return resolverData.info; case "custom": if (paramInfo.options.arg) { const arg = paramInfo.options.arg!; return validateArg( convertArgToInstance(arg, resolverData.args), arg.getType(), resolverData, globalValidate, arg.validateSettings, globalValidateFn, arg.validateFn, ).then(() => paramInfo.resolver(resolverData)); } return paramInfo.resolver(resolverData); // no default } }); if (paramValues.some(isPromiseLike)) { return Promise.all(paramValues); } return paramValues; } export function applyAuthChecker( middlewares: Array>, authChecker: AuthChecker | undefined, container: IOCContainer, authMode: AuthMode, roles: any[] | undefined, ) { if (authChecker && roles) { middlewares.unshift(AuthMiddleware(authChecker, container, authMode, roles)); } } export function applyMiddlewares( container: IOCContainer, resolverData: ResolverData, middlewares: Array>, resolverHandlerFunction: () => any, ): Promise { if (middlewares.length === 0) { return resolverHandlerFunction(); } let middlewaresIndex = -1; async function dispatchHandler(currentIndex: number): Promise { if (currentIndex <= middlewaresIndex) { throw new Error("next() called multiple times"); } middlewaresIndex = currentIndex; let handlerFn: MiddlewareFn; if (currentIndex === middlewares.length) { handlerFn = resolverHandlerFunction; } else { const currentMiddleware = middlewares[currentIndex]; // Arrow function or class if (currentMiddleware.prototype !== undefined) { const middlewareClassInstance = await container.getInstance( currentMiddleware as MiddlewareClass, resolverData, ); handlerFn = middlewareClassInstance.use.bind(middlewareClassInstance); } else { handlerFn = currentMiddleware as MiddlewareFn; } } let nextResult: any; const result = await handlerFn(resolverData, async () => { nextResult = await dispatchHandler(currentIndex + 1); return nextResult; }); return result !== undefined ? result : nextResult; } return dispatchHandler(0); } ================================================ FILE: src/resolvers/validate-arg.ts ================================================ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore 'class-validator' might not be installed by user import { type ValidationError, type ValidatorOptions } from "class-validator"; import { type TypeValue } from "@/decorators/types"; import { ArgumentValidationError } from "@/errors"; import { type ValidateSettings } from "@/schema/build-context"; import { type ResolverData, type ValidatorFn } from "@/typings"; const shouldArgBeValidated = (argValue: unknown): boolean => argValue !== null && typeof argValue === "object"; export async function validateArg( argValue: any | undefined, argType: TypeValue, resolverData: ResolverData, globalValidateSettings: ValidateSettings, argValidateSettings: ValidateSettings | undefined, globalValidateFn: ValidatorFn | undefined, argValidateFn: ValidatorFn | undefined, ): Promise { const validateFn = argValidateFn ?? globalValidateFn; if (typeof validateFn === "function") { await validateFn(argValue, argType, resolverData); return argValue; } const validate = argValidateSettings !== undefined ? argValidateSettings : globalValidateSettings; if (validate === false || !shouldArgBeValidated(argValue)) { return argValue; } const validatorOptions: ValidatorOptions = { ...(typeof globalValidateSettings === "object" ? globalValidateSettings : {}), ...(typeof argValidateSettings === "object" ? argValidateSettings : {}), }; if (validatorOptions.skipMissingProperties !== false) { validatorOptions.skipMissingProperties = true; } if (validatorOptions.forbidUnknownValues !== true) { validatorOptions.forbidUnknownValues = false; } // Dynamic import to avoid making 'class-validator' a peer dependency when `validate: true` is not set const { validateOrReject } = await import("class-validator"); try { if (Array.isArray(argValue)) { await Promise.all( argValue .filter(shouldArgBeValidated) .map(argItem => validateOrReject(argItem, validatorOptions)), ); } else { await validateOrReject(argValue, validatorOptions); } return argValue; } catch (err) { throw new ArgumentValidationError(err as ValidationError[]); } } ================================================ FILE: src/scalars/aliases.ts ================================================ import { GraphQLFloat, GraphQLID, GraphQLInt } from "graphql"; export const Int = GraphQLInt; export const Float = GraphQLFloat; export const ID = GraphQLID; ================================================ FILE: src/scalars/index.ts ================================================ export * from "./aliases"; export { GraphQLTimestamp, GraphQLDateTimeISO as GraphQLISODateTime } from "graphql-scalars"; ================================================ FILE: src/schema/build-context.ts ================================================ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore 'class-validator' might not be installed by user import { type ValidatorOptions } from "class-validator"; import { type GraphQLScalarType } from "graphql"; import { type AuthChecker, type AuthMode } from "@/typings"; import { type Middleware } from "@/typings/middleware"; import { type PubSub } from "@/typings/subscriptions"; import { type ValidatorFn } from "@/typings/ValidatorFn"; import { type ContainerGetter, type ContainerType, IOCContainer } from "@/utils/container"; export interface ScalarsTypeMap { type: Function; scalar: GraphQLScalarType; } export type ValidateSettings = boolean | ValidatorOptions; export interface BuildContextOptions { scalarsMap?: ScalarsTypeMap[]; /** * Indicates if class-validator should be used to auto validate objects injected into params. * You can directly pass validator options to enable validator with a given options. */ validate?: ValidateSettings; /** * Own validation function to check the args and inputs. */ validateFn?: ValidatorFn; authChecker?: AuthChecker; authMode?: AuthMode; pubSub?: PubSub; globalMiddlewares?: Array>; container?: ContainerType | ContainerGetter; /** * Default value for type decorators, like `@Field({ nullable: true })` */ nullableByDefault?: boolean; /** * Disable inferring default values from property initializers, like `created = new Date();` */ disableInferringDefaultValues?: boolean; } export abstract class BuildContext { static scalarsMaps: ScalarsTypeMap[]; static validate: ValidateSettings; static validateFn?: ValidatorFn; static authChecker?: AuthChecker; static authMode: AuthMode; static pubSub?: PubSub; static globalMiddlewares: Array>; static container: IOCContainer; static nullableByDefault: boolean; static disableInferringDefaultValues: boolean; /** * Set static fields with current building context data */ static create(options: BuildContextOptions) { if (options.scalarsMap !== undefined) { this.scalarsMaps = options.scalarsMap; } if (options.validate !== undefined) { this.validate = options.validate; } if (options.validateFn !== undefined) { this.validateFn = options.validateFn; } if (options.authChecker !== undefined) { this.authChecker = options.authChecker; } if (options.authMode !== undefined) { this.authMode = options.authMode; } if (options.pubSub !== undefined) { this.pubSub = options.pubSub; } if (options.globalMiddlewares) { this.globalMiddlewares = options.globalMiddlewares; } if (options.nullableByDefault !== undefined) { this.nullableByDefault = options.nullableByDefault; } if (options.disableInferringDefaultValues !== undefined) { this.disableInferringDefaultValues = options.disableInferringDefaultValues; } this.container = new IOCContainer(options.container); } /** * Restore default settings */ static reset() { this.scalarsMaps = []; this.validate = false; this.validateFn = undefined; this.authChecker = undefined; this.authMode = "error"; this.pubSub = undefined; this.globalMiddlewares = []; this.container = new IOCContainer(); this.nullableByDefault = false; this.disableInferringDefaultValues = false; } } // Initialize fields BuildContext.reset(); ================================================ FILE: src/schema/definition-node.ts ================================================ import { type ConstArgumentNode, type ConstDirectiveNode, type DocumentNode, type FieldDefinitionNode, type GraphQLInputType, type GraphQLOutputType, type InputObjectTypeDefinitionNode, type InputValueDefinitionNode, type InterfaceTypeDefinitionNode, Kind, type ObjectTypeDefinitionNode, parse, parseConstValue, } from "graphql"; import { InvalidDirectiveError } from "@/errors"; import { type DirectiveMetadata } from "@/metadata/definitions"; import { type SetRequired } from "@/typings"; export function getDirectiveNode(directive: DirectiveMetadata): ConstDirectiveNode { // Inline and trim start const nameOrDefinition = directive.nameOrDefinition.replaceAll("\n", " ").trimStart(); const { args } = directive; if (nameOrDefinition === "") { throw new InvalidDirectiveError( "Please pass at-least one directive name or definition to the @Directive decorator", ); } if (!nameOrDefinition.startsWith("@")) { return { kind: Kind.DIRECTIVE, name: { kind: Kind.NAME, value: nameOrDefinition, }, arguments: Object.keys(args).map(argKey => ({ kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: argKey, }, value: parseConstValue(args[argKey]), })), }; } let parsed: DocumentNode; try { parsed = parse(`type String ${nameOrDefinition}`); } catch (err) { throw new InvalidDirectiveError( `Error parsing directive definition "${directive.nameOrDefinition}"`, ); } const definitions = parsed.definitions as ObjectTypeDefinitionNode[]; const directives = definitions .filter( (it): it is SetRequired => !!it.directives && it.directives.length > 0, ) .map(it => it.directives) .flat(); if (directives.length !== 1) { throw new InvalidDirectiveError( `Please pass only one directive name or definition at a time to the @Directive decorator "${directive.nameOrDefinition}"`, ); } return directives[0]; } export function getObjectTypeDefinitionNode( name: string, directiveMetadata?: DirectiveMetadata[], ): ObjectTypeDefinitionNode | undefined { if (!directiveMetadata || !directiveMetadata.length) { return undefined; } return { kind: Kind.OBJECT_TYPE_DEFINITION, name: { kind: Kind.NAME, // FIXME: use proper AST representation value: name, }, directives: directiveMetadata.map(getDirectiveNode), }; } export function getInputObjectTypeDefinitionNode( name: string, directiveMetadata?: DirectiveMetadata[], ): InputObjectTypeDefinitionNode | undefined { if (!directiveMetadata || !directiveMetadata.length) { return undefined; } return { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, name: { kind: Kind.NAME, // FIXME: use proper AST representation value: name, }, directives: directiveMetadata.map(getDirectiveNode), }; } export function getFieldDefinitionNode( name: string, type: GraphQLOutputType, directiveMetadata?: DirectiveMetadata[], ): FieldDefinitionNode | undefined { if (!directiveMetadata || !directiveMetadata.length) { return undefined; } return { kind: Kind.FIELD_DEFINITION, type: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: type.toString(), }, }, name: { kind: Kind.NAME, value: name, }, directives: directiveMetadata.map(getDirectiveNode), }; } export function getInputValueDefinitionNode( name: string, type: GraphQLInputType, directiveMetadata?: DirectiveMetadata[], ): InputValueDefinitionNode | undefined { if (!directiveMetadata || !directiveMetadata.length) { return undefined; } return { kind: Kind.INPUT_VALUE_DEFINITION, type: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: type.toString(), }, }, name: { kind: Kind.NAME, value: name, }, directives: directiveMetadata.map(getDirectiveNode), }; } export function getInterfaceTypeDefinitionNode( name: string, directiveMetadata?: DirectiveMetadata[], ): InterfaceTypeDefinitionNode | undefined { if (!directiveMetadata || !directiveMetadata.length) { return undefined; } return { kind: Kind.INTERFACE_TYPE_DEFINITION, name: { kind: Kind.NAME, // FIXME: use proper AST representation value: name, }, directives: directiveMetadata.map(getDirectiveNode), }; } ================================================ FILE: src/schema/schema-generator.ts ================================================ import { Repeater, filter, pipe } from "@graphql-yoga/subscription"; import { type GraphQLDirective, GraphQLEnumType, type GraphQLEnumValueConfigMap, type GraphQLFieldConfigArgumentMap, type GraphQLFieldConfigMap, type GraphQLFieldResolver, type GraphQLInputFieldConfigMap, GraphQLInputObjectType, type GraphQLInputType, GraphQLInterfaceType, type GraphQLNamedType, GraphQLObjectType, type GraphQLOutputType, GraphQLSchema, type GraphQLTypeResolver, GraphQLUnionType, getIntrospectionQuery, graphqlSync, } from "graphql"; import { type TypeOptions, type TypeValue } from "@/decorators/types"; import { CannotDetermineGraphQLTypeError, ConflictingDefaultValuesError, GeneratingSchemaError, InterfaceResolveTypeError, MissingPubSubError, MissingSubscriptionTopicsError, UnionResolveTypeError, } from "@/errors"; import { convertTypeIfScalar, getEnumValuesMap, wrapWithTypeOptions } from "@/helpers/types"; import { type ClassMetadata, type FieldMetadata, type ParamMetadata, type ResolverMetadata, type SubscriptionResolverMetadata, } from "@/metadata/definitions"; import { type InterfaceClassMetadata } from "@/metadata/definitions/interface-class-metadata"; import { type ObjectClassMetadata } from "@/metadata/definitions/object-class-metadata"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type MetadataStorage } from "@/metadata/metadata-storage"; import { createAdvancedFieldResolver, createBasicFieldResolver, createHandlerResolver, wrapResolverWithAuthChecker, } from "@/resolvers/create"; import { type MaybePromise, type SubscribeResolverData, type SubscriptionHandlerData, type TypeResolver, } from "@/typings"; import { ensureInstalledCorrectGraphQLPackage } from "@/utils/graphql-version"; import { BuildContext, type BuildContextOptions } from "./build-context"; import { getFieldDefinitionNode, getInputObjectTypeDefinitionNode, getInputValueDefinitionNode, getInterfaceTypeDefinitionNode, getObjectTypeDefinitionNode, } from "./definition-node"; import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils"; interface ObjectTypeInfo { target: Function; type: GraphQLObjectType; metadata: ObjectClassMetadata; } interface InterfaceTypeInfo { target: Function; type: GraphQLInterfaceType; metadata: InterfaceClassMetadata; } interface InputObjectTypeInfo { target: Function; type: GraphQLInputObjectType; } interface EnumTypeInfo { enumObj: object; type: GraphQLEnumType; } interface UnionTypeInfo { unionSymbol: symbol; type: GraphQLUnionType; } export type SchemaGeneratorOptions = { /** * Array of resolvers classes */ resolvers: Function[]; /** * Array of orphaned type classes that are not used explicitly in GraphQL types definitions */ orphanedTypes?: Function[]; /** * Disable checking on build the correctness of a schema */ skipCheck?: boolean; /** * Array of graphql directives */ directives?: GraphQLDirective[]; } & BuildContextOptions; export abstract class SchemaGenerator { private static objectTypesInfoMap = new Map(); private static inputTypesInfoMap = new Map(); private static interfaceTypesInfoMap = new Map(); private static enumTypesInfoMap = new Map(); private static unionTypesInfoMap = new Map(); private static usedInterfaceTypes = new Set(); private static metadataStorage: MetadataStorage; static generateFromMetadata(options: SchemaGeneratorOptions): GraphQLSchema { this.metadataStorage = getMetadataStorage().clone(); this.metadataStorage.build(options); this.checkForErrors(options); BuildContext.create(options); this.buildTypesInfo(options.resolvers); const orphanedTypes = options.orphanedTypes ?? []; const prebuiltSchema = new GraphQLSchema({ query: this.buildRootQueryType(options.resolvers), mutation: this.buildRootMutationType(options.resolvers), subscription: this.buildRootSubscriptionType(options.resolvers), directives: options.directives, }); const finalSchema = new GraphQLSchema({ ...prebuiltSchema.toConfig(), // run after first build to make `usedInterfaceTypes` working types: this.buildOtherTypes(orphanedTypes), }); // cleanup after build to prevent memory leaks // and to reset state for next possible builds BuildContext.reset(); this.usedInterfaceTypes = new Set(); this.objectTypesInfoMap = new Map(); this.inputTypesInfoMap = new Map(); this.interfaceTypesInfoMap = new Map(); this.enumTypesInfoMap = new Map(); this.unionTypesInfoMap = new Map(); if (!options.skipCheck) { const { errors } = graphqlSync({ schema: finalSchema, source: getIntrospectionQuery() }); if (errors) { throw new GeneratingSchemaError(errors); } } return finalSchema; } private static checkForErrors(options: SchemaGeneratorOptions) { ensureInstalledCorrectGraphQLPackage(); if (this.metadataStorage.authorizedFields.length !== 0 && options.authChecker === undefined) { throw new Error( "You need to provide `authChecker` function for `@Authorized` decorator usage!", ); } } private static getDefaultValue( typeInstance: Record, typeOptions: TypeOptions, fieldName: string, typeName: string, ): unknown | undefined { const { disableInferringDefaultValues } = BuildContext; if (disableInferringDefaultValues) { return typeOptions.defaultValue; } const defaultValueFromInitializer = typeInstance[fieldName]; if ( typeOptions.defaultValue !== undefined && defaultValueFromInitializer !== undefined && typeOptions.defaultValue !== defaultValueFromInitializer ) { throw new ConflictingDefaultValuesError( typeName, fieldName, typeOptions.defaultValue, defaultValueFromInitializer, ); } return typeOptions.defaultValue !== undefined ? typeOptions.defaultValue : defaultValueFromInitializer; } private static buildTypesInfo(resolvers: Function[]) { this.unionTypesInfoMap = new Map( this.metadataStorage.unions.map(unionMetadata => { // use closure to capture values from this selected schema build const unionObjectTypesInfo: ObjectTypeInfo[] = []; // called once after building all `objectTypesInfo` const typesThunk = () => { unionObjectTypesInfo.push( ...unionMetadata .getClassTypes() .map(objectTypeCls => this.objectTypesInfoMap.get(objectTypeCls)!), ); return unionObjectTypesInfo.map(it => it.type); }; const unionTypeInfo: UnionTypeInfo = { unionSymbol: unionMetadata.symbol, type: new GraphQLUnionType({ name: unionMetadata.name, description: unionMetadata.description, types: typesThunk, resolveType: unionMetadata.resolveType ? this.getResolveTypeFunction( unionMetadata.resolveType, // use closure captured `unionObjectTypesInfo` unionObjectTypesInfo, ) : instance => { const instanceTarget = unionMetadata .getClassTypes() .find(ObjectClassType => instance instanceof ObjectClassType); if (!instanceTarget) { throw new UnionResolveTypeError(unionMetadata); } // use closure captured `unionObjectTypesInfo` const objectTypeInfo = unionObjectTypesInfo.find( type => type.target === instanceTarget, ); return objectTypeInfo?.type.name; }, }), }; return [unionMetadata.symbol, unionTypeInfo]; }), ); this.enumTypesInfoMap = new Map( this.metadataStorage.enums.map(enumMetadata => { const enumMap = getEnumValuesMap(enumMetadata.enumObj); const enumTypeInfo: EnumTypeInfo = { enumObj: enumMetadata.enumObj, type: new GraphQLEnumType({ name: enumMetadata.name, description: enumMetadata.description, values: Object.keys(enumMap).reduce( (enumConfig, enumKey) => { const valueConfig = enumMetadata.valuesConfig[enumKey] || {}; // eslint-disable-next-line no-param-reassign enumConfig[enumKey] = { value: enumMap[enumKey], description: valueConfig.description, deprecationReason: valueConfig.deprecationReason, }; return enumConfig; }, {}, ), }), }; return [enumMetadata.enumObj, enumTypeInfo]; }), ); this.objectTypesInfoMap = new Map( this.metadataStorage.objectTypes.map(objectType => { const objectSuperClass = Object.getPrototypeOf(objectType.target); const hasExtended = objectSuperClass.prototype !== undefined; const getSuperClassType = () => { const superClassTypeInfo = this.objectTypesInfoMap.get(objectSuperClass) ?? this.interfaceTypesInfoMap.get(objectSuperClass); return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; const interfaceClasses = objectType.interfaceClasses || []; const objectTypeInfo: ObjectTypeInfo = { metadata: objectType, target: objectType.target, type: new GraphQLObjectType({ name: objectType.name, description: objectType.description, astNode: getObjectTypeDefinitionNode(objectType.name, objectType.directives), extensions: objectType.extensions, interfaces: () => { let interfaces = interfaceClasses.map(interfaceClass => { const interfaceTypeInfo = this.interfaceTypesInfoMap.get(interfaceClass); if (!interfaceTypeInfo) { throw new Error( `Cannot find interface type metadata for class '${interfaceClass.name}' ` + `provided in 'implements' option for '${objectType.target.name}' object type class. ` + `Please make sure that class is annotated with an '@InterfaceType()' decorator.`, ); } return interfaceTypeInfo.type; }); // copy interfaces from super class if (hasExtended) { const superClass = getSuperClassType(); if (superClass) { const superInterfaces = superClass.getInterfaces(); interfaces = Array.from(new Set(interfaces.concat(superInterfaces))); } } return interfaces; }, fields: () => { const fieldsMetadata: FieldMetadata[] = []; // support for implicitly implementing interfaces // get fields from interfaces definitions if (objectType.interfaceClasses) { const implementedInterfaces = this.metadataStorage.interfaceTypes.filter(it => objectType.interfaceClasses!.includes(it.target), ); implementedInterfaces.forEach(it => { fieldsMetadata.push(...(it.fields || [])); }); } // push own fields at the end to overwrite the one inherited from interface fieldsMetadata.push(...objectType.fields!); let fields = fieldsMetadata.reduce>( (fieldsMap, field) => { const { fieldResolvers } = this.metadataStorage; const filteredFieldResolversMetadata = fieldResolvers.filter( it => it.kind === "internal" || resolvers.includes(it.target), ); const fieldResolverMetadata = filteredFieldResolversMetadata.find( it => it.getObjectType!() === field.target && it.schemaName === field.schemaName, ); const type = this.getGraphQLOutputType( field.target, field.name, field.getType(), field.typeOptions, ); const isSimpleResolver = // eslint-disable-next-line no-nested-ternary field.simple !== undefined ? field.simple === true : objectType.simpleResolvers !== undefined ? objectType.simpleResolvers === true : false; // eslint-disable-next-line no-param-reassign fieldsMap[field.schemaName] = { type, args: this.generateHandlerArgs(field.target, field.name, field.params!), // eslint-disable-next-line no-nested-ternary resolve: fieldResolverMetadata ? createAdvancedFieldResolver(fieldResolverMetadata) : isSimpleResolver ? undefined : createBasicFieldResolver(field), description: field.description, deprecationReason: field.deprecationReason, astNode: getFieldDefinitionNode(field.name, type, field.directives), extensions: { complexity: field.complexity, ...field.extensions, ...fieldResolverMetadata?.extensions, }, }; return fieldsMap; }, {}, ); // support for extending classes - get field info from prototype if (hasExtended) { const superClass = getSuperClassType(); if (superClass) { const superClassFields = getFieldMetadataFromObjectType(superClass); fields = { ...superClassFields, ...fields }; } } return fields; }, }), }; return [objectType.target, objectTypeInfo]; }), ); this.interfaceTypesInfoMap = new Map( this.metadataStorage.interfaceTypes.map(interfaceType => { const interfaceSuperClass = Object.getPrototypeOf(interfaceType.target); const hasExtended = interfaceSuperClass.prototype !== undefined; const getSuperClassType = () => { const superClassTypeInfo = this.interfaceTypesInfoMap.get(interfaceSuperClass); return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; // fetch ahead the subset of object types that implements this interface const implementingObjectTypesTargets = this.metadataStorage.objectTypes .filter( objectType => objectType.interfaceClasses && objectType.interfaceClasses.includes(interfaceType.target), ) .map(objectType => objectType.target); const implementingObjectTypesInfo = [...this.objectTypesInfoMap.values()].filter( objectTypesInfo => implementingObjectTypesTargets.includes(objectTypesInfo.target), ); const interfaceTypeInfo: InterfaceTypeInfo = { metadata: interfaceType, target: interfaceType.target, type: new GraphQLInterfaceType({ name: interfaceType.name, description: interfaceType.description, astNode: getInterfaceTypeDefinitionNode(interfaceType.name, interfaceType.directives), extensions: interfaceType.extensions, interfaces: () => { let interfaces = (interfaceType.interfaceClasses || []).map( interfaceClass => this.interfaceTypesInfoMap.get(interfaceClass)!.type, ); // copy interfaces from super class if (hasExtended) { const superClass = getSuperClassType(); if (superClass) { const superInterfaces = superClass.getInterfaces(); interfaces = Array.from(new Set(interfaces.concat(superInterfaces))); } } return interfaces; }, fields: () => { const fieldsMetadata: FieldMetadata[] = []; // support for implicitly implementing interfaces // get fields from interfaces definitions if (interfaceType.interfaceClasses) { const implementedInterfacesMetadata = this.metadataStorage.interfaceTypes.filter( it => interfaceType.interfaceClasses!.includes(it.target), ); implementedInterfacesMetadata.forEach(it => { fieldsMetadata.push(...(it.fields || [])); }); } // push own fields at the end to overwrite the one inherited from interface fieldsMetadata.push(...interfaceType.fields!); let fields = fieldsMetadata!.reduce>( (fieldsMap, field) => { const fieldResolverMetadata = this.metadataStorage.fieldResolvers.find( resolver => resolver.getObjectType!() === field.target && resolver.schemaName === field.schemaName, ); const type = this.getGraphQLOutputType( field.target, field.name, field.getType(), field.typeOptions, ); // eslint-disable-next-line no-param-reassign fieldsMap[field.schemaName] = { type, args: this.generateHandlerArgs(field.target, field.name, field.params!), resolve: fieldResolverMetadata ? createAdvancedFieldResolver(fieldResolverMetadata) : createBasicFieldResolver(field), description: field.description, deprecationReason: field.deprecationReason, astNode: getFieldDefinitionNode(field.name, type, field.directives), extensions: { complexity: field.complexity, ...field.extensions, }, }; return fieldsMap; }, {}, ); // support for extending interface classes - get field info from prototype if (hasExtended) { const superClass = getSuperClassType(); if (superClass) { const superClassFields = getFieldMetadataFromObjectType(superClass); fields = { ...superClassFields, ...fields }; } } return fields; }, resolveType: interfaceType.resolveType ? this.getResolveTypeFunction(interfaceType.resolveType, implementingObjectTypesInfo) : instance => { const typeTarget = implementingObjectTypesTargets.find( typeCls => instance instanceof typeCls, ); if (!typeTarget) { throw new InterfaceResolveTypeError(interfaceType); } const objectTypeInfo = implementingObjectTypesInfo.find( type => type.target === typeTarget, ); return objectTypeInfo?.type.name; }, }), }; return [interfaceType.target, interfaceTypeInfo]; }), ); this.inputTypesInfoMap = new Map( this.metadataStorage.inputTypes.map(inputType => { const objectSuperClass = Object.getPrototypeOf(inputType.target); const getSuperClassType = () => { const superClassTypeInfo = this.inputTypesInfoMap.get(objectSuperClass); return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; const inputInstance = new (inputType.target as any)(); const inputTypeInfo: InputObjectTypeInfo = { target: inputType.target, type: new GraphQLInputObjectType({ name: inputType.name, description: inputType.description, extensions: inputType.extensions, fields: () => { let fields = inputType.fields!.reduce( (fieldsMap, field) => { const defaultValue = this.getDefaultValue( inputInstance, field.typeOptions, field.name, inputType.name, ); const type = this.getGraphQLInputType(field.target, field.name, field.getType(), { ...field.typeOptions, defaultValue, }); // eslint-disable-next-line no-param-reassign fieldsMap[field.name] = { description: field.description, type, defaultValue, astNode: getInputValueDefinitionNode(field.name, type, field.directives), extensions: field.extensions, deprecationReason: field.deprecationReason, }; return fieldsMap; }, {}, ); // support for extending classes - get field info from prototype if (objectSuperClass.prototype !== undefined) { const superClass = getSuperClassType(); if (superClass) { const superClassFields = getFieldMetadataFromInputType(superClass); fields = { ...superClassFields, ...fields }; } } return fields; }, astNode: getInputObjectTypeDefinitionNode(inputType.name, inputType.directives), }), }; return [inputType.target, inputTypeInfo]; }), ); } private static buildRootQueryType(resolvers: Function[]): GraphQLObjectType { const queriesHandlers = this.filterHandlersByResolvers(this.metadataStorage.queries, resolvers); return new GraphQLObjectType({ name: "Query", fields: this.generateHandlerFields(queriesHandlers), }); } private static buildRootMutationType(resolvers: Function[]): GraphQLObjectType | undefined { const mutationsHandlers = this.filterHandlersByResolvers( this.metadataStorage.mutations, resolvers, ); if (mutationsHandlers.length === 0) { return undefined; } return new GraphQLObjectType({ name: "Mutation", fields: this.generateHandlerFields(mutationsHandlers), }); } private static buildRootSubscriptionType(resolvers: Function[]): GraphQLObjectType | undefined { const subscriptionsHandlers = this.filterHandlersByResolvers( this.metadataStorage.subscriptions, resolvers, ); if (subscriptionsHandlers.length === 0) { return undefined; } return new GraphQLObjectType({ name: "Subscription", fields: this.generateSubscriptionsFields(subscriptionsHandlers), }); } private static buildOtherTypes(orphanedTypes: Function[]): GraphQLNamedType[] { const autoRegisteredObjectTypesInfo = [...this.objectTypesInfoMap.values()].filter(typeInfo => typeInfo.metadata.interfaceClasses?.some(interfaceClass => { const implementedInterfaceInfo = this.interfaceTypesInfoMap.get(interfaceClass); if (!implementedInterfaceInfo) { return false; } if (implementedInterfaceInfo.metadata.autoRegisteringDisabled) { return false; } if (!this.usedInterfaceTypes.has(interfaceClass)) { return false; } return true; }), ); return [ ...this.filterTypesInfoByOrphanedTypesAndExtractType( [...this.objectTypesInfoMap.values()], orphanedTypes, ), ...this.filterTypesInfoByOrphanedTypesAndExtractType( [...this.interfaceTypesInfoMap.values()], orphanedTypes, ), ...this.filterTypesInfoByOrphanedTypesAndExtractType( [...this.inputTypesInfoMap.values()], orphanedTypes, ), ...autoRegisteredObjectTypesInfo.map(typeInfo => typeInfo.type), ]; } private static generateHandlerFields( handlers: ResolverMetadata[], ): GraphQLFieldConfigMap { return handlers.reduce>((fields, handler) => { const type = this.getGraphQLOutputType( handler.target, handler.methodName, handler.getReturnType(), handler.returnTypeOptions, ); // eslint-disable-next-line no-param-reassign fields[handler.schemaName] = { type, args: this.generateHandlerArgs(handler.target, handler.methodName, handler.params!), resolve: createHandlerResolver(handler), description: handler.description, deprecationReason: handler.deprecationReason, astNode: getFieldDefinitionNode(handler.schemaName, type, handler.directives), extensions: { complexity: handler.complexity, ...handler.extensions, }, }; return fields; }, {}); } private static generateSubscriptionsFields< TSource extends object = any, TContext extends object = any, >( subscriptionsHandlers: SubscriptionResolverMetadata[], ): GraphQLFieldConfigMap { if (!subscriptionsHandlers.length) { return {}; } const { pubSub, container } = BuildContext; if (!pubSub) { throw new MissingPubSubError(); } const basicFields = this.generateHandlerFields(subscriptionsHandlers); return subscriptionsHandlers.reduce>( (fields, handler) => { let subscribeFn: GraphQLFieldResolver< TSource, TContext, any, MaybePromise> >; if (handler.subscribe) { subscribeFn = (source, args, context, info) => { const subscribeResolverData: SubscribeResolverData = { source, args, context, info }; return handler.subscribe!(subscribeResolverData); }; } else { subscribeFn = (source, args, context, info) => { const subscribeResolverData: SubscribeResolverData = { source, args, context, info }; let topics: string | string[]; if (typeof handler.topics === "function") { const getTopics = handler.topics; topics = getTopics(subscribeResolverData); } else { topics = handler.topics!; } const topicId = handler.topicId?.(subscribeResolverData); let pubSubIterable: AsyncIterable; if (!Array.isArray(topics)) { pubSubIterable = pubSub.subscribe(topics, topicId); } else { if (topics.length === 0) { throw new MissingSubscriptionTopicsError(handler.target, handler.methodName); } pubSubIterable = Repeater.merge([ ...topics.map(topic => pubSub.subscribe(topic, topicId)), ]); } if (!handler.filter) { return pubSubIterable; } return pipe( pubSubIterable, filter(payload => { const handlerData: SubscriptionHandlerData = { payload, args, context, info }; return handler.filter!(handlerData); }), ); }; } // eslint-disable-next-line no-param-reassign fields[handler.schemaName].subscribe = wrapResolverWithAuthChecker( subscribeFn, container, handler.roles, ); return fields; }, basicFields, ); } private static generateHandlerArgs( target: Function, propertyName: string, params: ParamMetadata[], ): GraphQLFieldConfigArgumentMap { return params!.reduce((args, param) => { if (param.kind === "arg" || (param.kind === "custom" && param.options?.arg)) { const input = param.kind === "arg" ? param : param.options.arg!; const type = this.getGraphQLInputType( target, propertyName, input.getType(), input.typeOptions, input.index, input.name, ); const argDirectives = this.metadataStorage.argumentDirectives .filter( it => it.target === target && it.fieldName === propertyName && it.parameterIndex === param.index, ) .map(it => it.directive); // eslint-disable-next-line no-param-reassign args[input.name] = { description: input.description, type, defaultValue: input.typeOptions.defaultValue, deprecationReason: input.deprecationReason, astNode: getInputValueDefinitionNode(input.name, type, argDirectives), }; } else if (param.kind === "args") { const argumentType = this.metadataStorage.argumentTypes.find( it => it.target === param.getType(), ); if (!argumentType) { throw new Error( `The value used as a type of '@Args' for '${propertyName}' of '${target.name}' ` + `is not a class decorated with '@ArgsType' decorator!`, ); } const inheritanceChainClasses: Function[] = [argumentType.target]; for ( let superClass = argumentType.target; superClass.prototype !== undefined; superClass = Object.getPrototypeOf(superClass) ) { inheritanceChainClasses.push(superClass); } for (const argsTypeClass of inheritanceChainClasses.reverse()) { const inheritedArgumentType = this.metadataStorage.argumentTypes.find( it => it.target === argsTypeClass, ); if (inheritedArgumentType) { this.mapArgFields(inheritedArgumentType, args); } } } return args; }, {}); } private static mapArgFields( argumentType: ClassMetadata, args: GraphQLFieldConfigArgumentMap = {}, ) { const argumentInstance = new (argumentType.target as any)(); argumentType.fields!.forEach(field => { const defaultValue = this.getDefaultValue( argumentInstance, field.typeOptions, field.name, argumentType.name, ); const type = this.getGraphQLInputType(field.target, field.name, field.getType(), { ...field.typeOptions, defaultValue, }); // eslint-disable-next-line no-param-reassign args[field.schemaName] = { description: field.description, type, defaultValue, astNode: getInputValueDefinitionNode(field.name, type, field.directives), extensions: field.extensions, deprecationReason: field.deprecationReason, }; }); } private static getGraphQLOutputType( target: Function, propertyName: string, type: TypeValue, typeOptions: TypeOptions = {}, ): GraphQLOutputType { let gqlType: GraphQLOutputType | undefined; gqlType = convertTypeIfScalar(type); if (!gqlType) { const objectType = this.objectTypesInfoMap.get(type as Function); if (objectType) { gqlType = objectType.type; } } if (!gqlType) { const interfaceType = this.interfaceTypesInfoMap.get(type as Function); if (interfaceType) { this.usedInterfaceTypes.add(interfaceType.target); gqlType = interfaceType.type; } } if (!gqlType) { const enumType = this.enumTypesInfoMap.get(type as object); if (enumType) { gqlType = enumType.type; } } if (!gqlType) { const unionType = this.unionTypesInfoMap.get(type as symbol); if (unionType) { gqlType = unionType.type; } } if (!gqlType) { throw new CannotDetermineGraphQLTypeError("output", target.name, propertyName); } const { nullableByDefault } = BuildContext; return wrapWithTypeOptions(target, propertyName, gqlType, typeOptions, nullableByDefault); } private static getGraphQLInputType( target: Function, propertyName: string, type: TypeValue, typeOptions: TypeOptions = {}, parameterIndex?: number, argName?: string, ): GraphQLInputType { let gqlType: GraphQLInputType | undefined; gqlType = convertTypeIfScalar(type); if (!gqlType) { const inputType = this.inputTypesInfoMap.get(type as Function); if (inputType) { gqlType = inputType.type; } } if (!gqlType) { const enumType = this.enumTypesInfoMap.get(type as object); if (enumType) { gqlType = enumType.type; } } if (!gqlType) { throw new CannotDetermineGraphQLTypeError( "input", target.name, propertyName, parameterIndex, argName, ); } const { nullableByDefault } = BuildContext; return wrapWithTypeOptions(target, propertyName, gqlType, typeOptions, nullableByDefault); } private static getResolveTypeFunction( resolveType: TypeResolver, possibleObjectTypesInfo: ObjectTypeInfo[], ): GraphQLTypeResolver { return async (...args) => { const resolvedType = await resolveType(...args); if (!resolvedType || typeof resolvedType === "string") { return resolvedType ?? undefined; } return possibleObjectTypesInfo.find(objectType => objectType.target === resolvedType)?.type .name; }; } private static filterHandlersByResolvers( handlers: T[], resolvers: Function[], ) { return handlers.filter(query => resolvers.includes(query.target)); } private static filterTypesInfoByOrphanedTypesAndExtractType( typesInfo: Array, orphanedTypes: Function[], ) { return typesInfo.filter(it => orphanedTypes.includes(it.target)).map(it => it.type); } } ================================================ FILE: src/schema/utils.ts ================================================ import { type GraphQLFieldConfigArgumentMap, type GraphQLFieldConfigMap, type GraphQLInputFieldConfigMap, type GraphQLInputObjectType, type GraphQLInterfaceType, type GraphQLObjectType, } from "graphql"; export function getFieldMetadataFromInputType(type: GraphQLInputObjectType) { const fieldInfo = type.getFields(); const typeFields = Object.keys(fieldInfo).reduce( (fieldsMap, fieldName) => { const superField = fieldInfo[fieldName]; // eslint-disable-next-line no-param-reassign fieldsMap[fieldName] = { type: superField.type, astNode: superField.astNode, description: superField.description, defaultValue: superField.defaultValue, }; return fieldsMap; }, {}, ); return typeFields; } export function getFieldMetadataFromObjectType(type: GraphQLObjectType | GraphQLInterfaceType) { const fieldInfo = type.getFields(); const typeFields = Object.keys(fieldInfo).reduce>( (fieldsMap, fieldName) => { const superField = fieldInfo[fieldName]; // eslint-disable-next-line no-param-reassign fieldsMap[fieldName] = { type: superField.type, args: superField.args.reduce((argMap, { name, ...arg }) => { // eslint-disable-next-line no-param-reassign argMap[name] = arg; return argMap; }, {}), astNode: superField.astNode, resolve: superField.resolve, description: superField.description, deprecationReason: superField.deprecationReason, extensions: superField.extensions, }; return fieldsMap; }, {}, ); return typeFields; } ================================================ FILE: src/shim.ts ================================================ /* This "shim" can be used on the frontend to prevent from errors on undefined decorators, when you are sharing same classes across backend and frontend. To use this shim, simply set up your Webpack configuration to use this file instead of a normal TypeGraphQL module. ```js plugins: [ // ...here are any other existing plugins that you already have new webpack.NormalModuleReplacementPlugin(/type-graphql$/, resource => { resource.request = resource.request.replace(/type-graphql/, "type-graphql/shim"); }), ] ``` However, in some TypeScript projects like the ones using Angular, which AoT compiler requires that a full `*.ts` file is provided instead of just a `*.js` and `*.d.ts` files, to use this shim we have to simply set up our TypeScript configuration in `tsconfig.json` to use this file instead of a normal TypeGraphQL module: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "type-graphql": ["./node_modules/type-graphql/build/typings/shim.ts"] } } } ``` */ import type * as src from "./index"; export const dummyValue = ""; export function dummyFn() {} export function dummyDecorator() { return dummyFn; } export const Arg: typeof src.Arg = dummyDecorator; export const Args: typeof src.Args = dummyDecorator; export const ArgsType: typeof src.ArgsType = dummyDecorator; export const Authorized: typeof src.Authorized = dummyDecorator; export const createParameterDecorator: typeof src.createParameterDecorator = dummyFn as any; export const createMethodMiddlewareDecorator: typeof src.createMethodMiddlewareDecorator = dummyFn as any; export const createResolverClassMiddlewareDecorator: typeof src.createResolverClassMiddlewareDecorator = dummyFn as any; export const Ctx: typeof src.Ctx = dummyDecorator; export const Directive: typeof src.Directive = dummyDecorator; export const Extensions: typeof src.Extensions = dummyDecorator; export const registerEnumType: typeof src.registerEnumType = dummyFn; export const Field: typeof src.Field = dummyDecorator; export const FieldResolver: typeof src.FieldResolver = dummyDecorator; export const Info: typeof src.Info = dummyDecorator; export const InputType: typeof src.InputType = dummyDecorator; export const InterfaceType: typeof src.InterfaceType = dummyDecorator; export const Mutation: typeof src.Mutation = dummyDecorator; export const ObjectType: typeof src.ObjectType = dummyDecorator; export const Query: typeof src.Query = dummyDecorator; export const Resolver: typeof src.Resolver = dummyDecorator; export const Root: typeof src.Root = dummyDecorator; export const Subscription: typeof src.Subscription = dummyDecorator; export const createUnionType: typeof src.createUnionType = dummyFn as any; export const UseMiddleware: typeof src.UseMiddleware = dummyDecorator; export const Int: typeof src.Int = dummyValue as any; export const Float: typeof src.Float = dummyValue as any; export const ID: typeof src.ID = dummyValue as any; export const GraphQLISODateTime: typeof src.GraphQLISODateTime = dummyValue as any; export const GraphQLTimestamp: typeof src.GraphQLTimestamp = dummyValue as any; ================================================ FILE: src/typings/Complexity.ts ================================================ import { type ComplexityEstimator } from "graphql-query-complexity"; export type Complexity = ComplexityEstimator | number; ================================================ FILE: src/typings/ResolverInterface.ts ================================================ /** * Resolver classes can implement this type * to provide a proper resolver method signatures for fields of T. */ export type ResolverInterface = { [P in keyof T]?: (root: T, ...args: any[]) => T[P] | Promise; }; ================================================ FILE: src/typings/SubscribeResolverData.ts ================================================ import { type GraphQLResolveInfo } from "graphql"; import { type ArgsDictionary } from "./resolver-data"; export interface SubscribeResolverData { source: TSource; args: TArgs; context: TContext; info: GraphQLResolveInfo; } ================================================ FILE: src/typings/SubscriptionHandlerData.ts ================================================ import { type GraphQLResolveInfo } from "graphql"; import { type ArgsDictionary } from "./resolver-data"; export interface SubscriptionHandlerData { payload: TPayload; args: TArgs; context: TContext; info: GraphQLResolveInfo; } ================================================ FILE: src/typings/TypeResolver.ts ================================================ import { type GraphQLTypeResolver } from "graphql"; import { type ClassType, type Maybe, type MaybePromise } from "./utils"; export type TypeResolver = ( ...args: Parameters> ) => MaybePromise>; ================================================ FILE: src/typings/ValidatorFn.ts ================================================ import { type TypeValue } from "@/decorators/types"; import { type ResolverData } from "./resolver-data"; export type ValidatorFn = ( /** * The value of the argument. * It can by of any type, which means: * - undefined or null (if the argument is nullable) * - primitive type (string, number, boolean) * - underlying scalar type (Date, Buffer, etc.) * - object type (arg type class instance) * - array type (array of any of the above) */ argValue: any | undefined, argType: TypeValue, resolverData: ResolverData, ) => void | Promise; ================================================ FILE: src/typings/auth-checker.ts ================================================ import { type ResolverData } from "./resolver-data"; import { type ClassType } from "./utils"; export type AuthCheckerFn = ( resolverData: ResolverData, roles: TRoleType[], ) => boolean | Promise; export interface AuthCheckerInterface { check(resolverData: ResolverData, roles: TRoleType[]): boolean | Promise; } export type AuthChecker = | AuthCheckerFn | ClassType>; export type AuthMode = "error" | "null"; ================================================ FILE: src/typings/index.ts ================================================ export * from "./utils"; export * from "./auth-checker"; export * from "./Complexity"; export * from "./legacy-decorators"; export type { MiddlewareFn, NextFn, MiddlewareInterface } from "./middleware"; export * from "./resolver-data"; export * from "./ResolverInterface"; export * from "./resolvers-map"; export * from "./SubscribeResolverData"; export * from "./SubscriptionHandlerData"; export * from "./subscriptions"; export * from "./TypeResolver"; export * from "./ValidatorFn"; ================================================ FILE: src/typings/legacy-decorators.ts ================================================ export type ParameterDecorator = ( target: Object, propertyKey: string | symbol, // Removed 'undefined' from TS 5.0 parameterIndex: number, ) => void; ================================================ FILE: src/typings/middleware.ts ================================================ import { type ResolverData } from "./resolver-data"; export type NextFn = () => Promise; export type MiddlewareFn = ( action: ResolverData, next: NextFn, ) => Promise; export interface MiddlewareInterface { use: MiddlewareFn; } export type MiddlewareClass = new ( ...args: any[] ) => MiddlewareInterface; export type Middleware = | MiddlewareFn | MiddlewareClass; ================================================ FILE: src/typings/resolver-data.ts ================================================ import { type GraphQLResolveInfo } from "graphql"; export type ArgsDictionary = Record; export interface ResolverData { root: any; args: ArgsDictionary; context: TContextType; info: GraphQLResolveInfo; } ================================================ FILE: src/typings/resolvers-map.ts ================================================ import { type GraphQLFieldResolver, type GraphQLIsTypeOfFn, type GraphQLScalarType, type GraphQLTypeResolver, } from "graphql"; export type ResolversMap = Record< string, | ResolverObject | ResolverOptions | GraphQLScalarType | EnumResolver >; export type ResolverObject = Record< string, ResolverOptions | GraphQLFieldResolver >; export type EnumResolver = Record; export interface ResolverOptions { fragment?: string; resolve?: GraphQLFieldResolver; subscribe?: GraphQLFieldResolver; __resolveType?: GraphQLTypeResolver; __isTypeOf?: GraphQLIsTypeOfFn; } ================================================ FILE: src/typings/subscriptions.ts ================================================ export interface PubSub { /** * Publish a value for a given topic. */ publish(routingKey: string, ...args: unknown[]): void; /** * Subscribe to a topic. */ subscribe(routingKey: string, dynamicId?: unknown): AsyncIterable; } ================================================ FILE: src/typings/utils/ClassType.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/basic.d.ts) import { type Constructor } from "./Constructor"; /** Matches a [`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). */ export type ClassType = Constructor< T, Arguments > & { prototype: T; }; ================================================ FILE: src/typings/utils/Constructor.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/basic.d.ts) /** Matches a [`class` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). */ export type Constructor = new ( ...arguments_: Arguments ) => T; ================================================ FILE: src/typings/utils/Except.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/except.d.ts) import { type IsEqual } from "./IsEqual"; /** Filter out keys from an object. Returns `never` if `Exclude` is strictly equal to `Key`. Returns `never` if `Key` extends `Exclude`. Returns `Key` otherwise. @example ``` type Filtered = Filter<'foo', 'foo'>; //=> never ``` @example ``` type Filtered = Filter<'bar', string>; //=> never ``` @example ``` type Filtered = Filter<'bar', 'foo'>; //=> 'bar' ``` */ type Filter = IsEqual extends true ? never : KeyType extends ExcludeType ? never : KeyType; interface ExceptOptions { /** Disallow assigning non-specified properties. Note that any omitted properties in the resulting type will be present in autocomplete as `undefined`. @defaultValue false */ requireExactProps?: boolean; } /** Create a type from an object type without certain keys. We recommend setting the `requireExactProps` option to `true`. This type is a stricter version of [`Omit`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#the-omit-helper-type). The `Omit` type does not restrict the omitted keys to be keys present on the given type, while `Except` does. The benefits of a stricter type are avoiding typos and allowing the compiler to pick up on rename refactors automatically. This type was proposed to the TypeScript team, which declined it, saying they prefer that libraries implement stricter versions of the built-in types ([microsoft/TypeScript#30825](https://github.com/microsoft/TypeScript/issues/30825#issuecomment-523668235)). @example ``` import {Except} from 'type-fest'; type Foo = { a: number; b: string; }; type FooWithoutA = Except; //=> {b: string} const fooWithoutA: FooWithoutA = {a: 1, b: '2'}; //=> errors: 'a' does not exist in type '{ b: string; }' type FooWithoutB = Except; //=> {a: number} & Partial> const fooWithoutB: FooWithoutB = {a: 1, b: '2'}; //=> errors at 'b': Type 'string' is not assignable to type 'undefined'. ``` */ export type Except< ObjectType, KeysType extends keyof ObjectType, Options extends ExceptOptions = { requireExactProps: false }, > = { [KeyType in keyof ObjectType as Filter]: ObjectType[KeyType]; } & (Options["requireExactProps"] extends true ? Partial> : {}); ================================================ FILE: src/typings/utils/IsEqual.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/is-equal.d.ts) /** Returns a boolean for whether the two given types are equal. {@link https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650} {@link https://stackoverflow.com/questions/68961864/how-does-the-equals-work-in-typescript/68963796#68963796} Use-cases: - If you want to make a conditional branch based on the result of a comparison of two types. @example ``` import {IsEqual} from 'type-fest'; // This type returns a boolean for whether the given array includes the given item. // `IsEqual` is used to compare the given array at position 0 and the given item and then return true if they are equal. type Includes = Value extends readonly [Value[0], ...infer rest] ? IsEqual extends true ? true : Includes : false; ``` */ export type IsEqual = (() => G extends A ? 1 : 2) extends () => G extends B ? 1 : 2 ? true : false; ================================================ FILE: src/typings/utils/Maybe.ts ================================================ export type Maybe = T | null | undefined; ================================================ FILE: src/typings/utils/MaybePromise.ts ================================================ export type MaybePromise = Promise | T; ================================================ FILE: src/typings/utils/MergeExclusive.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/merge-exclusive.d.ts) // Helper type. Not useful on its own. type Without = { [KeyType in Exclude]?: never; }; /** Create a type that has mutually exclusive keys. This type was inspired by [this comment](https://github.com/Microsoft/TypeScript/issues/14094#issuecomment-373782604). This type works with a helper type, called `Without`. `Without` produces a type that has only keys from `FirstType` which are not present on `SecondType` and sets the value type for these keys to `never`. This helper type is then used in `MergeExclusive` to remove keys from either `FirstType` or `SecondType`. @example ``` import {MergeExclusive} from 'type-fest'; interface ExclusiveVariation1 { exclusive1: boolean; } interface ExclusiveVariation2 { exclusive2: string; } type ExclusiveOptions = MergeExclusive; let exclusiveOptions: ExclusiveOptions; exclusiveOptions = {exclusive1: true}; //=> Works exclusiveOptions = {exclusive2: 'hi'}; //=> Works exclusiveOptions = {exclusive1: true, exclusive2: 'hi'}; //=> Error ``` */ export type MergeExclusive = FirstType | SecondType extends object ? (Without & SecondType) | (Without & FirstType) : FirstType | SecondType; ================================================ FILE: src/typings/utils/NonEmptyArray.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts) export type NonEmptyArray = readonly [T, ...T[]] | [T, ...T[]]; ================================================ FILE: src/typings/utils/SetRequired.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts) import { type Except } from "./Except"; import { type Simplify } from "./Simplify"; /** Create a type that makes the given keys required. The remaining keys are kept as is. The sister of the `SetOptional` type. Use-case: You want to define a single model where the only thing that changes is whether or not some of the keys are required. @example ``` import {SetRequired} from 'type-fest'; type Foo = { a?: number; b: string; c?: boolean; } type SomeRequired = SetRequired; // type SomeRequired = { // a?: number; // b: string; // Was already required and still is. // c: boolean; // Is now required. // } ``` */ export type SetRequired = Simplify< // Pick just the keys that are optional from the base type. Except & // Pick the keys that should be required from the base type and make them required. Required> >; ================================================ FILE: src/typings/utils/Simplify.ts ================================================ // Copied from 'type-fest' (https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts) /** Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. @example ``` import {Simplify} from 'type-fest'; type PositionProps = { top: number; left: number; }; type SizeProps = { width: number; height: number; }; // In your editor, hovering over `Props` will show a flattened object with all the properties. type Props = Simplify; ``` Sometimes it is desired to pass a value as a function argument that has a different type. At first inspection it may seem assignable, and then you discover it is not because the `value`'s type definition was defined as an interface. In the following example, `fn` requires an argument of type `Record`. If the value is defined as a literal, then it is assignable. And if the `value` is defined as type using the `Simplify` utility the value is assignable. But if the `value` is defined as an interface, it is not assignable because the interface is not sealed and elsewhere a non-string property could be added to the interface. If the type definition must be an interface (perhaps it was defined in a third-party npm package), then the `value` can be defined as `const value: Simplify = ...`. Then `value` will be assignable to the `fn` argument. Or the `value` can be cast as `Simplify` if you can't re-declare the `value`. @example ``` import {Simplify} from 'type-fest'; interface SomeInterface { foo: number; bar?: string; baz: number | undefined; } type SomeType = { foo: number; bar?: string; baz: number | undefined; }; const literal = {foo: 123, bar: 'hello', baz: 456}; const someType: SomeType = literal; const someInterface: SomeInterface = literal; function fn(object: Record): void {} fn(literal); // Good: literal object type is sealed fn(someType); // Good: type is sealed fn(someInterface); // Error: Index signature for type 'string' is missing in type 'someInterface'. Because `interface` can be re-opened fn(someInterface as Simplify); // Good: transform an `interface` into a `type` ``` {@link https://github.com/microsoft/TypeScript/issues/15300} */ export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; ================================================ FILE: src/typings/utils/index.ts ================================================ export * from "./ClassType"; export * from "./Constructor"; export * from "./Except"; export * from "./IsEqual"; export * from "./Maybe"; export * from "./MaybePromise"; export * from "./MergeExclusive"; export * from "./NonEmptyArray"; export * from "./SetRequired"; export * from "./Simplify"; ================================================ FILE: src/utils/buildSchema.ts ================================================ import path from "node:path"; import { type GraphQLSchema } from "graphql"; import { SchemaGenerator, type SchemaGeneratorOptions } from "@/schema/schema-generator"; import { type NonEmptyArray } from "@/typings"; import { type PrintSchemaOptions, defaultPrintSchemaOptions, emitSchemaDefinitionFile, emitSchemaDefinitionFileSync, } from "./emitSchemaDefinitionFile"; type EmitSchemaFileOptions = { path?: string; } & Partial; function getEmitSchemaDefinitionFileOptions(buildSchemaOptions: BuildSchemaOptions): { schemaFileName: string; printSchemaOptions: PrintSchemaOptions; } { const defaultSchemaFilePath = path.resolve(process.cwd(), "schema.graphql"); return { schemaFileName: // eslint-disable-next-line no-nested-ternary typeof buildSchemaOptions.emitSchemaFile === "string" ? buildSchemaOptions.emitSchemaFile : typeof buildSchemaOptions.emitSchemaFile === "object" ? buildSchemaOptions.emitSchemaFile.path || defaultSchemaFilePath : defaultSchemaFilePath, printSchemaOptions: typeof buildSchemaOptions.emitSchemaFile === "object" ? { ...defaultPrintSchemaOptions, ...buildSchemaOptions.emitSchemaFile } : defaultPrintSchemaOptions, }; } function loadResolvers(options: BuildSchemaOptions): Function[] { // Additional runtime check as it should be covered by the `NonEmptyArray` type guard if (options.resolvers.length === 0) { throw new Error("Empty `resolvers` array property found in `buildSchema` options!"); } return options.resolvers as Function[]; } export type BuildSchemaOptions = { /** Array of resolvers classes to resolver files */ resolvers: NonEmptyArray; /** * Path to the file to where emit the schema * or config object with print schema options * or `true` for the default `./schema.graphql` one */ emitSchemaFile?: string | boolean | EmitSchemaFileOptions; } & Omit; export async function buildSchema(options: BuildSchemaOptions): Promise { const resolvers = loadResolvers(options); const schema = SchemaGenerator.generateFromMetadata({ ...options, resolvers }); if (options.emitSchemaFile) { const { schemaFileName, printSchemaOptions } = getEmitSchemaDefinitionFileOptions(options); await emitSchemaDefinitionFile(schemaFileName, schema, printSchemaOptions); } return schema; } export function buildSchemaSync(options: BuildSchemaOptions): GraphQLSchema { const resolvers = loadResolvers(options); const schema = SchemaGenerator.generateFromMetadata({ ...options, resolvers }); if (options.emitSchemaFile) { const { schemaFileName, printSchemaOptions } = getEmitSchemaDefinitionFileOptions(options); emitSchemaDefinitionFileSync(schemaFileName, schema, printSchemaOptions); } return schema; } ================================================ FILE: src/utils/buildTypeDefsAndResolvers.ts ================================================ import { type GraphQLSchema, printSchema } from "graphql"; import { type BuildSchemaOptions, buildSchema, buildSchemaSync } from "./buildSchema"; import { createResolversMap } from "./createResolversMap"; function createTypeDefsAndResolversMap(schema: GraphQLSchema) { const typeDefs = printSchema(schema); const resolvers = createResolversMap(schema); return { typeDefs, resolvers }; } export async function buildTypeDefsAndResolvers(options: BuildSchemaOptions) { const schema = await buildSchema(options); return createTypeDefsAndResolversMap(schema); } export function buildTypeDefsAndResolversSync(options: BuildSchemaOptions) { const schema = buildSchemaSync(options); return createTypeDefsAndResolversMap(schema); } ================================================ FILE: src/utils/container.ts ================================================ /* eslint-disable max-classes-per-file */ import { type ResolverData } from "@/typings"; export type SupportedType = (new (...args: any[]) => T) | Function; export interface ContainerType { get(someClass: any, resolverData: ResolverData): any | Promise; } export type ContainerGetter = ( resolverData: ResolverData, ) => ContainerType; /** * Container to be used by this library for inversion control. * If container was not implicitly set then by default * container simply creates a new instance of the given class. */ class DefaultContainer { private instances: Array<{ type: Function; object: any }> = []; get(someClass: SupportedType): T { let instance = this.instances.find(it => it.type === someClass); if (!instance) { instance = { type: someClass, object: new (someClass as any)() }; this.instances.push(instance); } return instance.object; } } export class IOCContainer { private container: ContainerType | undefined; private containerGetter: ContainerGetter | undefined; private defaultContainer = new DefaultContainer(); constructor(iocContainerOrContainerGetter?: ContainerType | ContainerGetter) { if ( iocContainerOrContainerGetter && "get" in iocContainerOrContainerGetter && typeof iocContainerOrContainerGetter.get === "function" ) { this.container = iocContainerOrContainerGetter; } else if (typeof iocContainerOrContainerGetter === "function") { this.containerGetter = iocContainerOrContainerGetter; } } getInstance( someClass: SupportedType, resolverData: ResolverData, ): T | Promise { const container = this.containerGetter ? this.containerGetter(resolverData) : this.container; if (!container) { return this.defaultContainer.get(someClass); } return container.get(someClass, resolverData); } } ================================================ FILE: src/utils/createResolversMap.ts ================================================ /* eslint-disable no-param-reassign */ import { type GraphQLAbstractType, GraphQLEnumType, type GraphQLFieldMap, GraphQLInterfaceType, GraphQLObjectType, GraphQLScalarType, type GraphQLSchema, type GraphQLTypeResolver, GraphQLUnionType, } from "graphql"; import { type EnumResolver, type ResolverObject, type ResolversMap } from "@/typings"; function generateTypeResolver( abstractType: GraphQLAbstractType, schema: GraphQLSchema, ): GraphQLTypeResolver { if (abstractType.resolveType) { return abstractType.resolveType; } const possibleObjectTypes = schema.getPossibleTypes(abstractType); return async (source, context, info) => { for (const objectType of possibleObjectTypes) { // eslint-disable-next-line no-await-in-loop if (objectType.isTypeOf && (await objectType.isTypeOf(source, context, info))) { return objectType.name; } } return undefined; }; } function generateFieldsResolvers(fields: GraphQLFieldMap): ResolverObject { return Object.keys(fields).reduce((fieldsMap, fieldName) => { const field = fields[fieldName]; if (field.subscribe) { fieldsMap[fieldName] = { subscribe: field.subscribe, resolve: field.resolve, }; } else if (field.resolve) { fieldsMap[fieldName] = field.resolve; } return fieldsMap; }, {}); } export function createResolversMap(schema: GraphQLSchema): ResolversMap { const typeMap = schema.getTypeMap(); return Object.keys(typeMap) .filter(typeName => !typeName.startsWith("__")) .reduce((resolversMap, typeName) => { const type = typeMap[typeName]; if (type instanceof GraphQLObjectType) { resolversMap[typeName] = { ...(type.isTypeOf && { __isTypeOf: type.isTypeOf, }), ...generateFieldsResolvers(type.getFields()), }; } if (type instanceof GraphQLInterfaceType) { resolversMap[typeName] = { __resolveType: generateTypeResolver(type, schema), ...generateFieldsResolvers(type.getFields()), }; } if (type instanceof GraphQLScalarType) { resolversMap[typeName] = type; } if (type instanceof GraphQLEnumType) { const enumValues = type.getValues(); resolversMap[typeName] = enumValues.reduce((enumMap, { name, value }) => { enumMap[name] = value; return enumMap; }, {}); } if (type instanceof GraphQLUnionType) { resolversMap[typeName] = { __resolveType: generateTypeResolver(type, schema), }; } return resolversMap; }, {}); } ================================================ FILE: src/utils/emitSchemaDefinitionFile.ts ================================================ import { type GraphQLSchema, lexicographicSortSchema, printSchema } from "graphql"; import { outputFile, outputFileSync } from "@/helpers/filesystem"; export interface PrintSchemaOptions { sortedSchema: boolean; } export const defaultPrintSchemaOptions: PrintSchemaOptions = { sortedSchema: true, }; const generatedSchemaWarning = /* graphql */ `\ # ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- `; function getSchemaFileContent(schema: GraphQLSchema, options: PrintSchemaOptions) { const schemaToEmit = options.sortedSchema ? lexicographicSortSchema(schema) : schema; return generatedSchemaWarning + printSchema(schemaToEmit); } export function emitSchemaDefinitionFileSync( schemaFilePath: string, schema: GraphQLSchema, options: PrintSchemaOptions = defaultPrintSchemaOptions, ) { const schemaFileContent = getSchemaFileContent(schema, options); outputFileSync(schemaFilePath, schemaFileContent); } export async function emitSchemaDefinitionFile( schemaFilePath: string, schema: GraphQLSchema, options: PrintSchemaOptions = defaultPrintSchemaOptions, ) { const schemaFileContent = getSchemaFileContent(schema, options); await outputFile(schemaFilePath, schemaFileContent); } ================================================ FILE: src/utils/graphql-version.ts ================================================ import * as graphql from "graphql"; import semVer from "semver"; // Avoid '@/' due to 'scripts/version.ts' import { UnmetGraphQLPeerDependencyError } from "../errors"; // This must be kept in sync with 'package.json' export const graphQLPeerDependencyVersion = "^16.12.0"; export function ensureInstalledCorrectGraphQLPackage() { if (!semVer.satisfies(graphql.version, graphQLPeerDependencyVersion)) { throw new UnmetGraphQLPeerDependencyError(graphql.version, graphQLPeerDependencyVersion); } } ================================================ FILE: src/utils/index.ts ================================================ export type { BuildSchemaOptions } from "./buildSchema"; export { buildSchema, buildSchemaSync } from "./buildSchema"; export { buildTypeDefsAndResolvers, buildTypeDefsAndResolversSync, } from "./buildTypeDefsAndResolvers"; export type { ContainerType, ContainerGetter } from "./container"; export { createResolversMap } from "./createResolversMap"; export type { PrintSchemaOptions } from "./emitSchemaDefinitionFile"; export { emitSchemaDefinitionFile, emitSchemaDefinitionFileSync, defaultPrintSchemaOptions, } from "./emitSchemaDefinitionFile"; export * from "./graphql-version"; ================================================ FILE: src/utils/isPromiseLike.ts ================================================ export function isPromiseLike( value: PromiseLike | TValue, ): value is PromiseLike { return value != null && typeof (value as PromiseLike).then === "function"; } ================================================ FILE: tests/.eslintrc ================================================ { "env": { "jest/globals": true }, "rules": { "max-classes-per-file": "off", "class-methods-use-this": "off", "import/no-extraneous-dependencies": [ "error", { "devDependencies": true } ] } } ================================================ FILE: tests/functional/authorization.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, graphql } from "graphql"; import { type AuthCheckerInterface, AuthenticationError, AuthorizationError, Authorized, Ctx, Field, FieldResolver, ObjectType, Query, Resolver, type ResolverData, buildSchema, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { expectToThrow } from "../helpers/expectToThrow"; describe("Authorization", () => { let schema: GraphQLSchema; let sampleResolver: any; beforeEach(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() normalField!: string; @Field() @Authorized() authedField!: string; @Field({ nullable: true }) @Authorized() nullableAuthedField!: string; @Field() @Authorized("ADMIN") adminField!: string; @Field() normalResolvedField!: string; @Field() authedResolvedField!: string; @Field() @Authorized() inlineAuthedResolvedField!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() normalQuery(): boolean { return true; } @Query() normalObjectQuery(): SampleObject { return { normalField: "normalField", authedField: "authedField", adminField: "adminField", } as SampleObject; } @Query() @Authorized() authedQuery(@Ctx() ctx: any): boolean { return ctx.user !== undefined; } @Query(() => Boolean, { nullable: true }) @Authorized() nullableAuthedQuery() { return true; } @Query() @Authorized("ADMIN") adminQuery(@Ctx() ctx: any): boolean { return ctx.user !== undefined; } @Query() @Authorized(["ADMIN", "REGULAR"]) adminOrRegularQuery(@Ctx() ctx: any): boolean { return ctx.user !== undefined; } @Query() @Authorized("ADMIN", "REGULAR") adminOrRegularRestQuery(@Ctx() ctx: any): boolean { return ctx.user !== undefined; } @FieldResolver() normalResolvedField() { return "normalResolvedField"; } @FieldResolver() @Authorized() authedResolvedField() { return "authedResolvedField"; } @FieldResolver() inlineAuthedResolvedField() { return "inlineAuthedResolvedField"; } } sampleResolver = SampleResolver; schema = await buildSchema({ resolvers: [SampleResolver], // dummy auth checker authChecker: () => false, }); }); describe("Reflection", () => { // helpers function findQuery(queryName: string) { return getMetadataStorage().queries.find(it => it.methodName === queryName)!; } it("should build schema without errors", async () => { expect(schema).toBeDefined(); }); it("should register correct roles for resolvers", async () => { const normalQuery = findQuery("normalQuery"); const authedQuery = findQuery("authedQuery"); const adminQuery = findQuery("adminQuery"); expect(normalQuery.roles).toBeUndefined(); expect(authedQuery.roles).toHaveLength(0); expect(adminQuery.roles).toHaveLength(1); }); it("should register correct roles for object type fields", async () => { const sampleObject = getMetadataStorage().objectTypes.find( type => type.name === "SampleObject", )!; const normalField = sampleObject.fields!.find(field => field.name === "normalField")!; const authedField = sampleObject.fields!.find(field => field.name === "authedField")!; const adminField = sampleObject.fields!.find(field => field.name === "adminField")!; expect(normalField.roles).toBeUndefined(); expect(authedField.roles).toHaveLength(0); expect(adminField.roles).toEqual(["ADMIN"]); }); it("should register correct roles for every decorator overload", async () => { const normalQuery = findQuery("normalQuery"); const authedQuery = findQuery("authedQuery"); const adminQuery = findQuery("adminQuery"); const adminOrRegularQuery = findQuery("adminOrRegularQuery"); const adminOrRegularRestQuery = findQuery("adminOrRegularRestQuery"); expect(normalQuery.roles).toBeUndefined(); expect(authedQuery.roles).toHaveLength(0); expect(adminQuery.roles).toEqual(["ADMIN"]); expect(adminOrRegularQuery.roles).toEqual(["ADMIN", "REGULAR"]); expect(adminOrRegularRestQuery.roles).toEqual(["ADMIN", "REGULAR"]); }); }); describe("Errors", () => { it("should throw error when `@Authorized` is used and no `authChecker` provided", async () => { const error = await expectToThrow(() => buildSchema({ resolvers: [sampleResolver], }), ); expect(error).toBeDefined(); expect(error.message).toContain("authChecker"); }); // TODO: check for wrong `@Authorized` usage it.todo("should throw error when `@Authorized` is used on args, input or interface class"); }); describe("Functional", () => { it("should allow to register auth checker", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => true, }); expect(localSchema).toBeDefined(); }); it("should allow to read not guarded query", async () => { const query = `query { normalQuery }`; const result: any = await graphql({ schema, source: query, contextValue: {} }); expect(result.data!.normalQuery).toEqual(true); }); it("should allow to read not guarded object field", async () => { const query = `query { normalObjectQuery { normalField } }`; const result: any = await graphql({ schema, source: query, contextValue: {} }); expect(result.data!.normalObjectQuery.normalField).toEqual("normalField"); }); it("should allow to read not guarded object field from resolver", async () => { const query = `query { normalObjectQuery { normalResolvedField } }`; const result: any = await graphql({ schema, source: query, contextValue: {} }); expect(result.data!.normalObjectQuery.normalResolvedField).toEqual("normalResolvedField"); }); it("should restrict access to authed query", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { authedQuery }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); expect(result.errors).toBeDefined(); }); it("should restrict access to authed resolver", async () => { Authorized()(sampleResolver); const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); // clear AuthorizedClassMetadata for other tests getMetadataStorage().authorizedResolver = []; const query = `query { normalQuery }`; const result = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); expect(result.errors).toBeDefined(); }); it("should return null when accessing nullable authed query in null mode", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, authMode: "null", }); const query = `query { nullableAuthedQuery }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.nullableAuthedQuery).toBeNull(); expect(result.errors).toBeUndefined(); }); it("should throw AuthenticationError when guest accessing authed query", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { authedQuery }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const error = result.errors![0]; expect(error.originalError).toBeInstanceOf(AuthenticationError); expect(error.path).toContain("authedQuery"); }); it("should throw AuthorizationError when guest accessing query authed with roles", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { adminQuery }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const error = result.errors![0]; expect(error.originalError).toBeInstanceOf(AuthorizationError); expect(error.path).toContain("adminQuery"); }); it("should allow for access to authed query when `authChecker` returns true", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => true, }); const query = `query { authedQuery }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.authedQuery).toEqual(false); }); it("should restrict access to authed object field", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { normalObjectQuery { authedField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); }); it("should return null while accessing nullable authed object field", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { normalObjectQuery { nullableAuthedField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.normalObjectQuery.nullableAuthedField).toBeNull(); }); it("should throw AuthenticationError when guest accessing authed object field", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { normalObjectQuery { authedField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const error = result.errors![0]; expect(error.originalError).toBeInstanceOf(AuthenticationError); expect(error.path).toContain("authedField"); }); it("should throw AuthorizationError when guest accessing object field authed with roles", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { normalObjectQuery { adminField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const error = result.errors![0]; expect(error.originalError).toBeInstanceOf(AuthorizationError); expect(error.path).toContain("adminField"); }); it("should allow for access to authed object field when `authChecker` returns true", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => true, }); const query = `query { normalObjectQuery { authedField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.normalObjectQuery.authedField).toEqual("authedField"); }); it("should restrict access to authed object field from resolver", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { normalObjectQuery { authedResolvedField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); }); it("should restrict access to inline authed object field from resolver", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => false, }); const query = `query { normalObjectQuery { inlineAuthedResolvedField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data).toBeNull(); }); it("should allow for access to authed object field from resolver when access granted", async () => { const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: () => true, }); const query = `query { normalObjectQuery { inlineAuthedResolvedField } }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.normalObjectQuery.inlineAuthedResolvedField).toEqual( "inlineAuthedResolvedField", ); }); it("should pass roles to `authChecker` when checking for access to handler", async () => { let authCheckerRoles: string[] | undefined; const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: (_, roles) => { authCheckerRoles = roles; return true; }, }); const query = `query { adminOrRegularQuery }`; const result: any = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.adminOrRegularQuery).toEqual(false); expect(authCheckerRoles).toEqual(["ADMIN", "REGULAR"]); }); it("should pass roles from Resolver to `authChecker` when checking for access to handler", async () => { let authCheckerRoles: string[] | undefined; Authorized(["ADMIN", "REGULAR"])(sampleResolver); const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: (_, roles) => { authCheckerRoles = roles; return true; }, }); // clear AuthorizedClassMetadata for other tests getMetadataStorage().authorizedResolver = []; const query = `query { normalQuery }`; const result = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.normalQuery).toEqual(true); expect(authCheckerRoles).toEqual(["ADMIN", "REGULAR"]); }); it("should pass roles from Field rather than Resolver to `authChecker`", async () => { let authCheckerRoles: string[] | undefined; Authorized(["REGULAR"])(sampleResolver); const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: (_, roles) => { authCheckerRoles = roles; return true; }, }); // clear AuthorizedClassMetadata for other tests getMetadataStorage().authorizedResolver = []; const query = `query { adminOrRegularQuery }`; const result = await graphql({ schema: localSchema, source: query, contextValue: {} }); expect(result.data!.adminOrRegularQuery).toEqual(false); expect(authCheckerRoles).toEqual(["ADMIN", "REGULAR"]); }); it("should pass resolver data to `authChecker` when checking for access to handler", async () => { let authCheckerResolverData: any; const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: resolverData => { authCheckerResolverData = resolverData; return true; }, }); const query = `query { adminOrRegularQuery }`; const result: any = await graphql({ schema: localSchema, source: query, rootValue: { field: "rootField" }, contextValue: { field: "contextField" }, }); expect(result.data!.adminOrRegularQuery).toEqual(false); expect(authCheckerResolverData.root.field).toEqual("rootField"); expect(authCheckerResolverData.context.field).toEqual("contextField"); expect(authCheckerResolverData.args).toEqual({}); expect(authCheckerResolverData.info).toBeDefined(); }); }); describe("with class-based auth checker", () => { it("should correctly call auth checker class instance 'check' method", async () => { let authCheckerResolverData: any; let authCheckerRoles: any; class TestAuthChecker implements AuthCheckerInterface { check(resolverData: ResolverData, roles: string[]) { authCheckerResolverData = resolverData; authCheckerRoles = roles; return false; } } const localSchema = await buildSchema({ resolvers: [sampleResolver], authChecker: TestAuthChecker, }); const query = /* graphql */ ` query { adminOrRegularQuery } `; const result: any = await graphql({ schema: localSchema, source: query, rootValue: { field: "rootField" }, contextValue: { field: "contextField" }, }); expect(result.data).toBeNull(); expect(result.errors).toMatchInlineSnapshot(` [ [GraphQLError: Access denied! You don't have permission for this action!], ] `); expect(authCheckerResolverData.root).toEqual({ field: "rootField" }); expect(authCheckerResolverData.context).toEqual({ field: "contextField" }); expect(authCheckerResolverData.args).toEqual({}); expect(authCheckerResolverData.info).toBeDefined(); expect(authCheckerRoles).toEqual(["ADMIN", "REGULAR"]); }); }); describe("with constant readonly array or roles", () => { let testResolver: Function; beforeEach(() => { getMetadataStorage().clear(); const CONSTANT_ROLES = ["a", "b", "c"] as const; @Resolver() class TestResolver { @Query() @Authorized(CONSTANT_ROLES) authedQuery(@Ctx() ctx: any): boolean { return ctx.user !== undefined; } } testResolver = TestResolver; }); it("should not throw an error", async () => { await expect( buildSchema({ resolvers: [testResolver], // dummy auth checker authChecker: () => false, }), ).resolves.not.toThrow(); }); }); }); ================================================ FILE: tests/functional/circular-refs.ts ================================================ import "reflect-metadata"; import { type IntrospectionObjectType, TypeKind, graphql } from "graphql"; import { Field, ObjectType, Query, Resolver, buildSchema } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { expectToThrow } from "../helpers/expectToThrow"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Circular references", () => { it("should resolve circular type dependencies when type functions are used", async () => { getMetadataStorage().clear(); const { CircularRef1 } = await import("../helpers/circular-refs/good/CircularRef1"); const { CircularRef2 } = await import("../helpers/circular-refs/good/CircularRef2"); @ObjectType() class SampleObject { @Field(() => CircularRef1) ref1: any; @Field(() => CircularRef2) ref2: any; } @Resolver() class SampleResolver { @Query() objectQuery(): SampleObject { return {} as any; } } const { schemaIntrospection: { types }, } = await getSchemaInfo({ resolvers: [SampleResolver] }); const circularRef1 = types.find( type => type.name === "CircularRef1", ) as IntrospectionObjectType; const circularRef2 = types.find( type => type.name === "CircularRef2", ) as IntrospectionObjectType; expect(circularRef1).toBeDefined(); expect(circularRef1.kind).toEqual(TypeKind.OBJECT); expect(circularRef2).toBeDefined(); expect(circularRef2.kind).toEqual(TypeKind.OBJECT); }); it("should throw error when not providing type function for circular type references", async () => { getMetadataStorage().clear(); const errorRef1 = await expectToThrow( async () => (await import("../helpers/circular-refs/wrong/CircularRef1")).CircularRef1, ); expect(errorRef1).toBeInstanceOf(Error); expect(errorRef1.message).toContain("provide explicit type"); expect(errorRef1.message).toContain("ref1Field"); jest.resetModules(); const errorRef2 = await expectToThrow( async () => (await import("../helpers/circular-refs/wrong/CircularRef2")).CircularRef2, ); expect(errorRef2).toBeInstanceOf(Error); expect(errorRef2.message).toContain("provide explicit type"); expect(errorRef2.message).toContain("ref2Field"); jest.resetModules(); }); it("should allow to have self-reference fields in object type", async () => { @ObjectType() class SampleObject { @Field() stringField!: string; @Field(() => SampleObject, { nullable: true }) selfReferenceField?: SampleObject; @Field(() => [SampleObject]) selfReferenceArrayField!: SampleObject[]; } @Resolver() class SampleResolver { @Query() objectQuery(): SampleObject { const obj: SampleObject = { stringField: "nestedStringField", selfReferenceArrayField: [], }; obj.selfReferenceField = obj; return { stringField: "stringField", selfReferenceArrayField: [obj], selfReferenceField: obj, }; } } const schema = await buildSchema({ resolvers: [SampleResolver], }); expect(schema).toBeDefined(); const query = /* graphql */ ` query { objectQuery { stringField selfReferenceField { stringField } selfReferenceArrayField { selfReferenceField { stringField } } } } `; const { data } = await graphql({ schema, source: query }); expect(data!.objectQuery).toEqual({ stringField: "stringField", selfReferenceField: { stringField: "nestedStringField", }, selfReferenceArrayField: [ { selfReferenceField: { stringField: "nestedStringField", }, }, ], }); }); }); ================================================ FILE: tests/functional/default-nullable.ts ================================================ import "reflect-metadata"; import { type IntrospectionListTypeRef, type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionSchema, TypeKind, } from "graphql"; import { Field, ObjectType, Query, Resolver } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("buildSchema -> nullableByDefault", () => { let SampleObjectClass: any; let SampleResolverClass: any; beforeEach(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() normalField!: string; @Field(() => [String]) normalArrayField!: string[]; @Field({ nullable: true }) nullableField!: string; @Field({ nullable: false }) nonNullableField!: string; } SampleObjectClass = SampleObject; @Resolver(() => SampleObject) class SampleResolver { @Query() normalQuery(): string { return "normalQuery"; } @Query(() => [String]) normalArrayQuery(): string[] { return ["normalArrayQuery"]; } @Query(() => String, { nullable: true }) nullableQuery() { return null; } @Query({ nullable: false }) nonNullableQuery(): string { return "nonNullableQuery"; } } SampleResolverClass = SampleResolver; }); describe("default behavior", () => { let schemaIntrospection: IntrospectionSchema; let queryType: IntrospectionObjectType; let sampleObjectType: IntrospectionObjectType; beforeEach(async () => { ({ schemaIntrospection, queryType } = await getSchemaInfo({ resolvers: [SampleResolverClass], orphanedTypes: [SampleObjectClass], })); sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; }); it("should emit non nullable fields by default", async () => { const normalField = sampleObjectType.fields.find(it => it.name === "normalField"); const normalFieldType = normalField!.type as IntrospectionNonNullTypeRef; const normalFieldInnerType = normalFieldType.ofType as IntrospectionNamedTypeRef; expect(normalFieldType.kind).toBe(TypeKind.NON_NULL); expect(normalFieldInnerType.name).toBe("String"); const normalArrayField = sampleObjectType.fields.find(it => it.name === "normalArrayField"); const normalArrayFieldType = normalArrayField!.type as IntrospectionNonNullTypeRef; const normalArrayFieldListType = normalArrayFieldType.ofType as IntrospectionListTypeRef; const normalArrayFieldListElementType = normalArrayFieldListType.ofType as IntrospectionNonNullTypeRef; const normalArrayFieldListElementInnerType = normalArrayFieldListElementType.ofType as IntrospectionNamedTypeRef; expect(normalArrayFieldType.kind).toBe(TypeKind.NON_NULL); expect(normalArrayFieldListType.kind).toBe(TypeKind.LIST); expect(normalArrayFieldListElementType.kind).toBe(TypeKind.NON_NULL); expect(normalArrayFieldListElementInnerType.kind).toBe(TypeKind.SCALAR); expect(normalArrayFieldListElementInnerType.name).toBe("String"); }); it("should emit non nullable queries by default", async () => { const normalQuery = queryType.fields.find(it => it.name === "normalQuery"); const normalQueryType = normalQuery!.type as IntrospectionNonNullTypeRef; const normalQueryInnerType = normalQueryType.ofType as IntrospectionNamedTypeRef; expect(normalQueryType.kind).toBe(TypeKind.NON_NULL); expect(normalQueryInnerType.name).toBe("String"); const normalArrayQuery = queryType.fields.find(it => it.name === "normalArrayQuery"); const normalArrayQueryType = normalArrayQuery!.type as IntrospectionNonNullTypeRef; const normalArrayQueryListType = normalArrayQueryType.ofType as IntrospectionListTypeRef; const normalArrayQueryListElementType = normalArrayQueryListType.ofType as IntrospectionNonNullTypeRef; const normalArrayQueryListElementInnerType = normalArrayQueryListElementType.ofType as IntrospectionNamedTypeRef; expect(normalArrayQueryType.kind).toBe(TypeKind.NON_NULL); expect(normalArrayQueryListType.kind).toBe(TypeKind.LIST); expect(normalArrayQueryListElementType.kind).toBe(TypeKind.NON_NULL); expect(normalArrayQueryListElementInnerType.kind).toBe(TypeKind.SCALAR); expect(normalArrayQueryListElementInnerType.name).toBe("String"); }); }); describe("nullableByDefault = true", () => { let schemaIntrospection: IntrospectionSchema; let queryType: IntrospectionObjectType; let sampleObjectType: IntrospectionObjectType; beforeEach(async () => { ({ schemaIntrospection, queryType } = await getSchemaInfo({ resolvers: [SampleResolverClass], orphanedTypes: [SampleObjectClass], nullableByDefault: true, })); sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; }); it("should emit nullable fields by default", async () => { const normalField = sampleObjectType.fields.find(it => it.name === "normalField")!; const normalFieldType = normalField.type as IntrospectionNamedTypeRef; expect(normalFieldType.name).toBe("String"); const normalArrayField = sampleObjectType.fields.find(it => it.name === "normalArrayField"); const normalArrayFieldType = normalArrayField!.type as IntrospectionListTypeRef; const normalArrayFieldListElementInnerType = normalArrayFieldType.ofType as IntrospectionNamedTypeRef; expect(normalArrayFieldType.kind).toBe(TypeKind.LIST); expect(normalArrayFieldListElementInnerType.kind).toBe(TypeKind.SCALAR); expect(normalArrayFieldListElementInnerType.name).toBe("String"); }); it("should emit nullable queries by default", async () => { const normalQuery = queryType.fields.find(it => it.name === "normalQuery")!; const normalQueryType = normalQuery.type as IntrospectionNamedTypeRef; expect(normalQueryType.name).toBe("String"); const normalArrayQuery = queryType.fields.find(it => it.name === "normalArrayQuery"); const normalArrayQueryType = normalArrayQuery!.type as IntrospectionListTypeRef; const normalArrayQueryListElementInnerType = normalArrayQueryType.ofType as IntrospectionNamedTypeRef; expect(normalArrayQueryType.kind).toBe(TypeKind.LIST); expect(normalArrayQueryListElementInnerType.kind).toBe(TypeKind.SCALAR); expect(normalArrayQueryListElementInnerType.name).toBe("String"); }); it("shouldn't affect explicit nullability options from decorators", async () => { const nullableField = sampleObjectType.fields.find(it => it.name === "nullableField")!; const nullableFieldType = nullableField.type as IntrospectionNamedTypeRef; expect(nullableFieldType.name).toBe("String"); const nonNullableField = sampleObjectType.fields.find(it => it.name === "nonNullableField")!; const nonNullableFieldType = nonNullableField.type as IntrospectionNonNullTypeRef; const nonNullableFieldInnerType = nonNullableFieldType.ofType as IntrospectionNamedTypeRef; expect(nonNullableFieldType.kind).toBe(TypeKind.NON_NULL); expect(nonNullableFieldInnerType.name).toBe("String"); const nullableQuery = queryType.fields.find(it => it.name === "nullableQuery")!; const nullableQueryType = nullableQuery.type as IntrospectionNamedTypeRef; expect(nullableQueryType.name).toBe("String"); const nonNullableQuery = queryType.fields.find(it => it.name === "nonNullableQuery")!; const nonNullableQueryType = nonNullableQuery.type as IntrospectionNonNullTypeRef; const nonNullableQueryInnerType = nonNullableQueryType.ofType as IntrospectionNamedTypeRef; expect(nonNullableQueryType.kind).toBe(TypeKind.NON_NULL); expect(nonNullableQueryInnerType.name).toBe("String"); }); }); }); ================================================ FILE: tests/functional/default-values.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, printType } from "graphql"; import { Arg, type ClassType, Field, InputType, Query, Resolver, buildSchema, getMetadataStorage, } from "type-graphql"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("default values", () => { describe("dynamic default value", () => { let sampleResolver: ClassType; beforeAll(async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field() sampleField: Date = new Date(); } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): string { return "sampleQuery"; } } sampleResolver = SampleResolver; await getSchemaInfo({ resolvers: [SampleResolver], }); }); it("should not throw error when schema with dynamic default has been built again", async () => { await expect(buildSchema({ resolvers: [sampleResolver] })).resolves.not.toThrow(); }); }); describe("when disableInferringDefaultValues is set", () => { let schema: GraphQLSchema; beforeEach(async () => { getMetadataStorage().clear(); @InputType() class SampleInitializerInput { @Field() inputField: string = "defaultValueFromPropertyInitializer"; } @InputType() class SampleOptionInput { @Field({ defaultValue: "defaultValueFromOption" }) inputField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery( @Arg("input1") _input1: SampleInitializerInput, @Arg("input2") _input2: SampleOptionInput, ): string { return "sampleQuery"; } } schema = await buildSchema({ resolvers: [SampleResolver], disableInferringDefaultValues: true, }); }); it("should not infer default value from a property initializer", async () => { const sampleInitializerInputType = schema.getType("SampleInitializerInput")!; const sampleInitializerInputSDL = printType(sampleInitializerInputType); expect(sampleInitializerInputSDL).toMatchInlineSnapshot(` "input SampleInitializerInput { inputField: String! }" `); }); it("should read default value from a decorator option", async () => { const sampleOptionInputType = schema.getType("SampleOptionInput")!; const sampleOptionInputSDL = printType(sampleOptionInputType); expect(sampleOptionInputSDL).toMatchInlineSnapshot(` "input SampleOptionInput { inputField: String! = "defaultValueFromOption" }" `); }); }); describe("with nullable settings", () => { describe("when `nullable: false` and defaultValue is provided", () => { let schema: GraphQLSchema; beforeEach(async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field({ defaultValue: "stringDefaultValue", nullable: false }) inputField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): string { return "sampleQuery"; } } schema = await buildSchema({ resolvers: [SampleResolver] }); }); it("should emit field of type non-nullable string with default value", async () => { const sampleInputType = schema.getType("SampleInput")!; const sampleInputSDL = printType(sampleInputType); expect(sampleInputSDL).toMatchInlineSnapshot(` "input SampleInput { inputField: String! = "stringDefaultValue" }" `); }); }); describe("when `nullable: true` and defaultValue is provided", () => { let schema: GraphQLSchema; beforeEach(async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field({ defaultValue: "stringDefaultValue", nullable: true }) inputField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): string { return "sampleQuery"; } } schema = await buildSchema({ resolvers: [SampleResolver] }); }); it("should emit field of type nullable string with default value", async () => { const sampleInputType = schema.getType("SampleInput")!; const sampleInputSDL = printType(sampleInputType); expect(sampleInputSDL).toMatchInlineSnapshot(` "input SampleInput { inputField: String = "stringDefaultValue" }" `); }); }); describe("when `nullableByDefault: true`", () => { describe("when defaultValue is provided", () => { let schema: GraphQLSchema; beforeEach(async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field({ defaultValue: "stringDefaultValue" }) inputField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): string { return "sampleQuery"; } } schema = await buildSchema({ resolvers: [SampleResolver], nullableByDefault: true, }); }); it("should emit field of type nullable string with default value", async () => { const sampleInputType = schema.getType("SampleInput")!; const sampleInputSDL = printType(sampleInputType); expect(sampleInputSDL).toMatchInlineSnapshot(` "input SampleInput { inputField: String = "stringDefaultValue" }" `); }); }); describe("when `nullable: false` and defaultValue is provided", () => { let schema: GraphQLSchema; beforeEach(async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field({ defaultValue: "stringDefaultValue", nullable: false }) inputField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): string { return "sampleQuery"; } } schema = await buildSchema({ resolvers: [SampleResolver], nullableByDefault: true, }); }); it("should emit field of type non-nullable string with default value", async () => { const sampleInputType = schema.getType("SampleInput")!; const sampleInputSDL = printType(sampleInputType); expect(sampleInputSDL).toMatchInlineSnapshot(` "input SampleInput { inputField: String! = "stringDefaultValue" }" `); }); }); }); }); }); ================================================ FILE: tests/functional/deprecation.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, type IntrospectionObjectType, type IntrospectionSchema, printType, } from "graphql"; import { Arg, Args, ArgsType, Field, InputType, Mutation, ObjectType, Query, Resolver, } from "type-graphql"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Deprecation", () => { describe("Schema", () => { let schema: GraphQLSchema; let schemaIntrospection: IntrospectionSchema; let mutationType: IntrospectionObjectType; let queryType: IntrospectionObjectType; beforeAll(async () => { // Create sample definitions @ObjectType() class SampleObject { @Field() normalField!: string; @Field({ deprecationReason: "sample object field deprecation reason" }) deprecatedField!: string; @Field({ deprecationReason: "sample object getter field deprecation reason" }) // eslint-disable-next-line @typescript-eslint/class-literal-property-style get deprecatedGetterField(): string { return "deprecatedGetterField"; } @Field({ deprecationReason: "sample object method field deprecation reason" }) methodField(): string { return "methodField"; } } @InputType() class SampleInput { @Field() normalField!: string; @Field({ deprecationReason: "sample input field deprecation reason", nullable: true, }) deprecatedField!: string; } @ArgsType() class SampleArgs { @Field() normalArg!: string; @Field({ deprecationReason: "sample args field deprecation reason", nullable: true, }) deprecatedArg!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() normalQuery(): SampleObject { return {} as SampleObject; } @Query() inputQuery(@Arg("input") _input: SampleInput): SampleObject { return {} as SampleObject; } @Query() argsQuery(@Args() _args: SampleArgs): SampleObject { return {} as SampleObject; } @Query() deprecatedArgQuery( @Arg("arg", { deprecationReason: "sample query arg deprecation reason", nullable: true, }) _arg?: string, ): SampleObject { return {} as SampleObject; } @Query({ deprecationReason: "sample query deprecation reason" }) describedQuery(): string { return "describedQuery"; } @Mutation() normalMutation(): string { return "normalMutation"; } @Mutation({ deprecationReason: "sample mutation deprecation reason" }) describedMutation(): string { return "describedMutation"; } } // get builded schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); schema = schemaInfo.schema; schemaIntrospection = schemaInfo.schemaIntrospection; queryType = schemaInfo.queryType; mutationType = schemaInfo.mutationType!; }); it("should generate proper object fields deprecation reason", async () => { const sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; const normalField = sampleObjectType.fields.find(field => field.name === "normalField")!; const deprecatedField = sampleObjectType.fields.find( field => field.name === "deprecatedField", )!; const deprecatedGetterField = sampleObjectType.fields.find( field => field.name === "deprecatedGetterField", )!; const methodField = sampleObjectType.fields.find(field => field.name === "methodField")!; expect(normalField.isDeprecated).toBeFalsy(); expect(normalField.deprecationReason).toBeNull(); expect(deprecatedField.isDeprecated).toBeTruthy(); expect(deprecatedField.deprecationReason).toEqual("sample object field deprecation reason"); expect(deprecatedGetterField.isDeprecated).toBeTruthy(); expect(deprecatedGetterField.deprecationReason).toEqual( "sample object getter field deprecation reason", ); expect(methodField.isDeprecated).toBeTruthy(); expect(methodField.deprecationReason).toEqual( "sample object method field deprecation reason", ); }); it("should generate proper input type fields deprecation reason", async () => { const sampleInputType = schema.getType("SampleInput")!; const sampleInputTypeSDL = printType(sampleInputType); expect(sampleInputTypeSDL).toMatchInlineSnapshot(` "input SampleInput { normalField: String! deprecatedField: String @deprecated(reason: "sample input field deprecation reason") }" `); }); it("should generate proper query type deprecation reason", async () => { const normalQuery = queryType.fields.find(field => field.name === "normalQuery")!; const describedQuery = queryType.fields.find(field => field.name === "describedQuery")!; expect(normalQuery.isDeprecated).toBeFalsy(); expect(normalQuery.deprecationReason).toBeNull(); expect(describedQuery.isDeprecated).toBeTruthy(); expect(describedQuery.deprecationReason).toEqual("sample query deprecation reason"); }); it("should generate proper mutation type deprecation reason", async () => { const normalMutation = mutationType.fields.find(field => field.name === "normalMutation")!; const describedMutation = mutationType.fields.find( field => field.name === "describedMutation", )!; expect(normalMutation.isDeprecated).toBeFalsy(); expect(normalMutation.deprecationReason).toBeNull(); expect(describedMutation.isDeprecated).toBeTruthy(); expect(describedMutation.deprecationReason).toEqual("sample mutation deprecation reason"); }); it("should generate proper single arg deprecation reason", async () => { const queryObjectType = schema.getQueryType()!; const queryObjectTypeSDL = printType(queryObjectType); expect(queryObjectTypeSDL).toContain( 'deprecatedArgQuery(arg: String @deprecated(reason: "sample query arg deprecation reason"))', ); }); it("should generate proper args type fields deprecation reason", async () => { const queryObjectType = schema.getQueryType()!; const queryObjectTypeSDL = printType(queryObjectType); expect(queryObjectTypeSDL).toContain( 'argsQuery(normalArg: String!, deprecatedArg: String @deprecated(reason: "sample args field deprecation reason")): SampleObject!', ); }); }); }); ================================================ FILE: tests/functional/description.ts ================================================ import "reflect-metadata"; import { type IntrospectionInputObjectType, type IntrospectionObjectType, type IntrospectionSchema, } from "graphql"; import { Arg, Args, ArgsType, Field, InputType, Mutation, ObjectType, Query, Resolver, } from "type-graphql"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Description", () => { describe("Schema", () => { let schemaIntrospection: IntrospectionSchema; let mutationType: IntrospectionObjectType; let queryType: IntrospectionObjectType; beforeAll(async () => { // Create sample definitions @ObjectType({ description: "sample object description" }) class SampleObject { @Field() normalField!: string; @Field({ description: "sample object field description" }) describedField!: string; @Field({ description: "sample object getter field description" }) // eslint-disable-next-line @typescript-eslint/class-literal-property-style get describedGetterField(): string { return "describedGetterField"; } @Field({ description: "sample object method field description" }) methodField( @Arg("arg", { description: "sample object method arg description" }) _arg: string, ): string { return "methodField"; } } @InputType({ description: "sample input description" }) class SampleInput { @Field() normalField!: string; @Field({ description: "sample input field description" }) describedField!: string; } @ArgsType() class SampleArguments { @Field() normalField!: string; @Field({ description: "sample argument field description" }) describedField!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() normalQuery(): string { return "normalQuery"; } @Query({ description: "sample query description" }) describedQuery( @Arg("normalArg") _normalArg: string, @Arg("describedArg", { description: "sample query arg description" }) _describedArg: string, ): string { return "describedQuery"; } @Query() argumentedQuery(@Args() _args: SampleArguments): string { return "argumentedQuery"; } @Query() inputQuery(@Arg("input") _input: SampleInput): string { return "inputQuery"; } @Mutation() normalMutation(): string { return "normalMutation"; } @Mutation({ description: "sample mutation description" }) describedMutation( @Arg("normalArg") _normalArg: string, @Arg("describedArg", { description: "sample mutation arg description" }) _describedArg: string, ): string { return "describedMutation"; } @Mutation() argumentedMutation(@Args() _args: SampleArguments): string { return "argumentedMutation"; } @Mutation() inputMutation(@Arg("input") _input: SampleInput): string { return "inputMutation"; } } // Get builded schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], orphanedTypes: [SampleObject], }); schemaIntrospection = schemaInfo.schemaIntrospection; queryType = schemaInfo.queryType; mutationType = schemaInfo.mutationType!; }); it("should generate proper object type description", async () => { const sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; expect(sampleObjectType.description).toEqual("sample object description"); }); it("should generate proper object fields descriptions", async () => { const sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; const normalField = sampleObjectType.fields.find(field => field.name === "normalField")!; const describedField = sampleObjectType.fields.find( field => field.name === "describedField", )!; const describedGetterField = sampleObjectType.fields.find( field => field.name === "describedGetterField", )!; const methodField = sampleObjectType.fields.find(field => field.name === "methodField")!; expect(normalField.description).toBeNull(); expect(describedField.description).toEqual("sample object field description"); expect(describedGetterField.description).toEqual("sample object getter field description"); expect(methodField.description).toEqual("sample object method field description"); }); it("should generate proper query type description", async () => { const normalQuery = queryType.fields.find(field => field.name === "normalQuery")!; const describedQuery = queryType.fields.find(field => field.name === "describedQuery")!; expect(normalQuery.description).toBeNull(); expect(describedQuery.description).toEqual("sample query description"); }); it("should generate proper query inline args description", async () => { const describedQuery = queryType.fields.find(field => field.name === "describedQuery")!; const normalArg = describedQuery.args.find(arg => arg.name === "normalArg")!; const describedArg = describedQuery.args.find(arg => arg.name === "describedArg")!; expect(describedQuery.args).toHaveLength(2); expect(normalArg.description).toBeNull(); expect(describedArg.description).toEqual("sample query arg description"); }); it("should generate proper query object args description", async () => { const argumentedQuery = queryType.fields.find(field => field.name === "argumentedQuery")!; const normalField = argumentedQuery.args.find(arg => arg.name === "normalField")!; const describedField = argumentedQuery.args.find(arg => arg.name === "describedField")!; expect(argumentedQuery.args).toHaveLength(2); expect(normalField.description).toBeNull(); expect(describedField.description).toEqual("sample argument field description"); }); it("should generate proper mutation type description", async () => { const normalMutation = mutationType.fields.find(field => field.name === "normalMutation")!; const describedMutation = mutationType.fields.find( field => field.name === "describedMutation", )!; expect(normalMutation.description).toBeNull(); expect(describedMutation.description).toEqual("sample mutation description"); }); it("should generate proper mutation inline args description", async () => { const describedQuery = mutationType.fields.find(field => field.name === "describedMutation")!; const normalArg = describedQuery.args.find(arg => arg.name === "normalArg")!; const describedArg = describedQuery.args.find(arg => arg.name === "describedArg")!; expect(describedQuery.args).toHaveLength(2); expect(normalArg.description).toBeNull(); expect(describedArg.description).toEqual("sample mutation arg description"); }); it("should generate proper mutation object args description", async () => { const argumentedMutation = mutationType.fields.find( field => field.name === "argumentedMutation", )!; const normalField = argumentedMutation.args.find(arg => arg.name === "normalField")!; const describedField = argumentedMutation.args.find(arg => arg.name === "describedField")!; expect(argumentedMutation.args).toHaveLength(2); expect(normalField.description).toBeNull(); expect(describedField.description).toEqual("sample argument field description"); }); it("should generate proper input type description", async () => { const sampleInputType = schemaIntrospection.types.find( type => type.name === "SampleInput", ) as IntrospectionInputObjectType; expect(sampleInputType.description).toEqual("sample input description"); }); it("should generate proper input fields description", async () => { const sampleInputType = schemaIntrospection.types.find( type => type.name === "SampleInput", ) as IntrospectionInputObjectType; const normalField = sampleInputType.inputFields.find(field => field.name === "normalField")!; const describedField = sampleInputType.inputFields.find( field => field.name === "describedField", )!; expect(normalField.description).toBeNull(); expect(describedField.description).toEqual("sample input field description"); }); }); }); ================================================ FILE: tests/functional/directives.ts ================================================ import "reflect-metadata"; import { createPubSub } from "@graphql-yoga/subscription"; import { type GraphQLInputObjectType, type GraphQLInterfaceType, type GraphQLObjectType, type GraphQLSchema, OperationTypeNode, } from "graphql"; import { Arg, Args, ArgsType, Directive, Field, InputType, InterfaceType, Mutation, ObjectType, Query, Resolver, Subscription, buildSchema, } from "type-graphql"; import { InvalidDirectiveError } from "@/errors/InvalidDirectiveError"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { assertValidDirective } from "../helpers/directives/assertValidDirective"; import { testDirective, testDirectiveTransformer } from "../helpers/directives/TestDirective"; import { expectToThrow } from "../helpers/expectToThrow"; describe("Directives", () => { describe("Schema", () => { beforeEach(async () => { getMetadataStorage().clear(); }); describe("on ObjectType", () => { let schema: GraphQLSchema; beforeAll(async () => { @Directive("@test") @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return { sampleField: "sampleField" }; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleObjectTypeInfo = schema.getType("SampleObject") as GraphQLObjectType; expect(() => { assertValidDirective(sampleObjectTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleObjectTypeInfo = schema.getType("SampleObject") as GraphQLObjectType; expect(sampleObjectTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on ObjectType field", () => { let schema: GraphQLSchema; beforeAll(async () => { @ObjectType() class SampleObject { @Field() @Directive("@test") sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return { sampleField: "sampleField" }; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleFieldTypeInfo = ( schema.getType("SampleObject") as GraphQLObjectType ).getFields().sampleField; expect(() => { assertValidDirective(sampleFieldTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleFieldTypeInfo = ( schema.getType("SampleObject") as GraphQLObjectType ).getFields().sampleField; expect(sampleFieldTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on ObjectType field argument", () => { let schema: GraphQLSchema; beforeAll(async () => { @ArgsType() class SampleArgs { @Directive("@test") @Field() sampleArgument!: string; } @ObjectType() class SampleObject { @Field() sampleField(@Args() { sampleArgument }: SampleArgs): string { return sampleArgument; } } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return new SampleObject(); } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleFieldArgTypeInfo = ( schema.getType("SampleObject") as GraphQLObjectType ).getFields().sampleField.args[0]; expect(() => { assertValidDirective(sampleFieldArgTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleFieldArgTypeInfo = ( schema.getType("SampleObject") as GraphQLObjectType ).getFields().sampleField.args[0]; expect(sampleFieldArgTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on InterfaceType", () => { let schema: GraphQLSchema; beforeAll(async () => { @Directive("@test") @InterfaceType() class SampleInterface { @Field() sampleField!: string; } @ObjectType({ implements: [SampleInterface] }) class SampleObject extends SampleInterface {} @Resolver() class SampleResolver { @Query() sampleQuery(): SampleInterface { const sampleObject = new SampleObject(); sampleObject.sampleField = "sampleField"; return sampleObject; } } schema = await buildSchema({ resolvers: [SampleResolver], orphanedTypes: [SampleObject], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleInterfaceTypeInfo = schema.getType("SampleInterface") as GraphQLInterfaceType; expect(() => { assertValidDirective(sampleInterfaceTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleInterfaceTypeInfo = schema.getType("SampleInterface") as GraphQLInterfaceType; expect(sampleInterfaceTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on InterfaceType field", () => { let schema: GraphQLSchema; beforeAll(async () => { @InterfaceType() class SampleInterface { @Directive("@test") @Field() sampleField!: string; } @ObjectType({ implements: [SampleInterface] }) class SampleObject extends SampleInterface {} @Resolver() class SampleResolver { @Query() sampleQuery(): SampleInterface { const sampleObject = new SampleObject(); sampleObject.sampleField = "sampleField"; return sampleObject; } } schema = await buildSchema({ resolvers: [SampleResolver], orphanedTypes: [SampleObject], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleFieldTypeInfo = ( schema.getType("SampleInterface") as GraphQLInterfaceType ).getFields().sampleField; expect(() => { assertValidDirective(sampleFieldTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleFieldTypeInfo = ( schema.getType("SampleInterface") as GraphQLInterfaceType ).getFields().sampleField; expect(sampleFieldTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on InputType", () => { let schema: GraphQLSchema; beforeAll(async () => { @Directive("@test") @InputType() class SampleInput { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): boolean { return true; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleInputTypeInfo = schema.getType("SampleInput") as GraphQLInputObjectType; expect(() => { assertValidDirective(sampleInputTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleInputTypeInfo = schema.getType("SampleInput") as GraphQLInputObjectType; expect(sampleInputTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on InputType field", () => { let schema: GraphQLSchema; beforeAll(async () => { @InputType() class SampleInput { @Field() @Directive("@test") sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): boolean { return true; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleFieldTypeInfo = ( schema.getType("SampleInput") as GraphQLInputObjectType ).getFields().sampleField; expect(() => { assertValidDirective(sampleFieldTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleFieldTypeInfo = ( schema.getType("SampleInput") as GraphQLInputObjectType ).getFields().sampleField; expect(sampleFieldTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on Query", () => { let schema: GraphQLSchema; beforeAll(async () => { @Resolver() class SampleResolver { @Directive("@test") @Query() sampleQuery(): boolean { return true; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleQueryInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().sampleQuery; expect(() => { assertValidDirective(sampleQueryInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleQueryInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().sampleQuery; expect(sampleQueryInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on Query field argument using @Args", () => { let schema: GraphQLSchema; beforeAll(async () => { @ArgsType() class SampleArgs { @Directive("@test") @Field() sampleArgument!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Args() { sampleArgument }: SampleArgs): string { return sampleArgument; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields() .sampleQuery.args[0]; expect(() => { assertValidDirective(sampleQueryArgTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields() .sampleQuery.args[0]; expect(sampleQueryArgTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on Query field argument using @Arg", () => { let schema: GraphQLSchema; beforeAll(async () => { @Resolver() class SampleResolver { @Query() sampleQuery( @Arg("sampleArgument") @Directive("@test") sampleArgument: string, ): string { return sampleArgument; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields() .sampleQuery.args[0]; expect(() => { assertValidDirective(sampleQueryArgTypeInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields() .sampleQuery.args[0]; expect(sampleQueryArgTypeInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on Mutation", () => { let schema: GraphQLSchema; beforeAll(async () => { @Resolver() class SampleResolver { @Query() sampleQuery(): boolean { return true; } @Directive("@test") @Mutation() sampleMutation(): boolean { return true; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleMutationInfo = schema .getRootType(OperationTypeNode.MUTATION)! .getFields().sampleMutation; expect(() => { assertValidDirective(sampleMutationInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleMutationInfo = schema .getRootType(OperationTypeNode.MUTATION)! .getFields().sampleMutation; expect(sampleMutationInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("on Subscription", () => { let schema: GraphQLSchema; beforeAll(async () => { @Resolver() class SampleResolver { @Query() sampleQuery(): boolean { return true; } @Directive("@test") @Subscription({ topics: "sample" }) sampleSubscription(): boolean { return true; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, pubSub: createPubSub(), }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const sampleSubscriptionInfo = schema .getRootType(OperationTypeNode.SUBSCRIPTION)! .getFields().sampleSubscription; expect(() => { assertValidDirective(sampleSubscriptionInfo.astNode, "test"); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const sampleSubscriptionInfo = schema .getRootType(OperationTypeNode.SUBSCRIPTION)! .getFields().sampleSubscription; expect(sampleSubscriptionInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); }); describe("multiline and leading white spaces", () => { let schema: GraphQLSchema; beforeAll(async () => { @Resolver() class SampleResolver { @Directive("\n@test") @Query() multiline(): boolean { return true; } @Directive(" @test") @Query() leadingWhiteSpaces(): boolean { return true; } @Directive("\n @test") @Query() multilineAndLeadingWhiteSpaces(): boolean { return true; } @Directive(` @test( argNonNullDefault: "argNonNullDefault", argNullDefault: "argNullDefault", argNull: "argNull" ) `) @Query() rawMultilineAndLeadingWhiteSpaces(): boolean { return true; } } schema = await buildSchema({ resolvers: [SampleResolver], directives: [testDirective], validate: false, }); schema = testDirectiveTransformer(schema); }); it("should properly emit directive in AST", () => { const multilineInfo = schema.getRootType(OperationTypeNode.QUERY)!.getFields().multiline; const leadingWhiteSpacesInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().leadingWhiteSpaces; const multilineAndLeadingWhiteSpacesInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().multilineAndLeadingWhiteSpaces; const rawMultilineAndLeadingWhiteSpacesInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().rawMultilineAndLeadingWhiteSpaces; expect(() => { assertValidDirective(multilineInfo.astNode, "test"); assertValidDirective(leadingWhiteSpacesInfo.astNode, "test"); assertValidDirective(multilineAndLeadingWhiteSpacesInfo.astNode, "test"); assertValidDirective(rawMultilineAndLeadingWhiteSpacesInfo.astNode, "test", { argNonNullDefault: `"argNonNullDefault"`, argNullDefault: `"argNullDefault"`, argNull: `"argNull"`, }); }).not.toThrow(); }); it("should properly apply directive mapper", async () => { const multilineInfo = schema.getRootType(OperationTypeNode.QUERY)!.getFields().multiline; const leadingWhiteSpacesInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().leadingWhiteSpaces; const multilineAndLeadingWhiteSpacesInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().multilineAndLeadingWhiteSpaces; const rawMultilineAndLeadingWhiteSpacesInfo = schema .getRootType(OperationTypeNode.QUERY)! .getFields().rawMultilineAndLeadingWhiteSpaces; expect(multilineInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); expect(leadingWhiteSpacesInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); expect(multilineAndLeadingWhiteSpacesInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); expect(rawMultilineAndLeadingWhiteSpacesInfo.extensions).toMatchObject({ TypeGraphQL: { isMappedByDirective: true }, }); }); }); describe("errors", () => { beforeEach(async () => { getMetadataStorage().clear(); }); it("throws error on multiple directive definitions", async () => { @Resolver() class InvalidQuery { @Query() @Directive("@upper @append") invalid(): string { return "invalid"; } } const error = await expectToThrow(() => buildSchema({ resolvers: [InvalidQuery] })); expect(error).toBeInstanceOf(InvalidDirectiveError); expect(error.message).toContain( 'Please pass only one directive name or definition at a time to the @Directive decorator "@upper @append"', ); }); it("throws error when parsing invalid directives", async () => { @Resolver() class InvalidQuery { @Query() @Directive("@invalid(@directive)") invalid(): string { return "invalid"; } } const error = await expectToThrow(() => buildSchema({ resolvers: [InvalidQuery] })); expect(error).toBeInstanceOf(InvalidDirectiveError); expect(error.message).toContain('Error parsing directive definition "@invalid(@directive)"'); }); it("throws error when no directives are defined", async () => { @Resolver() class InvalidQuery { @Query() @Directive("") invalid(): string { return "invalid"; } } const error = await expectToThrow(() => buildSchema({ resolvers: [InvalidQuery] })); expect(error).toBeInstanceOf(InvalidDirectiveError); expect(error.message).toContain( "Please pass at-least one directive name or definition to the @Directive decorator", ); }); }); }); ================================================ FILE: tests/functional/emit-schema-sdl.ts ================================================ import "reflect-metadata"; import fs from "node:fs"; import asyncFs from "node:fs/promises"; import path from "node:path"; import { type GraphQLSchema } from "graphql"; import shelljs from "shelljs"; import { Field, ObjectType, type PrintSchemaOptions, Query, Resolver, buildSchema, buildSchemaSync, defaultPrintSchemaOptions, emitSchemaDefinitionFile, emitSchemaDefinitionFileSync, } from "type-graphql"; import { expectToThrow } from "../helpers/expectToThrow"; const TEST_DIR = path.resolve(process.cwd(), "tests", "test-output-dir"); describe("Emitting schema definition file", () => { let schema: GraphQLSchema; let MyResolverClass: any; beforeAll(async () => { @ObjectType() class MyObject { @Field() normalProperty!: string; @Field({ description: "Description test" }) descriptionProperty!: boolean; } @Resolver() class MyResolver { @Query() myQuery(): MyObject { return {} as MyObject; } } MyResolverClass = MyResolver; schema = await buildSchema({ resolvers: [MyResolver], }); }); afterEach(() => { jest.restoreAllMocks(); shelljs.rm("-rf", TEST_DIR); }); function checkSchemaSDL( SDL: string, { sortedSchema }: PrintSchemaOptions = defaultPrintSchemaOptions, ) { expect(SDL).toContain("THIS FILE WAS GENERATED"); expect(SDL).toContain("MyObject"); if (sortedSchema) { expect(SDL.indexOf("descriptionProperty")).toBeLessThan(SDL.indexOf("normalProperty")); } else { expect(SDL.indexOf("descriptionProperty")).toBeGreaterThan(SDL.indexOf("normalProperty")); } expect(SDL).toContain(`"""Description test"""`); } describe("emitSchemaDefinitionFile", () => { it("should write file with schema SDL successfully", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test1", "schema.graphql"); await emitSchemaDefinitionFile(targetPath, schema); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString()); }); it("should use provided options to write file with schema SDL", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test1", "schema.graphql"); const options: PrintSchemaOptions = { sortedSchema: false, }; await emitSchemaDefinitionFile(targetPath, schema, options); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString(), options); }); it("should throw error when unknown error occur while writing file with schema SDL", async () => { jest.spyOn(asyncFs, "writeFile").mockRejectedValueOnce({ code: "TEST ERROR" }); const targetPath = path.join(TEST_DIR, "schemas", "fail1", "schema.graphql"); let error; try { await emitSchemaDefinitionFile(targetPath, schema); } catch (e) { error = e; } expect(error).toEqual({ code: "TEST ERROR" }); expect(fs.existsSync(targetPath)).toEqual(false); }); it("should throw error when unknown error occur while creating directory with schema SDL", async () => { jest.spyOn(asyncFs, "mkdir").mockRejectedValueOnce({ code: "TEST ERROR" }); const targetPath = path.join(TEST_DIR, "schemas", "fail2", "schema.graphql"); let error; try { await emitSchemaDefinitionFile(targetPath, schema); } catch (e) { error = e; } expect(error).toEqual({ code: "TEST ERROR" }); expect(fs.existsSync(targetPath)).toEqual(false); }); }); describe("emitSchemaDefinitionFileSync", () => { it("should write file with schema SDL successfully", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test2", "schema.graphql"); emitSchemaDefinitionFileSync(targetPath, schema); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString()); }); it("should use provided options to write file with schema SDL", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test1", "schema.graphql"); const options: PrintSchemaOptions = { sortedSchema: false, }; emitSchemaDefinitionFileSync(targetPath, schema, options); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString(), options); }); it("should throw error when unknown error occur while writing file with schema SDL", async () => { jest.spyOn(fs, "writeFileSync").mockImplementationOnce(() => { throw new Error("TYPE_GRAPHQL_WRITE_FILE_SYNC_ERROR"); }); const targetPath = path.join(TEST_DIR, "schemas", "fail3", "schema.graphql"); const error = await expectToThrow(() => emitSchemaDefinitionFileSync(targetPath, schema)); expect(error.message).toStrictEqual("TYPE_GRAPHQL_WRITE_FILE_SYNC_ERROR"); expect(fs.existsSync(targetPath)).toEqual(false); }); it("should throw error when unknown error occur while creating directory with schema SDL", async () => { jest.spyOn(fs, "mkdirSync").mockImplementationOnce(() => { throw new Error("TYPE_GRAPHQL_MKDIR_SYNC_ERROR"); }); const targetPath = path.join(TEST_DIR, "schemas", "fail4", "schema.graphql"); const error = await expectToThrow(() => emitSchemaDefinitionFileSync(targetPath, schema)); expect(error.message).toStrictEqual("TYPE_GRAPHQL_MKDIR_SYNC_ERROR"); expect(fs.existsSync(targetPath)).toEqual(false); }); }); describe("buildSchema", () => { it("should generate schema SDL file on selected file path", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test3", "schema.graphql"); await buildSchema({ resolvers: [MyResolverClass], emitSchemaFile: targetPath, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString()); }); it("should generate schema SDL file in current working dir", async () => { jest.spyOn(process, "cwd").mockImplementation(() => TEST_DIR); const targetPath = path.join(process.cwd(), "schema.graphql"); await buildSchema({ resolvers: [MyResolverClass], emitSchemaFile: true, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString()); }); it("should read EmitSchemaFileOptions and apply them in emit", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test4", "schema.graphql"); await buildSchema({ resolvers: [MyResolverClass], emitSchemaFile: { path: targetPath, sortedSchema: false, }, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString(), { sortedSchema: false, }); }); it("should read EmitSchemaFileOptions and set default path and sorting schema", async () => { jest.spyOn(process, "cwd").mockImplementation(() => TEST_DIR); const targetPath = path.join(process.cwd(), "schema.graphql"); await buildSchema({ resolvers: [MyResolverClass], emitSchemaFile: {}, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString(), { ...defaultPrintSchemaOptions, }); }); }); describe("buildSchemaSync", () => { it("should synchronously generate schema SDL file on selected file path", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test5", "schema.graphql"); buildSchemaSync({ resolvers: [MyResolverClass], emitSchemaFile: targetPath, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString()); }); it("should generate schema SDL file in current working dir", async () => { jest.spyOn(process, "cwd").mockImplementation(() => TEST_DIR); const targetPath = path.join(process.cwd(), "schema.graphql"); buildSchemaSync({ resolvers: [MyResolverClass], emitSchemaFile: true, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString()); }); it("should read EmitSchemaFileOptions and apply them in emit", async () => { const targetPath = path.join(TEST_DIR, "schemas", "test6", "schema.graphql"); buildSchemaSync({ resolvers: [MyResolverClass], emitSchemaFile: { path: targetPath, sortedSchema: false, }, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString(), { sortedSchema: false, }); }); it("should read EmitSchemaFileOptions and set default path and sorting schema", async () => { jest.spyOn(process, "cwd").mockImplementation(() => TEST_DIR); const targetPath = path.join(process.cwd(), "schema.graphql"); buildSchemaSync({ resolvers: [MyResolverClass], emitSchemaFile: {}, }); expect(fs.existsSync(targetPath)).toEqual(true); checkSchemaSDL(fs.readFileSync(targetPath).toString(), { ...defaultPrintSchemaOptions, }); }); }); }); ================================================ FILE: tests/functional/enums.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, type IntrospectionEnumType, type IntrospectionInputObjectType, type IntrospectionObjectType, type IntrospectionSchema, TypeKind, graphql, } from "graphql"; import { Arg, Field, InputType, Query, registerEnumType } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { getInnerInputFieldType, getInnerTypeOfNonNullableType, } from "../helpers/getInnerFieldType"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Enums", () => { let schemaIntrospection: IntrospectionSchema; let queryType: IntrospectionObjectType; let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); enum NumberEnum { One = 1, Two, Three, Four, } registerEnumType(NumberEnum, { name: "NumberEnum" }); enum StringEnum { One = "ONE", Two = "TWO", Three = "THREE", } registerEnumType(StringEnum, { name: "StringEnum", description: "custom string enum" }); enum AdvancedEnum { DescriptionProperty = "DescriptionProperty", DeprecationProperty = "DeprecationProperty", } registerEnumType(AdvancedEnum, { name: "AdvancedEnum", valuesConfig: { DescriptionProperty: { description: "One field description" }, DeprecationProperty: { deprecationReason: "Two field deprecation reason" }, }, }); @InputType() class NumberEnumInput { @Field(() => NumberEnum) numberEnumField!: NumberEnum; } @InputType() class StringEnumInput { @Field(() => StringEnum) stringEnumField!: StringEnum; } class SampleResolver { @Query(_returns => NumberEnum) getNumberEnumValue(@Arg("input") _input: NumberEnumInput): NumberEnum { return NumberEnum.Two; } @Query(() => StringEnum) getStringEnumValue(@Arg("input") _input: StringEnumInput): StringEnum { return StringEnum.Two; } @Query(() => AdvancedEnum) getAdvancedEnumValue(): AdvancedEnum { return AdvancedEnum.DescriptionProperty; } @Query() isNumberEnumEqualOne(@Arg("enum", () => NumberEnum) numberEnum: NumberEnum): boolean { return numberEnum === NumberEnum.One; } @Query() isStringEnumEqualOne(@Arg("enum", () => StringEnum) stringEnum: StringEnum): boolean { return stringEnum === StringEnum.One; } } const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); schema = schemaInfo.schema; schemaIntrospection = schemaInfo.schemaIntrospection; queryType = schemaInfo.queryType; }); describe("Schema", () => { it("should generate schema without errors", async () => { expect(schemaIntrospection).toBeDefined(); }); it("should generate correct enum output type", async () => { const getNumberEnumValueType = getInnerTypeOfNonNullableType( queryType.fields.find(field => field.name === "getNumberEnumValue")!, ); const getStringEnumValue = getInnerTypeOfNonNullableType( queryType.fields.find(field => field.name === "getStringEnumValue")!, ); expect(getNumberEnumValueType.kind).toEqual(TypeKind.ENUM); expect(getNumberEnumValueType.name).toEqual("NumberEnum"); expect(getStringEnumValue.kind).toEqual(TypeKind.ENUM); expect(getStringEnumValue.name).toEqual("StringEnum"); }); it("should generate correct enum input type", async () => { const numberEnumInput = schemaIntrospection.types.find( type => type.kind === "INPUT_OBJECT" && type.name === "NumberEnumInput", ) as IntrospectionInputObjectType; const stringEnumInput = schemaIntrospection.types.find( type => type.kind === "INPUT_OBJECT" && type.name === "StringEnumInput", ) as IntrospectionInputObjectType; const numberEnumInputType = getInnerInputFieldType(numberEnumInput, "numberEnumField"); const stringEnumInputType = getInnerInputFieldType(stringEnumInput, "stringEnumField"); expect(numberEnumInputType.kind).toEqual(TypeKind.ENUM); expect(numberEnumInputType.name).toEqual("NumberEnum"); expect(stringEnumInputType.kind).toEqual(TypeKind.ENUM); expect(stringEnumInputType.name).toEqual("StringEnum"); }); it("should generate correct enum arg type", async () => { const numberEnumArgType = getInnerTypeOfNonNullableType( queryType.fields.find(type => type.name === "isNumberEnumEqualOne")!.args[0], ); const stringEnumArgType = getInnerTypeOfNonNullableType( queryType.fields.find(type => type.name === "isStringEnumEqualOne")!.args[0], ); expect(numberEnumArgType.kind).toEqual(TypeKind.ENUM); expect(numberEnumArgType.name).toEqual("NumberEnum"); expect(stringEnumArgType.kind).toEqual(TypeKind.ENUM); expect(stringEnumArgType.name).toEqual("StringEnum"); }); it("should generate correct enum values for number enum", async () => { const numberEnumType = schemaIntrospection.types.find( type => type.kind === "ENUM" && type.name === "NumberEnum", ) as IntrospectionEnumType; expect(numberEnumType.name).toEqual("NumberEnum"); expect(numberEnumType.kind).toEqual(TypeKind.ENUM); expect(numberEnumType.enumValues).toHaveLength(4); expect(numberEnumType.enumValues[0].name).toEqual("One"); expect(numberEnumType.enumValues[1].name).toEqual("Two"); expect(numberEnumType.enumValues[2].name).toEqual("Three"); expect(numberEnumType.enumValues[3].name).toEqual("Four"); }); it("should generate correct enum values for string enum", async () => { const stringEnumType = schemaIntrospection.types.find( type => type.kind === "ENUM" && type.name === "StringEnum", ) as IntrospectionEnumType; expect(stringEnumType.name).toEqual("StringEnum"); expect(stringEnumType.kind).toEqual(TypeKind.ENUM); expect(stringEnumType.description).toEqual("custom string enum"); expect(stringEnumType.enumValues).toHaveLength(3); expect(stringEnumType.enumValues[0].name).toEqual("One"); expect(stringEnumType.enumValues[1].name).toEqual("Two"); expect(stringEnumType.enumValues[2].name).toEqual("Three"); }); it("should generate correct enum descriptions", async () => { const advancedEnumType = schemaIntrospection.types.find( type => type.kind === "ENUM" && type.name === "AdvancedEnum", ) as IntrospectionEnumType; expect(advancedEnumType.name).toEqual("AdvancedEnum"); expect(advancedEnumType.kind).toEqual(TypeKind.ENUM); expect(advancedEnumType.enumValues).toHaveLength(2); expect(advancedEnumType.enumValues[0].name).toEqual("DescriptionProperty"); expect(advancedEnumType.enumValues[0].description).toEqual("One field description"); }); it("should generate correct enum deprecation reason", async () => { const advancedEnumType = schemaIntrospection.types.find( type => type.kind === "ENUM" && type.name === "AdvancedEnum", ) as IntrospectionEnumType; expect(advancedEnumType.name).toEqual("AdvancedEnum"); expect(advancedEnumType.kind).toEqual(TypeKind.ENUM); expect(advancedEnumType.enumValues).toHaveLength(2); expect(advancedEnumType.enumValues[1].name).toEqual("DeprecationProperty"); expect(advancedEnumType.enumValues[1].isDeprecated).toEqual(true); expect(advancedEnumType.enumValues[1].deprecationReason).toEqual( "Two field deprecation reason", ); }); }); describe("Functional", () => { it("should correctly serialize number enum internal value", async () => { const query = `query { getNumberEnumValue(input: { numberEnumField: One }) }`; const result: any = await graphql({ schema, source: query }); expect(result.data!.getNumberEnumValue).toEqual("Two"); }); it("should correctly serialize string enum internal value", async () => { const query = `query { getStringEnumValue(input: { stringEnumField: One }) }`; const result: any = await graphql({ schema, source: query }); expect(result.data!.getStringEnumValue).toEqual("Two"); }); it("should correctly map number enum to internal value", async () => { const query1 = `query { isNumberEnumEqualOne(enum: One) }`; const query2 = `query { isNumberEnumEqualOne(enum: Two) }`; const result1 = await graphql({ schema, source: query1 }); const result2 = await graphql({ schema, source: query2 }); expect(result1.data!.isNumberEnumEqualOne).toEqual(true); expect(result2.data!.isNumberEnumEqualOne).toEqual(false); }); it("should correctly map string enum to internal value", async () => { const query1 = `query { isStringEnumEqualOne(enum: One) }`; const query2 = `query { isStringEnumEqualOne(enum: Two) }`; const result1 = await graphql({ schema, source: query1 }); const result2 = await graphql({ schema, source: query2 }); expect(result1.data!.isStringEnumEqualOne).toEqual(true); expect(result2.data!.isStringEnumEqualOne).toEqual(false); }); }); }); ================================================ FILE: tests/functional/errors/metadata-polyfill.ts ================================================ import { ReflectMetadataMissingError } from "type-graphql"; import { findType } from "@/helpers/findType"; import { expectToThrow } from "../../helpers/expectToThrow"; describe("Reflect metadata", () => { it("should throw ReflectMetadataMissingError when no polyfill provided", async () => { const error = await expectToThrow(() => findType({ metadataKey: "design:type", prototype: {}, propertyKey: "test", }), ); expect(error).toBeInstanceOf(ReflectMetadataMissingError); expect(error.message).toContain("metadata"); expect(error.message).toContain("polyfill"); }); }); ================================================ FILE: tests/functional/extensions.ts ================================================ import "reflect-metadata"; import { type GraphQLFieldMap, type GraphQLInputObjectType, GraphQLInterfaceType, type GraphQLObjectType, type GraphQLSchema, } from "graphql"; import { Arg, Extensions, Field, FieldResolver, InputType, InterfaceType, Mutation, ObjectType, Query, Resolver, buildSchema, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; describe("Extensions", () => { let schema: GraphQLSchema; describe("Schema", () => { beforeAll(async () => { getMetadataStorage().clear(); @InputType() class ExtensionsOnFieldInput { @Field() @Extensions({ role: "admin" }) withExtensions!: string; } @InputType() @Extensions({ roles: ["admin", "user"] }) class ExtensionsOnClassInput { @Field() regularField!: string; } @ObjectType() @Extensions({ id: 1234 }) class ExtensionsOnClassObjectType { @Field() regularField!: string; } @ObjectType() class SampleObjectType { @Field() @Extensions({ role: "user" }) withExtensions: string = "withExtensions"; @Field() @Extensions({ first: "first value", second: "second value" }) withMultipleExtensions: string = "withMultipleExtensions"; @Field() @Extensions({ first: "first value" }) @Extensions({ second: "second value", third: "third value" }) withMultipleExtensionsDecorators: string = "hello"; @Field() @Extensions({ duplicate: "first value" }) @Extensions({ duplicate: "second value" }) withConflictingExtensionsKeys: string = "hello"; @Field() withInput(@Arg("input") input: ExtensionsOnFieldInput): string { return `hello${input.withExtensions}`; } @Field() @Extensions({ other: "extension" }) withInputAndField(@Arg("input") input: ExtensionsOnFieldInput): string { return `hello${input.withExtensions}`; } @Field() withInputOnClass(@Arg("input") input: ExtensionsOnClassInput): string { return `hello${input.regularField}`; } @Field() @Extensions({ other: "extension" }) withInputAndFieldOnClass(@Arg("input") input: ExtensionsOnClassInput): string { return `hello${input.regularField}`; } } @InterfaceType() @Extensions({ meta: "interfaceExtensionData" }) class SampleInterfaceType { @Field() @Extensions({ meta: "interfaceFieldExtensionData" }) withInterfaceFieldExtension!: string; } @ObjectType({ implements: [SampleInterfaceType], }) class SampleObjectInterfaceImplementation extends SampleInterfaceType {} @InterfaceType({ implements: [SampleInterfaceType], }) class SampleInterfaceInterfaceImplementation extends SampleInterfaceType {} @ObjectType({ implements: [SampleInterfaceInterfaceImplementation], }) class SampleObjectInterfaceInterfaceImplementation extends SampleInterfaceInterfaceImplementation {} @Resolver() class SampleResolver { @Query(() => SampleObjectType) sampleObjectType(): SampleObjectType { return new SampleObjectType(); } @Query(() => ExtensionsOnClassObjectType) extensionsOnClassObjectType(): ExtensionsOnClassObjectType { return new ExtensionsOnClassObjectType(); } @Query(() => SampleObjectInterfaceImplementation) sampleObjectInterfaceImplementation(): SampleObjectInterfaceImplementation { return new SampleObjectInterfaceImplementation(); } @Query(() => SampleInterfaceInterfaceImplementation) sampleInterfaceInterfaceImplementation(): SampleInterfaceInterfaceImplementation { return new SampleInterfaceInterfaceImplementation(); } @Query(() => SampleObjectInterfaceInterfaceImplementation) sampleObjectInterfaceInterfaceImplementation(): SampleObjectInterfaceInterfaceImplementation { return new SampleObjectInterfaceInterfaceImplementation(); } @Query(() => SampleInterfaceType) sampleInterfaceType(): SampleInterfaceType { return new SampleObjectInterfaceImplementation(); } @Query() @Extensions({ mandatory: true }) queryWithExtensions(): string { return "queryWithExtensions"; } @Query() @Extensions({ first: "first query value", second: "second query value" }) queryWithMultipleExtensions(): string { return "hello"; } @Query() @Extensions({ first: "first query value" }) @Extensions({ second: "second query value", third: "third query value" }) queryWithMultipleExtensionsDecorators(): string { return "hello"; } @Mutation() @Extensions({ mandatory: false }) mutationWithExtensions(): string { return "mutationWithExtensions"; } @Mutation() @Extensions({ first: "first mutation value", second: "second mutation value" }) mutationWithMultipleExtensions(): string { return "mutationWithMultipleExtensions"; } @Mutation() @Extensions({ first: "first mutation value" }) @Extensions({ second: "second mutation value", third: "third mutation value" }) mutationWithMultipleExtensionsDecorators(): string { return "mutationWithMultipleExtensionsDecorators"; } } @Resolver(() => SampleObjectType) class SampleObjectTypeResolver { @FieldResolver() @Extensions({ some: "extension" }) fieldResolverWithExtensions(): string { return "hello"; } } schema = await buildSchema({ resolvers: [SampleResolver, SampleObjectTypeResolver], }); }); it("should generate schema without errors", async () => { expect(schema).toBeDefined(); }); describe("Fields", () => { let fields: GraphQLFieldMap; beforeAll(async () => { fields = (schema.getType("SampleObjectType") as GraphQLObjectType).getFields(); }); it("should add simple extensions to object fields", async () => { expect(fields.withExtensions.extensions).toEqual({ role: "user" }); }); it("should add extensions with multiple properties to object fields", async () => { expect(fields.withMultipleExtensions.extensions).toEqual({ first: "first value", second: "second value", }); }); it("should allow multiple extensions decorators for object fields", async () => { expect(fields.withMultipleExtensionsDecorators.extensions).toEqual({ first: "first value", second: "second value", third: "third value", }); }); it("should override extensions values when duplicate keys are provided", async () => { expect(fields.withConflictingExtensionsKeys.extensions).toEqual({ duplicate: "second value", }); }); }); describe("Query", () => { it("should add simple extensions to query types", async () => { const { queryWithExtensions } = schema.getQueryType()!.getFields(); expect(queryWithExtensions.extensions).toEqual({ mandatory: true }); }); it("should add extensions with multiple properties to query types", async () => { const { queryWithMultipleExtensions } = schema.getQueryType()!.getFields(); expect(queryWithMultipleExtensions.extensions).toEqual({ first: "first query value", second: "second query value", }); }); it("should allow multiple extensions decorators for query types", async () => { const { queryWithMultipleExtensionsDecorators } = schema.getQueryType()!.getFields(); expect(queryWithMultipleExtensionsDecorators.extensions).toEqual({ first: "first query value", second: "second query value", third: "third query value", }); }); }); describe("Mutation", () => { it("should add simple extensions to mutation types", async () => { const { mutationWithExtensions } = schema.getMutationType()!.getFields(); expect(mutationWithExtensions.extensions).toEqual({ mandatory: false }); }); it("should add extensions with multiple properties to mutation types", async () => { const { mutationWithMultipleExtensions } = schema.getMutationType()!.getFields(); expect(mutationWithMultipleExtensions.extensions).toEqual({ first: "first mutation value", second: "second mutation value", }); }); it("should allow multiple extensions decorators for mutation types", async () => { const { mutationWithMultipleExtensionsDecorators } = schema.getMutationType()!.getFields(); expect(mutationWithMultipleExtensionsDecorators.extensions).toEqual({ first: "first mutation value", second: "second mutation value", third: "third mutation value", }); }); }); describe("ObjectType", () => { it("should add extensions to object types", async () => { const objectType = schema.getType("ExtensionsOnClassObjectType") as GraphQLObjectType; expect(objectType.extensions).toEqual({ id: 1234 }); }); }); describe("InputType", () => { it("should add extensions to input types", async () => { const inputType = schema.getType("ExtensionsOnClassInput") as GraphQLInputObjectType; expect(inputType.extensions).toEqual({ roles: ["admin", "user"] }); }); it("should add extensions to input type fields", async () => { const fields = ( schema.getType("ExtensionsOnFieldInput") as GraphQLInputObjectType ).getFields(); expect(fields.withExtensions.extensions).toEqual({ role: "admin" }); }); }); describe("FieldResolver", () => { it("should add extensions to field resolvers", async () => { const fields = (schema.getType("SampleObjectType") as GraphQLObjectType).getFields(); expect(fields.fieldResolverWithExtensions.extensions).toEqual({ some: "extension" }); }); }); describe("Interface Fields", () => { it("should add extensions to interface types", async () => { const fields = (schema.getType("SampleInterfaceType") as GraphQLObjectType).getFields(); expect(fields.withInterfaceFieldExtension.extensions).toEqual({ meta: "interfaceFieldExtensionData", }); }); it("should add extensions to ObjectType which extends InterfaceType", async () => { const fields = ( schema.getType("SampleObjectInterfaceImplementation") as GraphQLObjectType ).getFields(); expect(fields.withInterfaceFieldExtension.extensions).toEqual({ meta: "interfaceFieldExtensionData", }); }); it("should add extensions to Interface which extends InterfaceType", async () => { const fields = ( schema.getType("SampleInterfaceInterfaceImplementation") as GraphQLObjectType ).getFields(); expect(fields.withInterfaceFieldExtension.extensions).toEqual({ meta: "interfaceFieldExtensionData", }); }); it("should add extensions to ObjectType which extends InterfaceType which extends InterfaceType", async () => { const fields = ( schema.getType("SampleInterfaceInterfaceImplementation") as GraphQLObjectType ).getFields(); expect(fields.withInterfaceFieldExtension.extensions).toEqual({ meta: "interfaceFieldExtensionData", }); }); }); describe("Interface Class", () => { it("should add extensions to interface types", async () => { const sampleInterface = schema.getType("SampleInterfaceType") as GraphQLInterfaceType; expect(sampleInterface.extensions).toEqual({ meta: "interfaceExtensionData", }); }); it("should add extensions to ObjectType which extends InterfaceType", async () => { const sampleInterface = schema.getType( "SampleObjectInterfaceImplementation", ) as GraphQLInterfaceType; expect(sampleInterface.extensions).toEqual({ meta: "interfaceExtensionData", }); }); it("should add extensions to InterfaceType which extends InterfaceType", async () => { const sampleInterface = schema.getType( "SampleInterfaceInterfaceImplementation", ) as GraphQLInterfaceType; expect(sampleInterface.extensions).toEqual({ meta: "interfaceExtensionData", }); }); it("should add extensions to ObjectType which extends InterfaceType which extends InterfaceType", async () => { const sampleInterface = schema.getType( "SampleObjectInterfaceInterfaceImplementation", ) as GraphQLInterfaceType; expect(sampleInterface.extensions).toEqual({ meta: "interfaceExtensionData", }); }); }); describe("Inheritance", () => { beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() @Extensions({ parentClass: true }) class Parent { @Field() @Extensions({ parentField: true }) parentField!: string; } @Extensions({ childClass: true }) @ObjectType() class Child extends Parent { @Field() @Extensions({ childField: true }) childField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): Child { return {} as Child; } } schema = await buildSchema({ resolvers: [SampleResolver], orphanedTypes: [Parent], }); }); it("should inherit object type extensions from parent object type class", () => { const childObjectType = schema.getType("Child") as GraphQLObjectType; expect(childObjectType.extensions).toEqual({ parentClass: true, childClass: true, }); }); it("should not get object type extensions from child object type class", () => { const parentObjectType = schema.getType("Parent") as GraphQLObjectType; expect(parentObjectType.extensions).toEqual({ parentClass: true, }); }); it("should inherit object type field extensions from parent object type field", () => { const childObjectType = schema.getType("Child") as GraphQLObjectType; const childObjectTypeParentField = childObjectType.getFields().parentField; expect(childObjectTypeParentField.extensions).toEqual({ parentField: true }); }); }); describe("Fields with field resolvers", () => { beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class Child { @Field() @Extensions({ childField: true }) childField!: string; } @Resolver(() => Child) class ChildResolver { @Query() sampleQuery(): Child { return {} as Child; } @Extensions({ childFieldResolver: true }) @FieldResolver() childField(): string { return "childField"; } } schema = await buildSchema({ resolvers: [ChildResolver], }); }); it("should merge field level with field resolver level extensions", () => { const childObjectType = schema.getType("Child") as GraphQLObjectType; expect(childObjectType.getFields().childField.extensions).toEqual({ childField: true, childFieldResolver: true, }); }); }); }); }); ================================================ FILE: tests/functional/fields.ts ================================================ import "reflect-metadata"; import { type IntrospectionListTypeRef, type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionScalarType, type IntrospectionSchema, TypeKind, } from "graphql"; import { Field, GraphQLISODateTime, ObjectType, Query, Resolver } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { expectToThrow } from "../helpers/expectToThrow"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Fields - schema", () => { let schemaIntrospection: IntrospectionSchema; let sampleObjectType: IntrospectionObjectType; beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleNestedObject { @Field() stringField!: string; } @ObjectType() class SampleObject { @Field() implicitStringField!: string; @Field(() => String) explicitStringField: any; @Field() implicitObjectField!: SampleNestedObject; @Field(() => String, { nullable: true }) explicitNullableStringField: any; @Field({ nullable: true }) implicitNullableStringField!: string; @Field(() => [String]) explicitStringArrayField!: string[]; @Field(() => [String], { nullable: true }) nullableArrayFieldNew!: string[] | null; @Field(() => [SampleNestedObject], { nullable: true }) nullableObjectArrayField!: SampleNestedObject[] | null; @Field(() => [String], { nullable: "itemsAndList" }) arrayWithNullableItemField!: string[]; @Field(() => [String], { nullable: "items" }) nonNullArrayWithNullableItemField!: string[]; @Field({ name: "overwrittenName", nullable: true }) overwrittenStringField!: string; @Field({ name: "complexField", complexity: 10 }) complexField!: string; @Field(() => [[String]], { nullable: true }) nullableNestedArrayField!: string[][] | null; @Field(() => [[String]], { nullable: "items" }) nonNullNestedArrayWithNullableItemField!: Array | null>; @Field(() => [[String]], { nullable: "itemsAndList" }) nestedArrayWithNullableItemField!: Array | null> | null; @Field(() => GraphQLISODateTime) overwrittenArrayScalarField!: string[]; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(): SampleObject { return {} as SampleObject; } } // get builded schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); schemaIntrospection = schemaInfo.schemaIntrospection; sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; }); // helpers function getInnerFieldType(name: string) { const fieldType = sampleObjectType.fields.find(field => field.name === name)!; return (fieldType.type as IntrospectionNonNullTypeRef).ofType! as IntrospectionNamedTypeRef; } // tests it("should generate schema without errors", async () => { expect(schemaIntrospection).toBeDefined(); }); it("should register complexity info for field", async () => { const metadataStorage = getMetadataStorage(); const sampleObj = metadataStorage.objectTypes.find(it => it.name === "SampleObject")!; const complexField = sampleObj.fields!.find(it => it.name === "complexField")!; expect(complexField.complexity).toBe(10); }); it("should throw error when field type not provided", async () => { getMetadataStorage().clear(); const error = await expectToThrow(() => { @ObjectType() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleObject { @Field() invalidSampleField: any; } }); expect(error).toBeInstanceOf(Error); expect(error.message).toContain("provide explicit type"); expect(error.message).toContain("invalidSampleField"); }); it("should throw error when field type is array and no explicit type provided", async () => { getMetadataStorage().clear(); const error = await expectToThrow(() => { @ObjectType() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleObject { @Field() invalidSampleArrayField!: string[]; } }); expect(error).toBeInstanceOf(Error); expect(error.message).toContain("provide explicit type"); expect(error.message).toContain("invalidSampleArrayField"); }); it("should throw error when cannot determine field type", async () => { getMetadataStorage().clear(); const error = await expectToThrow(() => { @ObjectType() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleObject { @Field({ nullable: true }) invalidSampleNullableField!: string | null; } }); expect(error).toBeInstanceOf(Error); expect(error.message).toContain("provide explicit type"); expect(error.message).toContain("invalidSampleNullableField"); }); it("should throw error when object type property key is symbol", async () => { getMetadataStorage().clear(); const symbolKey = Symbol("symbolKey"); const error = await expectToThrow(() => { @ObjectType() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleObject { @Field({ nullable: true }) [symbolKey]!: string | null; } }); expect(error.message).toContain("Symbol keys are not supported yet!"); }); it("should generate non-nullable field type by default", async () => { const implicitStringField = sampleObjectType.fields.find( field => field.name === "implicitStringField", )!; expect(implicitStringField.type.kind).toEqual(TypeKind.NON_NULL); }); it("should generate implicit field type for scalar", async () => { const implicitStringFieldType = getInnerFieldType("implicitStringField"); expect(implicitStringFieldType.kind).toEqual(TypeKind.SCALAR); expect(implicitStringFieldType.name).toEqual("String"); }); it("should generate explicit field type for scalar", async () => { const explicitStringFieldType = getInnerFieldType("explicitStringField"); expect(explicitStringFieldType.kind).toEqual(TypeKind.SCALAR); expect(explicitStringFieldType.name).toEqual("String"); }); it("should generate implicit field type for object type", async () => { const implicitObjectFieldType = getInnerFieldType("implicitObjectField"); expect(implicitObjectFieldType.kind).toEqual(TypeKind.OBJECT); expect(implicitObjectFieldType.name).toEqual("SampleNestedObject"); }); it("should generate nullable field type for implicit scalar", async () => { const implicitNullableStringField = sampleObjectType.fields.find( field => field.name === "implicitNullableStringField", )!; // prettier-ignore const implicitNullableStringFieldType = implicitNullableStringField.type as IntrospectionNamedTypeRef; expect(implicitNullableStringFieldType.kind).toEqual(TypeKind.SCALAR); expect(implicitNullableStringFieldType.name).toEqual("String"); }); it("should generate nullable field type for explicit type", async () => { const explicitNullableStringField = sampleObjectType.fields.find( field => field.name === "explicitNullableStringField", )!; // prettier-ignore const explicitNullableStringFieldType = explicitNullableStringField.type as IntrospectionNamedTypeRef; expect(explicitNullableStringFieldType.kind).toEqual(TypeKind.SCALAR); expect(explicitNullableStringFieldType.name).toEqual("String"); }); it("should generate non-nullable array of non-nullable items field type by default", async () => { const nonNullField = sampleObjectType.fields.find( field => field.name === "explicitStringArrayField", )!; const nonNullFieldType = nonNullField.type as IntrospectionNonNullTypeRef; const arrayFieldType = nonNullFieldType.ofType as IntrospectionListTypeRef; const arrayItemNonNullFieldType = arrayFieldType.ofType as IntrospectionNonNullTypeRef; const arrayItemFieldType = arrayItemNonNullFieldType.ofType as IntrospectionNamedTypeRef; expect(nonNullFieldType.kind).toEqual(TypeKind.NON_NULL); expect(arrayFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemNonNullFieldType.kind).toEqual(TypeKind.NON_NULL); expect(arrayItemFieldType.kind).toEqual(TypeKind.SCALAR); expect(arrayItemFieldType.name).toEqual("String"); }); it("should generate nullable array field type when declared using mongoose syntax", async () => { const nullableArrayFieldNew = sampleObjectType.fields.find( field => field.name === "nullableArrayFieldNew", )!; const arrayFieldType = nullableArrayFieldNew.type as IntrospectionListTypeRef; const arrayItemNonNullFieldType = arrayFieldType.ofType as IntrospectionNonNullTypeRef; const arrayItemFieldType = arrayItemNonNullFieldType.ofType as IntrospectionNamedTypeRef; expect(arrayFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemNonNullFieldType.kind).toEqual(TypeKind.NON_NULL); expect(arrayItemFieldType.kind).toEqual(TypeKind.SCALAR); expect(arrayItemFieldType.name).toEqual("String"); }); it("should generate nullable array field object type when declared using mongoose syntax", async () => { const nullableArrayFieldNew = sampleObjectType.fields.find( field => field.name === "nullableObjectArrayField", )!; const arrayFieldType = nullableArrayFieldNew.type as IntrospectionListTypeRef; const arrayItemNonNullFieldType = arrayFieldType.ofType as IntrospectionNonNullTypeRef; const arrayItemFieldType = arrayItemNonNullFieldType.ofType as IntrospectionNamedTypeRef; expect(arrayFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemNonNullFieldType.kind).toEqual(TypeKind.NON_NULL); expect(arrayItemFieldType.kind).toEqual(TypeKind.OBJECT); expect(arrayItemFieldType.name).toEqual("SampleNestedObject"); }); it("should generate nullable item array with nullable option 'itemAndList'", async () => { const arrayWithNullableItemField = sampleObjectType.fields.find( field => field.name === "arrayWithNullableItemField", )!; const nullableArrayType = arrayWithNullableItemField.type as IntrospectionListTypeRef; const nullableItemType = nullableArrayType.ofType as IntrospectionNamedTypeRef; expect(nullableArrayType.kind).toEqual(TypeKind.LIST); expect(nullableItemType.kind).toEqual(TypeKind.SCALAR); expect(nullableItemType.name).toEqual("String"); }); it("should generate nullable element nonNull array with nullable option 'item'", async () => { const nonNullArrayWithNullableItemField = sampleObjectType.fields.find( field => field.name === "nonNullArrayWithNullableItemField", )!; const nonNullArrayType = nonNullArrayWithNullableItemField.type as IntrospectionNonNullTypeRef; const arrayType = nonNullArrayType.ofType as IntrospectionListTypeRef; const elementType = arrayType.ofType as IntrospectionNamedTypeRef; expect(nonNullArrayType.kind).toEqual(TypeKind.NON_NULL); expect(arrayType.kind).toEqual(TypeKind.LIST); expect(elementType.kind).toEqual(TypeKind.SCALAR); expect(elementType.name).toEqual("String"); }); it("should generate field with overwritten name from decorator option", async () => { const overwrittenNameField = sampleObjectType.fields.find( field => field.name === "overwrittenName", )!; const overwrittenStringField = sampleObjectType.fields.find( field => field.name === "overwrittenStringField", ); const overwrittenNameFieldType = overwrittenNameField.type as IntrospectionNamedTypeRef; expect(overwrittenStringField).toBeUndefined(); expect(overwrittenNameFieldType.kind).toEqual(TypeKind.SCALAR); expect(overwrittenNameFieldType.name).toEqual("String"); }); it("should generate nullable nested array field type when declared using mongoose syntax", async () => { const nullableNestedArrayField = sampleObjectType.fields.find( field => field.name === "nullableNestedArrayField", )!; const arrayFieldType = nullableNestedArrayField.type as IntrospectionListTypeRef; const arrayItemNonNullFieldType = arrayFieldType.ofType as IntrospectionNonNullTypeRef; const arrayItemFieldType = arrayItemNonNullFieldType.ofType as IntrospectionListTypeRef; const arrayItemScalarNonNullFieldType = arrayItemFieldType.ofType as IntrospectionNonNullTypeRef; const arrayItemScalarFieldType = arrayItemScalarNonNullFieldType.ofType as IntrospectionNamedTypeRef; expect(arrayFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemNonNullFieldType.kind).toEqual(TypeKind.NON_NULL); expect(arrayItemFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemScalarNonNullFieldType.kind).toEqual(TypeKind.NON_NULL); expect(arrayItemScalarFieldType.kind).toEqual(TypeKind.SCALAR); expect(arrayItemScalarFieldType.name).toEqual("String"); }); it("should generate nested array with nullable option 'items'", async () => { const nestedArrayField = sampleObjectType.fields.find( field => field.name === "nonNullNestedArrayWithNullableItemField", )!; const arrayNonNullFieldType = nestedArrayField.type as IntrospectionNonNullTypeRef; const arrayItemFieldType = arrayNonNullFieldType.ofType as IntrospectionListTypeRef; const arrayItemInnerFieldType = arrayItemFieldType.ofType as IntrospectionListTypeRef; const arrayItemScalarFieldType = arrayItemInnerFieldType.ofType as IntrospectionNamedTypeRef; expect(arrayNonNullFieldType.kind).toEqual(TypeKind.NON_NULL); expect(arrayItemFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemInnerFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemScalarFieldType.kind).toEqual(TypeKind.SCALAR); expect(arrayItemScalarFieldType.name).toEqual("String"); }); it("should generate nullable nested array with nullable option 'itemsAndList'", async () => { const nullableNestedArrayField = sampleObjectType.fields.find( field => field.name === "nestedArrayWithNullableItemField", )!; const arrayFieldType = nullableNestedArrayField.type as IntrospectionListTypeRef; const arrayItemFieldType = arrayFieldType.ofType as IntrospectionListTypeRef; const arrayItemScalarFieldType = arrayItemFieldType.ofType as IntrospectionNamedTypeRef; expect(arrayFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemFieldType.kind).toEqual(TypeKind.LIST); expect(arrayItemScalarFieldType.kind).toEqual(TypeKind.SCALAR); expect(arrayItemScalarFieldType.name).toEqual("String"); }); it("should generate not a list type for explicit scalar even when the reflected type is array", async () => { const overwrittenArrayScalarField = sampleObjectType.fields.find( field => field.name === "overwrittenArrayScalarField", )!; const overwrittenArrayScalarFieldType = overwrittenArrayScalarField.type as IntrospectionNonNullTypeRef; const overwrittenArrayScalarFieldInnerType = overwrittenArrayScalarFieldType.ofType as IntrospectionScalarType; expect(overwrittenArrayScalarFieldType.kind).toEqual(TypeKind.NON_NULL); expect(overwrittenArrayScalarFieldInnerType.kind).toEqual(TypeKind.SCALAR); expect(overwrittenArrayScalarFieldInnerType.name).toEqual("DateTimeISO"); }); }); ================================================ FILE: tests/functional/generic-types.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, type IntrospectionInputObjectType, type IntrospectionInterfaceType, type IntrospectionListTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionScalarType, type IntrospectionSchema, TypeKind, graphql, } from "graphql"; import { Arg, type ClassType, Field, InputType, Int, InterfaceType, ObjectType, Query, Resolver, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Generic types", () => { beforeEach(() => { getMetadataStorage().clear(); }); it("shouldn't emit unused abstract object type", async () => { @ObjectType() abstract class BaseType { @Field() baseField!: string; } @ObjectType() class SampleType extends BaseType { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleType { return { sampleField: "sampleField", baseField: "baseField", }; } } const { schemaIntrospection } = await getSchemaInfo({ resolvers: [SampleResolver] }); const sampleTypeInfo = schemaIntrospection.types.find( it => it.name === "SampleType", ) as IntrospectionObjectType; const baseTypeInfo = schemaIntrospection.types.find(it => it.name === "BaseType") as undefined; expect(sampleTypeInfo.fields).toHaveLength(2); expect(baseTypeInfo).toBeUndefined(); }); it("shouldn't emit unused abstract interface type", async () => { @InterfaceType() abstract class BaseInterfaceType { @Field() baseField!: string; } @InterfaceType() abstract class SampleInterfaceType extends BaseInterfaceType { @Field() sampleField!: string; } @ObjectType({ implements: SampleInterfaceType }) class SampleType implements SampleInterfaceType { @Field() baseField!: string; @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleInterfaceType { const sample = new SampleType(); sample.baseField = "baseField"; sample.sampleField = "sampleField"; return sample; } } const { schemaIntrospection } = await getSchemaInfo({ resolvers: [SampleResolver] }); const sampleInterfaceTypeInfo = schemaIntrospection.types.find( it => it.name === "SampleInterfaceType", ) as IntrospectionInterfaceType; const baseInterfaceTypeInfo = schemaIntrospection.types.find( it => it.name === "BaseInterfaceType", ) as undefined; expect(sampleInterfaceTypeInfo.fields).toHaveLength(2); expect(baseInterfaceTypeInfo).toBeUndefined(); }); it("shouldn't emit unused abstract input object type", async () => { @InputType() abstract class BaseInput { @Field() baseField!: string; } @InputType() class SampleInput extends BaseInput { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): boolean { return true; } } const { schemaIntrospection } = await getSchemaInfo({ resolvers: [SampleResolver] }); const sampleInputInfo = schemaIntrospection.types.find( it => it.name === "SampleInput", ) as IntrospectionInputObjectType; const baseInputInfo = schemaIntrospection.types.find( it => it.name === "BaseInput", ) as undefined; expect(sampleInputInfo.inputFields).toHaveLength(2); expect(baseInputInfo).toBeUndefined(); }); describe("multiple children of base generic class", () => { let schema: GraphQLSchema; let schemaIntrospection: IntrospectionSchema; let dogsResponseMock: any; beforeEach(async () => { function Connection(TItemClass: ClassType) { @ObjectType(`${TItemClass.name}Connection`) class ConnectionClass { @Field(() => Int) count!: number; @Field(() => [TItemClass]) items!: TItem[]; } return ConnectionClass; } @ObjectType() class User { @Field() name!: string; } @ObjectType() class Dog { @Field() canBark!: boolean; } const UserConnection = Connection(User); // eslint-disable-next-line @typescript-eslint/no-redeclare type UserConnection = InstanceType; @ObjectType() class DogConnection extends Connection(Dog) {} dogsResponseMock = { count: 2, items: [{ canBark: false }, { canBark: true }], } as DogConnection; @Resolver() class GenericConnectionResolver { @Query(() => UserConnection) users(): UserConnection { return { count: 2, items: [{ name: "Tony" }, { name: "Michael" }], }; } @Query(() => DogConnection) dogs(): DogConnection { return dogsResponseMock; } } ({ schema, schemaIntrospection } = await getSchemaInfo({ resolvers: [GenericConnectionResolver], })); }); it("should register proper types in schema using const and class syntax", async () => { const schemaObjectTypes = schemaIntrospection.types.filter( it => it.kind === TypeKind.OBJECT && !it.name.startsWith("__"), ); const userConnectionTypeInfo = schemaObjectTypes.find( it => it.name === "UserConnection", ) as IntrospectionObjectType; const userConnectionCountField = userConnectionTypeInfo.fields.find( it => it.name === "count", )!; const userConnectionCountFieldType = ( userConnectionCountField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionScalarType; const userConnectionItemsField = userConnectionTypeInfo.fields.find( it => it.name === "items", )!; const userConnectionItemsFieldType = ( ( (userConnectionItemsField.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionListTypeRef ).ofType as IntrospectionNonNullTypeRef ).ofType as IntrospectionObjectType; expect(schemaObjectTypes).toHaveLength(5); // Query, User, Dog, UserCon, DogCon expect(userConnectionTypeInfo.fields).toHaveLength(2); expect(userConnectionCountFieldType.kind).toBe(TypeKind.SCALAR); expect(userConnectionCountFieldType.name).toBe("Int"); expect(userConnectionItemsFieldType.kind).toBe(TypeKind.OBJECT); expect(userConnectionItemsFieldType.name).toBe("User"); }); it("should return child class data from query", async () => { const query = /* graphql */ ` query { dogs { count items { canBark } } } `; const result: any = await graphql({ schema, source: query }); expect(result.data!.dogs).toEqual(dogsResponseMock); }); }); describe("adding new properties in child class", () => { let schema: GraphQLSchema; let schemaIntrospection: IntrospectionSchema; let recipeEdgeResponse: any; let friendshipEdgeResponse: any; beforeEach(async () => { function Edge(TNodeClass: ClassType) { @ObjectType() abstract class EdgeClass { @Field(() => TNodeClass) node!: TNode; @Field() cursor!: string; } return EdgeClass; } @ObjectType() class Recipe { @Field() title!: string; } @ObjectType() class User { @Field() name!: string; } @ObjectType() class RecipeEdge extends Edge(Recipe) { @Field() personalNotes!: string; } recipeEdgeResponse = { cursor: "recipeCursor", node: { title: "recipeTitle", }, personalNotes: "recipePersonalNotes", } as RecipeEdge; @ObjectType() class FriendshipEdge extends Edge(User) { @Field() friendedAt!: Date; } friendshipEdgeResponse = { cursor: "friendshipCursor", node: { name: "userName", }, friendedAt: new Date(), } as FriendshipEdge; @Resolver() class EdgeResolver { @Query() recipeEdge(): RecipeEdge { return recipeEdgeResponse; } @Query() friendshipEdge(): FriendshipEdge { return friendshipEdgeResponse; } } ({ schema, schemaIntrospection } = await getSchemaInfo({ resolvers: [EdgeResolver], })); }); it("should register fields properly in schema", async () => { const schemaObjectTypes = schemaIntrospection.types.filter( it => it.kind === TypeKind.OBJECT && !it.name.startsWith("__"), ); const recipeEdgeTypeInfo = schemaObjectTypes.find( it => it.name === "RecipeEdge", ) as IntrospectionObjectType; const recipeEdgeNodeField = recipeEdgeTypeInfo.fields.find(it => it.name === "node")!; const recipeEdgeNodeFieldType = (recipeEdgeNodeField.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionObjectType; const recipeEdgePersonalNotesField = recipeEdgeTypeInfo.fields.find( it => it.name === "personalNotes", )!; const recipeEdgePersonalNotesFieldType = ( recipeEdgePersonalNotesField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionObjectType; const friendshipEdgeTypeInfo = schemaObjectTypes.find( it => it.name === "FriendshipEdge", ) as IntrospectionObjectType; const friendshipEdgeNodeField = friendshipEdgeTypeInfo.fields.find(it => it.name === "node")!; const friendshipEdgeNodeFieldType = ( friendshipEdgeNodeField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionObjectType; const friendshipEdgeFriendedAtField = friendshipEdgeTypeInfo.fields.find( it => it.name === "friendedAt", )!; const friendshipEdgeFriendedAtFieldType = ( friendshipEdgeFriendedAtField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionObjectType; expect(schemaObjectTypes).toHaveLength(5); // Query, User, Dog, UserCon, DogCon expect(recipeEdgeTypeInfo.fields).toHaveLength(3); expect(recipeEdgeNodeFieldType.kind).toBe(TypeKind.OBJECT); expect(recipeEdgeNodeFieldType.name).toBe("Recipe"); expect(recipeEdgePersonalNotesFieldType.kind).toBe(TypeKind.SCALAR); expect(recipeEdgePersonalNotesFieldType.name).toBe("String"); expect(friendshipEdgeTypeInfo.fields).toHaveLength(3); expect(friendshipEdgeNodeFieldType.kind).toBe(TypeKind.OBJECT); expect(friendshipEdgeNodeFieldType.name).toBe("User"); expect(friendshipEdgeFriendedAtFieldType.kind).toBe(TypeKind.SCALAR); expect(friendshipEdgeFriendedAtFieldType.name).toBe("DateTimeISO"); }); it("should return child classes data from queries", async () => { const query = /* graphql */ ` query { recipeEdge { cursor node { title } personalNotes } friendshipEdge { cursor node { name } friendedAt } } `; const result: any = await graphql({ schema, source: query }); expect(result.data!.recipeEdge).toEqual(recipeEdgeResponse); expect(result.data!.friendshipEdge).toEqual({ ...friendshipEdgeResponse, friendedAt: friendshipEdgeResponse.friendedAt.toISOString(), }); }); }); describe("overwriting a property from base generic class in child class", () => { let schema: GraphQLSchema; let schemaIntrospection: IntrospectionSchema; beforeEach(async () => { function Base(TTypeClass: ClassType) { @ObjectType() class BaseClass { @Field(() => TTypeClass) baseField!: TType; } return BaseClass; } @ObjectType() class BaseSample { @Field() sampleField!: string; } @ObjectType() class ChildSample { @Field() sampleField!: string; @Field() childField!: string; } @ObjectType() class Child extends Base(BaseSample) { @Field() override baseField!: ChildSample; // Overwriting field with a up compatible type } @Resolver() class OverwriteResolver { @Query() child(): Child { return { baseField: { sampleField: "sampleField", childField: "childField", }, }; } } ({ schema, schemaIntrospection } = await getSchemaInfo({ resolvers: [OverwriteResolver], })); }); it("should register proper type with overwritten field from base generic class", async () => { const childTypeInfo = schemaIntrospection.types.find( it => it.name === "Child", ) as IntrospectionObjectType; const childTypeBaseField = childTypeInfo.fields.find(it => it.name === "baseField")!; const childTypeBaseFieldType = (childTypeBaseField.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionObjectType; expect(childTypeBaseFieldType.kind).toEqual(TypeKind.OBJECT); expect(childTypeBaseFieldType.name).toEqual("ChildSample"); }); it("should return overwritten child class data from query", async () => { const document = /* graphql */ ` query { child { baseField { sampleField childField } } } `; const result: any = await graphql({ schema, source: document }); expect(result.data!).toEqual({ child: { baseField: { sampleField: "sampleField", childField: "childField", }, }, }); }); }); }); ================================================ FILE: tests/functional/inputtype-enumerable-properties.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, graphql } from "graphql"; import { Arg, Field, InputType, Query, Resolver, buildSchema } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; describe("InputType enumerable properties", () => { let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field() requiredField!: string; @Field({ nullable: true }) optionalField?: string; @Field({ nullable: true }) anotherOptional?: number; } @InputType() class NestedInput { @Field({ nullable: true }) optionalNested?: string; } @InputType() class ParentInput { @Field() required!: string; @Field(() => NestedInput, { nullable: true }) nested?: NestedInput; } @Resolver() class SampleResolver { @Query(() => String) testSimpleInput(@Arg("input") input: SampleInput): string { return JSON.stringify({ keys: Object.keys(input), hasOptional: "optionalField" in input, hasAnother: "anotherOptional" in input, optionalValue: input.optionalField, }); } @Query(() => String) testNestedInput(@Arg("input") input: ParentInput): string { return JSON.stringify({ keys: Object.keys(input), hasNested: "nested" in input, }); } } schema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); }); describe("optional fields not provided", () => { it("should not create enumerable properties for undefined optional fields", async () => { const query = ` query { testSimpleInput(input: { requiredField: "test" }) } `; const result = await graphql({ schema, source: query }); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); const data = JSON.parse(result.data!.testSimpleInput as string); // Only requiredField should be in Object.keys() expect(data.keys).toEqual(["requiredField"]); // Optional fields should not be enumerable expect(data.hasOptional).toBe(false); expect(data.hasAnother).toBe(false); // But should still be accessible (undefined) expect(data.optionalValue).toBeUndefined(); }); it("should handle nested InputTypes correctly", async () => { const query = ` query { testNestedInput(input: { required: "value" }) } `; const result = await graphql({ schema, source: query }); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); const data = JSON.parse(result.data!.testNestedInput as string); // Only required field should be enumerable expect(data.keys).toEqual(["required"]); // Nested optional field should not be enumerable expect(data.hasNested).toBe(false); }); }); describe("optional fields provided", () => { it("should include provided optional fields in Object.keys()", async () => { const query = ` query { testSimpleInput(input: { requiredField: "test", optionalField: "provided" }) } `; const result = await graphql({ schema, source: query }); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); const data = JSON.parse(result.data!.testSimpleInput as string); // Both provided fields should be in Object.keys() expect(data.keys).toContain("requiredField"); expect(data.keys).toContain("optionalField"); // Provided field should be enumerable expect(data.hasOptional).toBe(true); // Non-provided field should not be enumerable expect(data.hasAnother).toBe(false); // Value should be set expect(data.optionalValue).toBe("provided"); }); it("should handle explicitly null values correctly", async () => { const query = ` query { testSimpleInput(input: { requiredField: "test", optionalField: null }) } `; const result = await graphql({ schema, source: query }); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); const data = JSON.parse(result.data!.testSimpleInput as string); // Explicitly null field should be in Object.keys() expect(data.keys).toContain("requiredField"); expect(data.keys).toContain("optionalField"); // Should be enumerable expect(data.hasOptional).toBe(true); // Value should be null (not undefined) expect(data.optionalValue).toBeNull(); }); }); }); ================================================ FILE: tests/functional/interface-resolvers-args.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, type IntrospectionInterfaceType, type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionSchema, graphql, } from "graphql"; import { Arg, Args, ArgsType, Field, FieldResolver, Int, InterfaceType, ObjectType, Query, Resolver, buildSchema, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Interfaces with resolvers and arguments", () => { describe("Schema", () => { let schemaIntrospection: IntrospectionSchema; beforeAll(async () => { getMetadataStorage().clear(); @ArgsType() class SampleArgs1 { @Field(_type => Int) classArg1!: number; @Field(_type => Int) classArg2!: number; } @InterfaceType() abstract class SampleInterfaceWithArgsFields { @Field() interfaceFieldInlineArgs( @Arg("inlineArg1", _type => Int) _inlineArg1: number, @Arg("inlineArg2", _type => Int) _inlineArg2: number, ): string { throw new Error("Method not implemented!"); } @Field() interfaceFieldArgsType(@Args() _args: SampleArgs1): string { throw new Error("Method not implemented!"); } } @InterfaceType() abstract class SampleInterfaceWithArgs { @Field() sampleFieldWithArgs(@Arg("sampleArg") _sampleArg: string): string { throw new Error("Method not implemented!"); } } @InterfaceType() abstract class SampleInterfaceWithArgsAndInlineResolver { @Field() sampleFieldWithArgs(@Arg("sampleArg") sampleArg: string): string { return `SampleInterfaceWithArgsAndInlineResolver: ${sampleArg}`; } } @InterfaceType() abstract class SampleInterfaceWithArgsAndFieldResolver {} @ObjectType({ implements: SampleInterfaceWithArgs }) class SampleImplementingObjectWithArgsAndOwnResolver implements SampleInterfaceWithArgs { sampleFieldWithArgs(sampleArg: string) { return `SampleImplementingObjectWithArgsAndOwnResolver: ${sampleArg}`; } } @ObjectType({ implements: SampleInterfaceWithArgsAndInlineResolver }) class SampleImplementingObjectWithArgsAndInheritedResolver extends SampleInterfaceWithArgsAndInlineResolver {} @ObjectType({ implements: SampleInterfaceWithArgsAndFieldResolver }) class SampleImplementingObjectWithArgsAndInheritedFieldResolver extends SampleInterfaceWithArgsAndFieldResolver {} @Resolver() class SampleResolver { @Query() sampleQuery(): string { return "sampleQuery"; } } @Resolver(_of => SampleInterfaceWithArgsAndFieldResolver) class SampleInterfaceWithArgsResolver { @FieldResolver() sampleFieldWithArgs(@Arg("sampleArg") sampleArg: string): string { return `SampleInterfaceWithArgsAndFieldResolver: ${sampleArg}`; } } const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver, SampleInterfaceWithArgsResolver], orphanedTypes: [ SampleInterfaceWithArgsFields, SampleInterfaceWithArgs, SampleInterfaceWithArgsAndInlineResolver, SampleInterfaceWithArgsAndFieldResolver, SampleImplementingObjectWithArgsAndOwnResolver, SampleImplementingObjectWithArgsAndInheritedResolver, SampleImplementingObjectWithArgsAndInheritedFieldResolver, ], }); schemaIntrospection = schemaInfo.schemaIntrospection; }); it("should generate schema without errors", async () => { expect(schemaIntrospection).toBeDefined(); }); it("should generate interface types fields with args correctly", async () => { const sampleInterfaceWithArgs = schemaIntrospection.types.find( type => type.name === "SampleInterfaceWithArgs", ) as IntrospectionInterfaceType; const sampleInterfaceWithArgsAndInlineResolver = schemaIntrospection.types.find( type => type.name === "SampleInterfaceWithArgsAndInlineResolver", ) as IntrospectionInterfaceType; const sampleInterfaceWithArgsAndFieldResolver = schemaIntrospection.types.find( type => type.name === "SampleInterfaceWithArgsAndFieldResolver", ) as IntrospectionInterfaceType; expect(sampleInterfaceWithArgs).toBeDefined(); expect(sampleInterfaceWithArgsAndInlineResolver).toBeDefined(); expect(sampleInterfaceWithArgsAndFieldResolver).toBeDefined(); [ sampleInterfaceWithArgs, sampleInterfaceWithArgsAndInlineResolver, sampleInterfaceWithArgsAndFieldResolver, ].forEach(type => { const sampleFieldWithArgsField = type.fields.find(it => it.name === "sampleFieldWithArgs")!; const sampleFieldWithArgsType = ( sampleFieldWithArgsField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionNamedTypeRef; expect(sampleFieldWithArgsField.args).toHaveLength(1); expect(sampleFieldWithArgsType.name).toEqual("String"); }); }); it("should generate interface types fields with args both for inline and args class", async () => { const sampleInterfaceWithArgsFields = schemaIntrospection.types.find( type => type.name === "SampleInterfaceWithArgsFields", ) as IntrospectionInterfaceType; expect(sampleInterfaceWithArgsFields).toBeDefined(); const interfaceFieldInlineArgsField = sampleInterfaceWithArgsFields.fields.find( it => it.name === "interfaceFieldInlineArgs", )!; const interfaceFieldArgsTypeField = sampleInterfaceWithArgsFields.fields.find( it => it.name === "interfaceFieldArgsType", )!; const interfaceFieldInlineArgsType = ( interfaceFieldInlineArgsField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionNamedTypeRef; const interfaceFieldArgsTypeFieldType = ( interfaceFieldArgsTypeField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionNamedTypeRef; expect(interfaceFieldInlineArgsField.args).toHaveLength(2); expect(interfaceFieldArgsTypeField.args).toHaveLength(2); expect(interfaceFieldInlineArgsType.name).toEqual("String"); expect(interfaceFieldArgsTypeFieldType.name).toEqual("String"); }); it("should generate object types inherited interface fields with args correctly", async () => { const sampleImplementingObjectWithArgsAndOwnResolver = schemaIntrospection.types.find( type => type.name === "SampleImplementingObjectWithArgsAndOwnResolver", ) as IntrospectionInterfaceType; const sampleImplementingObjectWithArgsAndInheritedResolver = schemaIntrospection.types.find( type => type.name === "SampleImplementingObjectWithArgsAndInheritedResolver", ) as IntrospectionInterfaceType; const sampleImplementingObjectWithArgsAndInheritedFieldResolver = schemaIntrospection.types.find( type => type.name === "SampleImplementingObjectWithArgsAndInheritedFieldResolver", ) as IntrospectionInterfaceType; expect(sampleImplementingObjectWithArgsAndOwnResolver).toBeDefined(); expect(sampleImplementingObjectWithArgsAndInheritedResolver).toBeDefined(); expect(sampleImplementingObjectWithArgsAndInheritedFieldResolver).toBeDefined(); [ sampleImplementingObjectWithArgsAndOwnResolver, sampleImplementingObjectWithArgsAndInheritedResolver, sampleImplementingObjectWithArgsAndInheritedFieldResolver, ].forEach(type => { const sampleFieldWithArgsField = type.fields.find(it => it.name === "sampleFieldWithArgs")!; const sampleFieldWithArgsType = ( sampleFieldWithArgsField.type as IntrospectionNonNullTypeRef ).ofType as IntrospectionNamedTypeRef; expect(sampleFieldWithArgsField.args).toHaveLength(1); expect(sampleFieldWithArgsType.name).toEqual("String"); }); }); }); describe("Functional", () => { let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); @InterfaceType() abstract class SampleInterfaceWithArgs { @Field() sampleFieldWithArgs(@Arg("sampleArg") _sampleArg: string): string { throw new Error("Method not implemented!"); } } @InterfaceType() abstract class SampleInterfaceWithArgsAndInlineResolver { @Field() sampleFieldWithArgs(@Arg("sampleArg") sampleArg: string): string { return `SampleInterfaceWithArgsAndInlineResolver: ${sampleArg}`; } } @InterfaceType({ implements: SampleInterfaceWithArgsAndInlineResolver }) abstract class SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver extends SampleInterfaceWithArgsAndInlineResolver {} @InterfaceType() abstract class SampleInterfaceWithArgsAndFieldResolver {} @ObjectType({ implements: SampleInterfaceWithArgs }) class SampleImplementingObjectWithArgsAndOwnResolver extends SampleInterfaceWithArgs { override sampleFieldWithArgs(sampleArg: string) { return `SampleImplementingObjectWithArgsAndOwnResolver: ${sampleArg}`; } } @ObjectType({ implements: SampleInterfaceWithArgsAndInlineResolver }) class SampleImplementingObjectWithArgsAndInheritedResolver extends SampleInterfaceWithArgsAndInlineResolver {} @ObjectType({ implements: [ SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver, SampleInterfaceWithArgsAndInlineResolver, ], }) class SampleObjectImplementingInterfaceImplementingWithArgsAndInheritedResolver extends SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver implements SampleInterfaceWithArgsAndInlineResolver {} @ObjectType({ implements: SampleInterfaceWithArgsAndFieldResolver }) class SampleImplementingObjectWithArgsAndInheritedFieldResolver extends SampleInterfaceWithArgsAndFieldResolver {} @Resolver(_of => SampleInterfaceWithArgsAndFieldResolver) class SampleInterfaceResolver { @FieldResolver() sampleFieldWithArgs(@Arg("sampleArg") sampleArg: string): string { return `SampleInterfaceResolver: ${sampleArg}`; } } @Resolver() class TestResolver { @Query() queryForSampleInterfaceWithArgs(): SampleInterfaceWithArgs { return new SampleImplementingObjectWithArgsAndOwnResolver(); } @Query() queryForSampleInterfaceWithArgsAndInlineResolver(): SampleInterfaceWithArgsAndInlineResolver { return new SampleImplementingObjectWithArgsAndInheritedResolver(); } @Query() queryForSampleInterfaceWithArgsAndFieldResolver(): SampleInterfaceWithArgsAndFieldResolver { return new SampleImplementingObjectWithArgsAndInheritedFieldResolver(); } @Query() queryForSampleImplementingObjectWithArgsAndOwnResolver(): SampleImplementingObjectWithArgsAndOwnResolver { return new SampleImplementingObjectWithArgsAndOwnResolver(); } @Query() queryForSampleImplementingObjectWithArgsAndInheritedResolver(): SampleImplementingObjectWithArgsAndInheritedResolver { return new SampleImplementingObjectWithArgsAndInheritedResolver(); } @Query() queryForSampleImplementingObjectWithArgsAndInheritedFieldResolver(): SampleImplementingObjectWithArgsAndInheritedFieldResolver { return new SampleImplementingObjectWithArgsAndInheritedFieldResolver(); } @Query() queryForSampleInterfaceImplementingInterfaceWithArgsAndInlineResolver(): SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver { return new SampleObjectImplementingInterfaceImplementingWithArgsAndInheritedResolver(); } } schema = await buildSchema({ resolvers: [SampleInterfaceResolver, TestResolver], orphanedTypes: [ SampleInterfaceWithArgs, SampleInterfaceWithArgsAndInlineResolver, SampleInterfaceWithArgsAndFieldResolver, SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver, SampleImplementingObjectWithArgsAndOwnResolver, SampleImplementingObjectWithArgsAndInheritedResolver, SampleImplementingObjectWithArgsAndInheritedFieldResolver, SampleObjectImplementingInterfaceImplementingWithArgsAndInheritedResolver, ], validate: false, }); }); it("should build the schema without errors", () => { expect(schema).toBeDefined(); }); it("should invoke object type field resolver for interface returned query if override the interface type one", async () => { const query = /* graphql */ ` query { queryForSampleInterfaceWithArgs { sampleFieldWithArgs(sampleArg: "sampleArgValue") } } `; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); const result = (data as any).queryForSampleInterfaceWithArgs.sampleFieldWithArgs; expect(result).toBeDefined(); expect(result).toEqual("SampleImplementingObjectWithArgsAndOwnResolver: sampleArgValue"); }); it("should invoke interface type inline field resolver for interface returned query", async () => { const query = /* graphql */ ` query { queryForSampleInterfaceWithArgsAndInlineResolver { sampleFieldWithArgs(sampleArg: "sampleArgValue") } } `; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); const result = (data as any).queryForSampleInterfaceWithArgsAndInlineResolver .sampleFieldWithArgs; expect(result).toBeDefined(); expect(result).toEqual("SampleInterfaceWithArgsAndInlineResolver: sampleArgValue"); }); it("should invoke interface type resolvers field resolver for interface returned query", async () => { const query = /* graphql */ ` query { queryForSampleInterfaceWithArgsAndFieldResolver { sampleFieldWithArgs(sampleArg: "sampleArgValue") } } `; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); const result = (data as any).queryForSampleInterfaceWithArgsAndFieldResolver .sampleFieldWithArgs; expect(result).toBeDefined(); expect(result).toEqual("SampleInterfaceResolver: sampleArgValue"); }); it("should invoke object type field resolver if override the interface type one", async () => { const query = /* graphql */ ` query { queryForSampleImplementingObjectWithArgsAndOwnResolver { sampleFieldWithArgs(sampleArg: "sampleArgValue") } } `; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); const result = (data as any).queryForSampleImplementingObjectWithArgsAndOwnResolver .sampleFieldWithArgs; expect(result).toBeDefined(); expect(result).toEqual("SampleImplementingObjectWithArgsAndOwnResolver: sampleArgValue"); }); it("should invoke interface type inline field resolver for implementing object type", async () => { const query = /* graphql */ ` query { queryForSampleImplementingObjectWithArgsAndInheritedResolver { sampleFieldWithArgs(sampleArg: "sampleArgValue") } } `; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); const result = (data as any).queryForSampleImplementingObjectWithArgsAndInheritedResolver .sampleFieldWithArgs; expect(result).toBeDefined(); expect(result).toEqual("SampleInterfaceWithArgsAndInlineResolver: sampleArgValue"); }); it("should invoke interface type resolvers field resolver for implementing object type", async () => { const query = /* graphql */ ` query { queryForSampleImplementingObjectWithArgsAndInheritedFieldResolver { sampleFieldWithArgs(sampleArg: "sampleArgValue") } } `; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); const result = (data as any).queryForSampleImplementingObjectWithArgsAndInheritedFieldResolver .sampleFieldWithArgs; expect(result).toBeDefined(); expect(result).toEqual("SampleInterfaceResolver: sampleArgValue"); }); it("should invoke interface type resolvers field resolver from implemented interface for implementing object type", async () => { const query = /* graphql */ ` query { queryForSampleInterfaceImplementingInterfaceWithArgsAndInlineResolver { sampleFieldWithArgs(sampleArg: "sampleArgValue") } } `; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); const result = (data as any) .queryForSampleInterfaceImplementingInterfaceWithArgsAndInlineResolver.sampleFieldWithArgs; expect(result).toBeDefined(); expect(result).toEqual("SampleInterfaceWithArgsAndInlineResolver: sampleArgValue"); }); }); }); ================================================ FILE: tests/functional/interfaces-and-inheritance.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, type IntrospectionInputObjectType, type IntrospectionInterfaceType, type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionSchema, TypeKind, graphql, } from "graphql"; import { Arg, Args, ArgsType, Field, ID, InputType, Int, InterfaceType, Mutation, ObjectType, Query, Resolver, buildSchema, } from "type-graphql"; import { GeneratingSchemaError } from "@/errors"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { expectToThrow } from "../helpers/expectToThrow"; import { getInnerFieldType, getInnerInputFieldType } from "../helpers/getInnerFieldType"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Interfaces and inheritance", () => { describe("Schema", () => { let schemaIntrospection: IntrospectionSchema; let queryType: IntrospectionObjectType; let sampleInterface1Type: IntrospectionInterfaceType; let sampleInterface2Type: IntrospectionInterfaceType; let sampleInterfaceImplementing1: IntrospectionInterfaceType; let sampleMultiImplementingObjectType: IntrospectionObjectType; let sampleExtendingImplementingObjectType: IntrospectionObjectType; let sampleImplementingObject1Type: IntrospectionObjectType; let sampleImplementingObject2Type: IntrospectionObjectType; let sampleExtendingObject2Type: IntrospectionObjectType; let sampleSecondExtendedInputType: IntrospectionInputObjectType; beforeAll(async () => { getMetadataStorage().clear(); @InterfaceType() abstract class SampleInterface1 { @Field(() => ID) id!: string; @Field() interfaceStringField1!: string; } @InterfaceType() abstract class SampleInterface2 { @Field(() => ID) id!: string; @Field() interfaceStringField2!: string; } @InterfaceType() abstract class SampleInterfaceExtending1 extends SampleInterface1 { @Field() ownStringField1!: string; } @InterfaceType({ implements: [SampleInterface1] }) abstract class SampleInterfaceImplementing1 implements SampleInterface1 { id!: string; interfaceStringField1!: string; @Field() ownStringField1!: string; } @ObjectType({ implements: SampleInterface1 }) class SampleImplementingObject1 implements SampleInterface1 { id!: string; interfaceStringField1!: string; @Field() ownField1!: number; } @ObjectType({ implements: SampleInterface1 }) class SampleImplementingObject2 implements SampleInterface1 { @Field(() => ID) id!: string; @Field() interfaceStringField1!: string; @Field() ownField2!: number; } @ObjectType({ implements: [SampleInterface1, SampleInterface2] }) class SampleMultiImplementingObject implements SampleInterface1, SampleInterface2 { id!: string; interfaceStringField1!: string; interfaceStringField2!: string; @Field() ownField3!: number; } @ObjectType({ implements: SampleInterface1 }) class SampleExtendingImplementingObject extends SampleImplementingObject2 implements SampleInterface1 { @Field() ownField4!: number; } @ObjectType() class SampleExtendingObject2 extends SampleImplementingObject2 { @Field() ownExtendingField2!: number; } @ArgsType() class SampleBaseArgs { @Field() baseArgField!: string; } @ArgsType() class SampleExtendingArgs extends SampleBaseArgs { @Field() extendingArgField!: boolean; } @InputType() class SampleBaseInput { @Field() baseInputField!: string; } @InputType() class SampleExtendingInput extends SampleBaseInput { @Field() extendingInputField!: boolean; } // overwriting fields case @InputType() class SampleFirstBaseInput { @Field() baseField!: string; } @InputType() class SampleFirstExtendedInput extends SampleFirstBaseInput { @Field() extendedField!: string; } @InputType() class SampleSecondBaseInput { @Field() baseInputField!: SampleFirstBaseInput; } @InputType() class SampleSecondExtendedInput extends SampleSecondBaseInput { @Field() override baseInputField!: SampleFirstExtendedInput; } class SampleResolver { @Query() sampleQuery(): boolean { return true; } @Query() queryWithArgs(@Args() _args: SampleExtendingArgs): boolean { return true; } @Mutation() mutationWithInput(@Arg("input") _input: SampleExtendingInput): boolean { return true; } } // get builded schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], orphanedTypes: [ SampleInterface1, SampleInterfaceExtending1, SampleInterfaceImplementing1, SampleImplementingObject1, SampleImplementingObject2, SampleMultiImplementingObject, SampleExtendingImplementingObject, SampleExtendingObject2, SampleSecondExtendedInput, ], }); queryType = schemaInfo.queryType; schemaIntrospection = schemaInfo.schemaIntrospection; sampleInterface1Type = schemaIntrospection.types.find( type => type.name === "SampleInterface1", ) as IntrospectionInterfaceType; sampleInterface2Type = schemaIntrospection.types.find( type => type.name === "SampleInterface2", ) as IntrospectionInterfaceType; sampleInterfaceImplementing1 = schemaIntrospection.types.find( type => type.name === "SampleInterfaceImplementing1", ) as IntrospectionInterfaceType; sampleImplementingObject1Type = schemaIntrospection.types.find( type => type.name === "SampleImplementingObject1", ) as IntrospectionObjectType; sampleImplementingObject2Type = schemaIntrospection.types.find( type => type.name === "SampleImplementingObject2", ) as IntrospectionObjectType; sampleExtendingImplementingObjectType = schemaIntrospection.types.find( type => type.name === "SampleExtendingImplementingObject", ) as IntrospectionObjectType; sampleMultiImplementingObjectType = schemaIntrospection.types.find( type => type.name === "SampleMultiImplementingObject", ) as IntrospectionObjectType; sampleExtendingObject2Type = schemaIntrospection.types.find( type => type.name === "SampleExtendingObject2", ) as IntrospectionObjectType; sampleSecondExtendedInputType = schemaIntrospection.types.find( type => type.name === "SampleSecondExtendedInput", ) as IntrospectionInputObjectType; }); // helpers function getInnerType(fieldType: any) { return (fieldType.type as IntrospectionNonNullTypeRef).ofType! as IntrospectionNamedTypeRef; } it("should generate schema without errors", async () => { expect(schemaIntrospection).toBeDefined(); }); it("should generate interface type correctly", async () => { expect(sampleInterface1Type).toBeDefined(); expect(sampleInterface2Type).toBeDefined(); expect(sampleInterface1Type.kind).toEqual(TypeKind.INTERFACE); expect(sampleInterface2Type.kind).toEqual(TypeKind.INTERFACE); expect(sampleInterface1Type.fields).toHaveLength(2); expect(sampleInterface2Type.fields).toHaveLength(2); const idFieldType1 = getInnerFieldType(sampleInterface1Type, "id"); const idFieldType2 = getInnerFieldType(sampleInterface1Type, "id"); const interfaceStringField1 = getInnerFieldType( sampleInterface1Type, "interfaceStringField1", ); const interfaceStringField2 = getInnerFieldType( sampleInterface2Type, "interfaceStringField2", ); expect(idFieldType1.name).toEqual("ID"); expect(idFieldType2.name).toEqual("ID"); expect(interfaceStringField1.name).toEqual("String"); expect(interfaceStringField2.name).toEqual("String"); }); it("should generate type of interface extending other interface correctly", async () => { const sampleInterfaceExtending1 = schemaIntrospection.types.find( type => type.name === "SampleInterfaceExtending1", ) as IntrospectionInterfaceType; expect(sampleInterfaceExtending1).toBeDefined(); expect(sampleInterfaceExtending1.kind).toEqual(TypeKind.INTERFACE); expect(sampleInterfaceExtending1.fields).toHaveLength(3); const idFieldType = getInnerFieldType(sampleInterfaceExtending1, "id"); const interfaceStringField = getInnerFieldType( sampleInterfaceExtending1, "interfaceStringField1", ); const ownStringField1 = getInnerFieldType(sampleInterfaceExtending1, "ownStringField1"); expect(idFieldType.name).toEqual("ID"); expect(interfaceStringField.name).toEqual("String"); expect(ownStringField1.name).toEqual("String"); }); it("should generate type of interface implementing other interface correctly", async () => { expect(sampleInterfaceImplementing1).toBeDefined(); expect(sampleInterfaceImplementing1.kind).toEqual(TypeKind.INTERFACE); expect(sampleInterfaceImplementing1.fields).toHaveLength(3); expect(sampleInterfaceImplementing1.interfaces).toContainEqual( expect.objectContaining({ name: "SampleInterface1", }), ); const idFieldType = getInnerFieldType(sampleInterfaceImplementing1, "id"); expect(idFieldType.name).toEqual("ID"); const interfaceStringField = getInnerFieldType( sampleInterfaceImplementing1, "interfaceStringField1", ); expect(interfaceStringField.name).toEqual("String"); const ownStringField1 = getInnerFieldType(sampleInterfaceImplementing1, "ownStringField1"); expect(ownStringField1.name).toEqual("String"); }); it("should generate object type explicitly implementing interface correctly", async () => { expect(sampleImplementingObject2Type).toBeDefined(); expect(sampleImplementingObject2Type.fields).toHaveLength(3); const idFieldType = getInnerFieldType(sampleImplementingObject2Type, "id"); const interfaceStringField = getInnerFieldType( sampleImplementingObject2Type, "interfaceStringField1", ); const ownField2 = getInnerFieldType(sampleImplementingObject2Type, "ownField2"); const implementedInterfaceInfo = sampleImplementingObject2Type.interfaces.find( it => it.name === "SampleInterface1", )!; expect(idFieldType.name).toEqual("ID"); expect(interfaceStringField.name).toEqual("String"); expect(ownField2.name).toEqual("Float"); expect(implementedInterfaceInfo.kind).toEqual(TypeKind.INTERFACE); }); it("should generate object type implicitly implementing interface correctly", async () => { expect(sampleImplementingObject1Type).toBeDefined(); expect(sampleImplementingObject1Type.fields).toHaveLength(3); const idFieldType = getInnerFieldType(sampleImplementingObject1Type, "id"); const interfaceStringField1 = getInnerFieldType( sampleImplementingObject1Type, "interfaceStringField1", ); const ownField1 = getInnerFieldType(sampleImplementingObject1Type, "ownField1"); const implementedInterfaceInfo = sampleImplementingObject2Type.interfaces.find( it => it.name === "SampleInterface1", )!; expect(idFieldType.name).toEqual("ID"); expect(interfaceStringField1.name).toEqual("String"); expect(ownField1.name).toEqual("Float"); expect(implementedInterfaceInfo.kind).toEqual(TypeKind.INTERFACE); }); it("should generate object type extending other object type correctly", async () => { expect(sampleExtendingObject2Type).toBeDefined(); expect(sampleExtendingObject2Type.fields).toHaveLength(4); const idFieldType = getInnerFieldType(sampleExtendingObject2Type, "id"); const interfaceStringField1 = getInnerFieldType( sampleExtendingObject2Type, "interfaceStringField1", ); const ownField2 = getInnerFieldType(sampleExtendingObject2Type, "ownField2"); const ownExtendingField2 = getInnerFieldType( sampleExtendingObject2Type, "ownExtendingField2", ); expect(idFieldType.name).toEqual("ID"); expect(interfaceStringField1.name).toEqual("String"); expect(ownField2.name).toEqual("Float"); expect(ownExtendingField2.name).toEqual("Float"); }); it("should generate object type implementing interface when extending object type", async () => { expect(sampleExtendingObject2Type).toBeDefined(); const implementedInterfaceInfo = sampleExtendingObject2Type.interfaces.find( it => it.name === "SampleInterface1", )!; expect(implementedInterfaceInfo).toBeDefined(); expect(implementedInterfaceInfo.kind).toEqual(TypeKind.INTERFACE); }); it("should generate object type implicitly implementing multiple interfaces correctly", async () => { expect(sampleMultiImplementingObjectType).toBeDefined(); expect(sampleMultiImplementingObjectType.fields).toHaveLength(4); const idFieldType = getInnerFieldType(sampleMultiImplementingObjectType, "id"); const interfaceStringField1 = getInnerFieldType( sampleMultiImplementingObjectType, "interfaceStringField1", ); const interfaceStringField2 = getInnerFieldType( sampleMultiImplementingObjectType, "interfaceStringField2", ); const ownField3 = getInnerFieldType(sampleMultiImplementingObjectType, "ownField3"); expect(idFieldType.name).toEqual("ID"); expect(interfaceStringField1.name).toEqual("String"); expect(interfaceStringField2.name).toEqual("String"); expect(ownField3.name).toEqual("Float"); }); it("should generate object type implicitly implementing and extending correctly", async () => { expect(sampleExtendingImplementingObjectType).toBeDefined(); expect(sampleExtendingImplementingObjectType.fields).toHaveLength(4); const idFieldType = getInnerFieldType(sampleExtendingImplementingObjectType, "id"); const interfaceStringField1 = getInnerFieldType( sampleExtendingImplementingObjectType, "interfaceStringField1", ); const ownField2 = getInnerFieldType(sampleExtendingImplementingObjectType, "ownField2"); const ownField4 = getInnerFieldType(sampleExtendingImplementingObjectType, "ownField4"); expect(idFieldType.name).toEqual("ID"); expect(interfaceStringField1.name).toEqual("String"); expect(ownField2.name).toEqual("Float"); expect(ownField4.name).toEqual("Float"); }); it("should generate query args when extending other args class", async () => { const queryWithArgs = queryType.fields.find(query => query.name === "queryWithArgs")!; expect(queryWithArgs.args).toHaveLength(2); const baseArgFieldType = getInnerType( queryWithArgs.args.find(arg => arg.name === "baseArgField")!, ); const extendingArgFieldType = getInnerType( queryWithArgs.args.find(arg => arg.name === "extendingArgField")!, ); expect(baseArgFieldType.name).toEqual("String"); expect(extendingArgFieldType.name).toEqual("Boolean"); }); it("should generate mutation input when extending other args class", async () => { const sampleExtendingInputType = schemaIntrospection.types.find( type => type.name === "SampleExtendingInput", ) as IntrospectionInputObjectType; const baseInputFieldType = getInnerType( sampleExtendingInputType.inputFields.find(field => field.name === "baseInputField")!, ); const extendingInputFieldType = getInnerType( sampleExtendingInputType.inputFields.find(field => field.name === "extendingInputField")!, ); expect(baseInputFieldType.name).toEqual("String"); expect(extendingInputFieldType.name).toEqual("Boolean"); }); it("should properly overwrite input type field", () => { const baseInputFieldType = getInnerInputFieldType( sampleSecondExtendedInputType, "baseInputField", ); expect(baseInputFieldType.name).toEqual("SampleFirstExtendedInput"); }); it("shouldn't throw error when extending wrong class type", async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field() inputField!: string; } @ArgsType() class SampleArgs extends SampleInput { @Field() argField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Args() _args: SampleArgs): boolean { return true; } } const schema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); expect(schema).toBeDefined(); }); }); describe("Schema > deeply nested inheritance chain", () => { beforeEach(() => { getMetadataStorage().clear(); }); it("should properly inherit overridden fields", async () => { @ArgsType() class BaseArgs { @Field({ nullable: true }) baseField?: string; } @ArgsType() class FirstLevelArgs extends BaseArgs { @Field({ nullable: false }) override baseField!: string; } @ArgsType() class SecondLevelArgs extends FirstLevelArgs { @Field() secondLevelField!: string; } @Resolver() class TestResolver { @Query(() => Boolean) testQuery(@Args() _args: SecondLevelArgs): boolean { return true; } } const { queryType } = await getSchemaInfo({ resolvers: [TestResolver], }); const testQuery = queryType.fields.find(field => field.name === "testQuery")!; const baseField = testQuery.args.find(arg => arg.name === "baseField")!; expect(baseField.type).toMatchObject({ kind: "NON_NULL", ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); }); describe("Errors", () => { beforeEach(() => { getMetadataStorage().clear(); }); it("should throw error when field type doesn't match with interface", async () => { const error = await expectToThrow(async () => { @InterfaceType() class IBase { @Field() baseField!: string; } @ObjectType({ implements: IBase }) class ChildObject implements IBase { @Field(() => Number, { nullable: true }) baseField!: string; @Field() argField!: string; } class SampleResolver { @Query() sampleQuery(): ChildObject { return {} as ChildObject; } } await buildSchema({ resolvers: [SampleResolver], validate: false, }); }); expect(error).toBeInstanceOf(GeneratingSchemaError); expect(error.message).toMatchInlineSnapshot(` "Some errors occurred while generating GraphQL schema: Interface field IBase.baseField expects type String! but ChildObject.baseField is type Float. Please check the \`details\` property of the error to get more detailed info." `); expect(JSON.stringify((error as GeneratingSchemaError).details, null, 2)) .toMatchInlineSnapshot(` "[ { "message": "Interface field IBase.baseField expects type String! but ChildObject.baseField is type Float." } ]" `); }); it("should throw error when not interface type is provided as `implements` option", async () => { const error = await expectToThrow(async () => { @ObjectType() class SampleNotInterface { @Field() sampleField!: string; } @ObjectType({ implements: [SampleNotInterface] }) class SampleImplementingObject implements SampleNotInterface { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleImplementingObject { return {} as SampleImplementingObject; } } await buildSchema({ resolvers: [SampleResolver], validate: false, }); }); expect(error).toBeInstanceOf(Error); expect(error.message).toMatchInlineSnapshot( `"Cannot find interface type metadata for class 'SampleNotInterface' provided in 'implements' option for 'SampleImplementingObject' object type class. Please make sure that class is annotated with an '@InterfaceType()' decorator."`, ); }); }); describe("Functional", () => { let schema: GraphQLSchema; let queryArgs: any; let mutationInput: any; let inputFieldValue: any; let argsFieldValue: any; beforeEach(() => { queryArgs = undefined; mutationInput = undefined; }); beforeAll(async () => { getMetadataStorage().clear(); @ArgsType() class BaseArgs { @Field() baseArgField!: string; @Field(() => Int, { nullable: true }) optionalBaseArgField: number = 255; } @ArgsType() class ChildArgs extends BaseArgs { @Field() childArgField!: string; } @InputType() class BaseInput { @Field() baseInputField!: string; @Field(() => Int, { nullable: true }) optionalBaseInputField: number = 255; } @InputType() class ChildInput extends BaseInput { @Field() childInputField!: string; } @InterfaceType() abstract class BaseInterface { @Field() baseInterfaceField!: string; @Field({ name: "renamedInterfaceField", nullable: true }) interfaceFieldToBeRenamed?: string; } @ObjectType({ implements: BaseInterface }) class FirstImplementation implements BaseInterface { baseInterfaceField!: string; interfaceFieldToBeRenamed?: string; @Field() firstField!: string; } @ObjectType({ implements: BaseInterface }) class SecondImplementation implements BaseInterface { baseInterfaceField!: string; @Field() secondField!: string; } @InterfaceType({ resolveType: value => { if ("firstField" in value) { return "FirstInterfaceWithStringResolveTypeObject"; } if ("secondField" in value) { return "SecondInterfaceWithStringResolveTypeObject"; } return undefined; }, }) abstract class InterfaceWithStringResolveType { @Field() baseInterfaceField!: string; } @ObjectType({ implements: InterfaceWithStringResolveType }) class FirstInterfaceWithStringResolveTypeObject implements InterfaceWithStringResolveType { baseInterfaceField!: string; @Field() firstField!: string; } @ObjectType({ implements: InterfaceWithStringResolveType }) class SecondInterfaceWithStringResolveTypeObject implements InterfaceWithStringResolveType { baseInterfaceField!: string; @Field() secondField!: string; } @InterfaceType({ resolveType: value => { if ("firstField" in value) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return FirstInterfaceWithClassResolveTypeObject; } if ("secondField" in value) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return SecondInterfaceWithClassResolveTypeObject; } return undefined; }, }) abstract class InterfaceWithClassResolveType { @Field() baseInterfaceField!: string; } @ObjectType({ implements: InterfaceWithClassResolveType }) class FirstInterfaceWithClassResolveTypeObject implements InterfaceWithClassResolveType { baseInterfaceField!: string; @Field() firstField!: string; } @ObjectType({ implements: InterfaceWithClassResolveType }) class SecondInterfaceWithClassResolveTypeObject implements InterfaceWithClassResolveType { baseInterfaceField!: string; @Field() secondField!: string; } class SampleBaseClass { static sampleStaticMethod() { return "sampleStaticMethod"; } } @ObjectType() class SampleExtendingNormalClassObject extends SampleBaseClass { @Field() sampleField!: string; } @InputType() class SampleExtendingNormalClassInput extends SampleBaseClass { @Field() sampleField!: string; } @ArgsType() class SampleExtendingNormalClassArgs extends SampleBaseClass { @Field() sampleField!: string; } // overwriting fields case @InputType() class SampleFirstBaseInput { @Field() baseField!: string; } @InputType() class SampleFirstExtendedInput extends SampleFirstBaseInput { @Field() extendedField!: string; } @InputType() class SampleSecondBaseInput { @Field() baseInputField!: SampleFirstBaseInput; } @InputType() class SampleSecondExtendedInput extends SampleSecondBaseInput { @Field() override baseInputField!: SampleFirstExtendedInput; } @Resolver() class InterfacesResolver { @Query() getInterfacePlainObject(): BaseInterface { return {} as FirstImplementation; } @Query() getFirstInterfaceImplementationObject(): BaseInterface { const obj = new FirstImplementation(); obj.baseInterfaceField = "baseInterfaceField"; obj.firstField = "firstField"; return obj; } @Query() getSecondInterfaceWithStringResolveTypeObject(): InterfaceWithStringResolveType { return { baseInterfaceField: "baseInterfaceField", secondField: "secondField", } as SecondInterfaceWithStringResolveTypeObject; } @Query() getSecondInterfaceWithClassResolveTypeObject(): InterfaceWithClassResolveType { return { baseInterfaceField: "baseInterfaceField", secondField: "secondField", } as SecondInterfaceWithClassResolveTypeObject; } @Query() notMatchingValueForInterfaceWithClassResolveTypeObject(): InterfaceWithClassResolveType { return { baseInterfaceField: "notMatchingValue" }; } @Query() queryWithArgs(@Args() args: ChildArgs): boolean { queryArgs = args; return true; } @Mutation() mutationWithInput(@Arg("input") input: ChildInput): boolean { mutationInput = input; return true; } @Query() baseClassQuery( @Arg("input") input: SampleExtendingNormalClassInput, @Args() args: SampleExtendingNormalClassArgs, ): string { inputFieldValue = input.sampleField; argsFieldValue = args.sampleField; return SampleExtendingNormalClassObject.sampleStaticMethod(); } @Query() secondImplementationPlainQuery(): SecondImplementation { return { baseInterfaceField: "baseInterfaceField", secondField: "secondField", }; } @Query() renamedFieldInterfaceQuery(): BaseInterface { const obj = new FirstImplementation(); obj.baseInterfaceField = "baseInterfaceField"; obj.firstField = "firstField"; obj.interfaceFieldToBeRenamed = "interfaceFieldToBeRenamed"; return obj; } @Mutation() overwritingInputFieldMutation(@Arg("input") input: SampleSecondExtendedInput): boolean { mutationInput = input; return true; } } schema = await buildSchema({ resolvers: [InterfacesResolver], orphanedTypes: [ FirstImplementation, SecondInterfaceWithStringResolveTypeObject, FirstInterfaceWithStringResolveTypeObject, SecondInterfaceWithClassResolveTypeObject, FirstInterfaceWithClassResolveTypeObject, ], validate: false, }); }); it("should return interface type fields data", async () => { const query = `query { getFirstInterfaceImplementationObject { baseInterfaceField } }`; const result: any = await graphql({ schema, source: query }); const data = result.data.getFirstInterfaceImplementationObject; expect(data.baseInterfaceField).toEqual("baseInterfaceField"); }); it("should correctly recognize returned object type using default `instance of` check", async () => { const query = `query { getFirstInterfaceImplementationObject { baseInterfaceField ... on FirstImplementation { firstField } ... on SecondImplementation { secondField } } }`; const result: any = await graphql({ schema, source: query }); const data = result.data.getFirstInterfaceImplementationObject; expect(data.baseInterfaceField).toEqual("baseInterfaceField"); expect(data.firstField).toEqual("firstField"); expect(data.secondField).toBeUndefined(); }); it("should correctly recognize returned object type using string provided by `resolveType` function", async () => { const query = `query { getSecondInterfaceWithStringResolveTypeObject { baseInterfaceField ... on FirstInterfaceWithStringResolveTypeObject { firstField } ... on SecondInterfaceWithStringResolveTypeObject { secondField } } }`; const result: any = await graphql({ schema, source: query }); const data = result.data.getSecondInterfaceWithStringResolveTypeObject; expect(data.baseInterfaceField).toEqual("baseInterfaceField"); expect(data.firstField).toBeUndefined(); expect(data.secondField).toEqual("secondField"); }); it("should correctly recognize returned object type using class provided by `resolveType` function", async () => { const query = `query { getSecondInterfaceWithClassResolveTypeObject { baseInterfaceField ... on FirstInterfaceWithClassResolveTypeObject { firstField } ... on SecondInterfaceWithClassResolveTypeObject { secondField } } }`; const result: any = await graphql({ schema, source: query }); const data = result.data.getSecondInterfaceWithClassResolveTypeObject; expect(data.baseInterfaceField).toEqual("baseInterfaceField"); expect(data.firstField).toBeUndefined(); expect(data.secondField).toEqual("secondField"); }); it("should should fail with error info when `resolveType` returns undefined", async () => { const query = `query { notMatchingValueForInterfaceWithClassResolveTypeObject { __typename baseInterfaceField ... on FirstInterfaceWithClassResolveTypeObject { firstField } ... on SecondInterfaceWithClassResolveTypeObject { secondField } } }`; const result: any = await graphql({ schema, source: query }); expect(result.errors?.[0]?.message).toMatchInlineSnapshot( `"Abstract type "InterfaceWithClassResolveType" must resolve to an Object type at runtime for field "Query.notMatchingValueForInterfaceWithClassResolveTypeObject". Either the "InterfaceWithClassResolveType" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function."`, ); }); it("should throw error when not returning instance of object class", async () => { const query = `query { getInterfacePlainObject { baseInterfaceField } }`; const result: any = await graphql({ schema, source: query }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const errorMessage = result.errors![0].message; expect(errorMessage).toContain("resolve"); expect(errorMessage).toContain("BaseInterface"); expect(errorMessage).toContain("instance"); expect(errorMessage).toContain("plain"); }); it("should return fields data of object type implementing interface", async () => { const query = `query { getFirstInterfaceImplementationObject { baseInterfaceField ... on FirstImplementation { firstField } } }`; const result: any = await graphql({ schema, source: query }); const data = result.data.getFirstInterfaceImplementationObject; expect(data.baseInterfaceField).toEqual("baseInterfaceField"); expect(data.firstField).toEqual("firstField"); }); it("should allow interfaces to specify custom schema names", async () => { const query = `query { renamedFieldInterfaceQuery { renamedInterfaceField } }`; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); expect((data as any).renamedFieldInterfaceQuery.renamedInterfaceField).toEqual( "interfaceFieldToBeRenamed", ); }); it("should pass args data of extended args class", async () => { const query = `query { queryWithArgs( baseArgField: "baseArgField" childArgField: "childArgField" ) }`; await graphql({ schema, source: query }); expect(queryArgs.baseArgField).toEqual("baseArgField"); expect(queryArgs.childArgField).toEqual("childArgField"); expect(queryArgs.optionalBaseArgField).toEqual(255); }); it("should pass input data of extended input class", async () => { const query = `mutation { mutationWithInput(input: { baseInputField: "baseInputField" childInputField: "childInputField" }) }`; await graphql({ schema, source: query }); expect(mutationInput.baseInputField).toEqual("baseInputField"); expect(mutationInput.childInputField).toEqual("childInputField"); expect(mutationInput.optionalBaseInputField).toEqual(255); }); it("should correctly extends non-TypeGraphQL class", async () => { const query = `query { baseClassQuery( input: { sampleField: "sampleInputValue" } sampleField: "sampleArgValue" ) }`; const { data } = await graphql({ schema, source: query }); expect((data as any).baseClassQuery).toEqual("sampleStaticMethod"); expect(inputFieldValue).toEqual("sampleInputValue"); expect(argsFieldValue).toEqual("sampleArgValue"); }); it("should allow to return plain object when return type is a class that implements an interface", async () => { const query = `mutation { overwritingInputFieldMutation(input: { baseInputField: { baseField: "baseField", extendedField: "extendedField", } }) }`; const { errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); expect(mutationInput).toEqual({ baseInputField: { baseField: "baseField", extendedField: "extendedField", }, }); }); it("should correctly transform data of overwritten input field", async () => { const query = `query { secondImplementationPlainQuery { baseInterfaceField secondField } }`; const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); expect((data as any).secondImplementationPlainQuery.baseInterfaceField).toEqual( "baseInterfaceField", ); expect((data as any).secondImplementationPlainQuery.secondField).toEqual("secondField"); }); }); describe("Multiple schemas", () => { beforeEach(() => { getMetadataStorage().clear(); }); it("should correctly return data from interface query for all schemas that uses the same interface", async () => { @InterfaceType() class BaseInterface { @Field() baseField!: string; } @ObjectType({ implements: [BaseInterface] }) class One extends BaseInterface { @Field() one!: string; } @ObjectType({ implements: [BaseInterface] }) class Two extends BaseInterface { @Field() two!: string; } @Resolver() class OneTwoResolver { @Query(() => BaseInterface) base(): BaseInterface { const one = new One(); one.baseField = "baseField"; one.one = "one"; return one; } } const query = /* graphql */ ` query { base { __typename baseField ... on One { one } ... on Two { two } } } `; const firstSchema = await buildSchema({ resolvers: [OneTwoResolver], orphanedTypes: [One, Two], validate: false, }); const secondSchema = await buildSchema({ resolvers: [OneTwoResolver], orphanedTypes: [One, Two], validate: false, }); const firstResult = await graphql({ schema: firstSchema, source: query }); const secondResult = await graphql({ schema: secondSchema, source: query }); expect(firstResult.errors).toBeUndefined(); expect(firstResult.data!.base).toEqual({ __typename: "One", baseField: "baseField", one: "one", }); expect(secondResult.errors).toBeUndefined(); expect(secondResult.data!.base).toEqual({ __typename: "One", baseField: "baseField", one: "one", }); }); it("should correctly return data from interface query for all schemas that uses the same interface when string `resolveType` is provided", async () => { @InterfaceType({ resolveType: value => { if ("one" in value) { return "One"; } if ("two" in value) { return "Two"; } throw new Error("Unknown resolveType error"); }, }) class BaseInterface { @Field() baseField!: string; } @ObjectType({ implements: [BaseInterface] }) class One extends BaseInterface { @Field() one!: string; } @ObjectType({ implements: [BaseInterface] }) class Two extends BaseInterface { @Field() two!: string; } @Resolver() class OneTwoResolver { @Query(() => BaseInterface) base(): BaseInterface { const one = new One(); one.baseField = "baseField"; one.one = "one"; return one; } } const query = /* graphql */ ` query { base { __typename baseField ... on One { one } ... on Two { two } } } `; const firstSchema = await buildSchema({ resolvers: [OneTwoResolver], orphanedTypes: [One, Two], validate: false, }); const secondSchema = await buildSchema({ resolvers: [OneTwoResolver], orphanedTypes: [One, Two], validate: false, }); const firstResult = await graphql({ schema: firstSchema, source: query }); const secondResult = await graphql({ schema: secondSchema, source: query }); expect(firstResult.errors).toBeUndefined(); expect(firstResult.data!.base).toEqual({ __typename: "One", baseField: "baseField", one: "one", }); expect(secondResult.errors).toBeUndefined(); expect(secondResult.data!.base).toEqual({ __typename: "One", baseField: "baseField", one: "one", }); }); it("should correctly return data from interface query for all schemas that uses the same interface when class `resolveType` is provided", async () => { @InterfaceType({ resolveType: value => { if ("one" in value) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return One; } if ("two" in value) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return Two; } throw new Error("Unknown resolveType error"); }, }) class BaseInterface { @Field() baseField!: string; } @ObjectType({ implements: [BaseInterface] }) class One extends BaseInterface { @Field() one!: string; } @ObjectType({ implements: [BaseInterface] }) class Two extends BaseInterface { @Field() two!: string; } @Resolver() class OneTwoResolver { @Query(() => BaseInterface) base(): BaseInterface { const one = new One(); one.baseField = "baseField"; one.one = "one"; return one; } } const query = /* graphql */ ` query { base { __typename baseField ... on One { one } ... on Two { two } } } `; const firstSchema = await buildSchema({ resolvers: [OneTwoResolver], orphanedTypes: [One, Two], validate: false, }); const secondSchema = await buildSchema({ resolvers: [OneTwoResolver], orphanedTypes: [One, Two], validate: false, }); const firstResult = await graphql({ schema: firstSchema, source: query }); const secondResult = await graphql({ schema: secondSchema, source: query }); expect(firstResult.errors).toBeUndefined(); expect(firstResult.data!.base).toEqual({ __typename: "One", baseField: "baseField", one: "one", }); expect(secondResult.errors).toBeUndefined(); expect(secondResult.data!.base).toEqual({ __typename: "One", baseField: "baseField", one: "one", }); }); it("should by default automatically register all and only the object types that implements an used interface type", async () => { @InterfaceType() abstract class SampleUnusedInterface { @Field() sampleField!: string; } @ObjectType({ implements: SampleUnusedInterface }) // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleUnusedObjectType implements SampleUnusedInterface { @Field() sampleField!: string; @Field() sampleUnusedInterfaceField!: SampleUnusedInterface; } @InterfaceType() abstract class SampleUsedInterface { @Field() sampleField!: string; } @ObjectType({ implements: SampleUsedInterface }) class SampleObjectTypeImplementingUsedInterface implements SampleUsedInterface { @Field() sampleField!: string; @Field() sampleAdditionalField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleUsedInterface { const sampleObject = new SampleObjectTypeImplementingUsedInterface(); sampleObject.sampleField = "sampleField"; sampleObject.sampleAdditionalField = "sampleAdditionalField"; return sampleObject; } } const { schemaIntrospection } = await getSchemaInfo({ resolvers: [SampleResolver], }); expect(schemaIntrospection.types).not.toContainEqual( expect.objectContaining({ kind: "OBJECT", name: "SampleUnusedObjectType", }), ); expect(schemaIntrospection.types).toContainEqual( expect.objectContaining({ kind: "OBJECT", name: "SampleObjectTypeImplementingUsedInterface", }), ); }); it("should by default automatically register all and only the object types that implements an interface type used as field type", async () => { @InterfaceType() class IFooBar { @Field(() => String) fooBarKind!: string; } @ObjectType({ implements: IFooBar }) // eslint-disable-next-line @typescript-eslint/no-unused-vars class Foo extends IFooBar { override fooBarKind = "Foo"; } @ObjectType({ implements: IFooBar }) // eslint-disable-next-line @typescript-eslint/no-unused-vars class Bar extends IFooBar { override fooBarKind = "Bar"; } @ObjectType() class FooBar { @Field(() => IFooBar) iFooBarField!: IFooBar; } @Resolver() class TestResolver { @Query(() => FooBar) foobar() { return new FooBar(); } } const { schemaIntrospection } = await getSchemaInfo({ resolvers: [TestResolver], }); expect(schemaIntrospection.types).toContainEqual( expect.objectContaining({ kind: TypeKind.INTERFACE, name: "IFooBar", }), ); expect(schemaIntrospection.types).toContainEqual( expect.objectContaining({ kind: TypeKind.OBJECT, name: "Bar", }), ); expect(schemaIntrospection.types).toContainEqual( expect.objectContaining({ kind: TypeKind.OBJECT, name: "Foo", }), ); }); it("should register only the object types from orphanedType when interface type has disabled auto registering", async () => { @InterfaceType({ autoRegisterImplementations: false }) abstract class SampleUsedInterface { @Field() sampleField!: string; } @ObjectType({ implements: SampleUsedInterface }) class FirstSampleObject implements SampleUsedInterface { @Field() sampleField!: string; @Field() sampleFirstAdditionalField!: string; } @ObjectType({ implements: SampleUsedInterface }) // eslint-disable-next-line @typescript-eslint/no-unused-vars class SecondSampleObject implements SampleUsedInterface { @Field() sampleField!: string; @Field() sampleSecondAdditionalField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleUsedInterface { const sampleObject: FirstSampleObject = { sampleField: "sampleField", sampleFirstAdditionalField: "sampleFirstAdditionalField", }; return sampleObject; } } const { schemaIntrospection } = await getSchemaInfo({ resolvers: [SampleResolver], orphanedTypes: [FirstSampleObject], }); expect(schemaIntrospection.types).toContainEqual( expect.objectContaining({ kind: "OBJECT", name: "FirstSampleObject", }), ); expect(schemaIntrospection.types).not.toContainEqual( expect.objectContaining({ kind: "OBJECT", name: "SecondSampleObject", }), ); }); }); }); ================================================ FILE: tests/functional/ioc-container.ts ================================================ import "reflect-metadata"; import { graphql } from "graphql"; import { Arg, type ContainerType, Field, ObjectType, Query, Resolver, type ResolverData, buildSchema, } from "type-graphql"; import { Container, Service } from "typedi"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; describe("IOC container", () => { beforeEach(() => { getMetadataStorage().clear(); Container.reset(); }); it("should use provided container to load resolver class dependencies", async () => { let serviceValue: number | undefined; const initValue = 5; @Service() class SampleService { value = initValue; } @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @Service() @Resolver(() => SampleObject) class SampleResolver { constructor(private service: SampleService) {} @Query() sampleQuery(): SampleObject { serviceValue = this.service.value; return {}; } } const schema = await buildSchema({ resolvers: [SampleResolver], container: Container, }); const query = /* graphql */ ` query { sampleQuery { field } } `; await graphql({ schema, source: query }); expect(serviceValue).toEqual(initValue); }); it("should use default container to instantiate resolver class", async () => { let resolverValue: number | undefined; @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @Resolver(() => SampleObject) class SampleResolver { value = Math.random(); @Query() sampleQuery(): SampleObject { resolverValue = this.value; return {}; } } const schema = await buildSchema({ resolvers: [SampleResolver], }); const query = /* graphql */ ` query { sampleQuery { field } } `; await graphql({ schema, source: query }); const firstCallValue = resolverValue; resolverValue = undefined; await graphql({ schema, source: query }); const secondCallValue = resolverValue; expect(firstCallValue).toBeDefined(); expect(secondCallValue).toBeDefined(); expect(firstCallValue).toEqual(secondCallValue); }); it("should pass resolver's data to container's get", async () => { let contextRequestId!: number; const testContainer: ContainerType = { get(someClass, resolverData: ResolverData<{ requestId: number }>) { contextRequestId = resolverData.context.requestId; return Container.get(someClass); }, }; @Resolver() class SampleResolver { @Query() sampleQuery(): string { return "sampleQuery"; } } const schema = await buildSchema({ resolvers: [SampleResolver], container: testContainer, }); const query = /* graphql */ ` query { sampleQuery } `; const requestId = Math.random(); await graphql({ schema, source: query, contextValue: { requestId } }); expect(contextRequestId).toEqual(requestId); }); it("should properly get container from container getter function", async () => { let called = false; @Resolver() class SampleResolver { @Query() sampleQuery(): string { return "sampleQuery"; } } interface TestContext { container: ContainerType; } const schema = await buildSchema({ resolvers: [SampleResolver], container: ({ context }: ResolverData) => context.container, }); const query = /* graphql */ ` query { sampleQuery } `; const mockedContainer: ContainerType = { get(someClass: any) { called = true; return Container.get(someClass); }, }; const queryContext: TestContext = { container: mockedContainer, }; await graphql({ schema, source: query, contextValue: queryContext }); expect(called).toEqual(true); }); it("should properly get instance from an async container", async () => { let called = false; @Service() @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("sampleArg") sampleArg: string): string { return sampleArg; } } const asyncContainer: ContainerType = { async get(someClass: any) { await new Promise(setImmediate); called = true; return Container.get(someClass); }, }; const schema = await buildSchema({ resolvers: [SampleResolver], container: asyncContainer, }); const query = /* graphql */ ` query { sampleQuery(sampleArg: "sampleArgValue") } `; const result: any = await graphql({ schema, source: query }); expect(result.errors).toBeUndefined(); expect(result.data!.sampleQuery).toEqual("sampleArgValue"); expect(called).toEqual(true); }); }); ================================================ FILE: tests/functional/manual-decorators.ts ================================================ import "reflect-metadata"; import { type IntrospectionObjectType } from "graphql"; import { Args, ArgsType, Field, ObjectType, Query, Resolver } from "type-graphql"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("manual decorators", () => { it("should not fail when field is dynamically registered", async () => { @ObjectType() class SampleObject { @Field() manualField!: string; } // Dynamically register field Field(() => String)(SampleObject.prototype, "dynamicField"); @ArgsType() class SampleArgs { @Field() sampleField!: string; } // Dynamically register field args Args(() => SampleArgs)(SampleObject.prototype, "dynamicField", 0); @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return new SampleObject(); } } // Get builded schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); const sampleObjectType = schemaInfo.schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; const dynamicField = sampleObjectType.fields.find(it => it.name === "dynamicField")!; expect(sampleObjectType.fields).toHaveLength(2); expect(dynamicField.type).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); expect(dynamicField.args).toEqual([ { defaultValue: null, deprecationReason: null, description: null, isDeprecated: false, name: "sampleField", type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }, }, ]); }); }); ================================================ FILE: tests/functional/metadata-storage.ts ================================================ import "reflect-metadata"; import { createPubSub } from "@graphql-yoga/subscription"; import { type ClassType, Field, FieldResolver, Mutation, ObjectType, Query, Resolver, Subscription, buildSchema, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; describe("MetadataStorage", () => { describe("resolvers inheritance", () => { const INHERITED_QUERY_NAME = "inheritedQueryName"; const INHERITED_MUTATION_NAME = "inheritedMutationName"; const INHERITED_SUBSCRIPTION_NAME = "inheritedSubscriptionName"; const INHERITED_FIELD_RESOLVER_NAME = "inheritedFieldResolverName"; beforeAll(async () => { getMetadataStorage().clear(); function createAbstractResolver(classType: ClassType) { @Resolver(() => classType) abstract class AbstractResolver { @Query({ name: INHERITED_QUERY_NAME }) abstractQuery(): boolean { return true; } @Mutation({ name: INHERITED_MUTATION_NAME }) abstractMutation(): boolean { return true; } @Subscription({ name: INHERITED_SUBSCRIPTION_NAME, topics: "sampleTopic" }) abstractSubscription(): boolean { return true; } @FieldResolver({ name: INHERITED_FIELD_RESOLVER_NAME }) abstractFieldResolver(): boolean { return true; } } return AbstractResolver; } @ObjectType() class SampleObject { @Field() sampleField!: boolean; @Field({ name: INHERITED_FIELD_RESOLVER_NAME }) abstractSampleField!: boolean; } @Resolver(() => SampleObject) class SubClassResolver extends createAbstractResolver(SampleObject) { @Query() subClassQuery(): boolean { return true; } @Mutation() subClassMutation(): boolean { return true; } @Subscription({ topics: "sampleTopic" }) subClassSubscription(): boolean { return true; } @FieldResolver() sampleField(): boolean { return true; } } await buildSchema({ resolvers: [SubClassResolver], pubSub: createPubSub(), }); }); it("should not have duplicated query metadata for inherited resolvers", async () => { expect( getMetadataStorage().queries.filter(query => query.schemaName === INHERITED_QUERY_NAME), ).toHaveLength(1); expect(getMetadataStorage().queries).toHaveLength(2); }); it("should not have duplicated mutation metadata for inherited resolvers", async () => { expect( getMetadataStorage().mutations.filter( mutation => mutation.schemaName === INHERITED_MUTATION_NAME, ), ).toHaveLength(1); expect(getMetadataStorage().mutations).toHaveLength(2); }); it("should not have duplicated subscription metadata for inherited resolvers", async () => { expect( getMetadataStorage().subscriptions.filter( subscription => subscription.schemaName === INHERITED_SUBSCRIPTION_NAME, ), ).toHaveLength(1); expect(getMetadataStorage().subscriptions).toHaveLength(2); }); it("should not have duplicated fieldResolver metadata for inherited resolvers", async () => { expect( getMetadataStorage().fieldResolvers.filter( fieldResolver => fieldResolver.schemaName === INHERITED_FIELD_RESOLVER_NAME, ), ).toHaveLength(1); expect(getMetadataStorage().fieldResolvers).toHaveLength(2); }); }); }); ================================================ FILE: tests/functional/middlewares.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, graphql } from "graphql"; import { Arg, Field, FieldResolver, type MiddlewareFn, type MiddlewareInterface, type NextFn, ObjectType, Query, Resolver, type ResolverData, UseMiddleware, buildSchema, } from "type-graphql"; import { createMethodMiddlewareDecorator } from "@/decorators"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; describe("Middlewares", () => { let schema: GraphQLSchema; let sampleResolver: any; let middlewareLogs: string[] = []; const sleep = (time: number) => new Promise(resolve => { setTimeout(resolve, time); }); const resolverMiddleware1: MiddlewareFn = async (_, next) => { middlewareLogs.push("resolver middleware1 before"); const result = await next(); middlewareLogs.push("resolver middleware1 after"); return result; }; const resolverMiddleware2: MiddlewareFn = async (_, next) => { middlewareLogs.push("resolver middleware2 before"); const result = await next(); middlewareLogs.push("resolver middleware2 after"); return result; }; beforeEach(() => { middlewareLogs = []; }); beforeAll(async () => { getMetadataStorage().clear(); const middleware1: MiddlewareFn = async (_, next) => { middlewareLogs.push("middleware1 before"); const result = await next(); middlewareLogs.push("middleware1 after"); return result; }; const middleware2: MiddlewareFn = async (_, next) => { middlewareLogs.push("middleware2 before"); const result = await next(); middlewareLogs.push("middleware2 after"); return result; }; const middleware3: MiddlewareFn = async (_, next) => { middlewareLogs.push("middleware3 before"); const result = await next(); middlewareLogs.push("middleware3 after"); return result; }; const interceptMiddleware: MiddlewareFn = async (_, next) => { const result = await next(); middlewareLogs.push(result); return "interceptMiddleware"; }; const returnUndefinedMiddleware: MiddlewareFn = async (_, next) => { const result = await next(); middlewareLogs.push(result); }; const errorCatchMiddleware: MiddlewareFn = async (_, next) => { try { const result = await next(); return result; } catch (err) { middlewareLogs.push((err as Error).message); return "errorCatchMiddleware"; } }; const errorThrowAfterMiddleware: MiddlewareFn = async (_, next) => { await next(); middlewareLogs.push("errorThrowAfterMiddleware"); throw new Error("errorThrowAfterMiddleware"); }; const errorThrowMiddleware: MiddlewareFn = async _ => { middlewareLogs.push("errorThrowMiddleware"); throw new Error("errorThrowMiddleware"); }; const fieldResolverMiddleware: MiddlewareFn = async (_, next) => { middlewareLogs.push("fieldResolverMiddlewareBefore"); const result = await next(); middlewareLogs.push("fieldResolverMiddlewareAfter"); return result; }; const doubleNextMiddleware: MiddlewareFn = async (_, next) => { const result1 = await next(); await next(); return result1; }; class ClassMiddleware implements MiddlewareInterface { private logName = "ClassMiddleware"; async use(_: ResolverData, next: NextFn) { middlewareLogs.push(`${this.logName} before`); const result = await next(); middlewareLogs.push(`${this.logName} after`); return result; } } const CustomMethodDecorator = createMethodMiddlewareDecorator(async (_, next) => { middlewareLogs.push("CustomMethodDecorator"); return next(); }); @ObjectType() class SampleObject { @Field() normalField!: string; @Field() resolverField!: string; @Field() @UseMiddleware(fieldResolverMiddleware) middlewareField!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() normalQuery(): boolean { return true; } @Query() sampleObjectQuery(): SampleObject { return { normalField: "normalField", middlewareField: "middlewareField", } as SampleObject; } @Query(() => String) @UseMiddleware(middleware1, middleware2, middleware3) async middlewareOrderQuery() { middlewareLogs.push("middlewareOrderQuery"); await sleep(25); return "middlewareOrderQueryResult"; } @UseMiddleware(middleware1) @UseMiddleware(middleware2) @UseMiddleware(middleware3) @Query(() => String) async multipleMiddlewareDecoratorsQuery() { middlewareLogs.push("multipleMiddlewareDecoratorsQuery"); return "multipleMiddlewareDecoratorsQueryResult"; } @Query() @UseMiddleware(interceptMiddleware) middlewareInterceptQuery(): string { middlewareLogs.push("middlewareInterceptQuery"); return "middlewareInterceptQueryResult"; } @Query() @UseMiddleware( returnUndefinedMiddleware, returnUndefinedMiddleware, returnUndefinedMiddleware, ) middlewareReturnUndefinedQuery(): string { middlewareLogs.push("middlewareReturnUndefinedQuery"); return "middlewareReturnUndefinedQueryResult"; } @Query() @UseMiddleware(errorCatchMiddleware) middlewareErrorCatchQuery(@Arg("throwError") throwError: boolean): string { middlewareLogs.push("middlewareErrorCatchQuery"); if (throwError) { throw new Error("middlewareErrorCatchQueryError"); } return "middlewareErrorCatchQueryResult"; } @Query() @UseMiddleware(errorThrowAfterMiddleware) middlewareThrowErrorAfterQuery(): string { middlewareLogs.push("middlewareThrowErrorAfterQuery"); return "middlewareThrowErrorAfterQueryResult"; } @Query() @UseMiddleware(errorThrowMiddleware) middlewareThrowErrorQuery(): string { middlewareLogs.push("middlewareThrowErrorQuery"); return "middlewareThrowErrorQueryResult"; } @Query() @UseMiddleware(doubleNextMiddleware) doubleNextMiddlewareQuery(): string { middlewareLogs.push("doubleNextMiddlewareQuery"); return "doubleNextMiddlewareQueryResult"; } @Query() @UseMiddleware(ClassMiddleware) classMiddlewareQuery(): string { middlewareLogs.push("classMiddlewareQuery"); return "classMiddlewareQueryResult"; } @Query() @CustomMethodDecorator customMethodDecoratorQuery(): string { middlewareLogs.push("customMethodDecoratorQuery"); return "customMethodDecoratorQuery"; } @FieldResolver() @UseMiddleware(fieldResolverMiddleware) resolverField(): string { middlewareLogs.push("resolverField"); return "resolverField"; } } sampleResolver = SampleResolver; schema = await buildSchema({ resolvers: [SampleResolver], }); }); it("should build the schema without errors", async () => { expect(schema).toBeDefined(); }); it("should correctly returns value from normal query", async () => { const query = `query { normalQuery }`; const { data } = await graphql({ schema, source: query }); expect((data as any).normalQuery).toEqual(true); }); it("should correctly call middlewares in order", async () => { const query = `query { middlewareOrderQuery }`; const { data } = await graphql({ schema, source: query }); expect((data as any).middlewareOrderQuery).toEqual("middlewareOrderQueryResult"); expect(middlewareLogs).toHaveLength(7); expect(middlewareLogs[0]).toEqual("middleware1 before"); expect(middlewareLogs[1]).toEqual("middleware2 before"); expect(middlewareLogs[2]).toEqual("middleware3 before"); expect(middlewareLogs[3]).toEqual("middlewareOrderQuery"); expect(middlewareLogs[4]).toEqual("middleware3 after"); expect(middlewareLogs[5]).toEqual("middleware2 after"); expect(middlewareLogs[6]).toEqual("middleware1 after"); }); it("should correctly call resolver middlewares in order", async () => { UseMiddleware(resolverMiddleware1, resolverMiddleware2)(sampleResolver); const localSchema = await buildSchema({ resolvers: [sampleResolver], }); // clear ResolverMiddlewareMetadata for other tests getMetadataStorage().resolverMiddlewares = []; getMetadataStorage().resolverMiddlewaresByTargetCache = new Map(); getMetadataStorage().middlewaresByTargetAndFieldCache = new Map(); const query = `query { middlewareOrderQuery }`; const { data } = await graphql({ schema: localSchema, source: query }); expect(data!.middlewareOrderQuery).toEqual("middlewareOrderQueryResult"); expect(middlewareLogs).toHaveLength(11); expect(middlewareLogs[0]).toEqual("resolver middleware1 before"); expect(middlewareLogs[1]).toEqual("resolver middleware2 before"); expect(middlewareLogs[2]).toEqual("middleware1 before"); expect(middlewareLogs[3]).toEqual("middleware2 before"); expect(middlewareLogs[4]).toEqual("middleware3 before"); expect(middlewareLogs[5]).toEqual("middlewareOrderQuery"); expect(middlewareLogs[6]).toEqual("middleware3 after"); expect(middlewareLogs[7]).toEqual("middleware2 after"); expect(middlewareLogs[8]).toEqual("middleware1 after"); expect(middlewareLogs[9]).toEqual("resolver middleware2 after"); expect(middlewareLogs[10]).toEqual("resolver middleware1 after"); }); it("should call middlewares in order of multiple decorators", async () => { const query = `query { multipleMiddlewareDecoratorsQuery }`; const { data } = await graphql({ schema, source: query }); expect((data as any).multipleMiddlewareDecoratorsQuery).toEqual( "multipleMiddlewareDecoratorsQueryResult", ); expect(middlewareLogs).toHaveLength(7); expect(middlewareLogs[0]).toEqual("middleware1 before"); expect(middlewareLogs[1]).toEqual("middleware2 before"); expect(middlewareLogs[2]).toEqual("middleware3 before"); expect(middlewareLogs[3]).toEqual("multipleMiddlewareDecoratorsQuery"); expect(middlewareLogs[4]).toEqual("middleware3 after"); expect(middlewareLogs[5]).toEqual("middleware2 after"); expect(middlewareLogs[6]).toEqual("middleware1 after"); }); it("should call resolver middlewares in order of multiple decorators", async () => { @UseMiddleware(resolverMiddleware1) @UseMiddleware(resolverMiddleware2) @Resolver() class LocalResolver { @Query() normalQuery(): boolean { middlewareLogs.push("normalQuery"); return true; } } const localSchema = await buildSchema({ resolvers: [LocalResolver], }); const query = `query { normalQuery }`; const { data } = await graphql({ schema: localSchema, source: query }); expect(data!.normalQuery).toEqual(true); expect(middlewareLogs).toHaveLength(5); expect(middlewareLogs[0]).toEqual("resolver middleware1 before"); expect(middlewareLogs[1]).toEqual("resolver middleware2 before"); expect(middlewareLogs[2]).toEqual("normalQuery"); expect(middlewareLogs[3]).toEqual("resolver middleware2 after"); expect(middlewareLogs[4]).toEqual("resolver middleware1 after"); }); it("should correctly intercept returned value", async () => { const query = `query { middlewareInterceptQuery }`; const { data } = await graphql({ schema, source: query }); expect((data as any).middlewareInterceptQuery).toEqual("interceptMiddleware"); expect(middlewareLogs).toHaveLength(2); expect(middlewareLogs[0]).toEqual("middlewareInterceptQuery"); expect(middlewareLogs[1]).toEqual("middlewareInterceptQueryResult"); }); it("should correctly use next middleware value when undefined returned", async () => { const query = `query { middlewareReturnUndefinedQuery }`; const { data } = await graphql({ schema, source: query }); expect((data as any).middlewareReturnUndefinedQuery).toEqual( "middlewareReturnUndefinedQueryResult", ); expect(middlewareLogs).toHaveLength(4); expect(middlewareLogs[0]).toEqual("middlewareReturnUndefinedQuery"); expect(middlewareLogs[1]).toEqual("middlewareReturnUndefinedQueryResult"); expect(middlewareLogs[2]).toEqual("middlewareReturnUndefinedQueryResult"); expect(middlewareLogs[3]).toEqual("middlewareReturnUndefinedQueryResult"); }); it("should correctly catch error thrown in resolver", async () => { const query = `query { middlewareErrorCatchQuery(throwError: true) }`; const { data } = await graphql({ schema, source: query }); expect((data as any).middlewareErrorCatchQuery).toEqual("errorCatchMiddleware"); expect(middlewareLogs).toHaveLength(2); expect(middlewareLogs[0]).toEqual("middlewareErrorCatchQuery"); expect(middlewareLogs[1]).toEqual("middlewareErrorCatchQueryError"); }); it("should not modify the response if error not thrown", async () => { const query = `query { middlewareErrorCatchQuery(throwError: false) }`; const { data } = await graphql({ schema, source: query }); expect((data as any).middlewareErrorCatchQuery).toEqual("middlewareErrorCatchQueryResult"); }); it("should propagate thrown error up to graphql handler", async () => { const query = `query { middlewareThrowErrorAfterQuery }`; const { errors } = await graphql({ schema, source: query }); expect(errors).toHaveLength(1); expect(errors![0].message).toEqual("errorThrowAfterMiddleware"); expect(middlewareLogs).toHaveLength(2); expect(middlewareLogs[0]).toEqual("middlewareThrowErrorAfterQuery"); expect(middlewareLogs[1]).toEqual("errorThrowAfterMiddleware"); }); it("should prevent calling handler when `next` not invoked", async () => { const query = `query { middlewareThrowErrorQuery }`; const { errors } = await graphql({ schema, source: query }); expect(errors).toHaveLength(1); expect(errors![0].message).toEqual("errorThrowMiddleware"); expect(middlewareLogs).toHaveLength(1); expect(middlewareLogs[0]).toEqual("errorThrowMiddleware"); }); it("should call middlewares for field resolver", async () => { const query = `query { sampleObjectQuery { resolverField } }`; const { data } = await graphql({ schema, source: query }); expect((data as any).sampleObjectQuery.resolverField).toEqual("resolverField"); expect(middlewareLogs).toHaveLength(3); expect(middlewareLogs[0]).toEqual("fieldResolverMiddlewareBefore"); expect(middlewareLogs[1]).toEqual("resolverField"); expect(middlewareLogs[2]).toEqual("fieldResolverMiddlewareAfter"); }); it("should correctly call class middleware", async () => { const query = `query { classMiddlewareQuery }`; const { data } = await graphql({ schema, source: query }); expect((data as any).classMiddlewareQuery).toEqual("classMiddlewareQueryResult"); expect(middlewareLogs).toHaveLength(3); expect(middlewareLogs[0]).toEqual("ClassMiddleware before"); expect(middlewareLogs[1]).toEqual("classMiddlewareQuery"); expect(middlewareLogs[2]).toEqual("ClassMiddleware after"); }); it("should correctly call resolver of custom method decorator", async () => { const query = `query { customMethodDecoratorQuery }`; const { data } = await graphql({ schema, source: query }); expect((data as any).customMethodDecoratorQuery).toEqual("customMethodDecoratorQuery"); expect(middlewareLogs).toHaveLength(2); expect(middlewareLogs[0]).toEqual("CustomMethodDecorator"); expect(middlewareLogs[1]).toEqual("customMethodDecoratorQuery"); }); it("should call middlewares for normal field", async () => { const query = `query { sampleObjectQuery { middlewareField } }`; const { data } = await graphql({ schema, source: query }); expect((data as any).sampleObjectQuery.middlewareField).toEqual("middlewareField"); expect(middlewareLogs).toHaveLength(2); expect(middlewareLogs[0]).toEqual("fieldResolverMiddlewareBefore"); expect(middlewareLogs[1]).toEqual("fieldResolverMiddlewareAfter"); }); it("should throw error if middleware called next more than once", async () => { const query = `query { doubleNextMiddlewareQuery }`; const { errors } = await graphql({ schema, source: query }); expect(errors).toHaveLength(1); expect(errors![0].message).toEqual("next() called multiple times"); }); it("should correctly call middlewares in the order of global, resolver, field", async () => { const globalMiddleware1: MiddlewareFn = async (_, next) => { middlewareLogs.push("globalMiddleware1 before"); const result = await next(); middlewareLogs.push("globalMiddleware1 after"); return result; }; const globalMiddleware2: MiddlewareFn = async (_, next) => { middlewareLogs.push("globalMiddleware2 before"); const result = await next(); middlewareLogs.push("globalMiddleware2 after"); return result; }; const localSchema = await buildSchema({ resolvers: [sampleResolver], globalMiddlewares: [globalMiddleware1, globalMiddleware2], }); const query = `query { middlewareOrderQuery }`; const { data } = await graphql({ schema: localSchema, source: query }); expect((data as any).middlewareOrderQuery).toEqual("middlewareOrderQueryResult"); expect(middlewareLogs).toHaveLength(11); expect(middlewareLogs[0]).toEqual("globalMiddleware1 before"); expect(middlewareLogs[1]).toEqual("globalMiddleware2 before"); expect(middlewareLogs[2]).toEqual("middleware1 before"); expect(middlewareLogs[3]).toEqual("middleware2 before"); expect(middlewareLogs[4]).toEqual("middleware3 before"); expect(middlewareLogs[5]).toEqual("middlewareOrderQuery"); expect(middlewareLogs[6]).toEqual("middleware3 after"); expect(middlewareLogs[7]).toEqual("middleware2 after"); expect(middlewareLogs[8]).toEqual("middleware1 after"); expect(middlewareLogs[9]).toEqual("globalMiddleware2 after"); expect(middlewareLogs[10]).toEqual("globalMiddleware1 after"); }); it("should correctly call global middlewares before local ones", async () => { UseMiddleware(resolverMiddleware1, resolverMiddleware2)(sampleResolver); const globalMiddleware1: MiddlewareFn = async (_, next) => { middlewareLogs.push("globalMiddleware1 before"); const result = await next(); middlewareLogs.push("globalMiddleware1 after"); return result; }; const globalMiddleware2: MiddlewareFn = async (_, next) => { middlewareLogs.push("globalMiddleware2 before"); const result = await next(); middlewareLogs.push("globalMiddleware2 after"); return result; }; const localSchema = await buildSchema({ resolvers: [sampleResolver], globalMiddlewares: [globalMiddleware1, globalMiddleware2], }); // clear ResolverMiddlewareMetadata for other tests getMetadataStorage().resolverMiddlewares = []; const query = `query { middlewareOrderQuery }`; const { data } = await graphql({ schema: localSchema, source: query }); expect(data!.middlewareOrderQuery).toEqual("middlewareOrderQueryResult"); expect(middlewareLogs).toHaveLength(15); expect(middlewareLogs[0]).toEqual("globalMiddleware1 before"); expect(middlewareLogs[1]).toEqual("globalMiddleware2 before"); expect(middlewareLogs[2]).toEqual("resolver middleware1 before"); expect(middlewareLogs[3]).toEqual("resolver middleware2 before"); expect(middlewareLogs[4]).toEqual("middleware1 before"); expect(middlewareLogs[5]).toEqual("middleware2 before"); expect(middlewareLogs[6]).toEqual("middleware3 before"); expect(middlewareLogs[7]).toEqual("middlewareOrderQuery"); expect(middlewareLogs[8]).toEqual("middleware3 after"); expect(middlewareLogs[9]).toEqual("middleware2 after"); expect(middlewareLogs[10]).toEqual("middleware1 after"); expect(middlewareLogs[11]).toEqual("resolver middleware2 after"); expect(middlewareLogs[12]).toEqual("resolver middleware1 after"); expect(middlewareLogs[13]).toEqual("globalMiddleware2 after"); expect(middlewareLogs[14]).toEqual("globalMiddleware1 after"); }); }); ================================================ FILE: tests/functional/nested-interface-inheritance.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, printSchema } from "graphql"; import { Field, InterfaceType, ObjectType, Query, Resolver } from "../../src"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("nested interface inheritance", () => { let schema: GraphQLSchema; beforeAll(async () => { @InterfaceType() abstract class A { @Field() a!: string; } @InterfaceType({ implements: A }) abstract class B extends A { @Field() b!: string; } @InterfaceType({ implements: B }) abstract class C extends B { @Field() c!: string; } @ObjectType({ implements: C }) class D extends C { @Field() d!: string; } @Resolver() class TestResolver { @Query() testQuery(): string { return "testQuery"; } } const schemaInfo = await getSchemaInfo({ resolvers: [TestResolver], orphanedTypes: [A, B, C, D], }); schema = schemaInfo.schema; }); it("should properly generate object type, implementing multi-inherited interface, with only one `implements`", async () => { expect(printSchema(schema)).toMatchInlineSnapshot(` "type D implements C & B & A { a: String! b: String! c: String! d: String! } interface A { a: String! } interface B implements A { a: String! b: String! } interface C implements B & A { a: String! b: String! c: String! } type Query { testQuery: String! }" `); }); }); ================================================ FILE: tests/functional/peer-dependency.ts ================================================ import { ensureInstalledCorrectGraphQLPackage } from "@/utils/graphql-version"; describe("`graphql` package peer dependency", () => { it("should have installed correct version", async () => { expect(ensureInstalledCorrectGraphQLPackage).not.toThrow(); }); }); ================================================ FILE: tests/functional/query-complexity.ts ================================================ import "reflect-metadata"; import { createPubSub } from "@graphql-yoga/subscription"; import { type GraphQLSchema, parse } from "graphql"; import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from "graphql-query-complexity"; import { Arg, type ClassType, Field, ObjectType, Query, Resolver, Subscription, buildSchema, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; // Helpers function calculateComplexityPoints(query: string, schema: GraphQLSchema) { const complexityPoints = getComplexity({ query: parse(query), schema, estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })], }); return complexityPoints; } describe("Query complexity", () => { describe("Queries", () => { let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ complexity: 10 }) complexResolverMethod!: number; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(): SampleObject { const obj = new SampleObject(); return obj; } } schema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); }); it("should build the schema without errors", () => { expect(schema).toBeDefined(); }); it("should properly calculate complexity points for a query with complex field resolver", () => { const query = /* graphql */ ` query { sampleQuery { complexResolverMethod } } `; const points = calculateComplexityPoints(query, schema); expect(points).toEqual(11); }); }); describe("Subscriptions", () => { let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() normalField!: string; } function createResolver(name: string, objectType: ClassType) { @Resolver(() => objectType) class BaseResolver { protected name = "baseName"; @Query({ name: `${name}Query` }) baseQuery(@Arg("arg") _arg: boolean): boolean { return true; } @Subscription({ topics: "baseTopic", name: `${name}Subscription` }) baseSubscription(@Arg("arg") _arg: boolean): boolean { return true; } } return BaseResolver; } @Resolver() class ChildResolver extends createResolver("prefix", SampleObject) { @Subscription({ topics: "childTopic", complexity: 4 }) childSubscription(): boolean { return true; } } const schemaInfo = await getSchemaInfo({ resolvers: [ChildResolver], pubSub: createPubSub(), }); schema = schemaInfo.schema; }); it("should build schema correctly", async () => { expect(schema).toBeDefined(); }); it("should properly calculate subscription complexity", () => { const query = `subscription { childSubscription }`; const points = calculateComplexityPoints(query, schema); expect(points).toEqual(4); }); }); }); ================================================ FILE: tests/functional/resolvers.ts ================================================ /* eslint "@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }] */ import "reflect-metadata"; import { createPubSub } from "@graphql-yoga/subscription"; import { type GraphQLSchema, type IntrospectionField, type IntrospectionInputObjectType, type IntrospectionListTypeRef, type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionSchema, TypeKind, graphql, } from "graphql"; import { Arg, Args, ArgsType, CannotDetermineGraphQLTypeError, type ClassType, ConflictingDefaultValuesError, Ctx, Field, FieldResolver, Info, InputType, Int, Mutation, NoExplicitTypeError, ObjectType, Query, Resolver, type ResolverInterface, Root, Subscription, WrongNullableListOptionError, buildSchema, buildSchemaSync, createParameterDecorator, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { type ReturnTypeFunc } from "../../src/decorators/types"; import { expectToThrow } from "../helpers/expectToThrow"; import { getInnerTypeOfNonNullableType } from "../helpers/getInnerFieldType"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Resolvers", () => { describe("Schema", () => { let schemaIntrospection: IntrospectionSchema; let queryType: IntrospectionObjectType; let mutationType: IntrospectionObjectType; let sampleObjectType: IntrospectionObjectType; let argMethodField: IntrospectionField; beforeAll(async () => { getMetadataStorage().clear(); @InputType() class SampleInput { @Field() field!: string; @Field({ defaultValue: "defaultStringFieldDefaultValue" }) defaultStringField!: string; @Field() implicitDefaultStringField: string = "implicitDefaultStringFieldDefaultValue"; @Field() inheritDefaultField: string = "inheritDefaultFieldValue"; } @InputType() class SampleInputChild extends SampleInput { @Field({ defaultValue: "defaultValueOverwritten" }) override defaultStringField!: string; @Field() override implicitDefaultStringField: string = "implicitDefaultValueOverwritten"; } @ArgsType() class SampleArgs { @Field() stringArg!: string; @Field(() => Int, { nullable: true }) numberArg!: number; @Field() inputObjectArg!: SampleInput; @Field({ defaultValue: "defaultStringArgDefaultValue" }) defaultStringArg!: string; @Field() implicitDefaultStringArg: string = "implicitDefaultStringArgDefaultValue"; @Field() inheritDefaultArg: string = "inheritDefaultArgValue"; } @ArgsType() class SampleArgsChild extends SampleArgs { @Field({ defaultValue: "defaultValueOverwritten" }) override defaultStringArg!: string; @Field() override implicitDefaultStringArg: string = "implicitDefaultValueOverwritten"; } @ObjectType() class SampleObject { @Field() normalField!: string; @Field() resolverFieldWithArgs!: string; @Field() // eslint-disable-next-line @typescript-eslint/class-literal-property-style get getterField(): string { return "getterField"; } @Field() simpleMethodField(): string { return "simpleMethodField"; } @Field(() => String) argMethodField( @Arg("stringArg") _stringArg: string, @Arg("booleanArg") _booleanArg: boolean, @Arg("numberArg") _numberArg: number, @Arg("inputArg") _inputArg: SampleInput, @Arg("inputChildArg") _inputChildArg: SampleInputChild, @Arg("explicitNullableArg", _type => String, { nullable: true }) _explicitNullableArg: any, @Arg("explicitArrayArg", () => [String]) _explicitArrayArg: any, @Arg("defaultStringArg", { defaultValue: "defaultStringArgDefaultValue" }) _defaultStringArg: string, @Arg("nullableStringArg", { nullable: true }) _nullableStringArg?: string, ): any { return "argMethodField"; } } @Resolver(() => SampleObject) // eslint-disable-next-line @typescript-eslint/no-unused-vars class LambdaResolver { @Query() lambdaQuery(): boolean { return true; } } @Resolver(SampleObject) // eslint-disable-next-line @typescript-eslint/no-unused-vars class ClassResolver { @Query() classQuery(): boolean { return true; } } @Resolver(() => SampleObject) class SampleResolver { @Query() emptyQuery(): boolean { return true; } @Query() implicitStringQuery(): string { return "implicitStringQuery"; } @Query(() => String) explicitStringQuery(): any { return "explicitStringQuery"; } @Query(() => String, { nullable: true }) nullableStringQuery(): string | null { return Math.random() > 0.5 ? "explicitStringQuery" : null; } @Query(() => [String]) explicitStringArrayQuery(): any { return []; } @Query(() => [String], { nullable: "items" }) explicitNullableItemArrayQuery(): any { return []; } @Query(() => [String], { nullable: "itemsAndList" }) explicitNullableArrayWithNullableItemsQuery(): any { return []; } @Query(() => String) async promiseStringQuery(): Promise { return "promiseStringQuery"; } @Query() implicitObjectQuery(): SampleObject { return {} as SampleObject; } @Query(() => SampleObject) async asyncObjectQuery(): Promise { return {} as SampleObject; } @Query() rootCtxQuery(@Root() _root: any, @Ctx() _ctx: any): boolean { return true; } @Query(() => String) argQuery(@Arg("arg1") _arg1: string, @Arg("arg2") _arg2: boolean): any { return "argQuery"; } @Query(() => String) argsQuery(@Args() _args: SampleArgs): any { return "argsQuery"; } @Query(() => String) argAndArgsQuery(@Arg("arg1") _arg1: string, @Args() _args: SampleArgs): any { return "argAndArgsQuery"; } @Query(() => String) argsInheritanceQuery(@Args() _args: SampleArgsChild): any { return "argsInheritanceQuery"; } @Mutation() emptyMutation(): boolean { return true; } @FieldResolver() resolverFieldWithArgs(@Arg("arg1") _arg1: string, @Arg("arg2") _arg2: boolean) { return "resolverFieldWithArgs"; } @FieldResolver(() => String, { nullable: true, description: "independent" }) independentFieldResolver(@Arg("arg1") _arg1: string, @Arg("arg2") _arg2: boolean) { return "resolverFieldWithArgs"; } @FieldResolver(() => String, { name: "overwrittenField", nullable: true }) overwrittenFieldResolver() { return "overwrittenFieldResolver"; } } // get builded schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); schemaIntrospection = schemaInfo.schemaIntrospection; queryType = schemaInfo.queryType; mutationType = schemaInfo.mutationType!; sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; argMethodField = sampleObjectType.fields.find(field => field.name === "argMethodField")!; }); // helpers function getQuery(queryName: string) { return queryType.fields.find(field => field.name === queryName)!; } function getMutation(mutationName: string) { return mutationType.fields.find(field => field.name === mutationName)!; } it("should generate schema without errors", async () => { expect(schemaIntrospection).toBeDefined(); }); describe("Fields", () => { it("should generate proper field type for property getter", async () => { const getterField = sampleObjectType.fields.find(field => field.name === "getterField")!; const getterFieldType = getterField.type as IntrospectionNonNullTypeRef; const getterFieldInnerType = getterFieldType.ofType as IntrospectionNamedTypeRef; expect(getterField.name).toEqual("getterField"); expect(getterField.args).toHaveLength(0); expect(getterFieldType.kind).toEqual(TypeKind.NON_NULL); expect(getterFieldInnerType.kind).toEqual(TypeKind.SCALAR); expect(getterFieldInnerType.name).toEqual("String"); }); it("should generate proper field type for simple class method", async () => { const simpleMethodField = sampleObjectType.fields.find( field => field.name === "simpleMethodField", )!; const simpleMethodFieldType = simpleMethodField.type as IntrospectionNonNullTypeRef; const simpleMethodFieldInnerType = simpleMethodFieldType.ofType as IntrospectionNamedTypeRef; expect(simpleMethodField.name).toEqual("simpleMethodField"); expect(simpleMethodField.args).toHaveLength(0); expect(simpleMethodFieldType.kind).toEqual(TypeKind.NON_NULL); expect(simpleMethodFieldInnerType.kind).toEqual(TypeKind.SCALAR); expect(simpleMethodFieldInnerType.name).toEqual("String"); }); it("should generate proper field type for class method with args", async () => { const argMethodFieldType = argMethodField.type as IntrospectionNonNullTypeRef; const argMethodFieldInnerType = argMethodFieldType.ofType as IntrospectionNamedTypeRef; expect(argMethodField.name).toEqual("argMethodField"); expect(argMethodField.args).toHaveLength(9); expect(argMethodFieldType.kind).toEqual(TypeKind.NON_NULL); expect(argMethodFieldInnerType.kind).toEqual(TypeKind.SCALAR); expect(argMethodFieldInnerType.name).toEqual("String"); }); }); describe("Inline args", () => { it("should generate normal string arg type for object field method", async () => { const stringArg = argMethodField.args.find(arg => arg.name === "stringArg")!; const stringArgType = stringArg.type as IntrospectionNonNullTypeRef; const stringArgInnerType = stringArgType.ofType as IntrospectionNamedTypeRef; expect(stringArg.name).toEqual("stringArg"); expect(stringArgType.kind).toEqual(TypeKind.NON_NULL); expect(stringArgInnerType.kind).toEqual(TypeKind.SCALAR); expect(stringArgInnerType.name).toEqual("String"); }); it("should generate normal boolean arg type for object field method", async () => { const booleanArg = argMethodField.args.find(arg => arg.name === "booleanArg")!; const booleanArgType = booleanArg.type as IntrospectionNonNullTypeRef; const booleanArgInnerType = booleanArgType.ofType as IntrospectionNamedTypeRef; expect(booleanArg.name).toEqual("booleanArg"); expect(booleanArgType.kind).toEqual(TypeKind.NON_NULL); expect(booleanArgInnerType.kind).toEqual(TypeKind.SCALAR); expect(booleanArgInnerType.name).toEqual("Boolean"); }); it("should generate normal number arg type for object field method", async () => { const numberArg = argMethodField.args.find(arg => arg.name === "numberArg")!; const numberArgType = numberArg.type as IntrospectionNonNullTypeRef; const numberArgInnerType = numberArgType.ofType as IntrospectionNamedTypeRef; expect(numberArg.name).toEqual("numberArg"); expect(numberArgType.kind).toEqual(TypeKind.NON_NULL); expect(numberArgInnerType.kind).toEqual(TypeKind.SCALAR); expect(numberArgInnerType.name).toEqual("Float"); }); it("should generate nullable string arg type for object field method when explicitly sets", async () => { const explicitNullableArg = argMethodField.args.find( arg => arg.name === "explicitNullableArg", )!; const explicitNullableArgType = explicitNullableArg.type as IntrospectionNamedTypeRef; expect(explicitNullableArg.name).toEqual("explicitNullableArg"); expect(explicitNullableArgType.kind).toEqual(TypeKind.SCALAR); expect(explicitNullableArgType.name).toEqual("String"); }); it("should generate string array arg type for object field method when explicitly sets", async () => { const explicitArrayArg = argMethodField.args.find(arg => arg.name === "explicitArrayArg")!; const explicitArrayArgType = explicitArrayArg.type as IntrospectionNonNullTypeRef; const explicitArrayArgArrayType = explicitArrayArgType.ofType as IntrospectionListTypeRef; const explicitArrayArgInnerType = explicitArrayArgArrayType.ofType as IntrospectionNonNullTypeRef; const explicitArrayArgArrayItemType = explicitArrayArgInnerType.ofType as IntrospectionNamedTypeRef; expect(explicitArrayArg.name).toEqual("explicitArrayArg"); expect(explicitArrayArgType.kind).toEqual(TypeKind.NON_NULL); expect(explicitArrayArgArrayType.kind).toEqual(TypeKind.LIST); expect(explicitArrayArgInnerType.kind).toEqual(TypeKind.NON_NULL); expect(explicitArrayArgArrayItemType.kind).toEqual(TypeKind.SCALAR); expect(explicitArrayArgArrayItemType.name).toEqual("String"); }); it("should generate nullable string arg type for object field method", async () => { const nullableStringArg = argMethodField.args.find( arg => arg.name === "nullableStringArg", )!; const nullableStringArgType = nullableStringArg.type as IntrospectionNamedTypeRef; expect(nullableStringArg.name).toEqual("nullableStringArg"); expect(nullableStringArgType.kind).toEqual(TypeKind.SCALAR); expect(nullableStringArgType.name).toEqual("String"); }); it("should generate input object arg type for object field method", async () => { const inputArg = argMethodField.args.find(arg => arg.name === "inputArg")!; const inputArgType = inputArg.type as IntrospectionNonNullTypeRef; const inputArgInnerType = inputArgType.ofType as IntrospectionNamedTypeRef; expect(inputArg.name).toEqual("inputArg"); expect(inputArgType.kind).toEqual(TypeKind.NON_NULL); expect(inputArgInnerType.kind).toEqual(TypeKind.INPUT_OBJECT); expect(inputArgInnerType.name).toEqual("SampleInput"); }); it("should generate non-nullable string arg type with defaultValue for object field method", async () => { const inputArg = argMethodField.args.find(arg => arg.name === "defaultStringArg")!; const defaultValueStringArgType = inputArg.type as IntrospectionNamedTypeRef; expect(inputArg.defaultValue).toBe('"defaultStringArgDefaultValue"'); expect(defaultValueStringArgType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); }); describe("Input object", () => { let sampleInputType: IntrospectionInputObjectType; let sampleInputChildType: IntrospectionInputObjectType; beforeAll(() => { sampleInputType = schemaIntrospection.types.find( field => field.name === "SampleInput", )! as IntrospectionInputObjectType; sampleInputChildType = schemaIntrospection.types.find( field => field.name === "SampleInputChild", )! as IntrospectionInputObjectType; }); it("should generate non-nullable string arg type with defaultValue for input object field", async () => { const defaultValueStringField = sampleInputType.inputFields.find( arg => arg.name === "defaultStringField", )!; const defaultValueStringFieldType = defaultValueStringField.type as IntrospectionNamedTypeRef; expect(defaultValueStringField.defaultValue).toBe('"defaultStringFieldDefaultValue"'); expect(defaultValueStringFieldType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should generate non-nullable string arg type with implicit defaultValue for input object field", async () => { const implicitDefaultValueStringField = sampleInputType.inputFields.find( arg => arg.name === "implicitDefaultStringField", )!; const implicitDefaultValueStringFieldType = implicitDefaultValueStringField.type as IntrospectionNamedTypeRef; expect(implicitDefaultValueStringField.defaultValue).toBe( '"implicitDefaultStringFieldDefaultValue"', ); expect(implicitDefaultValueStringFieldType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should overwrite defaultValue in child input object", async () => { const defaultValueStringField = sampleInputChildType.inputFields.find( arg => arg.name === "defaultStringField", )!; const defaultValueStringFieldType = defaultValueStringField.type as IntrospectionNamedTypeRef; expect(defaultValueStringField.defaultValue).toBe('"defaultValueOverwritten"'); expect(defaultValueStringFieldType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should overwrite implicit defaultValue in child input object", async () => { const implicitDefaultValueStringField = sampleInputChildType.inputFields.find( arg => arg.name === "implicitDefaultStringField", )!; const implicitDefaultValueStringFieldType = implicitDefaultValueStringField.type as IntrospectionNamedTypeRef; expect(implicitDefaultValueStringField.defaultValue).toBe( '"implicitDefaultValueOverwritten"', ); expect(implicitDefaultValueStringFieldType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should inherit field with defaultValue from parent", async () => { const inheritDefaultField = sampleInputChildType.inputFields.find( arg => arg.name === "inheritDefaultField", )!; const inheritDefaultFieldType = inheritDefaultField.type as IntrospectionNamedTypeRef; expect(inheritDefaultField.defaultValue).toBe('"inheritDefaultFieldValue"'); expect(inheritDefaultFieldType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); }); describe("Args object", () => { it("should generate simple arg from args object field", async () => { const argsQuery = getQuery("argsQuery"); const stringArg = argsQuery.args.find(arg => arg.name === "stringArg")!; const stringArgInnerType = (stringArg.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionNamedTypeRef; expect(stringArg.name).toEqual("stringArg"); expect(stringArgInnerType.kind).toEqual(TypeKind.SCALAR); expect(stringArgInnerType.name).toEqual("String"); }); it("should generate non-nullable type arg with defaultValue from args object field", async () => { const argsQuery = getQuery("argsQuery"); const defaultStringArg = argsQuery.args.find(arg => arg.name === "defaultStringArg")!; const defaultStringArgType = defaultStringArg.type as IntrospectionNamedTypeRef; expect(defaultStringArg.name).toEqual("defaultStringArg"); expect(defaultStringArg.defaultValue).toEqual('"defaultStringArgDefaultValue"'); expect(defaultStringArgType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should overwrite defaultValue in child args object field", async () => { const argsQuery = getQuery("argsInheritanceQuery"); const defaultStringArg = argsQuery.args.find(arg => arg.name === "defaultStringArg")!; const defaultStringArgType = defaultStringArg.type as IntrospectionNamedTypeRef; expect(defaultStringArg.name).toEqual("defaultStringArg"); expect(defaultStringArg.defaultValue).toEqual('"defaultValueOverwritten"'); expect(defaultStringArgType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should overwrite implicit defaultValue in child args object field", async () => { const argsQuery = getQuery("argsInheritanceQuery"); const implicitDefaultStringArg = argsQuery.args.find( arg => arg.name === "implicitDefaultStringArg", )!; const implicitDefaultStringArgType = implicitDefaultStringArg.type as IntrospectionNamedTypeRef; expect(implicitDefaultStringArg.name).toEqual("implicitDefaultStringArg"); expect(implicitDefaultStringArg.defaultValue).toEqual('"implicitDefaultValueOverwritten"'); expect(implicitDefaultStringArgType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should inherit defaultValue field from parent args object field", async () => { const argsQuery = getQuery("argsInheritanceQuery"); const inheritDefaultArg = argsQuery.args.find(arg => arg.name === "inheritDefaultArg")!; const inheritDefaultArgType = inheritDefaultArg.type as IntrospectionNamedTypeRef; expect(inheritDefaultArg.name).toEqual("inheritDefaultArg"); expect(inheritDefaultArg.defaultValue).toEqual('"inheritDefaultArgValue"'); expect(inheritDefaultArgType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should generate non-nullable type arg with implicit defaultValue from args object field", async () => { const argsQuery = getQuery("argsQuery"); const implicitDefaultStringArg = argsQuery.args.find( arg => arg.name === "implicitDefaultStringArg", )!; const implicitDefaultStringArgType = implicitDefaultStringArg.type as IntrospectionNamedTypeRef; expect(implicitDefaultStringArg.name).toEqual("implicitDefaultStringArg"); expect(implicitDefaultStringArg.defaultValue).toEqual( '"implicitDefaultStringArgDefaultValue"', ); expect(implicitDefaultStringArgType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); }); it("should generate nullable type arg from args object field", async () => { const argsQuery = getQuery("argsQuery"); const numberArg = argsQuery.args.find(arg => arg.name === "numberArg")!; const numberArgType = numberArg.type as IntrospectionNamedTypeRef; expect(numberArg.name).toEqual("numberArg"); expect(numberArgType.kind).toEqual(TypeKind.SCALAR); expect(numberArgType.name).toEqual("Int"); }); it("should generate input object type arg from args object field", async () => { const argsQuery = getQuery("argsQuery"); const inputObjectArg = argsQuery.args.find(arg => arg.name === "inputObjectArg")!; const inputObjectArgInnerType = (inputObjectArg.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionNamedTypeRef; expect(inputObjectArg.name).toEqual("inputObjectArg"); expect(inputObjectArgInnerType.kind).toEqual(TypeKind.INPUT_OBJECT); expect(inputObjectArgInnerType.name).toEqual("SampleInput"); }); it("should generate field args from field resolver args definition", async () => { const resolverFieldWithArgs = sampleObjectType.fields.find( field => field.name === "resolverFieldWithArgs", )!; const fieldResolverArgs = resolverFieldWithArgs.args; expect(fieldResolverArgs).toHaveLength(2); const arg1 = fieldResolverArgs.find(arg => arg.name === "arg1")!; const arg1InnerType = (arg1.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionNamedTypeRef; const arg2 = fieldResolverArgs.find(arg => arg.name === "arg2")!; const arg2InnerType = (arg2.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionNamedTypeRef; expect(arg1InnerType.kind).toEqual(TypeKind.SCALAR); expect(arg1InnerType.name).toEqual("String"); expect(arg2InnerType.kind).toEqual(TypeKind.SCALAR); expect(arg2InnerType.name).toEqual("Boolean"); }); it("should generate object field type from independent field resolver", async () => { const independentFieldResolver = sampleObjectType.fields.find( field => field.name === "independentFieldResolver", )!; const fieldResolverArgs = independentFieldResolver.args; const arg1Type = getInnerTypeOfNonNullableType( fieldResolverArgs.find(arg => arg.name === "arg1")!, ); const arg2Type = getInnerTypeOfNonNullableType( fieldResolverArgs.find(arg => arg.name === "arg2")!, ); const independentFieldResolverType = independentFieldResolver.type as IntrospectionNamedTypeRef; expect(independentFieldResolver.description).toEqual("independent"); expect(independentFieldResolverType.kind).toEqual("SCALAR"); expect(independentFieldResolverType.name).toEqual("String"); expect(fieldResolverArgs).toHaveLength(2); expect(arg1Type.kind).toEqual(TypeKind.SCALAR); expect(arg1Type.name).toEqual("String"); expect(arg2Type.kind).toEqual(TypeKind.SCALAR); expect(arg2Type.name).toEqual("Boolean"); }); it("should overwrite object field name from field resolver decorator option", async () => { const overwrittenField = sampleObjectType.fields.find( field => field.name === "overwrittenField", )!; const overwrittenFieldResolver = sampleObjectType.fields.find( field => field.name === "overwrittenFieldResolver", )!; const independentFieldResolverType = overwrittenField.type as IntrospectionNamedTypeRef; expect(overwrittenFieldResolver).toBeUndefined(); expect(independentFieldResolverType.kind).toEqual("SCALAR"); expect(independentFieldResolverType.name).toEqual("String"); }); }); describe("Handlers", () => { it("should generate proper definition for query method", async () => { const emptyQuery = getQuery("emptyQuery"); const emptyQueryReturnType = emptyQuery.type as IntrospectionNonNullTypeRef; const emptyQueryInnerReturnType = emptyQueryReturnType.ofType as IntrospectionNamedTypeRef; expect(emptyQuery.args).toHaveLength(0); expect(emptyQuery.name).toEqual("emptyQuery"); expect(emptyQueryReturnType.kind).toEqual(TypeKind.NON_NULL); expect(emptyQueryInnerReturnType.kind).toEqual(TypeKind.SCALAR); expect(emptyQueryInnerReturnType.name).toEqual("Boolean"); }); it("should generate proper definition for mutation method", async () => { const emptyMutation = getMutation("emptyMutation"); const emptyMutationReturnType = emptyMutation.type as IntrospectionNonNullTypeRef; const emptyMutationInnerReturnType = emptyMutationReturnType.ofType as IntrospectionNamedTypeRef; expect(emptyMutation.args).toHaveLength(0); expect(emptyMutation.name).toEqual("emptyMutation"); expect(emptyMutationReturnType.kind).toEqual(TypeKind.NON_NULL); expect(emptyMutationInnerReturnType.kind).toEqual(TypeKind.SCALAR); expect(emptyMutationInnerReturnType.name).toEqual("Boolean"); }); it("should generate implicit string return type for query method", async () => { const implicitStringQuery = getQuery("implicitStringQuery"); const implicitStringQueryType = implicitStringQuery.type as IntrospectionNonNullTypeRef; const implicitStringQueryInnerType = implicitStringQueryType.ofType as IntrospectionNamedTypeRef; expect(implicitStringQueryInnerType.kind).toEqual(TypeKind.SCALAR); expect(implicitStringQueryInnerType.name).toEqual("String"); }); it("should generate string return type for query when explicitly set", async () => { const explicitStringQuery = getQuery("explicitStringQuery"); const explicitStringQueryType = explicitStringQuery.type as IntrospectionNonNullTypeRef; const explicitStringQueryInnerType = explicitStringQueryType.ofType as IntrospectionNamedTypeRef; expect(explicitStringQueryInnerType.kind).toEqual(TypeKind.SCALAR); expect(explicitStringQueryInnerType.name).toEqual("String"); }); it("should generate nullable string return type for query when explicitly set", async () => { const nullableStringQuery = getQuery("nullableStringQuery"); const nullableStringQueryType = nullableStringQuery.type as IntrospectionNamedTypeRef; expect(nullableStringQueryType.kind).toEqual(TypeKind.SCALAR); expect(nullableStringQueryType.name).toEqual("String"); }); it("should generate explicit array string return type for query", async () => { const explicitStringArrayQuery = getQuery("explicitStringArrayQuery"); const type = explicitStringArrayQuery.type as IntrospectionNonNullTypeRef; const listType = type.ofType as IntrospectionListTypeRef; const nonNullItemType = listType.ofType as IntrospectionNonNullTypeRef; const itemType = nonNullItemType.ofType as IntrospectionNamedTypeRef; expect(listType.kind).toEqual(TypeKind.LIST); expect(itemType.kind).toEqual(TypeKind.SCALAR); expect(itemType.name).toEqual("String"); }); it("should generate explicit array of nullable string return type for query", async () => { const explicitNullableItemArrayQuery = getQuery("explicitNullableItemArrayQuery"); const type = explicitNullableItemArrayQuery.type as IntrospectionNonNullTypeRef; const listType = type.ofType as IntrospectionListTypeRef; const itemType = listType.ofType as IntrospectionNamedTypeRef; expect(type.kind).toEqual(TypeKind.NON_NULL); expect(listType.kind).toEqual(TypeKind.LIST); expect(itemType.kind).toEqual(TypeKind.SCALAR); expect(itemType.name).toEqual("String"); }); it("should generate explicit nullable array of nullable string return type for query", async () => { const explicitNullableArrayWithNullableItemsQuery = getQuery( "explicitNullableArrayWithNullableItemsQuery", ); const listType = explicitNullableArrayWithNullableItemsQuery.type as IntrospectionListTypeRef; const itemType = listType.ofType as IntrospectionNamedTypeRef; expect(listType.kind).toEqual(TypeKind.LIST); expect(itemType.kind).toEqual(TypeKind.SCALAR); expect(itemType.name).toEqual("String"); }); it("should generate string return type for query returning Promise", async () => { const promiseStringQuery = getQuery("promiseStringQuery"); const promiseStringQueryType = promiseStringQuery.type as IntrospectionNonNullTypeRef; const promiseStringQueryInnerType = promiseStringQueryType.ofType as IntrospectionNamedTypeRef; expect(promiseStringQueryInnerType.kind).toEqual(TypeKind.SCALAR); expect(promiseStringQueryInnerType.name).toEqual("String"); }); it("should generate object return type for query returning promise", async () => { const asyncObjectQuery = getQuery("asyncObjectQuery"); const asyncObjectQueryType = asyncObjectQuery.type as IntrospectionNonNullTypeRef; const asyncObjectQueryInnerType = asyncObjectQueryType.ofType as IntrospectionNamedTypeRef; expect(asyncObjectQueryInnerType.kind).toEqual(TypeKind.OBJECT); expect(asyncObjectQueryInnerType.name).toEqual("SampleObject"); }); it("should generate object return type for query method", async () => { const implicitObjectQuery = getQuery("implicitObjectQuery"); const implicitObjectQueryType = implicitObjectQuery.type as IntrospectionNonNullTypeRef; const implicitObjectQueryInnerType = implicitObjectQueryType.ofType as IntrospectionNamedTypeRef; expect(implicitObjectQueryInnerType.kind).toEqual(TypeKind.OBJECT); expect(implicitObjectQueryInnerType.name).toEqual("SampleObject"); }); it("should not generate args type for query using @Root and @Ctx decorators", async () => { const rootCtxQuery = getQuery("rootCtxQuery"); expect(rootCtxQuery.args).toHaveLength(0); }); it("should generate proper definition for query with @Arg", async () => { const argQuery = getQuery("argQuery"); expect(argQuery.name).toEqual("argQuery"); expect(argQuery.args).toHaveLength(2); }); it("should generate proper definition for query with @Args", async () => { const argsQuery = getQuery("argsQuery"); expect(argsQuery.name).toEqual("argsQuery"); expect(argsQuery.args).toHaveLength(6); }); it("should generate proper definition for query with both @Arg and @Args", async () => { const argAndArgsQuery = getQuery("argAndArgsQuery"); expect(argAndArgsQuery.name).toEqual("argAndArgsQuery"); expect(argAndArgsQuery.args).toHaveLength(7); }); }); describe("Errors", () => { beforeEach(() => { getMetadataStorage().clear(); }); it("should throw error when arg type is not correct", async () => { const error = await expectToThrow(() => { @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleResolverWithError { @Query(() => String) sampleQuery(@Arg("arg") _arg: any): string { return "sampleQuery"; } } }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(NoExplicitTypeError); expect(error.message).toMatchInlineSnapshot( `"Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for argument named 'arg' of 'sampleQuery' of 'SampleResolverWithError' class."`, ); }); it("should throw error when query return type not provided", async () => { const error = await expectToThrow(() => { @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleResolverWithError { @Query() sampleQuery() { return "sampleQuery"; } } }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(NoExplicitTypeError); expect(error.message).toMatchInlineSnapshot( `"Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for 'sampleQuery' of 'SampleResolverWithError' class."`, ); }); it("should throw error when provided query return type is not correct", async () => { const error = await expectToThrow(() => { @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleResolverWithError { @Query() sampleQuery(): any { return "sampleQuery"; } } }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(NoExplicitTypeError); expect(error.message).toMatchInlineSnapshot( `"Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for 'sampleQuery' of 'SampleResolverWithError' class."`, ); }); it("should throw error when mutation return type not provided", async () => { const error = await expectToThrow(() => { @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleResolverWithError { @Mutation() sampleMutation() { return "sampleMutation"; } } }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(NoExplicitTypeError); expect(error.message).toMatchInlineSnapshot( `"Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for 'sampleMutation' of 'SampleResolverWithError' class."`, ); }); it("should throw error provided mutation return type is not correct", async () => { const error = await expectToThrow(() => { @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleResolverWithError { @Mutation() sampleMutation(): any { return "sampleMutation"; } } }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(NoExplicitTypeError); expect(error.message).toMatchInlineSnapshot( `"Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for 'sampleMutation' of 'SampleResolverWithError' class."`, ); }); it("should throw error when creating field resolver in resolver with no object type info", async () => { const error = await expectToThrow(async () => { @Resolver() class SampleResolverWithError { @Query() sampleQuery(): string { return "sampleQuery"; } @FieldResolver() sampleField() { return "sampleField"; } } await buildSchema({ resolvers: [SampleResolverWithError], }); }); expect(error).toBeInstanceOf(Error); expect(error.message).toMatchInlineSnapshot( `"No provided object type in '@Resolver' decorator for class 'SampleResolverWithError!'"`, ); }); it("should throw error when creating independent field resolver with no type info", async () => { const error = await expectToThrow(async () => { @ObjectType() class SampleObjectWithError { @Field() sampleField!: string; } @Resolver(() => SampleObjectWithError) class SampleResolverWithError { @Query() sampleQuery(): string { return "sampleQuery"; } @FieldResolver() independentField() { return "independentField"; } } await buildSchema({ resolvers: [SampleResolverWithError], }); }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(NoExplicitTypeError); expect(error.message).toMatchInlineSnapshot( `"Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for 'independentField' of 'SampleResolverWithError' class."`, ); }); it("should throw error when using undecorated class as an explicit type", async () => { const error = await expectToThrow(async () => { class SampleUndecoratedObject { sampleField!: string; } @Resolver() class SampleResolverWithError { @Query(() => SampleUndecoratedObject) sampleQuery(): string { return "sampleQuery"; } } await buildSchema({ resolvers: [SampleResolverWithError], }); }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(CannotDetermineGraphQLTypeError); expect(error.message).toMatchInlineSnapshot( `"Cannot determine GraphQL output type for 'sampleQuery' of 'SampleResolverWithError' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper output value?"`, ); }); it("should throw error when using object type class is used as explicit type in place of input type", async () => { const error = await expectToThrow(async () => { @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolverWithError { @Query() sampleQuery(@Arg("input") _input: SampleObject): string { return "sampleQuery"; } } await buildSchema({ resolvers: [SampleResolverWithError], }); }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(CannotDetermineGraphQLTypeError); expect(error.message).toMatchInlineSnapshot( `"Cannot determine GraphQL input type for argument named 'input' of 'sampleQuery' of 'SampleResolverWithError' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper input value?"`, ); }); it("should throw error when using object type class is used as explicit type in place of args type", async () => { const error = await expectToThrow(async () => { @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolverWithError { @Query() sampleQuery(@Args() _args: SampleObject): string { return "sampleQuery"; } } await buildSchema({ resolvers: [SampleResolverWithError], }); }); expect(error).toBeInstanceOf(Error); expect(error.message).toMatchInlineSnapshot( `"The value used as a type of '@Args' for 'sampleQuery' of 'SampleResolverWithError' is not a class decorated with '@ArgsType' decorator!"`, ); }); it("should throw error when declared default values are not equal", async () => { const error = await expectToThrow(async () => { @InputType() class SampleInput { @Field({ defaultValue: "decoratorDefaultValue" }) inputField: string = "initializerDefaultValue"; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): string { return "sampleQuery"; } } await buildSchema({ resolvers: [SampleResolver] }); }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(ConflictingDefaultValuesError); expect(error.message).toMatchInlineSnapshot( `"The 'inputField' field of 'SampleInput' has conflicting default values. Default value from decorator ('decoratorDefaultValue') is not equal to the property initializer value ('initializerDefaultValue')."`, ); }); it("should throw error when list nullable option is combined with non-list type", async () => { const error = await expectToThrow(async () => { @InputType() class SampleInput { @Field({ nullable: "items" }) inputField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") _input: SampleInput): string { return "sampleQuery"; } } const resolvers = [SampleResolver] as const; await buildSchema({ resolvers }); }); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(WrongNullableListOptionError); expect(error.message).toMatchInlineSnapshot( `"Wrong nullable option set for SampleInput#inputField. You cannot combine non-list type with nullable 'items'."`, ); }); }); }); describe("Functional", () => { const classes: any = {}; let schema: GraphQLSchema; let queryRoot: any; let queryContext: any; let queryInfo: any; let queryFirstCustom: any; let querySecondCustom: any; let queryThirdCustom: any; let descriptorEvaluated: boolean; let sampleObjectConstructorCallCount: number; function DescriptorDecorator(): MethodDecorator { return (_, __, descriptor: any) => { const originalMethod: Function = descriptor.value; // eslint-disable-next-line no-param-reassign descriptor.value = (...args: unknown[]) => { descriptorEvaluated = true; return originalMethod.apply(null, ...args); }; }; } let mutationInputValue: any; beforeEach(() => { queryRoot = undefined; queryContext = undefined; queryInfo = undefined; queryFirstCustom = undefined; querySecondCustom = undefined; queryThirdCustom = undefined; descriptorEvaluated = false; sampleObjectConstructorCallCount = 0; mutationInputValue = undefined; }); beforeAll(async () => { getMetadataStorage().clear(); const FirstCustomArgDecorator = () => createParameterDecorator(resolverData => resolverData); const SecondCustomArgDecorator = (arg: string) => createParameterDecorator(async () => arg); const DefaultValueArgDecorator = ( argName: string, typeFunc: ReturnTypeFunc, defaultValue: any, ) => createParameterDecorator(resolverData => resolverData.args[argName] ?? defaultValue, { arg: { name: argName, typeFunc, options: { nullable: true }, }, }); @ArgsType() class SampleArgs { private readonly TRUE = true; instanceField = Math.random(); @Field() factor!: number; isTrue() { return this.TRUE; } } classes.SampleArgs = SampleArgs; @ArgsType() class SampleOptionalArgs { @Field() stringField!: string; @Field({ nullable: true }) optionalField?: string; } classes.SampleOptionalArgs = SampleOptionalArgs; @InputType() class SampleInput { private readonly TRUE = true; instanceField = Math.random(); @Field() factor!: number; isTrue() { return this.TRUE; } } classes.SampleInput = SampleInput; @InputType() class SampleNestedInput { instanceField = Math.random(); @Field() nestedField!: SampleInput; @Field({ nullable: true }) optionalNestedField?: SampleInput; @Field(() => [SampleInput]) nestedArrayField!: SampleInput[]; @Field(() => [SampleInput], { nullable: "itemsAndList" }) nestedOptionalArrayField?: Array; } classes.SampleNestedInput = SampleNestedInput; @InputType() class SampleTripleNestedInput { instanceField = Math.random(); @Field(() => [[[SampleInput]]]) deeplyNestedInputArrayField!: SampleInput[][][]; } classes.SampleTripleNestedInput = SampleTripleNestedInput; @ArgsType() class SampleNestedArgs { @Field() factor!: number; @Field() input!: SampleInput; } classes.SampleNestedArgs = SampleNestedArgs; @ObjectType() class SampleObject { private readonly TRUE = true; isTrue() { return this.TRUE; } constructor() { sampleObjectConstructorCallCount += 1; } instanceValue = Math.random(); @Field() fieldResolverField!: number; @Field() fieldResolverGetter!: number; @Field({ complexity: 5 }) fieldResolverMethod!: number; @Field() fieldResolverMethodWithArgs!: number; @Field() fieldResolverWithRoot!: number; @Field({ complexity: 10 }) complexResolverMethod!: number; @Field() get getterField(): number { return this.instanceValue; } @Field() methodField(): number { return this.instanceValue; } @Field(() => String) async asyncMethodField() { return "asyncMethodField"; } @Field() methodFieldWithArg(@Arg("factor") factor: number): number { return this.instanceValue * factor; } } @Resolver(() => SampleObject) class SampleResolver implements ResolverInterface { factor = 1; randomValueField = Math.random() * this.factor; get randomValueGetter() { return Math.random() * this.factor; } getRandomValue() { return Math.random() * this.factor; } @Query() sampleQuery(): SampleObject { const obj = new SampleObject(); return obj; } @Query() notInstanceQuery(): SampleObject { return {} as SampleObject; } @Query() queryWithRootContextAndInfo( @Root() root: any, @Ctx() context: any, @Info() info: any, ): boolean { queryRoot = root; queryContext = context; queryInfo = info; return true; } @Query() queryWithPartialRootAndContext( @Root("rootField") rootField: any, @Ctx("contextField") contextField: any, ): boolean { queryRoot = rootField; queryContext = contextField; return true; } @Query() queryWithCustomDecorators( @FirstCustomArgDecorator() firstCustom: any, @SecondCustomArgDecorator("secondCustom") secondCustom: any, @DefaultValueArgDecorator("thirdCustom", () => String, "Default") thirdCustom: string, ): boolean { queryFirstCustom = firstCustom; querySecondCustom = secondCustom; queryThirdCustom = thirdCustom; return true; } @Query() @DescriptorDecorator() queryWithCustomDescriptorDecorator(): boolean { return true; } @Mutation() mutationWithArgs(@Args() args: SampleArgs): number { if (args.isTrue()) { return args.factor * args.instanceField; } return -1.0; } @Mutation() mutationWithOptionalArgs(@Args() args: SampleOptionalArgs): boolean { mutationInputValue = args; return true; } @Mutation() mutationWithInput(@Arg("input") input: SampleInput): number { if (input.isTrue()) { return input.factor * input.instanceField; } return -1.0; } @Mutation() mutationWithNestedInputs(@Arg("input") input: SampleNestedInput): number { mutationInputValue = input; return input.instanceField; } @Mutation() mutationWithTripleNestedInputs(@Arg("input") input: SampleTripleNestedInput): number { mutationInputValue = input; return input.deeplyNestedInputArrayField[0][0][0].factor; } @Mutation() mutationWithNestedArgsInput(@Args() { factor, input }: SampleNestedArgs): number { mutationInputValue = input; return factor; } @Mutation() mutationWithInputs(@Arg("inputs", () => [SampleInput]) inputs: SampleInput[]): number { // eslint-disable-next-line prefer-destructuring mutationInputValue = inputs[0]; return inputs[0].factor; } @Mutation() mutationWithTripleArrayInputs( @Arg("inputs", () => [[[SampleInput]]]) inputs: SampleInput[][][], ): number { mutationInputValue = inputs; return inputs[0][0][0].factor; } @Mutation() mutationWithOptionalArg( @Arg("input", { nullable: true }) input?: SampleNestedInput, ): number { mutationInputValue = typeof input; return 0; } @FieldResolver() fieldResolverField() { return this.randomValueField; } @FieldResolver() fieldResolverGetter() { return this.randomValueGetter; } @FieldResolver({ complexity: 10 }) fieldResolverMethod() { return this.getRandomValue(); } @FieldResolver() fieldResolverWithRoot(@Root() root: SampleObject) { if (root.isTrue()) { return root.instanceValue; } return -1.0; } @FieldResolver() fieldResolverMethodWithArgs(@Root() _: SampleObject, @Arg("arg") arg: number): number { return arg; } } schema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); }); it("should build the schema without errors", () => { expect(schema).toBeDefined(); }); it("should return value from object getter resolver", async () => { const query = `query { sampleQuery { getterField } }`; const result: any = await graphql({ schema, source: query }); const getterFieldResult = result.data!.sampleQuery.getterField; expect(getterFieldResult).toBeGreaterThanOrEqual(0); expect(getterFieldResult).toBeLessThanOrEqual(1); }); it("should return value from object method resolver", async () => { const query = `query { sampleQuery { methodField } }`; const result: any = await graphql({ schema, source: query }); const methodFieldResult = result.data!.sampleQuery.methodField; expect(methodFieldResult).toBeGreaterThanOrEqual(0); expect(methodFieldResult).toBeLessThanOrEqual(1); }); it("should return value from object async method resolver", async () => { const query = `query { sampleQuery { asyncMethodField } }`; const result: any = await graphql({ schema, source: query }); const asyncMethodFieldResult = result.data!.sampleQuery.asyncMethodField; expect(asyncMethodFieldResult).toEqual("asyncMethodField"); }); it("should return value from object method resolver with arg", async () => { const query = `query { sampleQuery { methodFieldWithArg(factor: 10) } }`; const result: any = await graphql({ schema, source: query }); const methodFieldWithArgResult = result.data!.sampleQuery.methodFieldWithArg; expect(methodFieldWithArgResult).toBeGreaterThanOrEqual(0); expect(methodFieldWithArgResult).toBeLessThanOrEqual(10); }); it("should return value from field resolver with field access", async () => { const query = `query { sampleQuery { fieldResolverField } }`; const result: any = await graphql({ schema, source: query }); const fieldResolverFieldResult = result.data!.sampleQuery.fieldResolverField; expect(fieldResolverFieldResult).toBeGreaterThanOrEqual(0); expect(fieldResolverFieldResult).toBeLessThanOrEqual(1); }); it("should return value from field resolver with getter access", async () => { const query = `query { sampleQuery { fieldResolverGetter } }`; const result: any = await graphql({ schema, source: query }); const fieldResolverGetterResult = result.data!.sampleQuery.fieldResolverGetter; expect(fieldResolverGetterResult).toBeGreaterThanOrEqual(0); expect(fieldResolverGetterResult).toBeLessThanOrEqual(1); }); it("should return value from field resolver with method access", async () => { const query = `query { sampleQuery { fieldResolverMethod } }`; const result: any = await graphql({ schema, source: query }); const fieldResolverMethodResult = result.data!.sampleQuery.fieldResolverMethod; expect(fieldResolverMethodResult).toBeGreaterThanOrEqual(0); expect(fieldResolverMethodResult).toBeLessThanOrEqual(1); }); it("should return value from field resolver arg", async () => { const value = 21.37; const query = `query { sampleQuery { fieldResolverMethodWithArgs(arg: ${value}) } }`; const result: any = await graphql({ schema, source: query }); const resultFieldData = result.data!.sampleQuery.fieldResolverMethodWithArgs; expect(resultFieldData).toEqual(value); }); it("should create new instances of object type for consecutive queries", async () => { const query = `query { sampleQuery { getterField } }`; const result1: any = await graphql({ schema, source: query }); const result2: any = await graphql({ schema, source: query }); const getterFieldResult1 = result1.data!.sampleQuery.getterField; const getterFieldResult2 = result2.data!.sampleQuery.getterField; expect(getterFieldResult1).not.toEqual(getterFieldResult2); }); it("shouldn't create new instance for object type if it's already an instance of its class", async () => { const query = /* graphql */ ` query { sampleQuery { getterField methodField } } `; const result: any = await graphql({ schema, source: query }); const getterFieldValue = result.data!.sampleQuery.getterField; const methodFieldValue = result.data!.sampleQuery.getterField; expect(getterFieldValue).toEqual(methodFieldValue); expect(sampleObjectConstructorCallCount).toBe(1); }); it("should use the same instance of resolver class for consecutive queries", async () => { const query = `query { sampleQuery { fieldResolverField } }`; const result1: any = await graphql({ schema, source: query }); const result2: any = await graphql({ schema, source: query }); const resolverFieldResult1 = result1.data!.sampleQuery.fieldResolverField; const resolverFieldResult2 = result2.data!.sampleQuery.fieldResolverField; expect(resolverFieldResult1).toEqual(resolverFieldResult2); }); it("should create instance of args object", async () => { const mutation = `mutation { mutationWithArgs(factor: 10) }`; const mutationResult = await graphql({ schema, source: mutation }); const result = mutationResult.data!.mutationWithArgs; expect(result).toBeGreaterThanOrEqual(0); expect(result).toBeLessThanOrEqual(10); }); it("shouldn't create properties of nullable args", async () => { const mutation = `mutation { mutationWithOptionalArgs(stringField: "stringField") }`; const { errors } = await graphql({ schema, source: mutation }); expect(errors).toBeUndefined(); expect(mutationInputValue).toBeInstanceOf(classes.SampleOptionalArgs); expect(mutationInputValue).not.toHaveProperty("optionalField"); }); it("should create instance of input object", async () => { const mutation = `mutation { mutationWithInput(input: { factor: 10 }) }`; const mutationResult = await graphql({ schema, source: mutation }); const result = mutationResult.data!.mutationWithInput; expect(result).toBeGreaterThanOrEqual(0); expect(result).toBeLessThanOrEqual(10); }); it("should create instances of nested input fields input objects without undefined", async () => { const mutation = `mutation { mutationWithNestedInputs(input: { nestedField: { factor: 20 } nestedArrayField: [{ factor: 30 }] }) }`; const mutationResult = await graphql({ schema, source: mutation }); const result = mutationResult.data!.mutationWithNestedInputs; expect(result).toBeGreaterThanOrEqual(0); expect(result).toBeLessThanOrEqual(1); expect(mutationInputValue).toBeInstanceOf(classes.SampleNestedInput); expect(mutationInputValue.nestedField).toBeInstanceOf(classes.SampleInput); expect(mutationInputValue.nestedArrayField[0]).toBeInstanceOf(classes.SampleInput); expect(mutationInputValue).not.toHaveProperty("optionalNestedField"); }); it("shouldn't create instances of nested input fields nullable input objects when null provided", async () => { const mutation = `mutation { mutationWithNestedInputs(input: { nestedField: { factor: 20 } nestedArrayField: [{ factor: 30 }] optionalNestedField: null nestedOptionalArrayField: [null, { factor: 40 }] }) }`; const mutationResult = await graphql({ schema, source: mutation }); expect(mutationResult.errors).toBeUndefined(); const mutationWithNestedInputsData = mutationResult.data!.mutationWithNestedInputs; expect(mutationWithNestedInputsData).toBeGreaterThanOrEqual(0); expect(mutationWithNestedInputsData).toBeLessThanOrEqual(1); expect(mutationInputValue).toBeInstanceOf(classes.SampleNestedInput); expect(mutationInputValue.nestedField).toBeInstanceOf(classes.SampleInput); expect(mutationInputValue.nestedArrayField[0]).toBeInstanceOf(classes.SampleInput); expect(mutationInputValue.optionalNestedField).toBeNull(); expect(mutationInputValue.nestedOptionalArrayField).toEqual([ null, expect.any(classes.SampleInput), ]); }); it("should create instance of nested input field of args type object", async () => { const mutation = `mutation { mutationWithNestedArgsInput(factor: 20, input: { factor: 30 }) }`; const mutationResult = await graphql({ schema, source: mutation }); const result = mutationResult.data!.mutationWithNestedArgsInput; expect(result).toEqual(20); expect(mutationInputValue).toBeInstanceOf(classes.SampleInput); expect(mutationInputValue.instanceField).toBeGreaterThanOrEqual(0); expect(mutationInputValue.instanceField).toBeLessThanOrEqual(1); }); it("should create instance of inputs array from arg", async () => { const mutation = `mutation { mutationWithInputs(inputs: [{ factor: 30 }]) }`; const mutationResult = await graphql({ schema, source: mutation }); const result = mutationResult.data!.mutationWithInputs; expect(result).toEqual(30); expect(mutationInputValue).toBeInstanceOf(classes.SampleInput); expect(mutationInputValue.instanceField).toBeGreaterThanOrEqual(0); expect(mutationInputValue.instanceField).toBeLessThanOrEqual(1); }); it("should create instance of nested arrays input from arg", async () => { const mutation = `mutation { mutationWithTripleArrayInputs(inputs: [[[{ factor: 30 }]]]) }`; const mutationResult = await graphql({ schema, source: mutation }); const result = mutationResult.data!.mutationWithTripleArrayInputs; const nestedInput = mutationInputValue[0][0][0]; expect(mutationResult.errors).toBeUndefined(); expect(result).toEqual(30); expect(mutationInputValue).toBeInstanceOf(Array); expect(mutationInputValue).toHaveLength(1); expect(mutationInputValue[0]).toBeInstanceOf(Array); expect(mutationInputValue[0]).toHaveLength(1); expect(mutationInputValue[0][0]).toBeInstanceOf(Array); expect(mutationInputValue[0][0]).toHaveLength(1); expect(nestedInput).toBeInstanceOf(classes.SampleInput); expect(nestedInput.instanceField).toBeGreaterThanOrEqual(0); expect(nestedInput.instanceField).toBeLessThanOrEqual(1); }); it("should create instance of nested arrays input field", async () => { const mutation = `mutation { mutationWithTripleNestedInputs(input: { deeplyNestedInputArrayField: [[[{ factor: 30 }]]] }) }`; const mutationResult = await graphql({ schema, source: mutation }); expect(mutationResult.errors).toBeUndefined(); const result = mutationResult.data!.mutationWithTripleNestedInputs; expect(result).toEqual(30); expect(mutationInputValue).toBeInstanceOf(classes.SampleTripleNestedInput); expect(mutationInputValue.deeplyNestedInputArrayField).toHaveLength(1); expect(mutationInputValue.deeplyNestedInputArrayField[0]).toBeInstanceOf(Array); expect(mutationInputValue.deeplyNestedInputArrayField[0]).toHaveLength(1); expect(mutationInputValue.deeplyNestedInputArrayField[0][0]).toBeInstanceOf(Array); expect(mutationInputValue.deeplyNestedInputArrayField[0][0]).toHaveLength(1); const nestedInput = mutationInputValue.deeplyNestedInputArrayField[0][0][0]; expect(nestedInput).toBeInstanceOf(classes.SampleInput); expect(nestedInput.instanceField).toBeGreaterThanOrEqual(0); expect(nestedInput.instanceField).toBeLessThanOrEqual(1); }); it("shouldn't create instance of an argument if the value is null or not provided", async () => { const mutation = `mutation { mutationWithOptionalArg }`; const { data, errors } = await graphql({ schema, source: mutation }); expect(errors).toBeUndefined(); expect(data!.mutationWithOptionalArg).toBeDefined(); expect(mutationInputValue).toEqual("undefined"); }); it("should create instance of root object when root type is provided", async () => { const query = `query { sampleQuery { fieldResolverWithRoot getterField } }`; const queryResult: any = await graphql({ schema, source: query }); const fieldResolverWithRootValue = queryResult.data!.sampleQuery.fieldResolverWithRoot; const getterFieldValue = queryResult.data!.sampleQuery.getterField; expect(fieldResolverWithRootValue).toBeGreaterThanOrEqual(0); expect(fieldResolverWithRootValue).toBeLessThanOrEqual(1); expect(fieldResolverWithRootValue).toEqual(getterFieldValue); }); it("should reuse data from instance of root object", async () => { const query = `query { notInstanceQuery { fieldResolverWithRoot getterField } }`; const queryResult: any = await graphql({ schema, source: query }); const fieldResolverWithRootValue = queryResult.data!.notInstanceQuery.fieldResolverWithRoot; const getterFieldValue = queryResult.data!.notInstanceQuery.getterField; expect(fieldResolverWithRootValue).toBeGreaterThanOrEqual(0); expect(fieldResolverWithRootValue).toBeLessThanOrEqual(1); expect(fieldResolverWithRootValue).not.toEqual(getterFieldValue); }); it("should inject root and context object to resolver", async () => { const query = `query { queryWithRootContextAndInfo }`; const root = { isRoot: true }; const context = { isContext: true }; await graphql({ schema, source: query, rootValue: root, contextValue: context }); expect(queryRoot).toEqual(root); expect(queryContext).toEqual(context); expect(queryInfo).toBeDefined(); expect(queryInfo.fieldName).toEqual("queryWithRootContextAndInfo"); }); it("should inject parts of root and context objects to resolver", async () => { const query = `query { queryWithPartialRootAndContext }`; const root = { rootField: 2 }; const context = { contextField: "present" }; await graphql({ schema, source: query, rootValue: root, contextValue: context }); expect(queryRoot).toEqual(2); expect(queryContext).toEqual("present"); }); it("should inject resolver data to custom arg decorator resolver and return its value", async () => { const query = /* graphql */ ` query { queryWithCustomDecorators } `; const root = { rootField: 2 }; const context = { contextField: "present" }; await graphql({ schema, source: query, rootValue: root, contextValue: context }); expect(queryFirstCustom.root).toEqual(root); expect(queryFirstCustom.context).toEqual(context); expect(queryFirstCustom.info).toBeDefined(); expect(querySecondCustom).toEqual("secondCustom"); expect(queryThirdCustom).toEqual("Default"); }); it("should allow custom arg decorators to create args", async () => { const query = /* graphql */ ` query { queryWithCustomDecorators(thirdCustom: "Overridden") } `; const root = { rootField: 2 }; const context = { contextField: "present" }; await graphql({ schema, source: query, rootValue: root, contextValue: context }); expect(queryFirstCustom.root).toEqual(root); expect(queryFirstCustom.context).toEqual(context); expect(queryFirstCustom.info).toBeDefined(); expect(querySecondCustom).toEqual("secondCustom"); expect(queryThirdCustom).toEqual("Overridden"); }); it("should allow for overwriting descriptor value in custom decorator", async () => { const query = /* graphql */ ` query { queryWithCustomDescriptorDecorator } `; const { data } = await graphql({ schema, source: query }); expect(descriptorEvaluated).toBe(true); expect(data!.queryWithCustomDescriptorDecorator).toBe(true); }); }); describe("buildSchema", () => { it("should emit only things from provided `resolvers` property", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return { sampleField: "sampleField" }; } } @ObjectType() class OmittedObject { @Field() omittedField!: string; } @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars class OmittedResolver { @Query() omittedQuery(): OmittedObject { return { omittedField: "omittedField" }; } } const { queryType, schemaIntrospection } = await getSchemaInfo({ resolvers: [SampleResolver], }); const objectTypes = schemaIntrospection.types.filter( type => type.kind === "OBJECT" && !type.name.startsWith("__") && !["Query", "Mutation", "Subscription"].includes(type.name), ); expect(queryType.fields).toHaveLength(1); expect(queryType.fields[0].name).toEqual("sampleQuery"); expect(objectTypes).toHaveLength(1); expect(objectTypes[0].name).toEqual("SampleObject"); }); it("should build the schema synchronously", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() sampleFieldSync!: string; } @Resolver() class SampleResolver { @Query() sampleQuerySync(): SampleObject { return { sampleFieldSync: "sampleFieldSync" }; } } const schema = buildSchemaSync({ resolvers: [SampleResolver], validate: false, }); const query = ` query { sampleQuerySync { sampleFieldSync } } `; const result: any = await graphql({ schema, source: query }); expect(result.data.sampleQuerySync.sampleFieldSync).toEqual("sampleFieldSync"); }); it("should generate the schema when schema is incorrect but `skipCheck` is set to true", async () => { getMetadataStorage().clear(); @Resolver() class SampleResolver { @Mutation() sampleMutation(): string { return "sampleMutation"; } } const schema = await buildSchema({ resolvers: [SampleResolver], skipCheck: true, }); expect(schema).toBeDefined(); }); it("should throw errors when no resolvers provided", async () => { getMetadataStorage().clear(); const error = await expectToThrow(() => buildSchema({ resolvers: [] as any })); expect(error.message).toContain("Empty"); expect(error.message).toContain("resolvers"); }); }); describe("Schemas leaks", () => { it("should not call field resolver if resolver class is not provided to `buildSchema`", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() sampleField!: string; @Field() resolvedField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return { sampleField: "sampleField", resolvedField: "resolvedField" }; } } @Resolver(() => SampleObject) // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleObjectResolver { @FieldResolver() resolvedField(): string { return "SampleObjectResolver resolvedField"; } } const query = /* graphql */ ` query { sampleQuery { sampleField resolvedField } } `; const schema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); const result: any = await graphql({ schema, source: query }); expect(result.errors).toBeUndefined(); expect(result.data!.sampleQuery).toEqual({ sampleField: "sampleField", resolvedField: "resolvedField", }); }); it("should not emit field in schema if resolver class is not provided to `buildSchema`", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return { sampleField: "sampleField" }; } } @Resolver(() => SampleObject) // eslint-disable-next-line @typescript-eslint/no-unused-vars class SampleObjectResolver { @FieldResolver() resolvedField(): string { return "SampleObjectResolver resolvedField"; } } const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); const { schemaIntrospection } = schemaInfo; const sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; expect(sampleObjectType.fields).toHaveLength(1); expect(sampleObjectType.fields[0].name).toEqual("sampleField"); }); it("should emit field in schema if resolver class is not provided to `buildSchema` but is in inheritance chain", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): SampleObject { return { sampleField: "sampleField" }; } } function createResolver() { @Resolver(() => SampleObject) class SampleObjectResolver { @FieldResolver() resolvedField(): string { return "SampleObjectResolver resolvedField"; } } return SampleObjectResolver; } class ChildResolver extends createResolver() {} const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver, ChildResolver], }); const { schemaIntrospection } = schemaInfo; const sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; expect(sampleObjectType.fields).toHaveLength(2); }); it("should not duplicate resolver params when buildSchema is called twice", async () => { getMetadataStorage().clear(); @Resolver() class SampleResolver { @Query(() => String) greet(@Arg("name") name: string, @Ctx() ctx: { prefix: string }): string { return `${ctx.prefix} ${name}`; } } const context = { prefix: "hi" }; const query = /* graphql */ ` query { greet(name: "world") } `; const schema1 = await buildSchema({ resolvers: [SampleResolver], validate: false }); const result1 = await graphql({ schema: schema1, source: query, contextValue: context, }); expect(result1.errors).toBeUndefined(); expect(result1.data).toEqual({ greet: "hi world" }); const schema2 = await buildSchema({ resolvers: [SampleResolver], validate: false }); const result2 = await graphql({ schema: schema2, source: query, contextValue: context, }); expect(result2.errors).toBeUndefined(); expect(result2.data).toEqual({ greet: "hi world" }); }); }); describe("Inheritance", () => { let schema: GraphQLSchema; let schemaIntrospection: IntrospectionSchema; let queryType: IntrospectionObjectType; let mutationType: IntrospectionObjectType; let subscriptionType: IntrospectionObjectType; let self: any; let childResolver: any; let overrideResolver: any; const pubSub = createPubSub(); beforeEach(() => { self = null; }); beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() normalField!: string; } @ObjectType() class DummyObject { @Field() normalField!: string; } function createResolver(name: string, objectType: ClassType) { @Resolver(() => objectType) class BaseResolver { protected name = "baseName"; @Query({ name: `${name}Query` }) baseQuery(@Arg("arg") _arg: boolean): boolean { self = this; return true; } @Mutation({ name: `${name}Mutation` }) baseMutation(@Arg("arg") _arg: boolean): boolean { self = this; return true; } @Subscription({ topics: "baseTopic", name: `${name}Subscription` }) baseSubscription(@Arg("arg") _arg: boolean): boolean { self = this; return true; } @Mutation(() => Boolean, { name: `${name}Trigger` }) async baseTrigger(): Promise { pubSub.publish("baseTopic", null); return true; } @FieldResolver() resolverField(): string { self = this; return "resolverField"; } } return BaseResolver; } @Resolver() class ChildResolver extends createResolver("prefix", SampleObject) { @Query() childQuery(): boolean { self = this; return true; } @Query() objectQuery(): SampleObject { return { normalField: "normalField" }; } @Mutation() childMutation(): boolean { self = this; return true; } @Subscription({ topics: "childTopic", complexity: 4 }) childSubscription(): boolean { self = this; return true; } @Mutation(() => Boolean) async childTrigger(): Promise { pubSub.publish("childTopic", null); return true; } } childResolver = ChildResolver; @Resolver() class OverrideResolver extends createResolver("overridden", DummyObject) { @Query() overriddenQuery(@Arg("overriddenArg") _arg: boolean): string { self = this; return "overriddenQuery"; } @Mutation({ name: "overriddenMutation" }) overriddenMutationHandler(@Arg("overriddenArg") _arg: boolean): string { self = this; return "overriddenMutationHandler"; } } overrideResolver = OverrideResolver; const schemaInfo = await getSchemaInfo({ resolvers: [childResolver, overrideResolver], pubSub, }); schemaIntrospection = schemaInfo.schemaIntrospection; queryType = schemaInfo.queryType; mutationType = schemaInfo.mutationType!; subscriptionType = schemaInfo.subscriptionType!; schema = schemaInfo.schema; }); it("should build schema correctly", async () => { expect(schema).toBeDefined(); }); it("should generate proper queries in schema", async () => { const queryNames = queryType.fields.map(it => it.name); expect(queryNames).toContain("childQuery"); expect(queryNames).toContain("objectQuery"); expect(queryNames).toContain("prefixQuery"); expect(queryNames).toContain("overriddenQuery"); }); it("should generate proper mutations in schema", async () => { const mutationNames = mutationType.fields.map(it => it.name); expect(mutationNames).toContain("childMutation"); expect(mutationNames).toContain("childTrigger"); expect(mutationNames).toContain("prefixMutation"); expect(mutationNames).toContain("prefixTrigger"); expect(mutationNames).toContain("overriddenTrigger"); expect(mutationNames).toContain("overriddenMutation"); }); it("should generate proper subscriptions in schema", async () => { const subscriptionNames = subscriptionType.fields.map(it => it.name); const prefixSubscription = subscriptionType.fields.find( it => it.name === "prefixSubscription", )!; expect(subscriptionNames).toContain("childSubscription"); expect(subscriptionNames).toContain("prefixSubscription"); expect(subscriptionNames).toContain("overriddenSubscription"); expect(prefixSubscription.args).toHaveLength(1); }); it("should generate proper object fields in schema", async () => { const sampleObjectType = schemaIntrospection.types.find( type => type.kind === TypeKind.OBJECT && type.name === "SampleObject", ) as IntrospectionObjectType; const sampleObjectTypeFieldsNames = sampleObjectType.fields.map(it => it.name); expect(sampleObjectType.fields).toHaveLength(2); expect(sampleObjectTypeFieldsNames).toContain("normalField"); expect(sampleObjectTypeFieldsNames).toContain("resolverField"); }); it("should overwrite args in schema when handler has been overridden", async () => { const prefixQuery = queryType.fields.find(it => it.name === "prefixQuery")!; const overriddenQuery = queryType.fields.find(it => it.name === "overriddenQuery")!; const prefixMutation = mutationType.fields.find(it => it.name === "prefixMutation")!; const overriddenMutation = mutationType.fields.find(it => it.name === "overriddenMutation")!; expect(prefixQuery.args).toHaveLength(1); expect(prefixQuery.args[0].name).toEqual("arg"); expect(overriddenQuery.args).toHaveLength(1); expect(overriddenQuery.args[0].name).toEqual("overriddenArg"); expect(prefixMutation.args).toHaveLength(1); expect(prefixMutation.args[0].name).toEqual("arg"); expect(overriddenMutation.args).toHaveLength(1); expect(overriddenMutation.args[0].name).toEqual("overriddenArg"); }); it("should overwrite return type in schema when handler has been overridden", async () => { const prefixQuery = queryType.fields.find(it => it.name === "prefixQuery")!; const overriddenQuery = queryType.fields.find(it => it.name === "overriddenQuery")!; const prefixMutation = mutationType.fields.find(it => it.name === "prefixMutation")!; const overriddenMutation = mutationType.fields.find(it => it.name === "overriddenMutation")!; const prefixQueryType = getInnerTypeOfNonNullableType(prefixQuery); const overriddenQueryType = getInnerTypeOfNonNullableType(overriddenQuery); const prefixMutationType = getInnerTypeOfNonNullableType(prefixMutation); const overriddenMutationType = getInnerTypeOfNonNullableType(overriddenMutation); expect(prefixQueryType.kind).toEqual(TypeKind.SCALAR); expect(prefixQueryType.name).toEqual("Boolean"); expect(overriddenQueryType.kind).toEqual(TypeKind.SCALAR); expect(overriddenQueryType.name).toEqual("String"); expect(prefixMutationType.kind).toEqual(TypeKind.SCALAR); expect(prefixMutationType.name).toEqual("Boolean"); expect(overriddenMutationType.kind).toEqual(TypeKind.SCALAR); expect(overriddenMutationType.name).toEqual("String"); }); it("should correctly call query handler from base resolver class", async () => { const query = `query { prefixQuery(arg: true) }`; const { data } = await graphql({ schema, source: query }); expect(data!.prefixQuery).toEqual(true); expect(self.constructor).toEqual(childResolver); }); it("should correctly call mutation handler from base resolver class", async () => { const mutation = `mutation { prefixMutation(arg: true) }`; const { data } = await graphql({ schema, source: mutation }); expect(data!.prefixMutation).toEqual(true); expect(self.constructor).toEqual(childResolver); }); it("should correctly call query handler from child resolver class", async () => { const query = `query { childQuery }`; const { data } = await graphql({ schema, source: query }); expect(data!.childQuery).toEqual(true); expect(self.constructor).toEqual(childResolver); }); it("should correctly call mutation handler from child resolver class", async () => { const mutation = `mutation { childMutation }`; const { data } = await graphql({ schema, source: mutation }); expect(data!.childMutation).toEqual(true); expect(self.constructor).toEqual(childResolver); }); it("should correctly call field resolver handler from base resolver class", async () => { const query = `query { objectQuery { resolverField } }`; const result: any = await graphql({ schema, source: query }); expect(result.data!.objectQuery.resolverField).toEqual("resolverField"); expect(self.constructor).toEqual(childResolver); }); it("should correctly call overridden query handler from child resolver class", async () => { const query = `query { overriddenQuery(overriddenArg: true) }`; const { data } = await graphql({ schema, source: query }); expect(data!.overriddenQuery).toEqual("overriddenQuery"); expect(self.constructor).toEqual(overrideResolver); }); it("should correctly call overridden mutation handler from child resolver class", async () => { const mutation = `mutation { overriddenMutation(overriddenArg: true) }`; const { data } = await graphql({ schema, source: mutation }); expect(data!.overriddenMutation).toEqual("overriddenMutationHandler"); expect(self.constructor).toEqual(overrideResolver); }); it("should have access to inherited properties from base resolver class", async () => { const query = `query { childQuery }`; await graphql({ schema, source: query }); expect(self.name).toEqual("baseName"); }); it("should get child class instance when calling base resolver handler", async () => { const query = `query { prefixQuery(arg: true) }`; await graphql({ schema, source: query }); expect(self).toBeInstanceOf(childResolver); }); it("should allow duplicate fieldResolver methods with different schema names for inherited resolvers", async () => { getMetadataStorage().clear(); const INHERITED_DYNAMIC_FIELD_NAME_1 = "dynamicallyNamedMethod1"; const INHERITED_DYNAMIC_FIELD_NAME_2 = "dynamicallyNamedMethod2"; const withDynamicallyNamedFieldResolver = ( classType: ClassType, BaseResolverClass: ClassType, name: string, ) => { @Resolver(() => classType) class DynamicallyNamedFieldResolver extends BaseResolverClass { @FieldResolver({ name }) dynamicallyNamedField(): boolean { return true; } } return DynamicallyNamedFieldResolver; }; @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query(() => SampleObject) sampleObject(): SampleObject { return { sampleField: "sampleText" }; } } const DynamicallyNamedFieldResolver1 = withDynamicallyNamedFieldResolver( SampleObject, SampleResolver, INHERITED_DYNAMIC_FIELD_NAME_1, ); const DynamicallyNamedFieldResolver2 = withDynamicallyNamedFieldResolver( SampleObject, DynamicallyNamedFieldResolver1, INHERITED_DYNAMIC_FIELD_NAME_2, ); const schemaInfo = await getSchemaInfo({ resolvers: [DynamicallyNamedFieldResolver2], }); schemaIntrospection = schemaInfo.schemaIntrospection; const sampleObjectType = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; const dynamicField1 = sampleObjectType.fields.find( field => field.name === INHERITED_DYNAMIC_FIELD_NAME_1, )!; const dynamicField2 = sampleObjectType.fields.find( field => field.name === INHERITED_DYNAMIC_FIELD_NAME_2, )!; expect(dynamicField1).toBeDefined(); expect(dynamicField2).toBeDefined(); }); it("should resolve independent factory-created field resolvers with different schema names correctly", async () => { getMetadataStorage().clear(); @ObjectType() class FactoryUser { @Field() name!: string; } function createFieldResolver(fieldName: string, returnValue: string) { @Resolver(() => FactoryUser) class DynamicFieldResolver { @FieldResolver(() => [String], { name: fieldName }) getItems(): string[] { return [returnValue]; } } return DynamicFieldResolver; } const FollowersResolver = createFieldResolver("followers", "follower1"); const FollowingResolver = createFieldResolver("following", "following1"); @Resolver() class FactoryUserResolver { @Query(() => FactoryUser) factoryUser(): FactoryUser { return { name: "TestUser" } as FactoryUser; } } const schemaInfo = await getSchemaInfo({ resolvers: [FactoryUserResolver, FollowersResolver, FollowingResolver], }); const { schema: factorySchema, schemaIntrospection: factorySchemaIntrospection } = schemaInfo; // Introspection check const factoryUserType = factorySchemaIntrospection.types.find( type => type.name === "FactoryUser", ) as IntrospectionObjectType; expect(factoryUserType.fields.find(f => f.name === "followers")).toBeDefined(); expect(factoryUserType.fields.find(f => f.name === "following")).toBeDefined(); // Runtime execution check const query = `{ factoryUser { name followers following } }`; const result = await graphql({ schema: factorySchema, source: query }); expect(result.errors).toBeUndefined(); expect(result.data).toEqual({ factoryUser: { name: "TestUser", followers: ["follower1"], following: ["following1"], }, }); }); }); describe("Shared generic resolver", () => { beforeEach(async () => { getMetadataStorage().clear(); }); it("should handle arguments correctly on multiple buildSchema runs", async () => { @ObjectType() class TestResponse { @Field() data!: string; } @ArgsType() class TestArgs { @Field(() => Int, { defaultValue: 0 }) testField!: number; } function makeResolverClass() { @Resolver(() => TestResponse) abstract class TestResolver { @Query(() => TestResponse) async exampleQuery(@Args() args: TestArgs): Promise { return { data: `resolver ${args.testField}`, }; } } return TestResolver; } @Resolver() class TestResolver extends makeResolverClass() {} const fistSchemaInfo = await getSchemaInfo({ resolvers: [TestResolver], }); expect(fistSchemaInfo.queryType.fields).toHaveLength(1); expect(fistSchemaInfo.queryType.fields[0].args).toHaveLength(1); const secondSchemaInfo = await getSchemaInfo({ resolvers: [TestResolver], }); expect(secondSchemaInfo.queryType.fields).toHaveLength(1); expect(secondSchemaInfo.queryType.fields[0].args).toHaveLength(1); }); it("should handle field resolvers correctly on multiple buildSchema runs", async () => { @ObjectType() class TestResponse { @Field() data!: string; } @ArgsType() class TestArgs { @Field(() => Int, { defaultValue: 0 }) testField!: number; } function makeResolverClass() { @Resolver(() => TestResponse) abstract class TestResolver { @Query(() => TestResponse) async exampleQuery(@Args() args: TestArgs): Promise { return { data: `resolver ${args.testField}`, }; } } return TestResolver; } @Resolver(() => TestResponse) class TestResolver extends makeResolverClass() { @FieldResolver(() => Boolean, { nullable: false }) public async exampleFieldResolver(): Promise { return true; } } @ObjectType() class OtherTestResponse { @Field() data!: string; } @ArgsType() class OtherTestArgs { @Field(() => Int, { defaultValue: 0 }) testField!: number; } function makeOtherResolverClass() { @Resolver(() => OtherTestResponse) abstract class OtherTestResolver { @Query(() => OtherTestResponse) async exampleQuery(@Args() args: OtherTestArgs): Promise { return { data: `resolver ${args.testField}`, }; } } return OtherTestResolver; } @Resolver(() => OtherTestResponse) class OtherTestResolver extends makeOtherResolverClass() { @FieldResolver(() => Boolean, { nullable: false }) public async exampleFieldResolver(): Promise { return true; } } const fistSchemaInfo = await getSchemaInfo({ resolvers: [TestResolver], }); const hasFoundFieldResolverInSchema = fistSchemaInfo.schemaIntrospection.types.some( type => type.kind === "OBJECT" && type.name === "TestResponse" && type.fields?.some(field => field.name === "exampleFieldResolver"), ); expect(hasFoundFieldResolverInSchema).toBeTruthy(); const secondSchemaInfo = await getSchemaInfo({ resolvers: [OtherTestResolver], }); const hasFoundFieldResolverInOtherSchema = secondSchemaInfo.schemaIntrospection.types.some( type => type.kind === "OBJECT" && type.name === "OtherTestResponse" && type.fields?.some(field => field.name === "exampleFieldResolver"), ); expect(hasFoundFieldResolverInOtherSchema).toBeTruthy(); }); }); }); ================================================ FILE: tests/functional/scalars.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionSchema, TypeKind, graphql, } from "graphql"; import { Arg, Field, Float, GraphQLISODateTime, GraphQLTimestamp, ID, Int, ObjectType, Query, Resolver, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { CustomScalar, CustomType, ObjectScalar } from "../helpers/customScalar"; import { getSampleObjectFieldType } from "../helpers/getSampleObjectFieldType"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Scalars", () => { let schemaIntrospection: IntrospectionSchema; let sampleObject: IntrospectionObjectType; let schema: GraphQLSchema; let argScalar: string | undefined; beforeEach(() => { argScalar = undefined; }); beforeAll(async () => { // create sample definitions @ObjectType() class SampleObject { @Field(() => ID) idField: any; @Field() implicitFloatField!: number; @Field(() => Float) explicitFloatField: any; @Field(() => Int) intField: any; @Field() implicitStringField!: string; @Field(() => String) explicitStringField: any; @Field() implicitBooleanField!: boolean; @Field(() => Boolean) explicitBooleanField: any; @Field() implicitDateField!: Date; @Field(() => Date) explicitDateField: any; @Field(() => GraphQLISODateTime) ISODateField: any; @Field(() => GraphQLTimestamp) timestampField: any; @Field(() => CustomScalar) customScalarField: any; } @Resolver(() => SampleObject) class SampleResolver { @Query() mainQuery(): SampleObject { return {} as any; } @Query(() => CustomScalar) returnScalar(): string { return "returnScalar"; } @Query(() => Boolean) argScalar(@Arg("scalar", () => CustomScalar) scalar: any): any { argScalar = scalar; return true; } @Query(() => Boolean) objectArgScalar(@Arg("scalar", () => ObjectScalar) scalar: any): any { argScalar = scalar; return true; } @Query(() => Date) returnDate(): any { return new Date(); } @Query() argDate(@Arg("date", () => Date) _date: any): boolean { return true; } } // get builded schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); schema = schemaInfo.schema; schemaIntrospection = schemaInfo.schemaIntrospection; sampleObject = schemaIntrospection.types.find( field => field.name === "SampleObject", ) as IntrospectionObjectType; }); describe("Schema", () => { function getFieldType(name: string) { const field = sampleObject.fields.find(it => it.name === name)!; const fieldType = (field.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionNamedTypeRef; return fieldType; } it("should generate ID scalar field type", async () => { const idFieldType = getFieldType("idField"); expect(idFieldType.kind).toEqual(TypeKind.SCALAR); expect(idFieldType.name).toEqual("ID"); }); it("should generate Float scalar field type", async () => { const explicitFloatFieldType = getFieldType("explicitFloatField"); expect(explicitFloatFieldType.kind).toEqual(TypeKind.SCALAR); expect(explicitFloatFieldType.name).toEqual("Float"); }); it("should generate Float scalar field type when prop type is number", async () => { const implicitFloatFieldType = getFieldType("implicitFloatField"); expect(implicitFloatFieldType.kind).toEqual(TypeKind.SCALAR); expect(implicitFloatFieldType.name).toEqual("Float"); }); it("should generate Int scalar field type", async () => { const intFieldType = getFieldType("intField"); expect(intFieldType.kind).toEqual(TypeKind.SCALAR); expect(intFieldType.name).toEqual("Int"); }); it("should generate String scalar field type", async () => { const explicitStringFieldType = getFieldType("explicitStringField"); expect(explicitStringFieldType.kind).toEqual(TypeKind.SCALAR); expect(explicitStringFieldType.name).toEqual("String"); }); it("should generate String scalar field type when prop type is string", async () => { const implicitStringFieldType = getFieldType("implicitStringField"); expect(implicitStringFieldType.kind).toEqual(TypeKind.SCALAR); expect(implicitStringFieldType.name).toEqual("String"); }); it("should generate Date scalar field type", async () => { const explicitDateFieldType = getFieldType("explicitDateField"); expect(explicitDateFieldType.kind).toEqual(TypeKind.SCALAR); expect(explicitDateFieldType.name).toEqual("DateTimeISO"); }); it("should generate Date scalar field type when prop type is Date", async () => { const implicitStringFieldType = getFieldType("implicitDateField"); expect(implicitStringFieldType.kind).toEqual(TypeKind.SCALAR); expect(implicitStringFieldType.name).toEqual("DateTimeISO"); }); it("should generate ISODate scalar field type", async () => { const ISODateFieldType = getFieldType("ISODateField"); expect(ISODateFieldType.kind).toEqual(TypeKind.SCALAR); expect(ISODateFieldType.name).toEqual("DateTimeISO"); }); it("should generate Timestamp scalar field type", async () => { const timestampFieldType = getFieldType("timestampField"); expect(timestampFieldType.kind).toEqual(TypeKind.SCALAR); expect(timestampFieldType.name).toEqual("Timestamp"); }); it("should generate custom scalar field type", async () => { const customScalarFieldType = getFieldType("customScalarField"); expect(customScalarFieldType.kind).toEqual(TypeKind.SCALAR); expect(customScalarFieldType.name).toEqual("Custom"); }); }); describe("Custom scalar", () => { it("should properly serialize data", async () => { const query = `query { returnScalar }`; const result: any = await graphql({ schema, source: query }); const { returnScalar } = result.data!; expect(returnScalar).toEqual("TypeGraphQL serialize"); }); it("should properly parse args", async () => { const query = `query { argScalar(scalar: "test") }`; await graphql({ schema, source: query }); expect(argScalar!).toEqual("TypeGraphQL parseLiteral"); }); it("should properly parse scalar object", async () => { const query = `query { objectArgScalar(scalar: "test") }`; await graphql({ schema, source: query }); expect(argScalar!).toEqual({ value: "TypeGraphQL parseLiteral" }); }); }); describe("Settings", () => { let sampleResolver: any; beforeAll(() => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field(() => Date) dateField: any; } @Resolver(() => SampleObject) class SampleResolver { @Query() mainQuery(): SampleObject { return {} as any; } } sampleResolver = SampleResolver; }); it("should generate iso date scalar field type by default", async () => { const schemaInfo = await getSchemaInfo({ resolvers: [sampleResolver], }); const dateFieldType = getSampleObjectFieldType(schemaInfo.schemaIntrospection)("dateField"); expect(dateFieldType.kind).toEqual(TypeKind.SCALAR); expect(dateFieldType.name).toEqual("DateTimeISO"); }); it("should generate DateTime scalar field type when scalarsMap is using GraphQLISODateTime", async () => { const schemaInfo = await getSchemaInfo({ resolvers: [sampleResolver], scalarsMap: [{ type: Date, scalar: GraphQLISODateTime }], }); const dateFieldType = getSampleObjectFieldType(schemaInfo.schemaIntrospection)("dateField"); expect(dateFieldType.kind).toEqual(TypeKind.SCALAR); expect(dateFieldType.name).toEqual("DateTimeISO"); }); it("should generate Timestamp scalar field type when scalarsMap is using GraphQLTimestamp", async () => { const schemaInfo = await getSchemaInfo({ resolvers: [sampleResolver], scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }], }); const dateFieldType = getSampleObjectFieldType(schemaInfo.schemaIntrospection)("dateField"); expect(dateFieldType.kind).toEqual(TypeKind.SCALAR); expect(dateFieldType.name).toEqual("Timestamp"); }); it("should generate custom scalar field type when defined in scalarMap", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() customField!: CustomType; } @Resolver(() => SampleObject) class SampleResolver { @Query() mainQuery(): SampleObject { return {} as any; } } const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], scalarsMap: [{ type: CustomType, scalar: CustomScalar }], }); const dateFieldType = getSampleObjectFieldType(schemaInfo.schemaIntrospection)("customField"); expect(dateFieldType.kind).toEqual(TypeKind.SCALAR); expect(dateFieldType.name).toEqual("Custom"); }); it("should generate custom scalar field type when overwriteDate in scalarMap", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field(() => Date) dateField: any; } @Resolver(() => SampleObject) class SampleResolver { @Query() mainQuery(): SampleObject { return {} as any; } } const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], scalarsMap: [{ type: Date, scalar: CustomScalar }], }); const dateFieldType = getSampleObjectFieldType(schemaInfo.schemaIntrospection)("dateField"); expect(dateFieldType.kind).toEqual(TypeKind.SCALAR); expect(dateFieldType.name).toEqual("Custom"); }); }); }); ================================================ FILE: tests/functional/simple-resolvers.ts ================================================ import "reflect-metadata"; import { type GraphQLSchema, execute } from "graphql"; import gql from "graphql-tag"; import { Field, type MiddlewareFn, ObjectType, Query, Resolver, buildSchema } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; describe("Simple resolvers", () => { let schema: GraphQLSchema; let middlewareLogs: string[] = []; beforeAll(async () => { getMetadataStorage().clear(); const testMiddleware: MiddlewareFn = async (_, next) => { middlewareLogs.push("middleware executed"); return next(); }; @ObjectType() class NormalObject { @Field() normalField!: string; } @ObjectType() class ObjectWithSimpleField { @Field({ simple: true }) simpleField!: string; } @ObjectType({ simpleResolvers: true }) class SimpleObject { @Field() simpleField!: string; } @ObjectType({ simpleResolvers: true }) class SimpleObjectWithNormalField { @Field({ simple: false }) normalField!: string; } @Resolver() class TestResolver { @Query() normalObjectQuery(): NormalObject { return { normalField: "normalField" }; } @Query() objectWithSimpleFieldQuery(): ObjectWithSimpleField { return { simpleField: "simpleField" }; } @Query() simpleObjectQuery(): SimpleObject { return { simpleField: "simpleField" }; } @Query() simpleObjectWithNormalFieldQuery(): SimpleObjectWithNormalField { return { normalField: "normalField" }; } } schema = await buildSchema({ resolvers: [TestResolver], globalMiddlewares: [testMiddleware], }); }); beforeEach(() => { middlewareLogs = []; }); it("should execute middlewares for field resolvers for normal object", async () => { const document = gql` query { normalObjectQuery { normalField } } `; await execute({ schema, document }); expect(middlewareLogs).toHaveLength(2); }); it("shouldn't execute middlewares for simple field resolvers", async () => { const document = gql` query { objectWithSimpleFieldQuery { simpleField } } `; await execute({ schema, document }); expect(middlewareLogs).toHaveLength(1); }); it("shouldn't execute middlewares for field resolvers of simple objects", async () => { const document = gql` query { simpleObjectQuery { simpleField } } `; await execute({ schema, document }); expect(middlewareLogs).toHaveLength(1); }); it("should execute middlewares for not simple field resolvers of simple objects", async () => { const document = gql` query { simpleObjectWithNormalFieldQuery { normalField } } `; await execute({ schema, document }); expect(middlewareLogs).toHaveLength(2); }); }); ================================================ FILE: tests/functional/subscriptions.ts ================================================ import "reflect-metadata"; import { createPubSub } from "@graphql-yoga/subscription"; import { type DocumentNode, type ExecutionResult, type GraphQLSchema, type IntrospectionObjectType, TypeKind, execute, subscribe, } from "graphql"; import gql from "graphql-tag"; import { Arg, Authorized, Field, Float, Int, MissingPubSubError, MissingSubscriptionTopicsError, Mutation, ObjectType, Query, Resolver, Root, Subscription, buildSchema, } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { expectToThrow } from "../helpers/expectToThrow"; import { getInnerTypeOfNonNullableType, getItemTypeOfList } from "../helpers/getInnerFieldType"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; import { sleep } from "../helpers/sleep"; describe("Subscriptions", () => { const pubSub = createPubSub(); describe("Schema", () => { let schema: GraphQLSchema; let subscriptionType: IntrospectionObjectType; beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field() sampleField!: string; } @Resolver() class SampleResolver { @Query() sampleQuery(): boolean { return true; } @Subscription({ topics: "STH" }) sampleSubscription(): boolean { return true; } @Subscription({ topics: "STH" }) subscriptionWithArgs( @Arg("stringArg") _stringArg: string, @Arg("booleanArg") _booleanArg: boolean, ): boolean { return true; } @Subscription(() => [SampleObject], { topics: "STH" }) subscriptionWithExplicitType(): any { return true; } } const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], pubSub, }); schema = schemaInfo.schema; subscriptionType = schemaInfo.subscriptionType!; }); it("should build schema without errors", () => { expect(schema).toBeDefined(); }); it("should correctly generate simple subscription type", () => { const sampleSubscription = subscriptionType.fields.find( field => field.name === "sampleSubscription", )!; const sampleSubscriptionType = getInnerTypeOfNonNullableType(sampleSubscription); expect(sampleSubscription).toBeDefined(); expect(sampleSubscription.args).toHaveLength(0); expect(sampleSubscriptionType.kind).toEqual(TypeKind.SCALAR); expect(sampleSubscriptionType.name).toEqual("Boolean"); }); it("should correctly generate type of subscription with args", () => { const subscriptionWithArgs = subscriptionType.fields.find( field => field.name === "subscriptionWithArgs", )!; const subscriptionWithArgsType = getInnerTypeOfNonNullableType(subscriptionWithArgs); expect(subscriptionWithArgs).toBeDefined(); expect(subscriptionWithArgs.args).toHaveLength(2); expect(subscriptionWithArgsType.kind).toEqual(TypeKind.SCALAR); expect(subscriptionWithArgsType.name).toEqual("Boolean"); }); it("should correctly generate type of subscription with explicit type", () => { const subscriptionWithExplicitType = subscriptionType.fields.find( field => field.name === "subscriptionWithExplicitType", )!; const innerType = getInnerTypeOfNonNullableType(subscriptionWithExplicitType); const itemType = getItemTypeOfList(subscriptionWithExplicitType); expect(subscriptionWithExplicitType).toBeDefined(); expect(subscriptionWithExplicitType.args).toHaveLength(0); expect(innerType.kind).toEqual(TypeKind.LIST); expect(itemType.kind).toEqual(TypeKind.OBJECT); expect(itemType.name).toEqual("SampleObject"); }); }); describe("Functional", () => { let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field(() => Float) value!: number; } const SAMPLE_TOPIC = "SAMPLE"; const OTHER_TOPIC = "OTHER"; const CUSTOM_SUBSCRIBE_TOPIC = "CUSTOM_SUBSCRIBE_TOPIC"; @Resolver() class SampleResolver { @Query() dummyQuery(): boolean { return true; } @Mutation(() => Boolean) async pubSubMutation(@Arg("value") value: number): Promise { pubSub.publish(SAMPLE_TOPIC, value); return true; } @Mutation(() => Boolean) async pubSubMutationCustomSubscription(@Arg("value") value: number): Promise { pubSub.publish(CUSTOM_SUBSCRIBE_TOPIC, value); return true; } @Mutation(() => Boolean) async pubSubMutationDynamicTopic( @Arg("value") value: number, @Arg("topic") topic: string, ): Promise { pubSub.publish(topic, value); return true; } @Mutation(() => Boolean) async pubSubOtherMutation(@Arg("value") value: number): Promise { pubSub.publish(OTHER_TOPIC, value); return true; } @Subscription({ topics: SAMPLE_TOPIC, }) sampleTopicSubscription(@Root() value: number): SampleObject { return { value }; } @Subscription({ topics: SAMPLE_TOPIC, filter: ({ payload: value }) => value > 0.5, }) sampleTopicSubscriptionWithFilter(@Root() value: number): SampleObject { return { value }; } @Subscription({ topics: [SAMPLE_TOPIC, OTHER_TOPIC], }) multipleTopicSubscription(@Root() value: number): SampleObject { return { value }; } @Subscription({ topics: ({ args }) => args.topic, }) dynamicTopicSubscription( @Root() value: number, @Arg("topic") _topic: string, ): SampleObject { return { value }; } @Subscription({ subscribe: () => pubSub.subscribe(CUSTOM_SUBSCRIBE_TOPIC), }) customSubscribeSubscription(@Root() value: number): SampleObject { return { value }; } } schema = await buildSchema({ resolvers: [SampleResolver], pubSub, }); }); it("should build schema without errors", async () => { expect(schema).toBeDefined(); }); async function subscribeOnceAndMutate(options: { mutation: DocumentNode; mutationVariables?: object; subscription: DocumentNode; subscriptionVariables?: object; onSubscribedData: (data: any) => void; }) { const results = (await subscribe({ schema, document: options.subscription, variableValues: options.subscriptionVariables as any, })) as AsyncIterableIterator; const onDataPromise = results.next().then(async ({ value }) => { options.onSubscribedData(value.data); }); await execute({ schema, document: options.mutation, variableValues: options.mutationVariables as any, }); await onDataPromise; } it("should successfully get data from subscription after publishing mutation", async () => { let subscriptionValue!: number; const testedValue = Math.PI; const subscription = gql` subscription { sampleTopicSubscription { value } } `; const mutation = gql` mutation { pubSubMutation(value: ${testedValue}) } `; await subscribeOnceAndMutate({ subscription, mutation, onSubscribedData: data => { subscriptionValue = data.sampleTopicSubscription.value; }, }); expect(subscriptionValue).toEqual(testedValue); }); it("should successfully get data from subscription using fragments", async () => { let subscriptionValue!: number; const testedValue = Math.PI; const subscription = gql` fragment TestFragment on SampleObject { value } subscription { sampleTopicSubscription { ...TestFragment } } `; const mutation = gql` mutation { pubSubMutation(value: ${testedValue}) } `; await subscribeOnceAndMutate({ subscription, mutation, onSubscribedData: data => { subscriptionValue = data.sampleTopicSubscription.value; }, }); expect(subscriptionValue).toEqual(testedValue); }); it("should successfully get data from subscription after sequential mutations", async () => { let subscriptionValue!: number; const subscriptionQuery = gql` subscription { sampleTopicSubscription { value } } `; const mutation = gql` mutation SimpleMutation($value: Float!) { pubSubMutation(value: $value) } `; const subscription = (await subscribe({ schema, document: subscriptionQuery, })) as AsyncIterableIterator; // run subscription in a separate async "thread" (async () => { for await (const result of subscription) { subscriptionValue = (result.data as any).sampleTopicSubscription.value; } })(); await execute({ schema, document: mutation, variableValues: { value: 1.23 } }); await sleep(0); expect(subscriptionValue).toEqual(1.23); await execute({ schema, document: mutation, variableValues: { value: 2.37 } }); await sleep(0); expect(subscriptionValue).toEqual(2.37); await execute({ schema, document: mutation, variableValues: { value: 4.53 } }); await sleep(0); expect(subscriptionValue).toEqual(4.53); }); it("should doesn't trigger subscription when published to other topic", async () => { let subscriptionValue!: number; const subscriptionQuery = gql` subscription { sampleTopicSubscription { value } } `; const sampleTopicMutation = gql` mutation SampleTopicMutation($value: Float!) { pubSubMutation(value: $value) } `; const otherTopicMutation = gql` mutation OtherTopicMutation($value: Float!) { pubSubOtherMutation(value: $value) } `; const subscription = (await subscribe({ schema, document: subscriptionQuery, })) as AsyncIterableIterator; // run subscription in a separate async "thread" (async () => { for await (const result of subscription) { subscriptionValue = (result.data as any).sampleTopicSubscription.value; } })(); await execute({ schema, document: otherTopicMutation, variableValues: { value: 1.23 } }); await sleep(0); expect(subscriptionValue).toBeUndefined(); await execute({ schema, document: otherTopicMutation, variableValues: { value: 2.37 } }); await sleep(0); expect(subscriptionValue).toBeUndefined(); await execute({ schema, document: sampleTopicMutation, variableValues: { value: 3.47 } }); await sleep(0); expect(subscriptionValue).toEqual(3.47); }); it("should correctly filter triggering subscription", async () => { let subscriptionValue!: number; const subscriptionQuery = gql` subscription { sampleTopicSubscriptionWithFilter { value } } `; const mutation = gql` mutation SimpleMutation($value: Float!) { pubSubMutation(value: $value) } `; const subscription = (await subscribe({ schema, document: subscriptionQuery, })) as AsyncIterableIterator; // run subscription in a separate async "thread" (async () => { for await (const result of subscription) { subscriptionValue = (result.data as any).sampleTopicSubscriptionWithFilter.value; } })(); await execute({ schema, document: mutation, variableValues: { value: 0.23 } }); await sleep(0); expect(subscriptionValue).toBeUndefined(); await execute({ schema, document: mutation, variableValues: { value: 0.77 } }); await sleep(0); expect(subscriptionValue).toEqual(0.77); await execute({ schema, document: mutation, variableValues: { value: 0.44 } }); await sleep(0); expect(subscriptionValue).toEqual(0.77); }); it("should correctly subscribe to multiple topics", async () => { let subscriptionValue!: number; const subscriptionQuery = gql` subscription { multipleTopicSubscription { value } } `; const sampleTopicMutation = gql` mutation SampleTopicMutation($value: Float!) { pubSubMutation(value: $value) } `; const otherTopicMutation = gql` mutation OtherTopicMutation($value: Float!) { pubSubOtherMutation(value: $value) } `; const subscription = (await subscribe({ schema, document: subscriptionQuery, })) as AsyncIterableIterator; // run subscription in a separate async "thread" (async () => { for await (const result of subscription) { subscriptionValue = (result.data as any).multipleTopicSubscription.value; } })(); await execute({ schema, document: sampleTopicMutation, variableValues: { value: 0.23 } }); await sleep(0); expect(subscriptionValue).toEqual(0.23); await execute({ schema, document: otherTopicMutation, variableValues: { value: 0.77 } }); await sleep(0); expect(subscriptionValue).toEqual(0.77); await execute({ schema, document: sampleTopicMutation, variableValues: { value: 0.44 } }); await sleep(0); expect(subscriptionValue).toEqual(0.44); }); it("should correctly subscribe to dynamic topics", async () => { let subscriptionValue!: number; const SAMPLE_TOPIC = "MY_DYNAMIC_TOPIC"; const dynamicTopicSubscription = gql` subscription dynamicTopicSubscription($topic: String!) { dynamicTopicSubscription(topic: $topic) { value } } `; const pubSubMutationDynamicTopic = gql` mutation pubSubMutationDynamicTopic($value: Float!, $topic: String!) { pubSubMutationDynamicTopic(value: $value, topic: $topic) } `; await subscribeOnceAndMutate({ subscription: dynamicTopicSubscription, subscriptionVariables: { topic: SAMPLE_TOPIC }, mutation: pubSubMutationDynamicTopic, mutationVariables: { value: 0.23, topic: SAMPLE_TOPIC }, onSubscribedData: data => { subscriptionValue = data.dynamicTopicSubscription.value; }, }); expect(subscriptionValue).toEqual(0.23); }); it("should correctly subscribe with custom subscribe function", async () => { let subscriptionValue!: number; const testedValue = Math.PI; const subscription = gql` subscription { customSubscribeSubscription { value } } `; const mutation = gql` mutation { pubSubMutationCustomSubscription(value: ${testedValue}) } `; await subscribeOnceAndMutate({ subscription, mutation, onSubscribedData: data => { subscriptionValue = data.customSubscribeSubscription.value; }, }); expect(subscriptionValue).toEqual(testedValue); }); }); describe("errors", () => { it("should throw error when using subscriptions but not providing pub sub implementation", async () => { getMetadataStorage().clear(); const error = await expectToThrow(async () => { class SampleResolver { @Query() dumbQuery(): boolean { return true; } @Subscription({ topics: "TEST" }) sampleSubscription(): boolean { return true; } } await buildSchema({ resolvers: [SampleResolver], pubSub: undefined, }); }); expect(error).toBeDefined(); expect(error).toBeInstanceOf(MissingPubSubError); }); it("should throw error while passing empty topics array to Subscription", async () => { getMetadataStorage().clear(); const error = await expectToThrow(async () => { class SampleResolver { @Query() dumbQuery(): boolean { return true; } @Mutation(() => Boolean) async pubSubMutation(@Arg("value") value: number): Promise { pubSub.publish("TEST", value); return true; } @Subscription({ topics: [] }) sampleSubscription(): boolean { return true; } } await buildSchema({ resolvers: [SampleResolver], pubSub, }); }); expect(error).toBeDefined(); expect(error).toBeInstanceOf(MissingSubscriptionTopicsError); expect(error.message).toContain("SampleResolver"); expect(error.message).toContain("sampleSubscription"); expect(error.message).not.toContain("class SampleResolver"); }); it("should throw authorization error just on subscribe", async () => { getMetadataStorage().clear(); // expect.assertions(3); @Resolver() class SampleResolver { @Query() sampleQuery(): number { return 2137; } @Authorized("prevent") @Subscription(_returns => Int, { topics: "SAMPLE_TOPIC" }) authedSubscription(): number { return 0; } } const schema = await buildSchema({ resolvers: [SampleResolver], pubSub, authChecker: () => false, }); const document = gql` subscription { authedSubscription } `; const subscribeResult = await subscribe({ schema, document }); expect(subscribeResult).toHaveProperty("errors"); const { errors } = subscribeResult as ExecutionResult; expect(errors).toHaveLength(1); expect(errors![0].message).toContain("Access denied!"); }); }); }); ================================================ FILE: tests/functional/typedefs-resolvers.ts ================================================ /* eslint "no-underscore-dangle": ["error", { "allow": ["__schema"] }] */ import "reflect-metadata"; import { makeExecutableSchema } from "@graphql-tools/schema"; import { createPubSub } from "@graphql-yoga/subscription"; import { MinLength } from "class-validator"; import { type ExecutionResult, type GraphQLSchema, type IntrospectionEnumType, type IntrospectionInputObjectType, type IntrospectionInterfaceType, type IntrospectionNamedTypeRef, type IntrospectionObjectType, type IntrospectionQuery, type IntrospectionScalarType, type IntrospectionSchema, type IntrospectionUnionType, TypeKind, execute, getIntrospectionQuery, graphql, subscribe, } from "graphql"; import gql from "graphql-tag"; import { Arg, Authorized, Field, FieldResolver, InputType, InterfaceType, Mutation, ObjectType, type PubSub, Query, Resolver, type ResolverObject, type ResolverOptions, type ResolversMap, Root, Subscription, UseMiddleware, buildTypeDefsAndResolvers, buildTypeDefsAndResolversSync, createUnionType, registerEnumType, } from "type-graphql"; import Container, { Service } from "typedi"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; describe("typeDefs and resolvers", () => { describe("buildTypeDefsAndResolvers", () => { const timestamp = 1547398942902; let typeDefs: string; let resolvers: ResolversMap; let schemaIntrospection: IntrospectionSchema; let schema: GraphQLSchema; let pubSub: PubSub; let inputValue: any; let enumValue: any; let middlewareLogs: string[]; beforeEach(async () => { middlewareLogs = []; enumValue = undefined; }); beforeAll(async () => { getMetadataStorage().clear(); @Service() class SampleService { getSampleString() { return "SampleString"; } } @InterfaceType() abstract class SampleInterface { @Field() sampleInterfaceStringField!: string; } @ObjectType({ implements: SampleInterface }) class SampleType1 implements SampleInterface { @Field() sampleInterfaceStringField!: string; @Field({ description: "sampleType1StringFieldDescription" }) sampleType1StringField!: string; } @ObjectType({ implements: SampleInterface }) class SampleType2 implements SampleInterface { @Field() sampleInterfaceStringField!: string; @Field({ deprecationReason: "sampleType2StringFieldDeprecation" }) sampleType2StringField!: string; } @ObjectType() class SampleType3 { @Field() sampleInterfaceStringField!: string; @Field() sampleType3StringField!: string; } @ObjectType("SampleType__4") class SampleType4 { @Field() sampleInterfaceStringField!: string; @Field() sampleType4StringField!: string; } @InputType() class SampleInput { @Field() @MinLength(10) sampleInputStringField!: string; @Field() sampleInputDefaultStringField: string = "sampleInputDefaultStringField"; } enum SampleNumberEnum { OptionOne, OptionTwo, } registerEnumType(SampleNumberEnum, { name: "SampleNumberEnum" }); enum SampleStringEnum { OptionOne = "OptionOneString", OptionTwo = "OptionTwoString", } registerEnumType(SampleStringEnum, { name: "SampleStringEnum" }); const SampleUnion = createUnionType({ types: () => [SampleType2, SampleType3], name: "SampleUnion", description: "SampleUnion description", }); const SampleResolveUnion = createUnionType({ types: () => [SampleType2, SampleType3], name: "SampleResolveUnion", resolveType: value => { if ("sampleType2StringField" in value) { return "SampleType2"; } if ("sampleType3StringField" in value) { return "SampleType3"; } return undefined; }, }); @Service() @Resolver() class SampleResolver { constructor(private readonly sampleService: SampleService) {} @Query({ description: "sampleDateQueryDescription" }) sampleDateQuery(): Date { return new Date(timestamp); } @Query() sampleServiceQuery(): string { return this.sampleService.getSampleString(); } @Query() @UseMiddleware(async (_, next) => { middlewareLogs.push("sampleMiddlewareBooleanQuery"); return next(); }) sampleMiddlewareBooleanQuery(): boolean { return true; } @Mutation() sampleBooleanMutation(): boolean { return true; } @Mutation() sampleMutationWithInput(@Arg("input") input: SampleInput): boolean { inputValue = input; return true; } @Mutation() @Authorized() sampleAuthorizedMutation(): boolean { return true; } @Query() sampleInterfaceQuery(): SampleInterface { const type1 = new SampleType1(); type1.sampleInterfaceStringField = "sampleInterfaceStringField"; type1.sampleType1StringField = "sampleType1StringField"; return type1; } @Query(() => SampleUnion) sampleUnionQuery(): typeof SampleUnion { const type3 = new SampleType3(); type3.sampleInterfaceStringField = "sampleInterfaceStringField"; type3.sampleType3StringField = "sampleType3StringField"; return type3; } @Query(() => SampleResolveUnion) sampleResolveUnionQuery(): typeof SampleResolveUnion { return { sampleInterfaceStringField: "sampleInterfaceStringField", sampleType3StringField: "sampleType3StringField", }; } @Query(() => SampleNumberEnum) sampleNumberEnumQuery( @Arg("numberEnum", () => SampleNumberEnum) numberEnum: SampleNumberEnum, ): SampleNumberEnum { enumValue = numberEnum; return numberEnum; } @Query(() => SampleStringEnum) sampleStringEnumQuery( @Arg("stringEnum", () => SampleStringEnum) stringEnum: SampleStringEnum, ): SampleStringEnum { enumValue = stringEnum; return stringEnum; } @Subscription({ topics: "SAMPLE", }) sampleSubscription(@Root() payload: number): number { return payload; } } pubSub = createPubSub(); @Service() @Resolver(() => SampleType4) class SampleObjectTypeWithDoubleUnderscoreInNameResolver { @FieldResolver(() => String) sampleResolvedField(): string { return "sampleResolvedField"; } @Query(() => SampleType4) async sampleQueryOnObjectTypeWithDoubleUnderScore(): Promise { const type4 = new SampleType4(); type4.sampleInterfaceStringField = "sampleInterfaceStringField"; type4.sampleType4StringField = "sampleType4StringField"; return type4; } } ({ typeDefs, resolvers } = await buildTypeDefsAndResolvers({ resolvers: [SampleResolver, SampleObjectTypeWithDoubleUnderscoreInNameResolver], authChecker: () => false, pubSub, container: Container, orphanedTypes: [SampleType1], validate: true, })); schema = makeExecutableSchema({ typeDefs, resolvers, }); const introspectionResult = await graphql({ schema, source: getIntrospectionQuery() }); schemaIntrospection = (introspectionResult.data as unknown as IntrospectionQuery).__schema; }); it("should generate schema without errors", () => { expect(schemaIntrospection).toBeDefined(); }); describe("typeDefs", () => { it("should generate typeDefs correctly", async () => { expect(typeDefs).toBeDefined(); }); it("should generate interface type", async () => { const sampleInterface = schemaIntrospection.types.find( it => it.name === "SampleInterface", ) as IntrospectionInterfaceType; expect(sampleInterface.kind).toBe(TypeKind.INTERFACE); expect(sampleInterface.fields).toHaveLength(1); expect(sampleInterface.fields[0].name).toBe("sampleInterfaceStringField"); expect(sampleInterface.possibleTypes).toHaveLength(2); expect(sampleInterface.possibleTypes.map(it => it.name)).toContain("SampleType1"); expect(sampleInterface.possibleTypes.map(it => it.name)).toContain("SampleType2"); }); it("should generate object types", async () => { const sampleType1 = schemaIntrospection.types.find( it => it.name === "SampleType1", ) as IntrospectionObjectType; const sampleType2 = schemaIntrospection.types.find( it => it.name === "SampleType2", ) as IntrospectionObjectType; const sampleType4 = schemaIntrospection.types.find( it => it.name === "SampleType__4", ) as IntrospectionObjectType; const sampleType1StringField = sampleType1.fields.find( it => it.name === "sampleType1StringField", )!; const sampleType2StringField = sampleType2.fields.find( it => it.name === "sampleType2StringField", )!; expect(sampleType1.kind).toBe(TypeKind.OBJECT); expect(sampleType1.fields).toHaveLength(2); expect(sampleType1StringField.description).toEqual("sampleType1StringFieldDescription"); expect(sampleType1.interfaces).toHaveLength(1); expect(sampleType1.interfaces[0].name).toBe("SampleInterface"); expect(sampleType2StringField.deprecationReason).toBe("sampleType2StringFieldDeprecation"); expect(sampleType4.fields).toHaveLength(3); }); it("should generate input type", async () => { const sampleInput = schemaIntrospection.types.find( it => it.name === "SampleInput", ) as IntrospectionInputObjectType; const sampleInputDefaultStringField = sampleInput.inputFields.find( it => it.name === "sampleInputDefaultStringField", )!; const sampleInputDefaultStringFieldType = sampleInputDefaultStringField.type as IntrospectionNamedTypeRef; expect(sampleInput.kind).toBe(TypeKind.INPUT_OBJECT); expect(sampleInput.inputFields).toHaveLength(2); expect(sampleInputDefaultStringFieldType).toEqual({ kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null, }, }); expect(sampleInputDefaultStringField.defaultValue).toBe('"sampleInputDefaultStringField"'); }); it("should generate enum types", async () => { const sampleNumberEnum = schemaIntrospection.types.find( it => it.name === "SampleNumberEnum", ) as IntrospectionEnumType; const sampleStringEnum = schemaIntrospection.types.find( it => it.name === "SampleStringEnum", ) as IntrospectionEnumType; expect(sampleNumberEnum.kind).toBe(TypeKind.ENUM); expect(sampleNumberEnum).toBeDefined(); expect(sampleNumberEnum.enumValues).toHaveLength(2); expect(sampleStringEnum.enumValues).toHaveLength(2); }); it("should generate union type", async () => { const sampleUnion = schemaIntrospection.types.find( it => it.name === "SampleUnion", ) as IntrospectionUnionType; expect(sampleUnion.kind).toBe(TypeKind.UNION); expect(sampleUnion.description).toBe("SampleUnion description"); expect(sampleUnion.possibleTypes).toHaveLength(2); expect(sampleUnion.possibleTypes.map(it => it.name)).toContain("SampleType2"); expect(sampleUnion.possibleTypes.map(it => it.name)).toContain("SampleType3"); }); it("should generate queries", async () => { const queryType = schemaIntrospection.types.find( it => it.name === schemaIntrospection.queryType.name, ) as IntrospectionObjectType; expect(queryType.fields).toHaveLength(9); }); it("should generate mutations", async () => { const mutationType = schemaIntrospection.types.find( it => it.name === schemaIntrospection.mutationType!.name, ) as IntrospectionObjectType; expect(mutationType.fields).toHaveLength(3); }); it("should generate subscription", async () => { const subscriptionType = schemaIntrospection.types.find( it => it.name === schemaIntrospection.subscriptionType!.name, ) as IntrospectionObjectType; expect(subscriptionType.fields).toHaveLength(1); }); it("should emit Date scalar", async () => { const dateScalar = schemaIntrospection.types.find( it => it.name === "DateTimeISO", ) as IntrospectionScalarType; expect(dateScalar.kind).toBe(TypeKind.SCALAR); }); }); describe("resolvers", () => { it("should generate resolversMap without errors", async () => { expect(resolvers).toBeDefined(); }); it("should not emit `__isTypeOf` for root objects", async () => { expect(resolvers.Query).not.toHaveProperty("__isTypeOf"); expect(resolvers.Mutation).not.toHaveProperty("__isTypeOf"); expect(resolvers.Subscription).not.toHaveProperty("__isTypeOf"); }); it("should properly serialize Date scalar", async () => { const document = gql` query { sampleDateQuery } `; const result: any = await execute({ schema, document }); const parsedDate = new Date(result.data.sampleDateQuery); expect(typeof result.data.sampleDateQuery).toBe("string"); expect(parsedDate.getTime()).toEqual(timestamp); }); it("should use container to resolve dependency", async () => { const document = gql` query { sampleServiceQuery } `; const { data } = await execute({ schema, document }); expect(data!.sampleServiceQuery).toEqual("SampleString"); }); it("should run resolver method middleware", async () => { const document = gql` query { sampleMiddlewareBooleanQuery } `; const { data } = await execute({ schema, document }); expect(data!.sampleMiddlewareBooleanQuery).toEqual(true); expect(middlewareLogs).toHaveLength(1); expect(middlewareLogs[0]).toEqual("sampleMiddlewareBooleanQuery"); }); it("should allow for simple boolean mutation", async () => { const document = gql` mutation { sampleBooleanMutation } `; const { data } = await execute({ schema, document }); expect(data!.sampleBooleanMutation).toBe(true); }); it("should properly transform input argument", async () => { const document = gql` mutation { sampleMutationWithInput(input: { sampleInputStringField: "sampleInputStringField" }) } `; const { data } = await execute({ schema, document }); expect(data!.sampleMutationWithInput).toBe(true); expect(inputValue.constructor.name).toBe("SampleInput"); expect(inputValue.sampleInputStringField).toBe("sampleInputStringField"); expect(inputValue.sampleInputDefaultStringField).toBe("sampleInputDefaultStringField"); }); it("should validate the input", async () => { const document = gql` mutation { sampleMutationWithInput(input: { sampleInputStringField: "short" }) } `; const { errors } = await execute({ schema, document }); expect(errors).toHaveLength(1); expect(errors![0].message).toContain("Argument Validation Error"); }); it("should properly guard authorized resolver method", async () => { const document = gql` mutation { sampleAuthorizedMutation } `; const { errors } = await execute({ schema, document }); expect(errors).toHaveLength(1); expect(errors![0].message).toContain("Access denied"); }); it("should detect returned object type from interface", async () => { const document = gql` query { sampleInterfaceQuery { sampleInterfaceStringField ... on SampleType1 { sampleType1StringField } } } `; const { data } = await execute({ schema, document }); expect(data!.sampleInterfaceQuery).toEqual({ sampleInterfaceStringField: "sampleInterfaceStringField", sampleType1StringField: "sampleType1StringField", }); }); it("should detect returned object type from union", async () => { const document = gql` query { sampleUnionQuery { ... on SampleType3 { sampleInterfaceStringField sampleType3StringField } } } `; const { data } = await execute({ schema, document }); expect(data!.sampleUnionQuery).toEqual({ sampleInterfaceStringField: "sampleInterfaceStringField", sampleType3StringField: "sampleType3StringField", }); }); it("should detect returned object type using resolveType from union", async () => { const document = gql` query { sampleResolveUnionQuery { ... on SampleType3 { sampleInterfaceStringField sampleType3StringField } } } `; const { data } = await execute({ schema, document }); expect(data!.sampleResolveUnionQuery).toEqual({ sampleInterfaceStringField: "sampleInterfaceStringField", sampleType3StringField: "sampleType3StringField", }); }); it("should properly transform number enum argument", async () => { const document = gql` query { sampleNumberEnumQuery(numberEnum: OptionOne) } `; const { data } = await execute({ schema, document }); expect(data!.sampleNumberEnumQuery).toBe("OptionOne"); expect(enumValue).toBe(0); }); it("should properly transform string enum argument", async () => { const document = gql` query { sampleStringEnumQuery(stringEnum: OptionTwo) } `; const { data } = await execute({ schema, document }); expect(data!.sampleStringEnumQuery).toBe("OptionTwo"); expect(enumValue).toBe("OptionTwoString"); }); it("should properly execute field resolver for object type with two underscores NOT in the beginning", async () => { const document = gql` query { sampleQueryOnObjectTypeWithDoubleUnderScore { sampleResolvedField sampleInterfaceStringField sampleType4StringField } } `; const { data } = await execute({ schema, document }); expect(data!.sampleQueryOnObjectTypeWithDoubleUnderScore).toEqual({ sampleResolvedField: "sampleResolvedField", sampleInterfaceStringField: "sampleInterfaceStringField", sampleType4StringField: "sampleType4StringField", }); }); it("should properly run subscriptions", async () => { const document = gql` subscription { sampleSubscription } `; const payload = 5.4321; const iterator = (await subscribe({ schema, document })) as AsyncIterator; const firstValuePromise = iterator.next(); pubSub.publish("SAMPLE", payload); const data = await firstValuePromise; expect(data.value.data!.sampleSubscription).toBe(payload); }); it("should generate simple resolvers function for queries and mutations", async () => { expect((resolvers.Query as ResolverObject).sampleDateQuery).toBeInstanceOf( Function, ); expect( (resolvers.Mutation as ResolverObject).sampleBooleanMutation, ).toBeInstanceOf(Function); }); it("should generate resolvers object for subscriptions", async () => { const sampleSubscription = (resolvers.Subscription as ResolverObject) .sampleSubscription as ResolverOptions; expect(sampleSubscription.resolve).toBeInstanceOf(Function); expect(sampleSubscription.subscribe).toBeInstanceOf(Function); }); }); }); describe("buildTypeDefsAndResolversSync", () => { let typeDefs: string; let resolvers: ResolversMap; let schemaIntrospection: IntrospectionSchema; let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleType { @Field() sampleInterfaceStringField!: string; @Field({ description: "sampleTypeStringFieldDescription" }) sampleTypeStringField!: string; } @Resolver() class SampleResolver { @Query() sampleBooleanQuery(): boolean { return true; } } ({ typeDefs, resolvers } = buildTypeDefsAndResolversSync({ resolvers: [SampleResolver], authChecker: () => false, orphanedTypes: [SampleType], })); schema = makeExecutableSchema({ typeDefs, resolvers, }); const introspectionResult = await graphql({ schema, source: getIntrospectionQuery() }); schemaIntrospection = (introspectionResult.data as unknown as IntrospectionQuery).__schema; }); it("should generate schema without errors", () => { expect(schemaIntrospection).toBeDefined(); }); describe("typeDefs", () => { it("should generate typeDefs correctly", async () => { expect(typeDefs).toBeDefined(); }); it("should generate object types", async () => { const sampleType = schemaIntrospection.types.find( it => it.name === "SampleType", ) as IntrospectionObjectType; const sampleTypeStringField = sampleType.fields.find( it => it.name === "sampleTypeStringField", )!; expect(sampleType.kind).toBe(TypeKind.OBJECT); expect(sampleType.fields).toHaveLength(2); expect(sampleTypeStringField.description).toEqual("sampleTypeStringFieldDescription"); expect(sampleType.interfaces).toHaveLength(0); }); }); describe("resolvers", () => { it("should generate resolversMap without errors", async () => { expect(resolvers).toBeDefined(); }); it("should allow for simple boolean query", async () => { const document = gql` query { sampleBooleanQuery } `; const { data, errors } = await execute({ schema, document }); expect(errors).toBeUndefined(); expect(data!.sampleBooleanQuery).toBe(true); }); }); }); }); ================================================ FILE: tests/functional/unions.ts ================================================ /* eslint "no-underscore-dangle": ["error", { "allow": ["__typename"] }] */ import "reflect-metadata"; import { type GraphQLSchema, type IntrospectionObjectType, type IntrospectionSchema, type IntrospectionUnionType, TypeKind, graphql, } from "graphql"; import { Field, ObjectType, Query, Resolver, buildSchema, createUnionType } from "type-graphql"; import { getMetadataStorage } from "@/metadata/getMetadataStorage"; import { getInnerFieldType, getInnerTypeOfNonNullableType } from "../helpers/getInnerFieldType"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; describe("Unions", () => { let schemaIntrospection: IntrospectionSchema; let queryType: IntrospectionObjectType; let schema: GraphQLSchema; beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class ObjectOne { @Field() fieldOne!: string; } @ObjectType() class ObjectTwo { @Field() fieldTwo!: string; } @ObjectType() class ObjectThree { @Field() fieldThree!: string; } const OneTwoThreeUnion = createUnionType({ name: "OneTwoThreeUnion", description: "OneTwoThreeUnion description", types: () => [ObjectOne, ObjectTwo, ObjectThree], }); const OneTwoThreeUnionFn = createUnionType({ name: "OneTwoThreeUnionFn", description: "OneTwoThreeUnionFn description", types: () => [ObjectOne, ObjectTwo, ObjectThree], }); const UnionWithStringResolveType = createUnionType({ name: "UnionWithStringResolveType", types: () => [ObjectOne, ObjectTwo], resolveType: value => { if ("fieldOne" in value) { return "ObjectOne"; } if ("fieldTwo" in value) { return "ObjectTwo"; } return undefined; }, }); const UnionWithClassResolveType = createUnionType({ name: "UnionWithClassResolveType", types: () => [ObjectOne, ObjectTwo], resolveType: value => { if ("fieldOne" in value) { return ObjectOne; } if ("fieldTwo" in value) { return ObjectTwo; } return undefined; }, }); @ObjectType() class ObjectUnion { @Field(() => OneTwoThreeUnion) unionField!: typeof OneTwoThreeUnion; } class SampleResolver { @Query(() => OneTwoThreeUnion) getObjectOneFromUnion(): typeof OneTwoThreeUnion { const oneInstance = new ObjectTwo(); oneInstance.fieldTwo = "fieldTwo"; return oneInstance; } @Query(() => OneTwoThreeUnionFn) getObjectOneFromUnionFn(): typeof OneTwoThreeUnionFn { const oneInstance = new ObjectTwo(); oneInstance.fieldTwo = "fieldTwo"; return oneInstance; } @Query() getObjectWithUnion(): ObjectUnion { const oneInstance = new ObjectTwo(); oneInstance.fieldTwo = "fieldTwo"; return { unionField: oneInstance, }; } @Query(() => OneTwoThreeUnion) getPlainObjectFromUnion(): typeof OneTwoThreeUnion { return { fieldTwo: "fieldTwo", }; } @Query(() => UnionWithStringResolveType) getObjectOneFromStringResolveTypeUnion(): typeof UnionWithStringResolveType { return { fieldTwo: "fieldTwo", }; } @Query(() => UnionWithClassResolveType) getObjectOneFromClassResolveTypeUnion(): typeof UnionWithClassResolveType { return { fieldTwo: "fieldTwo", }; } } const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); schema = schemaInfo.schema; schemaIntrospection = schemaInfo.schemaIntrospection; queryType = schemaInfo.queryType; }); describe("Schema", () => { it("should generate schema without errors", async () => { expect(schemaIntrospection).toBeDefined(); }); it("should correctly generate union type", async () => { const oneTwoThreeUnionType = schemaIntrospection.types.find( type => type.name === "OneTwoThreeUnion", ) as IntrospectionUnionType; const objectOne = oneTwoThreeUnionType.possibleTypes.find(type => type.name === "ObjectOne")!; const objectTwo = oneTwoThreeUnionType.possibleTypes.find(type => type.name === "ObjectTwo")!; const objectThree = oneTwoThreeUnionType.possibleTypes.find( type => type.name === "ObjectThree", )!; expect(oneTwoThreeUnionType.kind).toEqual(TypeKind.UNION); expect(oneTwoThreeUnionType.name).toEqual("OneTwoThreeUnion"); expect(oneTwoThreeUnionType.description).toEqual("OneTwoThreeUnion description"); expect(objectOne.kind).toEqual(TypeKind.OBJECT); expect(objectTwo.kind).toEqual(TypeKind.OBJECT); expect(objectThree.kind).toEqual(TypeKind.OBJECT); }); it("should correctly generate union type from function syntax", async () => { const oneTwoThreeUnionFnType = schemaIntrospection.types.find( type => type.name === "OneTwoThreeUnionFn", ) as IntrospectionUnionType; const objectOne = oneTwoThreeUnionFnType.possibleTypes.find( type => type.name === "ObjectOne", )!; const objectTwo = oneTwoThreeUnionFnType.possibleTypes.find( type => type.name === "ObjectTwo", )!; const objectThree = oneTwoThreeUnionFnType.possibleTypes.find( type => type.name === "ObjectThree", )!; expect(oneTwoThreeUnionFnType.kind).toEqual(TypeKind.UNION); expect(oneTwoThreeUnionFnType.name).toEqual("OneTwoThreeUnionFn"); expect(oneTwoThreeUnionFnType.description).toEqual("OneTwoThreeUnionFn description"); expect(objectOne.kind).toEqual(TypeKind.OBJECT); expect(objectTwo.kind).toEqual(TypeKind.OBJECT); expect(objectThree.kind).toEqual(TypeKind.OBJECT); }); it("should correctly generate query's union output type", async () => { const getObjectOneFromUnion = queryType.fields.find( field => field.name === "getObjectOneFromUnion", )!; const getObjectOneFromUnionType = getInnerTypeOfNonNullableType(getObjectOneFromUnion); expect(getObjectOneFromUnionType.kind).toEqual(TypeKind.UNION); expect(getObjectOneFromUnionType.name).toEqual("OneTwoThreeUnion"); }); it("should correctly generate object type's union output type", async () => { const objectUnion = schemaIntrospection.types.find( type => type.name === "ObjectUnion", ) as IntrospectionObjectType; const objectUnionFieldType = getInnerFieldType(objectUnion, "unionField"); expect(objectUnionFieldType.kind).toEqual(TypeKind.UNION); expect(objectUnionFieldType.name).toEqual("OneTwoThreeUnion"); }); }); describe("Functional", () => { it("should correctly recognize returned object type using default `instance of` check", async () => { const query = `query { getObjectOneFromUnion { __typename ... on ObjectOne { fieldOne } ... on ObjectTwo { fieldTwo } } }`; const result: any = await graphql({ schema, source: query }); const data = result.data!.getObjectOneFromUnion; expect(data.__typename).toEqual("ObjectTwo"); expect(data.fieldTwo).toEqual("fieldTwo"); expect(data.fieldOne).toBeUndefined(); }); it("should correctly recognize returned object type using string provided by `resolveType` function", async () => { const query = `query { getObjectOneFromStringResolveTypeUnion { __typename ... on ObjectOne { fieldOne } ... on ObjectTwo { fieldTwo } } }`; const result: any = await graphql({ schema, source: query }); const data = result.data!.getObjectOneFromStringResolveTypeUnion; expect(data.__typename).toEqual("ObjectTwo"); expect(data.fieldTwo).toEqual("fieldTwo"); expect(data.fieldOne).toBeUndefined(); }); it("should correctly recognize returned object type using class provided by `resolveType` function", async () => { const query = `query { getObjectOneFromClassResolveTypeUnion { __typename ... on ObjectOne { fieldOne } ... on ObjectTwo { fieldTwo } } }`; const result: any = await graphql({ schema, source: query }); const data = result.data!.getObjectOneFromClassResolveTypeUnion; expect(data.__typename).toEqual("ObjectTwo"); expect(data.fieldTwo).toEqual("fieldTwo"); expect(data.fieldOne).toBeUndefined(); }); it("should correctly recognize returned object type from union on object field", async () => { const query = `query { getObjectWithUnion { unionField { __typename ... on ObjectOne { fieldOne } ... on ObjectTwo { fieldTwo } } } }`; const result: any = await graphql({ schema, source: query }); const unionFieldData = result.data!.getObjectWithUnion.unionField; expect(unionFieldData.__typename).toEqual("ObjectTwo"); expect(unionFieldData.fieldTwo).toEqual("fieldTwo"); expect(unionFieldData.fieldOne).toBeUndefined(); }); it("should throw error when not returning instance of object class", async () => { const query = `query { getPlainObjectFromUnion { __typename ... on ObjectOne { fieldOne } ... on ObjectTwo { fieldTwo } } }`; const result: any = await graphql({ schema, source: query }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const errorMessage = result.errors![0].message; expect(errorMessage).toContain("resolve"); expect(errorMessage).toContain("OneTwoThreeUnion"); expect(errorMessage).toContain("instance"); expect(errorMessage).toContain("plain"); }); }); describe("typings", () => { it("should correctly transform to TS union type when using extending classes", async () => { getMetadataStorage().clear(); @ObjectType() class Base { @Field() base!: string; } @ObjectType() class Extended extends Base { @Field() extended!: string; } expect(() => { createUnionType({ name: "ExtendedBase", types: () => [Base, Extended] as const, }); }).not.toThrow(); }); }); describe("Multiple schemas", () => { it("should correctly return data from union query for all schemas that uses the same union", async () => { getMetadataStorage().clear(); @ObjectType() class One { @Field() one!: string; } @ObjectType() class Two { @Field() two!: string; } const OneTwo = createUnionType({ name: "OneTwo", types: () => [One, Two], }); @Resolver() class OneTwoResolver { @Query(() => OneTwo) oneTwo(): typeof OneTwo { const one = new One(); one.one = "one"; return one; } } const query = /* graphql */ ` query { oneTwo { __typename ... on One { one } ... on Two { two } } } `; const firstSchema = await buildSchema({ resolvers: [OneTwoResolver], }); const secondSchema = await buildSchema({ resolvers: [OneTwoResolver], }); const firstResult = await graphql({ schema: firstSchema, source: query }); const secondResult = await graphql({ schema: secondSchema, source: query }); expect(firstResult.errors).toBeUndefined(); expect(firstResult.data!.oneTwo).toEqual({ __typename: "One", one: "one", }); expect(secondResult.errors).toBeUndefined(); expect(secondResult.data!.oneTwo).toEqual({ __typename: "One", one: "one", }); }); it("should correctly return data from union query for all schemas that uses the same union when string `resolveType` is provided", async () => { getMetadataStorage().clear(); @ObjectType() class One { @Field() one!: string; } @ObjectType() class Two { @Field() two!: string; } const OneTwo = createUnionType({ name: "OneTwo", types: () => [One, Two], resolveType: value => { if ("one" in value) { return "One"; } if ("two" in value) { return "Two"; } throw new Error("Unknown union error"); }, }); @Resolver() class OneTwoResolver { @Query(() => OneTwo) oneTwo(): typeof OneTwo { const one = new One(); one.one = "one"; return one; } } const query = /* graphql */ ` query { oneTwo { __typename ... on One { one } ... on Two { two } } } `; const firstSchema = await buildSchema({ resolvers: [OneTwoResolver], }); const secondSchema = await buildSchema({ resolvers: [OneTwoResolver], }); const firstResult = await graphql({ schema: firstSchema, source: query }); const secondResult = await graphql({ schema: secondSchema, source: query }); expect(firstResult.errors).toBeUndefined(); expect(firstResult.data!.oneTwo).toEqual({ __typename: "One", one: "one", }); expect(secondResult.errors).toBeUndefined(); expect(secondResult.data!.oneTwo).toEqual({ __typename: "One", one: "one", }); }); it("should correctly return data from union query for all schemas that uses the same union when class `resolveType` is provided", async () => { getMetadataStorage().clear(); @ObjectType() class One { @Field() one!: string; } @ObjectType() class Two { @Field() two!: string; } const OneTwo = createUnionType({ name: "OneTwo", types: () => [One, Two], resolveType: value => { if ("one" in value) { return One; } if ("two" in value) { return Two; } throw new Error("Unknown union error"); }, }); @Resolver() class OneTwoResolver { @Query(() => OneTwo) oneTwo(): typeof OneTwo { const one = new One(); one.one = "one"; return one; } } const query = /* graphql */ ` query { oneTwo { __typename ... on One { one } ... on Two { two } } } `; const firstSchema = await buildSchema({ resolvers: [OneTwoResolver], }); const secondSchema = await buildSchema({ resolvers: [OneTwoResolver], }); const firstResult = await graphql({ schema: firstSchema, source: query }); const secondResult = await graphql({ schema: secondSchema, source: query }); expect(firstResult.errors).toBeUndefined(); expect(firstResult.data!.oneTwo).toEqual({ __typename: "One", one: "one", }); expect(secondResult.errors).toBeUndefined(); expect(secondResult.data!.oneTwo).toEqual({ __typename: "One", one: "one", }); }); it("should should fail with error info when `resolveType` returns undefined", async () => { getMetadataStorage().clear(); @ObjectType() class One { @Field() one!: string; } @ObjectType() class Two { @Field() two!: string; } const OneTwo = createUnionType({ name: "OneTwo", types: () => [One, Two], resolveType: () => undefined, }); @Resolver() class OneTwoResolver { @Query(() => OneTwo) oneTwo(): typeof OneTwo { const one = new One(); one.one = "one"; return one; } } const query = /* graphql */ ` query { oneTwo { __typename ... on One { one } ... on Two { two } } } `; const testSchema = await buildSchema({ resolvers: [OneTwoResolver], }); const result: any = await graphql({ schema: testSchema, source: query }); expect(result.errors?.[0]?.message).toMatchInlineSnapshot( `"Abstract type "OneTwo" must resolve to an Object type at runtime for field "Query.oneTwo". Either the "OneTwo" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function."`, ); }); }); }); ================================================ FILE: tests/functional/validation.ts ================================================ import "reflect-metadata"; import { Max, MaxLength, Min, ValidateNested } from "class-validator"; import { type GraphQLSchema, graphql } from "graphql"; import { Arg, Args, ArgsType, ArgumentValidationError, Field, InputType, Mutation, ObjectType, Query, Resolver, type ResolverData, buildSchema, getMetadataStorage, } from "type-graphql"; import { type TypeValue } from "@/decorators/types"; describe("Validation", () => { describe("Functional", () => { let schema: GraphQLSchema; let argInput: any; let argsData: any; beforeEach(() => { argInput = undefined; argsData = undefined; }); beforeAll(async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @InputType() class SampleInput { @Field() @MaxLength(5) stringField!: string; @Field() @Max(5) numberField!: number; @Field({ nullable: true }) @Min(5) optionalField?: number; @Field(() => SampleInput, { nullable: true }) @ValidateNested() nestedField?: SampleInput; @Field(() => [SampleInput], { nullable: true }) @ValidateNested({ each: true }) arrayField?: SampleInput[]; } @ArgsType() class SampleArguments { @Field() @MaxLength(5) stringField!: string; @Field() @Max(5) numberField!: number; @Field({ nullable: true }) @Min(5) optionalField?: number; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args() args: SampleArguments): SampleObject { argsData = args; return {}; } @Mutation() sampleMutation(@Arg("input") input: SampleInput): SampleObject { argInput = input; return {}; } @Mutation() mutationWithInputsArray( @Arg("inputs", () => [SampleInput]) inputs: SampleInput[], ): SampleObject { argInput = inputs; return {}; } @Mutation() mutationWithOptionalInputsArray( @Arg("inputs", () => [SampleInput], { nullable: "items" }) inputs: Array, ): SampleObject { argInput = inputs; return {}; } } schema = await buildSchema({ resolvers: [SampleResolver], validate: true, }); }); it("should pass input validation when data without optional field is correct", async () => { const mutation = `mutation { sampleMutation(input: { stringField: "12345", numberField: 5, }) { field } }`; await graphql({ schema, source: mutation }); expect(argInput).toEqual({ stringField: "12345", numberField: 5 }); }); it("should pass input validation when data with optional field is correct", async () => { const mutation = `mutation { sampleMutation(input: { stringField: "12345", numberField: 5, optionalField: 5, }) { field } }`; await graphql({ schema, source: mutation }); expect(argInput).toEqual({ stringField: "12345", numberField: 5, optionalField: 5 }); }); it("should throw validation error when input is incorrect", async () => { const mutation = `mutation { sampleMutation(input: { stringField: "12345", numberField: 15, }) { field } }`; const result: any = await graphql({ schema, source: mutation }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("numberField"); }); it("should throw validation error when nested input field is incorrect", async () => { const mutation = `mutation { sampleMutation(input: { stringField: "12345", numberField: 5, nestedField: { stringField: "12345", numberField: 15, } }) { field } }`; const result: any = await graphql({ schema, source: mutation }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("nestedField"); }); it("should throw validation error when nested array input field is incorrect", async () => { const mutation = `mutation { sampleMutation(input: { stringField: "12345", numberField: 5, arrayField: [{ stringField: "12345", numberField: 15, }] }) { field } }`; const result: any = await graphql({ schema, source: mutation }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("arrayField"); }); it("should throw validation error when one of input array is incorrect", async () => { const mutation = `mutation { mutationWithInputsArray(inputs: [ { stringField: "12345", numberField: 5, }, { stringField: "12345", numberField: 15, }, ]) { field } }`; const result: any = await graphql({ schema, source: mutation }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("numberField"); }); it("should not throw error when one of optional items in the input array is null", async () => { const mutation = `mutation { mutationWithOptionalInputsArray(inputs: [ null, { stringField: "12345", numberField: 5 }, ]) { field } }`; const result = await graphql({ schema, source: mutation }); expect(result.errors).toBeUndefined(); expect(result.data).toEqual({ mutationWithOptionalInputsArray: { field: null } }); }); it("should properly validate arg array when one of optional items in the input array is incorrect", async () => { const mutation = `mutation { mutationWithOptionalInputsArray(inputs: [ null, { stringField: "12345", numberField: 5 }, { stringField: "12345", numberField: 15, }, ]) { field } }`; const result = await graphql({ schema, source: mutation }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("numberField"); }); it("should throw validation error when optional input field is incorrect", async () => { const mutation = `mutation { sampleMutation(input: { stringField: "12345", numberField: 5, optionalField: -5, }) { field } }`; const result: any = await graphql({ schema, source: mutation }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("optionalField"); }); it("should pass input validation when arguments data without optional field is correct", async () => { const query = `query { sampleQuery( stringField: "12345", numberField: 5, ) { field } }`; await graphql({ schema, source: query }); expect(argsData).toEqual({ stringField: "12345", numberField: 5 }); }); it("should pass input validation when arguments data with optional field is correct", async () => { const query = `query { sampleQuery( stringField: "12345", numberField: 5, optionalField: 5, ) { field } }`; await graphql({ schema, source: query }); expect(argsData).toEqual({ stringField: "12345", numberField: 5, optionalField: 5 }); }); it("should throw validation error when one of arguments is incorrect", async () => { const query = `query { sampleQuery( stringField: "12345", numberField: 15, ) { field } }`; const result: any = await graphql({ schema, source: query }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("numberField"); }); it("should throw validation error when optional argument is incorrect", async () => { const query = `query { sampleQuery( stringField: "12345", numberField: 5, optionalField: -5, ) { field } }`; const result: any = await graphql({ schema, source: query }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("optionalField"); }); }); describe("Settings", () => { let localArgsData: any; beforeEach(() => { localArgsData = undefined; }); it("should pass incorrect args when validation is turned off by default", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @ArgsType() class SampleArguments { @Field() @MaxLength(5) field!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args() args: SampleArguments): SampleObject { localArgsData = args; return {}; } } const localSchema = await buildSchema({ resolvers: [SampleResolver], // default - `validate: false,` }); const query = `query { sampleQuery( field: "12345678", ) { field } }`; await graphql({ schema: localSchema, source: query }); expect(localArgsData).toEqual({ field: "12345678" }); }); it("should pass incorrect args when validation is turned off explicitly", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @ArgsType() class SampleArguments { @Field() @MaxLength(5) field!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args() args: SampleArguments): SampleObject { localArgsData = args; return {}; } } const localSchema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); const query = `query { sampleQuery( field: "12345678", ) { field } }`; await graphql({ schema: localSchema, source: query }); expect(localArgsData).toEqual({ field: "12345678" }); }); it("should pass incorrect args when validation is locally turned off", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @ArgsType() class SampleArguments { @Field() @MaxLength(5) field!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args({ validate: false }) args: SampleArguments): SampleObject { localArgsData = args; return {}; } } const localSchema = await buildSchema({ resolvers: [SampleResolver], validate: true, }); const query = `query { sampleQuery( field: "12345678", ) { field } }`; await graphql({ schema: localSchema, source: query }); expect(localArgsData).toEqual({ field: "12345678" }); }); it("should throw validation error when validation is locally turned on", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @ArgsType() class SampleArguments { @Field() @MaxLength(5) field!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args({ validate: true }) args: SampleArguments): SampleObject { localArgsData = args; return {}; } } const localSchema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); const query = `query { sampleQuery( field: "12345678", ) { field } }`; const result: any = await graphql({ schema: localSchema, source: query }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("field"); }); it("should throw validation error for incorrect args when applied local validation settings", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @ArgsType() class SampleArguments { @Field() @MaxLength(5, { groups: ["test"] }) field!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args({ validate: { groups: ["test"] } }) args: SampleArguments): SampleObject { localArgsData = args; return {}; } } const localSchema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); const query = `query { sampleQuery( field: "12345678", ) { field } }`; const result: any = await graphql({ schema: localSchema, source: query }); expect(result.data).toBeNull(); expect(result.errors).toHaveLength(1); const validationError = result.errors![0].originalError! as ArgumentValidationError; expect(validationError).toBeInstanceOf(ArgumentValidationError); expect(validationError.extensions.validationErrors).toHaveLength(1); expect(validationError.extensions.validationErrors[0].property).toEqual("field"); }); it("should pass validation of incorrect args when applied local validation settings", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @ArgsType() class SampleArguments { @Field() @MaxLength(5, { groups: ["not-test"] }) field!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args({ validate: { groups: ["test"] } }) args: SampleArguments): SampleObject { localArgsData = args; return {}; } } const localSchema = await buildSchema({ resolvers: [SampleResolver], validate: false, }); const query = `query { sampleQuery( field: "123456789", ) { field } }`; await graphql({ schema: localSchema, source: query }); expect(localArgsData).toEqual({ field: "123456789" }); }); it("should merge local validation settings with global one", async () => { getMetadataStorage().clear(); @ObjectType() class SampleObject { @Field({ nullable: true }) field?: string; } @ArgsType() class SampleArguments { @Field() @MaxLength(5, { groups: ["test"] }) field!: string; } @Resolver(() => SampleObject) class SampleResolver { @Query() sampleQuery(@Args({ validate: { groups: ["test"] } }) args: SampleArguments): SampleObject { localArgsData = args; return {}; } } const localSchema = await buildSchema({ resolvers: [SampleResolver], validate: { validationError: { target: false } }, }); const query = `query { sampleQuery( field: "123456789", ) { field } }`; const { errors } = await graphql({ schema: localSchema, source: query }); const error = errors![0].originalError! as ArgumentValidationError; expect(localArgsData).toBeUndefined(); expect(error.extensions.validationErrors[0].target).toBeUndefined(); }); }); }); describe("Custom validation", () => { let schema: GraphQLSchema; const document = /* graphql */ ` query { sampleQuery(sampleField: "sampleFieldValue") } `; let sampleArgsCls: Function; let sampleInputCls: Function; let sampleResolverCls: Function; let validateArgs: Array = []; let validateTypes: TypeValue[] = []; let validateResolverData: ResolverData[] = []; let sampleQueryArgs: any[] = []; beforeEach(() => { // Reset ALL shared state getMetadataStorage().clear(); validateArgs = []; validateTypes = []; validateResolverData = []; sampleQueryArgs = []; @ArgsType() class SampleArgs { @Field() sampleField!: string; } sampleArgsCls = SampleArgs; @InputType() class SampleInput { @Field() sampleField!: string; } sampleInputCls = SampleInput; @Resolver() class SampleResolver { @Query(() => Boolean) sampleQuery(@Args() args: SampleArgs) { sampleQueryArgs.push(args); return true; } @Query(() => Boolean) sampleArrayArgQuery(@Arg("arrayArg", () => [SampleInput]) arrayArg: SampleInput[]) { sampleQueryArgs.push(arrayArg); return true; } @Query() sampleInlineArgValidateFnQuery( @Arg("arg", { validateFn: (arg, type, resolverData) => { validateArgs.push(arg); validateTypes.push(type); validateResolverData.push(resolverData); }, }) arg: SampleInput, ): string { return arg.sampleField; } } sampleResolverCls = SampleResolver; }); it("should call `validateFn` function provided in option with proper params", async () => { schema = await buildSchema({ resolvers: [sampleResolverCls], validateFn: (arg, type, resolverData) => { validateArgs.push(arg); validateTypes.push(type); validateResolverData.push(resolverData); }, }); await graphql({ schema, source: document, contextValue: { isContext: true } }); expect(validateArgs).toEqual([{ sampleField: "sampleFieldValue" }]); expect(validateArgs[0]).toBeInstanceOf(sampleArgsCls); expect(validateTypes).toEqual([sampleArgsCls]); expect(validateResolverData).toEqual([ expect.objectContaining({ context: { isContext: true }, }), ]); }); it("should let `validateFn` function handle array args", async () => { schema = await buildSchema({ resolvers: [sampleResolverCls], validateFn: (arg, type) => { validateArgs.push(arg); validateTypes.push(type); }, }); await graphql({ schema, source: /* graphql */ ` query { sampleArrayArgQuery(arrayArg: [{ sampleField: "sampleFieldValue" }]) } `, }); expect(validateArgs).toEqual([[{ sampleField: "sampleFieldValue" }]]); expect((validateArgs[0] as object[])[0]).toBeInstanceOf(sampleInputCls); expect(validateTypes).toEqual([sampleInputCls]); }); it("should inject validated arg as resolver param", async () => { schema = await buildSchema({ resolvers: [sampleResolverCls], validateFn: () => { // do nothing }, }); await graphql({ schema, source: document }); expect(sampleQueryArgs).toEqual([{ sampleField: "sampleFieldValue" }]); }); it("should call `validateFn` function provided inline in arg option with proper params", async () => { schema = await buildSchema({ resolvers: [sampleResolverCls], }); await graphql({ schema, source: /* graphql */ ` query { sampleInlineArgValidateFnQuery(arg: { sampleField: "sampleArgValue" }) } `, contextValue: { isContext: true }, }); expect(validateArgs).toEqual([{ sampleField: "sampleArgValue" }]); expect(validateArgs[0]).toBeInstanceOf(sampleInputCls); expect(validateTypes).toEqual([sampleInputCls]); expect(validateResolverData).toEqual([ expect.objectContaining({ context: { isContext: true }, }), ]); }); it("should rethrow wrapped error when error thrown in `validate`", async () => { schema = await buildSchema({ resolvers: [sampleResolverCls], validateFn: () => { throw new Error("Test validate error"); }, }); const result: any = await graphql({ schema, source: document }); expect(result.errors).toHaveLength(1); expect(result.errors![0].message).toEqual("Test validate error"); expect(result.data).toBeNull(); expect(sampleQueryArgs).toHaveLength(0); }); }); ================================================ FILE: tests/helpers/circular-refs/good/CircularRef1.ts ================================================ import { Field, ObjectType } from "type-graphql"; // eslint-disable-next-line import/no-cycle import { CircularRef2 } from "./CircularRef2"; let hasModuleFinishedInitialLoad = false; @ObjectType() export class CircularRef1 { @Field(() => { if (!hasModuleFinishedInitialLoad) { throw new Error("Field type function was called synchronously during module load"); } return [CircularRef2]; }) ref2Field!: any[]; } hasModuleFinishedInitialLoad = true; ================================================ FILE: tests/helpers/circular-refs/good/CircularRef2.ts ================================================ import { Field, ObjectType } from "type-graphql"; // eslint-disable-next-line import/no-cycle import { CircularRef1 } from "./CircularRef1"; let hasModuleFinishedInitialLoad = false; @ObjectType() export class CircularRef2 { @Field(() => { if (!hasModuleFinishedInitialLoad) { throw new Error("Field type function was called synchronously during module load"); } return [CircularRef1]; }) ref1Field!: any[]; } hasModuleFinishedInitialLoad = true; ================================================ FILE: tests/helpers/circular-refs/wrong/CircularRef1.ts ================================================ import { Field, ObjectType } from "type-graphql"; // eslint-disable-next-line import/no-cycle import { CircularRef2 } from "./CircularRef2"; @ObjectType() export class CircularRef1 { @Field() ref2Field!: CircularRef2; } ================================================ FILE: tests/helpers/circular-refs/wrong/CircularRef2.ts ================================================ import { Field, ObjectType } from "type-graphql"; // eslint-disable-next-line import/no-cycle import { CircularRef1 } from "./CircularRef1"; @ObjectType() export class CircularRef2 { @Field() ref1Field!: CircularRef1; } ================================================ FILE: tests/helpers/customScalar.ts ================================================ import { GraphQLScalarType } from "graphql"; export const CustomScalar = new GraphQLScalarType({ name: "Custom", parseLiteral: () => "TypeGraphQL parseLiteral", parseValue: () => "TypeGraphQL parseValue", serialize: () => "TypeGraphQL serialize", }); export class CustomType {} export const ObjectScalar = new GraphQLScalarType({ name: "ObjectScalar", parseLiteral: () => ({ value: "TypeGraphQL parseLiteral", }), parseValue: () => ({ value: "TypeGraphQL parseValue", }), serialize: (obj: any) => obj.value, }); ================================================ FILE: tests/helpers/directives/TestDirective.ts ================================================ import { MapperKind, getDirective, mapSchema } from "@graphql-tools/utils"; import { DirectiveLocation, type GraphQLArgumentConfig, GraphQLDirective, type GraphQLFieldConfig, type GraphQLInputFieldConfig, GraphQLInputObjectType, type GraphQLInputObjectTypeConfig, GraphQLInterfaceType, type GraphQLInterfaceTypeConfig, GraphQLNonNull, GraphQLObjectType, type GraphQLObjectTypeConfig, type GraphQLSchema, GraphQLString, } from "graphql"; function mapConfig< TConfig extends | GraphQLFieldConfig | GraphQLObjectTypeConfig | GraphQLInterfaceTypeConfig | GraphQLInputObjectTypeConfig | GraphQLInputFieldConfig | GraphQLArgumentConfig, >(config: TConfig) { return { ...config, extensions: { ...config.extensions, TypeGraphQL: { isMappedByDirective: true, }, }, }; } export const testDirective = new GraphQLDirective({ name: "test", args: { argNonNullDefault: { type: new GraphQLNonNull(GraphQLString), defaultValue: "argNonNullDefault", }, argNullDefault: { type: GraphQLString, defaultValue: "argNullDefault", }, argNull: { type: GraphQLString, }, }, locations: [ DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.INTERFACE, DirectiveLocation.ARGUMENT_DEFINITION, ], }); export function testDirectiveTransformer(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.OBJECT_TYPE]: typeInfo => { const testDirectiveConfig = getDirective(schema, typeInfo, testDirective.name)?.[0]; if (testDirectiveConfig) { const config = typeInfo.toConfig(); return new GraphQLObjectType(mapConfig(config)); } return typeInfo; }, [MapperKind.OBJECT_FIELD]: fieldConfig => { const testDirectiveConfig = getDirective(schema, fieldConfig, testDirective.name)?.[0]; if (testDirectiveConfig) { return mapConfig(fieldConfig); } return fieldConfig; }, [MapperKind.INTERFACE_TYPE]: interfaceConfig => { const testDirectiveConfig = getDirective(schema, interfaceConfig, testDirective.name)?.[0]; if (testDirectiveConfig) { const config = interfaceConfig.toConfig(); return new GraphQLInterfaceType(mapConfig(config)); } return interfaceConfig; }, [MapperKind.INTERFACE_FIELD]: fieldConfig => { const testDirectiveConfig = getDirective(schema, fieldConfig, testDirective.name)?.[0]; if (testDirectiveConfig) { return mapConfig(fieldConfig); } return fieldConfig; }, [MapperKind.INPUT_OBJECT_TYPE]: typeInfo => { const testDirectiveConfig = getDirective(schema, typeInfo, testDirective.name)?.[0]; if (testDirectiveConfig) { const config = typeInfo.toConfig(); return new GraphQLInputObjectType(mapConfig(config)); } return typeInfo; }, [MapperKind.INPUT_OBJECT_FIELD]: fieldConfig => { const testDirectiveConfig = getDirective(schema, fieldConfig, testDirective.name)?.[0]; if (testDirectiveConfig) { return mapConfig(fieldConfig); } return fieldConfig; }, [MapperKind.ARGUMENT]: argumentConfig => { const testDirectiveConfig = getDirective(schema, argumentConfig, testDirective.name)?.[0]; if (testDirectiveConfig) { return mapConfig(argumentConfig); } return argumentConfig; }, }); } ================================================ FILE: tests/helpers/directives/assertValidDirective.ts ================================================ import { type FieldDefinitionNode, type InputObjectTypeDefinitionNode, type InputValueDefinitionNode, type InterfaceTypeDefinitionNode, type ObjectTypeDefinitionNode, parseValue, } from "graphql"; import { type Maybe } from "@/typings"; export function assertValidDirective( astNode: Maybe< | FieldDefinitionNode | ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode | InputValueDefinitionNode | InterfaceTypeDefinitionNode >, name: string, args?: Record, ): void { if (!astNode) { throw new Error(`Directive with name ${name} does not exist`); } const directives = (astNode && astNode.directives) || []; const directive = directives.find(it => it.name.kind === "Name" && it.name.value === name); if (!directive) { throw new Error(`Directive with name ${name} does not exist`); } if (!args) { if (Array.isArray(directive.arguments)) { expect(directive.arguments).toHaveLength(0); } else { expect(directive.arguments).toBeFalsy(); } } else { expect(directive.arguments).toHaveLength(Object.keys(args).length); expect(directive.arguments).toEqual( expect.arrayContaining( Object.keys(args).map(arg => expect.objectContaining({ kind: "Argument", name: expect.objectContaining({ kind: "Name", value: arg }), value: expect.objectContaining(parseValue(args[arg], { noLocation: true })), }), ), ), ); } } ================================================ FILE: tests/helpers/expectToThrow.ts ================================================ export async function expectToThrow(call: () => unknown): Promise { try { await call(); } catch (error: unknown) { return error as TError; } throw new Error("You've expected a function to throw, but it didn't throw anything."); } ================================================ FILE: tests/helpers/getInnerFieldType.ts ================================================ import { type IntrospectionInputObjectType, type IntrospectionInterfaceType, type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionTypeRef, } from "graphql"; export function getInnerTypeOfNonNullableType(definition: { type: IntrospectionTypeRef }) { return (definition.type as IntrospectionNonNullTypeRef).ofType! as IntrospectionNamedTypeRef; } export function getInnerFieldType( type: IntrospectionObjectType | IntrospectionInterfaceType, name: string, ) { return getInnerTypeOfNonNullableType(type.fields.find(field => field.name === name)!); } export function getInnerInputFieldType(type: IntrospectionInputObjectType, name: string) { return getInnerTypeOfNonNullableType(type.inputFields.find(field => field.name === name)!); } export function getItemTypeOfList(definition: { type: IntrospectionTypeRef }) { const listType = (definition.type as IntrospectionNonNullTypeRef) .ofType! as IntrospectionNonNullTypeRef; const itemType = (listType.ofType! as IntrospectionNonNullTypeRef) .ofType as IntrospectionNamedTypeRef; return itemType; } ================================================ FILE: tests/helpers/getSampleObjectFieldType.ts ================================================ import { type IntrospectionNamedTypeRef, type IntrospectionNonNullTypeRef, type IntrospectionObjectType, type IntrospectionSchema, } from "graphql"; export function getSampleObjectFieldType(schemaIntrospection: IntrospectionSchema) { const sampleObject = schemaIntrospection.types.find( type => type.name === "SampleObject", ) as IntrospectionObjectType; return (fieldName: string) => { const field = sampleObject.fields.find(it => it.name === fieldName)!; const fieldType = (field.type as IntrospectionNonNullTypeRef) .ofType as IntrospectionNamedTypeRef; return fieldType; }; } ================================================ FILE: tests/helpers/getSchemaInfo.ts ================================================ /* eslint "no-underscore-dangle": ["error", { "allow": ["__schema"] }] */ import { type IntrospectionObjectType, type IntrospectionSchema, getIntrospectionQuery, graphql, } from "graphql"; import { type BuildSchemaOptions, buildSchema } from "type-graphql"; export async function getSchemaInfo(options: BuildSchemaOptions) { // Build schema from definitions const schema = await buildSchema({ ...options, validate: false, skipCheck: true, }); // Get built schema info from retrospection const result = await graphql({ schema, source: getIntrospectionQuery({ inputValueDeprecation: true, }), }); expect(result.errors).toBeUndefined(); const schemaIntrospection = result.data!.__schema as IntrospectionSchema; expect(schemaIntrospection).toBeDefined(); const queryType = schemaIntrospection.types.find( type => type.name === schemaIntrospection.queryType.name, ) as IntrospectionObjectType; const mutationTypeNameRef = schemaIntrospection.mutationType; let mutationType: IntrospectionObjectType | undefined; if (mutationTypeNameRef) { mutationType = schemaIntrospection.types.find( type => type.name === mutationTypeNameRef.name, ) as IntrospectionObjectType; } const subscriptionTypeNameRef = schemaIntrospection.subscriptionType; let subscriptionType: IntrospectionObjectType | undefined; if (subscriptionTypeNameRef) { subscriptionType = schemaIntrospection.types.find( type => type.name === subscriptionTypeNameRef.name, ) as IntrospectionObjectType; } return { schema, schemaIntrospection, queryType, mutationType, subscriptionType, }; } ================================================ FILE: tests/helpers/getTypeField.ts ================================================ import { type IntrospectionField, type IntrospectionInterfaceType, type IntrospectionObjectType, } from "graphql"; export function getTypeField( type: IntrospectionObjectType | IntrospectionInterfaceType, fieldName: string, ): IntrospectionField { return type.fields.find(field => field.name === fieldName)!; } ================================================ FILE: tests/helpers/sleep.ts ================================================ export function sleep(ms: number): Promise { return new Promise(resolve => { setTimeout(resolve, ms); }); } ================================================ FILE: tests/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "noEmit": true, "noUnusedLocals": false, "emitDecoratorMetadata": true }, "include": [".", "../src"] } ================================================ FILE: tsconfig.cjs.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs", "outDir": "./build/cjs" } } ================================================ FILE: tsconfig.esm.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "es2020", "moduleResolution": "bundler", "outDir": "./build/esm" } } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "module": "commonjs", "declaration": false, "sourceMap": false, "outDir": "./build", "moduleResolution": "node", "removeComments": true, "importHelpers": true, "strict": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": false, "resolveJsonModule": true, "plugins": [ { "transform": "typescript-transform-paths" }, { "transform": "typescript-transform-paths", "afterDeclarations": true }, { "transform": "typescript-transformer-esm", "after": true }, { "transform": "typescript-transformer-esm", "afterDeclarations": true } ], "paths": { "@/*": ["./src/*"], "type-graphql": ["./src/index.ts"] } }, "exclude": ["./node_modules", "./build"], "include": ["./src"] } ================================================ FILE: tsconfig.typings.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./build/typings", "emitDeclarationOnly": true, "declaration": true, "removeComments": false } } ================================================ FILE: website/.gitignore ================================================ node_modules .DS_Store lib/core/metadata.js lib/core/MetadataBlog.js website/translated_docs website/build/ website/yarn.lock website/node_modules website/i18n/* !website/i18n/en.json ================================================ FILE: website/blog/2018-03-25-medium-article.md ================================================ --- title: GraphQL + TypeScript = TypeGraphQL author: Michał Lytek authorURL: https://github.com/MichalLytek authorImageURL: /img/author.jpg --- We all love GraphQL! It’s so great and solves many problems that we have with REST API, like overfetching and underfetching. But developing a GraphQL API in Node.js with TypeScript is sometimes a bit of pain. **TypeGraphQL** makes that process enjoyable, i.a. by defining the schema using only classes and a bit of decorators magic. ![type-graphql-logo](/blog/assets/logo_mini.png) ## Motivation As I mentioned, developing a GraphQL API in Node.js with TypeScript might be a painful process. Why? Let’s take a look at the steps we usually have to make. At first, we create the all the schema types in SDL. We also create our data models using ORM classes, which represents our db entities. Then we start to write resolvers for our queries, mutations and fields but this force us to begin with creating TS interfaces for all arguments and inputs or even object types. And after that we can actually implements the resolvers, using weird generic signatures, e.g.: ```typescript export const recipesResolver: GraphQLFieldResolver = async ( _, args, ) => { // stuffs like validation, auth checking, getting from container // and our business logic, e.g.: const repository = getRepository(Recipe); return repository.find(); }; ``` The biggest problem is the rendundancy in our codebase, that makes difficult to keep this things in sync. To add new field to our entity, we have to jump through all the files — modify entity class, then modify part of the schema and then update the interface. The same goes with inputs or arguments, it’s easy to forget to update one or make a mistake with the type. Also, what if we’ve made a typo in field name? The rename feature (F2) won’t work correctly. **TypeGraphQL** comes to address this issues, based on experience from over a dozen months of developing GraphQL APIs in TypeScript. The main idea is to have only one source of truth by defining the schema using classes and a bit of decorators help. Additional features like dependency injection, validation or auth guards helps with common task that normally we would have to handle by ourselves. ## Getting started To explore all powerful capabilities of TypeGraphQL, we will create a sample GraphQL API for cooking recipes. Let’s start with the Recipe type, which is the foundations of our API. We want to get equivalent of this type described in SDL: ```graphql type Recipe { id: ID! title: String! description: String creationDate: Date! ingredients: [String!]! } ``` So we create the Recipe class with all properties and types: ```typescript class Recipe { id: string; title: string; description?: string; creationDate: Date; ingredients: string[]; } ``` Then we annotate the class and it properties with decorators: ```typescript @ObjectType() class Recipe { @Field(type => ID) id: string; @Field() title: string; @Field({ nullable: true }) description?: string; @Field() creationDate: Date; @Field(type => [String]) ingredients: string[]; } ``` The detailed rules when to use `nullable`, `array` and other options are described in [fields and types docs](https://github.com/MichalLytek/type-graphql/blob/master/docs/types-and-fields.md). ### Resolvers After that we want to create typical crud queries and mutation. To do that we create the resolver (controller) class that will have injected RecipeService in constructor: ```typescript @Resolver(Recipe) class RecipeResolver { constructor(private recipeService: RecipeService) {} @Query(returns => Recipe) async recipe(@Arg("id") id: string) { const recipe = await this.recipeService.findById(id); if (recipe === undefined) { throw new RecipeNotFoundError(id); } return recipe; } @Query(returns => [Recipe]) recipes(@Args() { skip, take }: RecipesArgs) { return this.recipeService.findAll({ skip, take }); } @Mutation(returns => Recipe) @Authorized() addRecipe( @Arg("newRecipeData") newRecipeData: NewRecipeInput, @Ctx("user") user: User, ): Promise { return this.recipeService.addNew({ data: newRecipeData, user }); } @Mutation(returns => Boolean) @Authorized(Roles.Admin) async removeRecipe(@Arg("id") id: string) { try { await this.recipeService.removeById(id); return true; } catch { return false; } } } ``` We use `@Authorized()` decorator to restrict access only for authorized users or the one that fulfill the roles requirements. The detailed rules when and why we declare `returns => Recipe` functions and others are described in [resolvers docs](https://github.com/MichalLytek/type-graphql/blob/master/docs/resolvers.md). ### Inputs and arguments Ok, but what are theNewRecipeInput and RecipesArgs? There are of course classes that declares input type and arguments: ```typescript @InputType() class NewRecipeDataInput { @Field() @MaxLength(30) title: string; @Field({ nullable: true }) @Length(30, 255) description?: string; @Field(type => [String]) @MaxArraySize(30) ingredients: string[]; } @ArgsType() class RecipesArgs { @Field(type => Int, { nullable: true }) @Min(0) skip: number = 0; @Field(type => Int, { nullable: true }) @Min(1) @Max(50) take: number = 25; } ``` `@Length`, `@Min` or `@MaxArraySize` are decorators from [`class-validator`](https://github.com/typestack/class-validator) that automatically perform fields validation in TypeGraphQL. ### Building schema The last step that we have to do is to actually build the schema from TypeGraphQL definition. We use buildSchema function for this: ```typescript const schema = await buildSchema({ resolvers: [RecipeResolver], }); // ...creating express server or sth ``` Et voilà! Now we have fully working GraphQL schema! If we print it, we would receive exactly this: ```graphql type Recipe { id: ID! title: String! description: String creationDate: Date! ingredients: [String!]! } input NewRecipeInput { title: String! description: String ingredients: [String!]! } type Query { recipe(id: ID!): Recipe recipes(skip: Int, take: Int): [Recipe!]! } type Mutation { addRecipe(newRecipeData: NewRecipeInput!): Recipe! removeRecipe(id: ID!): Boolean! } ``` ## Want more? That was only a tip of the iceberg — a very simple example with basic GraphQL types. Do you use interfaces, enums, unions and custom scalars? That’s great because TypeGraphQL fully supports them too! If you want to see how it looks in more complicated case, you can go to the [Examples section](https://github.com/MichalLytek/type-graphql/blob/master/examples) where you can find how nice TypeGraphQL integrates with TypeORM. Want to learn about more advanced concepts like authorization checker, inheritance support or field resolvers? Check out the [Docs section](https://github.com/MichalLytek/type-graphql/blob/master/docs). ## Work in progress Currently released version is a **MVP** (Minimum Viable Product). It is well tested (95% coverage, 4400 lines of test code) and has 90% of the planned features already implemented. However there’s some work to be done before 1.0.0 release and it’s mostly about documentation (website, api reference and jsdoc). There are also plans for more features like better TypeORM and dataloader integration or middlewares and custom decorators support — [the full list of ideas](https://github.com/MichalLytek/type-graphql/issues?q=is%3Aissue+is%3Aopen+label%3A%22Enhancement+%3Anew%3A%22) is available on the GitHub repo. You can also keep track of [development’s progress on project board](https://github.com/MichalLytek/type-graphql/projects/1). ## Spread the word I strongly encourage you to give it a try and experiment with **TypeGraphQL**. I promise, it will reduce your codebase by a half or more! If you find this framework interesting, please [star the GitHub repository](https://github.com/MichalLytek/type-graphql), clap for this article and share it on your social media, if you don’t mind. The more feedback I receive, the more time I will devote to continue the development of TypeGraphQL! ================================================ FILE: website/blog/2020-08-19-devto-article.md ================================================ --- title: Announcing TypeGraphQL 1.0 🚀 author: Michał Lytek authorURL: https://github.com/MichalLytek authorImageURL: /img/author.jpg --- It's finally happening! Over two years after the [initial announcement](http://localhost:3000/blog/2018/03/25/medium-article.html), [TypeGraphQL](https://typegraphql.com/) is now ready for its first stable release - `v1.0.0` 🎉 It was a really long journey that started in 31st of January 2018 with [releasing `v0.1.0`](https://www.npmjs.com/package/type-graphql/v/0.1.0) and which contained 650+ commits, 85+ merged PRs and [4.9k+ stars on GitHub](https://github.com/MichalLytek/type-graphql). This post is focused mostly on presenting new features and describing changes in the newest stable release. Well, then, without further ado... let's take a look what the TypeGraphQL 1.0 brings us! - [Performance](#performance) - [Schema isolation](#isolation) - [Directives and extensions](#directives-extensions) - [Resolvers and arguments for interface fields](#interfaces) - [More descriptive errors messages](#errors) - [Transforming nested inputs and arrays](#transforming) - [...and others 👀](#others) ## Performance One of the most important things which is also often neglected by developers - the performance. One of the key focus area for the 1.0 release was making it blazingly fast ⚡ TypeGraphQL is basically an abstraction layer built on top of the reference GraphQL implementation for JavaScript - `graphql-js`. To measure the overhead of the abstraction, a few demo examples were made to compare it against the "bare metal" - using raw `graphql-js` library. It turned out that in the most demanding cases like returning an array of 25 000 nested objects, the old version `0.17` was even about 5 times slower! | library | execution time | | ------------------- | :------------: | | TypeGraphQL `v0.17` | 1253.28 ms | | `graphql-js` | 265.52 ms | After profiling the code and finding all the root causes (like always using async execution path), the overhead was reduced from 500% to **just 17%** in `v1.0.0`! By using [`simpleResolvers`](https://typegraphql.com/docs/performance.html#further-performance-tweaks) it can be reduced even further, up to 13%: | | execution time | | ------------------------ | :------------: | | `graphql-js` | 265.52 ms | | **TypeGraphQL `v1.0`** | 310.36 ms | | with "simpleResolvers" | 299.61 ms | | with a global middleware | 1267.82 ms | Such small overhead is much easier to accept than the initial 500%! More info about how to enable the performance optimizations in the more complex cases can be found [in the docs 📖](https://typegraphql.com/docs/performance.html). ## Schema isolation This is another feature that is not visible from the first sight but gives new possibilities like splitting the schema to public and private ones 👀 In 0.17.x and before, the schema was built from all the metadata collected by evaluating the TypeGraphQL decorators. The drawback of this approach was the schema leaks - every subsequent calls of `buildSchema` was returning the same schema which was combined from all the types and resolvers that could be find in the metadata storage. In TypeGraphQL 1.0 it's no longer true! The schemas are now isolated which means that the [`buildSchema` call takes the `resolvers` array from options](https://typegraphql.com/docs/bootstrap.html#create-executable-schema) and emit only the queries, mutation and types that are related to those resolvers. ```ts const firstSchema = await buildSchema({ resolvers: [FirstResolver], }); const secondSchema = await buildSchema({ resolvers: [SecondResolver], }); ``` So just by modifying the `resolvers` option we can have different sets of operations exposed in the GraphQL schemas! Proper isolation also makes serverless development easier as it allows to get rid of the _"Schema must contain uniquely named types"_ errors and others. ## Directives and extensions This two new features are two complementary ways to put some metadata about the schema items. GraphQL directives though the syntax might remind the TS decorators, as "a directive is an identifier preceded by a @ character", but in fact, they are a purely Schema Definition Language feature. Apart from the metadata capabilities, they can also modify the schema and e.g. generate the connection type for pagination purposes. Basically, the looks like this: ```graphql type Query { foobar: String! @auth(requires: USER) } ``` To apply them, we just need to put the `@Directive` decorator above and supply the string argument, e.g.: ```ts @Resolver() class FooBarResolver { @Directive("@auth(requires: USER)") @Query() foobar(): string { return "foobar"; } } ``` However, on the other side we have the GraphQL extensions which are the JS way to achieve the same goal. It's the recommended way of putting the metadata about the types when applying some custom logic. To declare the extensions for type or selected field, we need to use `@Extensions` decorator, e.g.: ```ts @ObjectType() class Foo { @Extensions({ roles: [Role.User] }) @Field() bar: string; } ``` We can then read that metadata in the resolvers or middlewares, just by exploring the `GraphQLResolveInfo` object, e.g.: ```ts export const ExtensionsMiddleware: MiddlewareFn = async ({ info }, next) => { const { extensions } = info.parentType.getFields()[info.fieldName]; console.log(extensions?.roles); // log the metadata return next(); }; ``` More info about [directives](https://typegraphql.com/docs/directives.html) and [extensions](https://typegraphql.com/docs/extensions.html) features can be found in docs 📖 ## Resolvers and arguments for interface fields The last thing that was preventing TypeGraphQL from being fully GraphQL compliant thus blocking the 1.0 release - an ability to provide interface fields resolvers implementations and declare its arguments. Basically, we can define resolvers for the interface fields using the same syntax we would use in case of the `@ObjectType`, e.g.: ```ts @InterfaceType() abstract class IPerson { @Field() avatar(@Arg("size") size: number): string { return `http://i.pravatar.cc/${size}`; } } ``` ...with only a few exceptions for cases like abstract methods and inheritance, which you can [read about in the docs](https://typegraphql.com/docs/interfaces.html#resolvers-and-arguments). ## More descriptive errors messages One of the most irritating issues for newcomers were the laconic error messages that haven't provided enough info to easily find the mistakes in the code. Messages like _"Cannot determine GraphQL input type for users"_ or even the a generic _"Generating schema error"_ were clearly not helpful enough while searching for the place where the flaws were located. Now, when the error occurs, it is broadly explained, why it happened and what could we do to fix that, e.g.: ```text Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for argument named 'filter' of 'getUsers' of 'UserResolver' class. ``` or: ```text Some errors occurred while generating GraphQL schema: Interface field 'IUser.accountBalance' expects type 'String!' but 'Student.accountBalance' is of type 'Float' ``` That should allow developers to safe tons of time and really speed up the development 🏎 ## Transforming nested inputs and arrays In the previous releases, an instance of the input type class was created only on the first level of inputs nesting. So, in cases like this: ```ts @InputType() class SampleInput { @Field() sampleStringField: string; @Field() nestedField: SomeNestedInput; } @Resolver() class SampleResolver { @Query() sampleQuery(@Arg("input") input: SampleInput): boolean { return input.nestedField instanceof SomeNestedInput; } } ``` the `nestedField` property of `input` was just a plain `Object`, not an instance of the `SomeNestedInput` class. That behavior was producing some unwanted issues, including limited support for [inputs and args validation](https://typegraphql.com/docs/validation.html). Since 1.0 release, it's no longer an issue and all the nested args and inputs are properly transformed to the corresponding input type classes instances, even including deeply nested arrays 💪 ## One more thing... The 1.0 release is not our last word! We have plenty of feature requests from the community and [tons of our ideas to implement](https://github.com/MichalLytek/type-graphql/labels/Enhancement%20%3Anew%3A), so stay tuned and wait for more! 💪 Also, please keep in mind that TypeGraphQL is an MIT-licensed open source project. It doesn't have a large company that sits behind - its ongoing development is possible only thanks to the support by the community. [![GitHub Sponsors](https://dev-to-uploads.s3.amazonaws.com/i/5hylzjhbjte7lq8ev7gf.png)](https://github.com/sponsors/TypeGraphQL) [![Open Collective](https://opencollective.com/typegraphql/donate/button.png?color=blue)](https://opencollective.com/typegraphql) If you fell in love with TypeGraphQL, please consider supporting our efforts and help it grow, especially if you are using it commercially - just to ensure that the project which your product relies on is actively maintained and improved. ================================================ FILE: website/core/Footer.js ================================================ /** * Copyright (c) 2017-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const React = require("react"); const toggleButtonHtml = /* html */ `
🌙 ☀️
`; const DarkModeButton = () => { return ( <>