├── website ├── versions.json ├── static │ ├── img │ │ ├── logo.png │ │ ├── author.jpg │ │ ├── favicon.png │ │ ├── ts-logo.png │ │ └── favicon │ │ │ └── favicon.ico │ └── css │ │ └── prism-theme.css ├── blog │ └── assets │ │ └── logo_mini.png ├── .gitignore ├── pages │ ├── snippets │ │ ├── validation.md │ │ ├── object-type.md │ │ ├── typeorm.md │ │ └── testability.md │ └── en │ │ ├── users.js │ │ └── help.js ├── package.json ├── sidebars.json ├── versioned_sidebars │ └── version-0.16.0-sidebars.json └── versioned_docs │ └── version-0.16.0 │ ├── browser-usage.md │ └── emit-schema.md ├── logo.png ├── .vscode ├── settings.json └── launch.json ├── examples ├── middlewares │ ├── context.ts │ ├── logger.ts │ ├── middlewares │ │ ├── resolve-time.ts │ │ ├── number-interceptor.ts │ │ ├── log-access.ts │ │ └── error-logger.ts │ ├── recipe │ │ ├── recipe.args.ts │ │ ├── recipe.samples.ts │ │ ├── recipe.resolver.ts │ │ └── recipe.type.ts │ ├── examples.gql │ ├── decorators │ │ └── validate-args.ts │ └── index.ts ├── resolvers-inheritance │ ├── resource │ │ ├── resource.ts │ │ └── resource.service.ts │ ├── person │ │ ├── person.role.ts │ │ ├── person.type.ts │ │ └── person.resolver.ts │ ├── queries.gql │ ├── recipe │ │ ├── recipe.type.ts │ │ └── recipe.resolver.ts │ └── index.ts ├── authorization │ ├── user.interface.ts │ ├── context.interface.ts │ ├── examples.gql │ ├── auth-checker.ts │ ├── recipe.type.ts │ ├── recipe.helpers.ts │ ├── index.ts │ └── resolver.ts ├── redis-subscriptions │ ├── topics.ts │ ├── recipe.resolver.args.ts │ ├── newComment.interface.ts │ ├── comment.type.ts │ ├── comment.input.ts │ ├── recipe.type.ts │ ├── examples.gql │ ├── index.ts │ ├── recipe.samples.ts │ └── recipe.resolver.ts ├── typeorm-lazy-relations │ ├── resolvers │ │ ├── types │ │ │ ├── context.ts │ │ │ ├── rate-input.ts │ │ │ └── recipe-input.ts │ │ └── recipe-resolver.ts │ ├── entities │ │ ├── user.ts │ │ ├── rate.ts │ │ └── recipe.ts │ ├── examples.gql │ ├── helpers.ts │ └── index.ts ├── using-scoped-container │ ├── types.ts │ ├── recipe │ │ ├── recipe.type.ts │ │ ├── recipe.input.ts │ │ ├── recipe.samples.ts │ │ ├── recipe.service.ts │ │ └── recipe.resolver.ts │ ├── logger.ts │ ├── examples.gql │ └── index.ts ├── interfaces-inheritance │ ├── resource │ │ └── resource.interface.ts │ ├── person │ │ ├── person.input.ts │ │ ├── person.type.ts │ │ └── person.interface.ts │ ├── employee │ │ ├── employee.type.ts │ │ └── employee.input.ts │ ├── student │ │ ├── student.type.ts │ │ └── student.input.ts │ ├── helpers.ts │ ├── index.ts │ ├── examples.gql │ └── resolver.ts ├── enums-and-unions │ ├── cook.type.ts │ ├── search-result.union.ts │ ├── difficulty.enum.ts │ ├── cook.samples.ts │ ├── recipe.type.ts │ ├── index.ts │ ├── examples.gql │ ├── resolver.ts │ └── recipe.samples.ts ├── typeorm-basic-usage │ ├── resolvers │ │ ├── types │ │ │ ├── rate-input.ts │ │ │ └── recipe-input.ts │ │ └── rate-resolver.ts │ ├── entities │ │ ├── user.ts │ │ ├── rate.ts │ │ └── recipe.ts │ ├── examples.gql │ ├── helpers.ts │ └── index.ts ├── automatic-validation │ ├── recipe-type.ts │ ├── recipes-arguments.ts │ ├── helpers.ts │ ├── recipe-input.ts │ ├── examples.gql │ ├── index.ts │ └── recipe-resolver.ts ├── apollo-engine │ ├── examples.gql │ ├── cache-control.ts │ ├── recipe-samples.ts │ ├── recipe-type.ts │ ├── recipe-resolver.ts │ └── index.ts ├── simple-usage │ ├── recipe-input.ts │ ├── examples.gql │ ├── recipe-samples.ts │ ├── index.ts │ ├── schema.gql │ ├── recipe-type.ts │ └── recipe-resolver.ts ├── using-container │ ├── recipe-input.ts │ ├── examples.gql │ ├── recipe-type.ts │ ├── sample-recipes.ts │ ├── index.ts │ ├── recipe-resolver.ts │ └── recipe-service.ts ├── simple-subscriptions │ ├── notification.type.ts │ ├── examples.gql │ └── index.ts ├── query-complexity │ ├── examples.gql │ ├── recipe-samples.ts │ ├── recipe-type.ts │ └── recipe-resolver.ts └── README.md ├── src ├── interfaces │ ├── Publisher.ts │ ├── ClassType.ts │ ├── Complexity.ts │ ├── ResolverInterface.ts │ ├── AuthChecker.ts │ ├── ResolverTopicData.ts │ ├── ResolverData.ts │ ├── ResolverFilterData.ts │ ├── index.ts │ ├── Middleware.ts │ └── resolvers-map.ts ├── scalars │ ├── index.ts │ ├── aliases.ts │ ├── isodate.ts │ └── timestamp.ts ├── metadata │ ├── definitions │ │ ├── authorized-metadata.ts │ │ ├── enum-metadata.ts │ │ ├── middleware-metadata.ts │ │ ├── class-metadata.ts │ │ ├── union-metadata.ts │ │ ├── index.ts │ │ ├── field-metadata.ts │ │ ├── param-metadata.ts │ │ └── resolver-metadata.ts │ ├── getMetadataStorage.ts │ └── utils.ts ├── helpers │ ├── isThrowing.ts │ ├── returnTypes.ts │ ├── utils.ts │ ├── loadResolversFromGlob.ts │ ├── auth-middleware.ts │ ├── resolver-metadata.ts │ ├── params.ts │ ├── decorators.ts │ └── findType.ts ├── index.ts ├── errors │ ├── ForbiddenError.ts │ ├── SymbolKeysNotSupportedError.ts │ ├── UnauthorizedError.ts │ ├── MissingSubscriptionTopicsError.ts │ ├── GeneratingSchemaError.ts │ ├── ArgumentValidationError.ts │ ├── ReflectMetadataMissingError.ts │ ├── UnionResolveTypeError.ts │ ├── CannotDetermineTypeError.ts │ ├── NoExplicitTypeError.ts │ ├── WrongNullableListOptionError.ts │ ├── ConflictingDefaultWithNullableError.ts │ ├── ConflictingDefaultValuesError.ts │ ├── helpers │ │ └── formatArgumentValidationError.ts │ ├── UnmetGraphQLPeerDependencyError.ts │ └── index.ts ├── graphql.d.ts ├── decorators │ ├── ArgsType.ts │ ├── enums.ts │ ├── Info.ts │ ├── Ctx.ts │ ├── PubSub.ts │ ├── InputType.ts │ ├── index.ts │ ├── InterfaceType.ts │ ├── unions.ts │ ├── Query.ts │ ├── Mutation.ts │ ├── Args.ts │ ├── Authorized.ts │ ├── Root.ts │ ├── UseMiddleware.ts │ ├── ObjectType.ts │ ├── Arg.ts │ ├── Subscription.ts │ ├── Resolver.ts │ ├── types.ts │ ├── FieldResolver.ts │ └── Field.ts ├── utils │ ├── index.ts │ ├── buildTypeDefsAndResolvers.ts │ ├── graphql-version.ts │ ├── emitSchemaDefinitionFile.ts │ └── container.ts ├── declarations.d.ts ├── resolvers │ └── validate-arg.ts ├── browser-shim.ts └── schema │ └── utils.ts ├── .gitignore ├── .prettierrc ├── tests ├── helpers │ ├── loading-from-directories │ │ ├── sample.type.ts │ │ └── sample.resolver.ts │ ├── circular-refs │ │ ├── wrong │ │ │ ├── CircularRef1.ts │ │ │ └── CircularRef2.ts │ │ └── good │ │ │ ├── CircularRef1.ts │ │ │ └── CircularRef2.ts │ ├── getTypeField.ts │ ├── subscriptions │ │ ├── subscribeToPromise.ts │ │ └── createWebSocketGQL.ts │ ├── customScalar.ts │ ├── getSampleObjectFieldType.ts │ ├── getInnerFieldType.ts │ └── getSchemaInfo.ts ├── functional │ ├── errors │ │ └── metadata-polyfill.ts │ └── peer-dependency.ts └── units │ └── formatArgumentValidationError.ts ├── docs ├── README.md ├── browser-usage.md ├── installation.md └── examples.md ├── .editorconfig ├── publish-website.sh ├── jest.config.js ├── .github └── ISSUE_TEMPLATE │ ├── question.md │ ├── documentation-issue-or-request.md │ ├── feature_request.md │ └── bug_report.md ├── .travis.yml ├── dev.js ├── LICENSE └── tslint.json /website/versions.json: -------------------------------------------------------------------------------- 1 | ["0.16.0"] 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/type-graphql/master/logo.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /examples/middlewares/context.ts: -------------------------------------------------------------------------------- 1 | export interface Context { 2 | username?: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/interfaces/Publisher.ts: -------------------------------------------------------------------------------- 1 | export type Publisher = (payload: T) => Promise; 2 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/resource/resource.ts: -------------------------------------------------------------------------------- 1 | export interface Resource { 2 | id: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/interfaces/ClassType.ts: -------------------------------------------------------------------------------- 1 | export interface ClassType { 2 | new (...args: any[]): T; 3 | } 4 | -------------------------------------------------------------------------------- /src/scalars/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./aliases"; 2 | export * from "./isodate"; 3 | export * from "./timestamp"; 4 | -------------------------------------------------------------------------------- /website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/type-graphql/master/website/static/img/logo.png -------------------------------------------------------------------------------- /website/static/img/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/type-graphql/master/website/static/img/author.jpg -------------------------------------------------------------------------------- /website/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/type-graphql/master/website/static/img/favicon.png -------------------------------------------------------------------------------- /website/static/img/ts-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/type-graphql/master/website/static/img/ts-logo.png -------------------------------------------------------------------------------- /website/blog/assets/logo_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/type-graphql/master/website/blog/assets/logo_mini.png -------------------------------------------------------------------------------- /examples/authorization/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | name: string; 4 | roles: string[]; 5 | } 6 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/topics.ts: -------------------------------------------------------------------------------- 1 | export enum Topic { 2 | NewComment = "NEW_COMMENT", 3 | NewRecipe = "NEW_RECIPE", 4 | } 5 | -------------------------------------------------------------------------------- /website/static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/type-graphql/master/website/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /examples/authorization/context.interface.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./user.interface"; 2 | 3 | export interface Context { 4 | user?: User; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/Complexity.ts: -------------------------------------------------------------------------------- 1 | import { ComplexityEstimator } from "graphql-query-complexity"; 2 | 3 | export type Complexity = ComplexityEstimator | number; 4 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/resolvers/types/context.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../../entities/user"; 2 | 3 | export interface Context { 4 | user?: User; 5 | } 6 | -------------------------------------------------------------------------------- /src/metadata/definitions/authorized-metadata.ts: -------------------------------------------------------------------------------- 1 | export interface AuthorizedMetadata { 2 | target: Function; 3 | fieldName: string; 4 | roles: any[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/metadata/definitions/enum-metadata.ts: -------------------------------------------------------------------------------- 1 | export interface EnumMetadata { 2 | enumObj: object; 3 | name: string; 4 | description: string | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/helpers/isThrowing.ts: -------------------------------------------------------------------------------- 1 | export function isThrowing(fn: () => void) { 2 | try { 3 | fn(); 4 | return false; 5 | } catch { 6 | return true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node modules 2 | node_modules 3 | 4 | # builded sources 5 | build 6 | 7 | # coverage reports 8 | coverage 9 | 10 | # IntelliJ stuffs 11 | .idea/ 12 | -------------------------------------------------------------------------------- /src/helpers/returnTypes.ts: -------------------------------------------------------------------------------- 1 | export const allowedTypes: Function[] = [String, Number, Date, Boolean]; 2 | export const bannedTypes: Function[] = [Promise, Array, Object, Function]; 3 | -------------------------------------------------------------------------------- /examples/using-scoped-container/types.ts: -------------------------------------------------------------------------------- 1 | import { ContainerInstance } from "typedi"; 2 | 3 | export interface Context { 4 | requestId: number; 5 | container: ContainerInstance; 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "bracketSpacing": true, 6 | "semi": true, 7 | "singleQuote": false, 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/loading-from-directories/sample.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../../src"; 2 | 3 | @ObjectType() 4 | export class SampleObject { 5 | @Field() 6 | sampleField: string; 7 | } 8 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/recipe.resolver.args.ts: -------------------------------------------------------------------------------- 1 | import { ID, Field, ArgsType } from "../../src"; 2 | 3 | @ArgsType() 4 | export class NewCommentsArgs { 5 | @Field(type => ID) 6 | recipeId: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/scalars/aliases.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFloat, GraphQLID, GraphQLInt, GraphQLScalarType } from "graphql"; 2 | 3 | export const Int = GraphQLInt; 4 | export const Float = GraphQLFloat; 5 | export const ID = GraphQLID; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./decorators"; 2 | export * from "./scalars"; 3 | export * from "./errors"; 4 | export * from "./interfaces"; 5 | export * from "./utils"; 6 | 7 | export { PubSubEngine } from "graphql-subscriptions"; 8 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/resource/resource.interface.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceType, ID, Field } from "../../../src"; 2 | 3 | @InterfaceType() 4 | export abstract class IResource { 5 | @Field(type => ID) 6 | id: string; 7 | } 8 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/newComment.interface.ts: -------------------------------------------------------------------------------- 1 | export interface NewCommentPayload { 2 | recipeId: string; 3 | dateString: string; // limitation of Redis payload serialization 4 | content: string; 5 | nickname?: string; 6 | } 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | **The documentation has been moved to [TypeGraphQL website](https://19majkel94.github.io/type-graphql), please update your bookmarks!** 4 | https://19majkel94.github.io/type-graphql/docs/introduction.html 5 | -------------------------------------------------------------------------------- /examples/middlewares/logger.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "typedi"; 2 | 3 | @Service() 4 | export class Logger { 5 | log(...args: any[]) { 6 | // replace with more sophisticated solution :) 7 | console.log(...args); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/ForbiddenError.ts: -------------------------------------------------------------------------------- 1 | export class ForbiddenError extends Error { 2 | constructor() { 3 | super("Access denied! You don't have permission for this action!"); 4 | 5 | Object.setPrototypeOf(this, new.target.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/enums-and-unions/cook.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, Int } from "../../src"; 2 | 3 | @ObjectType() 4 | export class Cook { 5 | @Field() 6 | name: string; 7 | 8 | @Field(type => Int) 9 | yearsOfExperience: number; 10 | } 11 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/person/person.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "../../../src"; 2 | 3 | @InputType() 4 | export class PersonInput { 5 | @Field() 6 | name: string; 7 | 8 | @Field() 9 | dateOfBirth: Date; 10 | } 11 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/person/person.role.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from "../../../src"; 2 | 3 | export enum PersonRole { 4 | Normal, 5 | Pro, 6 | Admin, 7 | } 8 | 9 | registerEnumType(PersonRole, { name: "PersonRole" }); 10 | -------------------------------------------------------------------------------- /src/errors/SymbolKeysNotSupportedError.ts: -------------------------------------------------------------------------------- 1 | export class SymbolKeysNotSupportedError extends Error { 2 | constructor() { 3 | super("Symbol keys are not supported yet!"); 4 | 5 | Object.setPrototypeOf(this, new.target.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/metadata/definitions/middleware-metadata.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../../interfaces/Middleware"; 2 | 3 | export interface MiddlewareMetadata { 4 | target: Function; 5 | fieldName: string; 6 | middlewares: Array>; 7 | } 8 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | lib/core/metadata.js 4 | lib/core/MetadataBlog.js 5 | website/translated_docs 6 | website/build/ 7 | website/yarn.lock 8 | website/node_modules 9 | 10 | website/i18n/* 11 | !website/i18n/en.json 12 | -------------------------------------------------------------------------------- /src/errors/UnauthorizedError.ts: -------------------------------------------------------------------------------- 1 | export class UnauthorizedError extends Error { 2 | constructor() { 3 | super("Access denied! You need to be authorized to perform this action!"); 4 | 5 | Object.setPrototypeOf(this, new.target.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from "../interfaces"; 2 | 3 | export type ArrayElementTypes = T extends Array ? TElement : never; 4 | export type InstanceSideOfClass = U extends Function ? U["prototype"] : never; 5 | -------------------------------------------------------------------------------- /tests/helpers/circular-refs/wrong/CircularRef1.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../../../src"; 2 | 3 | import { CircularRef2 } from "./CircularRef2"; 4 | 5 | @ObjectType() 6 | export class CircularRef1 { 7 | @Field() 8 | ref2Field: CircularRef2; 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/circular-refs/wrong/CircularRef2.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../../../src"; 2 | 3 | import { CircularRef1 } from "./CircularRef1"; 4 | 5 | @ObjectType() 6 | export class CircularRef2 { 7 | @Field() 8 | ref1Field: CircularRef1; 9 | } 10 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/employee/employee.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../../src"; 2 | 3 | import { Person } from "../person/person.type"; 4 | 5 | @ObjectType() 6 | export class Employee extends Person { 7 | @Field() 8 | companyName: string; 9 | } 10 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/student/student.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../../src"; 2 | 3 | import { Person } from "../person/person.type"; 4 | 5 | @ObjectType() 6 | export class Student extends Person { 7 | @Field() 8 | universityName: string; 9 | } 10 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/resolvers/types/rate-input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field, Int, ID } from "../../../../src"; 2 | 3 | @InputType() 4 | export class RateInput { 5 | @Field(type => ID) 6 | recipeId: string; 7 | 8 | @Field(type => Int) 9 | value: number; 10 | } 11 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/resolvers/types/rate-input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field, Int, ID } from "../../../../src"; 2 | 3 | @InputType() 4 | export class RateInput { 5 | @Field(type => ID) 6 | recipeId: string; 7 | 8 | @Field(type => Int) 9 | value: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/metadata/definitions/class-metadata.ts: -------------------------------------------------------------------------------- 1 | import { FieldMetadata } from "./field-metadata"; 2 | 3 | export interface ClassMetadata { 4 | name: string; 5 | target: Function; 6 | fields?: FieldMetadata[]; 7 | description?: string; 8 | interfaceClasses?: Function[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql.d.ts: -------------------------------------------------------------------------------- 1 | import { ComplexityEstimator } from "graphql-query-complexity"; 2 | 3 | declare module "graphql/type/definition" { 4 | export interface GraphQLFieldConfig { 5 | complexity?: ComplexityEstimator | number; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/metadata/getMetadataStorage.ts: -------------------------------------------------------------------------------- 1 | import { MetadataStorage } from "../metadata/metadata-storage"; 2 | 3 | export function getMetadataStorage(): MetadataStorage { 4 | return ( 5 | global.TypeGraphQLMetadataStorage || (global.TypeGraphQLMetadataStorage = new MetadataStorage()) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/employee/employee.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "../../../src"; 2 | 3 | import { PersonInput } from "../person/person.input"; 4 | 5 | @InputType() 6 | export class EmployeeInput extends PersonInput { 7 | @Field() 8 | companyName: string; 9 | } 10 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/student/student.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "../../../src"; 2 | 3 | import { PersonInput } from "../person/person.input"; 4 | 5 | @InputType() 6 | export class StudentInput extends PersonInput { 7 | @Field() 8 | universityName: string; 9 | } 10 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/comment.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../src"; 2 | 3 | @ObjectType() 4 | export class Comment { 5 | @Field({ nullable: true }) 6 | nickname?: string; 7 | 8 | @Field() 9 | content: string; 10 | 11 | @Field() 12 | date: Date; 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/ResolverInterface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Resolver classes can implement this type 3 | * to provide a proper resolver method signatures for fields of T. 4 | */ 5 | export type ResolverInterface = { 6 | [P in keyof T]?: (root: T, ...args: any[]) => T[P] | Promise 7 | }; 8 | -------------------------------------------------------------------------------- /examples/automatic-validation/recipe-type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../src"; 2 | 3 | @ObjectType() 4 | export class Recipe { 5 | @Field() 6 | title: string; 7 | 8 | @Field({ nullable: true }) 9 | description?: string; 10 | 11 | @Field() 12 | creationDate: Date; 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/AuthChecker.ts: -------------------------------------------------------------------------------- 1 | import { ResolverData } from "./ResolverData"; 2 | 3 | export type AuthChecker = ( 4 | resolverData: ResolverData, 5 | roles: RoleType[], 6 | ) => boolean | Promise; 7 | 8 | export type AuthMode = "error" | "null"; 9 | -------------------------------------------------------------------------------- /examples/enums-and-unions/search-result.union.ts: -------------------------------------------------------------------------------- 1 | import { createUnionType } from "../../src"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | import { Cook } from "./cook.type"; 5 | 6 | export const SearchResult = createUnionType({ 7 | name: "SearchResult", 8 | types: [Recipe, Cook], 9 | }); 10 | -------------------------------------------------------------------------------- /src/decorators/ArgsType.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | 3 | export function ArgsType(): ClassDecorator { 4 | return target => { 5 | getMetadataStorage().collectArgsMetadata({ 6 | name: target.name, 7 | target, 8 | }); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/ResolverTopicData.ts: -------------------------------------------------------------------------------- 1 | import { ResolverFilterData } from "./ResolverFilterData"; 2 | import { ArgsDictionary } from "./ResolverData"; 3 | 4 | export type ResolverTopicData< 5 | TPayload = any, 6 | TArgs = ArgsDictionary, 7 | TContext = {} 8 | > = ResolverFilterData; 9 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/person/person.type.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType } from "../../../src"; 2 | 3 | import { IPerson } from "./person.interface"; 4 | 5 | @ObjectType({ implements: IPerson }) 6 | export class Person implements IPerson { 7 | id: string; 8 | name: string; 9 | age: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/errors/MissingSubscriptionTopicsError.ts: -------------------------------------------------------------------------------- 1 | export class MissingSubscriptionTopicsError extends Error { 2 | constructor(target: Function, methodName: string) { 3 | super(`${target}#${methodName} subscription has no provided topics!`); 4 | 5 | Object.setPrototypeOf(this, new.target.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/metadata/definitions/union-metadata.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from "../../interfaces"; 2 | 3 | export interface UnionMetadata { 4 | types: ClassType[]; 5 | name: string; 6 | description?: string; 7 | } 8 | export interface UnionMetadataWithSymbol extends UnionMetadata { 9 | symbol: symbol; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /examples/apollo-engine/examples.gql: -------------------------------------------------------------------------------- 1 | query CachedQuery { 2 | cachedRecipe(title: "Recipe 1") { 3 | title 4 | description 5 | cachedAverageRating 6 | } 7 | } 8 | 9 | query NotCachedQuery { 10 | recipe(title: "Recipe 1") { 11 | title 12 | description 13 | averageRating 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple-usage/recipe-input.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from "./recipe-type"; 2 | import { InputType, Field } from "../../src"; 3 | 4 | @InputType() 5 | export class RecipeInput implements Partial { 6 | @Field() 7 | title: string; 8 | 9 | @Field({ nullable: true }) 10 | description?: string; 11 | } 12 | -------------------------------------------------------------------------------- /tests/helpers/loading-from-directories/sample.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query } from "../../../src"; 2 | 3 | import { SampleObject } from "./sample.type"; 4 | 5 | export class Resolver { 6 | @Query() 7 | sampleQuery(): SampleObject { 8 | return { 9 | sampleField: "sampleField", 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/GeneratingSchemaError.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from "graphql"; 2 | 3 | export class GeneratingSchemaError extends Error { 4 | constructor(public details: ReadonlyArray) { 5 | super("Generating schema error"); 6 | 7 | Object.setPrototypeOf(this, new.target.prototype); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/ResolverData.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from "graphql"; 2 | 3 | export interface ArgsDictionary { 4 | [argName: string]: any; 5 | } 6 | 7 | export interface ResolverData { 8 | root: any; 9 | args: ArgsDictionary; 10 | context: ContextType; 11 | info: GraphQLResolveInfo; 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/ArgumentValidationError.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "class-validator"; 2 | 3 | export class ArgumentValidationError extends Error { 4 | constructor(public validationErrors: ValidationError[]) { 5 | super("Argument Validation Error"); 6 | 7 | Object.setPrototypeOf(this, new.target.prototype); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/metadata/definitions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./authorized-metadata"; 2 | export * from "./class-metadata"; 3 | export * from "./enum-metadata"; 4 | export * from "./field-metadata"; 5 | export * from "./middleware-metadata"; 6 | export * from "./param-metadata"; 7 | export * from "./resolver-metadata"; 8 | export * from "./union-metadata"; 9 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { buildSchema, buildSchemaSync, BuildSchemaOptions } from "./buildSchema"; 2 | export { buildTypeDefsAndResolvers } from "./buildTypeDefsAndResolvers"; 3 | export { emitSchemaDefinitionFile, emitSchemaDefinitionFileSync } from "./emitSchemaDefinitionFile"; 4 | export { ContainerType, ContainerGetter } from "./container"; 5 | -------------------------------------------------------------------------------- /examples/enums-and-unions/difficulty.enum.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from "../../src"; 2 | 3 | export enum Difficulty { 4 | Beginner, 5 | Easy, 6 | Medium, 7 | Hard, 8 | MasterChef, 9 | } 10 | 11 | registerEnumType(Difficulty, { 12 | name: "Difficulty", 13 | description: "All possible preparation difficulty levels", 14 | }); 15 | -------------------------------------------------------------------------------- /tests/helpers/getTypeField.ts: -------------------------------------------------------------------------------- 1 | import { IntrospectionField, IntrospectionObjectType, IntrospectionInterfaceType } from "graphql"; 2 | 3 | export function getTypeField( 4 | type: IntrospectionObjectType | IntrospectionInterfaceType, 5 | fieldName: string, 6 | ): IntrospectionField { 7 | return type.fields.find(field => field.name === fieldName)!; 8 | } 9 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/queries.gql: -------------------------------------------------------------------------------- 1 | query AllPersons { 2 | persons { 3 | id 4 | name 5 | age 6 | role 7 | } 8 | } 9 | 10 | query OneRecipe { 11 | recipe(id: 1) { 12 | uuid 13 | title 14 | ratings 15 | averageRating 16 | } 17 | } 18 | 19 | mutation PromotePeronsOne { 20 | promote(personId: 1) 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/ResolverFilterData.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from "graphql"; 2 | 3 | import { ArgsDictionary } from "./ResolverData"; 4 | 5 | export interface ResolverFilterData { 6 | payload: TPayload; 7 | args: TArgs; 8 | context: TContext; 9 | info: GraphQLResolveInfo; 10 | } 11 | -------------------------------------------------------------------------------- /website/pages/snippets/validation.md: -------------------------------------------------------------------------------- 1 | ```typescript 2 | @InputType() 3 | export class RecipeInput { 4 | @Field() 5 | @MaxLength(30) 6 | title: string; 7 | 8 | @Field({ nullable: true }) 9 | @Length(30, 255) 10 | description?: string; 11 | 12 | @Field(type => [String]) 13 | @MaxArraySize(25) 14 | ingredients: string[]; 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/middlewares/middlewares/resolve-time.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFn } from "../../../src"; 2 | 3 | export const ResolveTimeMiddleware: MiddlewareFn = async ({ info }, next) => { 4 | const start = Date.now(); 5 | await next(); 6 | const resolveTime = Date.now() - start; 7 | console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`); 8 | }; 9 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/resolvers/types/recipe-input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "../../../../src"; 2 | 3 | import { Recipe } from "../../entities/recipe"; 4 | 5 | @InputType() 6 | export class RecipeInput implements Partial { 7 | @Field() 8 | title: string; 9 | 10 | @Field({ nullable: true }) 11 | description?: string; 12 | } 13 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/resolvers/types/recipe-input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "../../../../src"; 2 | 3 | import { Recipe } from "../../entities/recipe"; 4 | 5 | @InputType() 6 | export class RecipeInput implements Partial { 7 | @Field() 8 | title: string; 9 | 10 | @Field({ nullable: true }) 11 | description: string; 12 | } 13 | -------------------------------------------------------------------------------- /examples/automatic-validation/recipes-arguments.ts: -------------------------------------------------------------------------------- 1 | import { Max, Min } from "class-validator"; 2 | import { ArgsType, Field, Int } from "../../src"; 3 | 4 | @ArgsType() 5 | export class RecipesArguments { 6 | @Field(type => Int) 7 | @Min(0) 8 | skip: number = 0; 9 | 10 | @Field(type => Int) 11 | @Min(1) 12 | @Max(50) 13 | take: number = 10; 14 | } 15 | -------------------------------------------------------------------------------- /examples/middlewares/recipe/recipe.args.ts: -------------------------------------------------------------------------------- 1 | import { IsPositive, Max, Min } from "class-validator"; 2 | import { ArgsType, Field, Int } from "../../../src"; 3 | 4 | @ArgsType() 5 | export class RecipesArgs { 6 | @Field(type => Int) 7 | @Min(0) 8 | skip: number = 0; 9 | 10 | @Field(type => Int) 11 | @Min(1) 12 | @Max(50) 13 | take: number = 10; 14 | } 15 | -------------------------------------------------------------------------------- /examples/apollo-engine/cache-control.ts: -------------------------------------------------------------------------------- 1 | import { CacheHint } from "apollo-cache-control"; 2 | import { UseMiddleware } from "../../src"; 3 | 4 | export function CacheControl(hint: CacheHint) { 5 | return UseMiddleware(({ info }, next) => { 6 | console.log("Called CacheControl"); 7 | info.cacheControl.setCacheHint(hint); 8 | return next(); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /examples/automatic-validation/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from "./recipe-type"; 2 | 3 | export function generateRecipes(count: number): Recipe[] { 4 | return new Array(count).fill(null).map( 5 | (_, i): Recipe => ({ 6 | title: `Recipe #${i + 1}`, 7 | description: `Description #${i + 1}`, 8 | creationDate: new Date(), 9 | }), 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/recipe/recipe.type.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, Int } from "../../../src"; 2 | 3 | import { Resource } from "../resource/resource"; 4 | 5 | @ObjectType() 6 | export class Recipe implements Resource { 7 | @Field() 8 | id: number; 9 | 10 | @Field() 11 | title: string; 12 | 13 | @Field(type => [Int]) 14 | ratings: number[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/middlewares/examples.gql: -------------------------------------------------------------------------------- 1 | query InvalidArgs { 2 | recipes(take: -1) { 3 | title 4 | description 5 | } 6 | } 7 | 8 | query LogginQuery { 9 | recipes { 10 | title 11 | description 12 | ratings 13 | } 14 | } 15 | 16 | query InterceptorsQuery { 17 | recipes(skip: 1, take: 2) { 18 | title 19 | ratings 20 | averageRating 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/using-scoped-container/recipe/recipe.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from "../../../src"; 2 | 3 | @ObjectType() 4 | export class Recipe { 5 | @Field(type => ID) 6 | id: string; 7 | 8 | @Field() 9 | title: string; 10 | 11 | @Field({ nullable: true }) 12 | description?: string; 13 | 14 | @Field(type => [String]) 15 | ingredients: string[]; 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/loadResolversFromGlob.ts: -------------------------------------------------------------------------------- 1 | import * as glob from "glob"; 2 | 3 | export function findFileNamesFromGlob(globString: string) { 4 | return glob.sync(globString); 5 | } 6 | 7 | export function loadResolversFromGlob(globString: string) { 8 | const filePaths = findFileNamesFromGlob(globString); 9 | const modules = filePaths.map(fileName => require(fileName)); 10 | } 11 | -------------------------------------------------------------------------------- /examples/using-container/recipe-input.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from "./recipe-type"; 2 | import { InputType, Field } from "../../src"; 3 | 4 | @InputType() 5 | export class RecipeInput implements Partial { 6 | @Field({ nullable: true }) 7 | description: string; 8 | 9 | @Field(type => [String]) 10 | ingredients: string[]; 11 | 12 | @Field() 13 | title: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Reflect { 2 | function getMetadata(metadataKey: any, target: Object): any; 3 | function getMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any; 4 | } 5 | 6 | declare namespace NodeJS { 7 | interface Global { 8 | TypeGraphQLMetadataStorage: import("../src/metadata/metadata-storage").MetadataStorage; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/decorators/enums.ts: -------------------------------------------------------------------------------- 1 | import { EnumConfig } from "./types"; 2 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 3 | 4 | export function registerEnumType(enumObj: T, enumConfig: EnumConfig) { 5 | getMetadataStorage().collectEnumMetadata({ 6 | enumObj, 7 | name: enumConfig.name, 8 | description: enumConfig.description, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/comment.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, InputType } from "../../src"; 2 | 3 | import { Comment } from "./comment.type"; 4 | 5 | @InputType() 6 | export class CommentInput implements Partial { 7 | @Field(type => ID) 8 | recipeId: string; 9 | 10 | @Field({ nullable: true }) 11 | nickname?: string; 12 | 13 | @Field() 14 | content: string; 15 | } 16 | -------------------------------------------------------------------------------- /examples/using-scoped-container/recipe/recipe.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "../../../src"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | 5 | @InputType() 6 | export class RecipeInput implements Partial { 7 | @Field({ nullable: true }) 8 | description: string; 9 | 10 | @Field(type => [String]) 11 | ingredients: string[]; 12 | 13 | @Field() 14 | title: string; 15 | } 16 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/person/person.interface.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceType, Field, Int, ID } from "../../../src"; 2 | 3 | import { IResource } from "../resource/resource.interface"; 4 | 5 | @InterfaceType() 6 | export abstract class IPerson implements IResource { 7 | @Field(type => ID) 8 | id: string; 9 | 10 | @Field() 11 | name: string; 12 | 13 | @Field(type => Int) 14 | age: number; 15 | } 16 | -------------------------------------------------------------------------------- /examples/middlewares/middlewares/number-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFn } from "../../../src"; 2 | 3 | export function NumberInterceptor(minValue: number): MiddlewareFn { 4 | return async (_, next) => { 5 | const result = await next(); 6 | // hide ratings below minValue 7 | if (typeof result === "number" && result < minValue) { 8 | return null; 9 | } 10 | return result; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/recipe.type.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, ID } from "../../src"; 2 | 3 | import { Comment } from "./comment.type"; 4 | 5 | @ObjectType() 6 | export class Recipe { 7 | @Field(type => ID) 8 | id: string; 9 | 10 | @Field() 11 | title: string; 12 | 13 | @Field({ nullable: true }) 14 | description?: string; 15 | 16 | @Field(type => [Comment]) 17 | comments: Comment[]; 18 | } 19 | -------------------------------------------------------------------------------- /examples/simple-subscriptions/notification.type.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, ID } from "../../src"; 2 | 3 | @ObjectType() 4 | export class Notification { 5 | @Field(type => ID) 6 | id: number; 7 | 8 | @Field({ nullable: true }) 9 | message?: string; 10 | 11 | @Field(type => Date) 12 | date: Date; 13 | } 14 | 15 | export interface NotificationPayload { 16 | id: number; 17 | message?: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export { MiddlewareFn, NextFn, MiddlewareInterface } from "./Middleware"; 2 | export * from "./AuthChecker"; 3 | export * from "./ClassType"; 4 | export * from "./Complexity"; 5 | export * from "./Publisher"; 6 | export * from "./ResolverData"; 7 | export * from "./ResolverFilterData"; 8 | export * from "./ResolverInterface"; 9 | export * from "./resolvers-map"; 10 | export * from "./ResolverTopicData"; 11 | -------------------------------------------------------------------------------- /examples/automatic-validation/recipe-input.ts: -------------------------------------------------------------------------------- 1 | import { MaxLength, Length } from "class-validator"; 2 | import { InputType, Field } from "../../src"; 3 | 4 | import { Recipe } from "./recipe-type"; 5 | 6 | @InputType() 7 | export class RecipeInput implements Partial { 8 | @Field() 9 | @MaxLength(30) 10 | title: string; 11 | 12 | @Field({ nullable: true }) 13 | @Length(30, 255) 14 | description?: string; 15 | } 16 | -------------------------------------------------------------------------------- /publish-website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$TRAVIS_BRANCH" = "master" ]; then 4 | git config user.email "$GIT_USER@users.noreply.github.com" 5 | git config user.name "Travis" 6 | echo "machine github.com login $GIT_USER password $GIT_TOKEN" > ~/.netrc 7 | 8 | cd website 9 | npm install 10 | GIT_USER=$GIT_USER CURRENT_BRANCH=master npm run publish-gh-pages 11 | exit 0; 12 | fi 13 | -------------------------------------------------------------------------------- /examples/query-complexity/examples.gql: -------------------------------------------------------------------------------- 1 | # You have to copy and paste only one query to GraphQL Playground 2 | # because sending this whole document will cause query complexity cost error 3 | 4 | query GetRecipesWithComplexityError { 5 | recipes(count: 3) { 6 | title 7 | averageRating 8 | } 9 | } 10 | 11 | query GetRecipesWithoutComplexityError { 12 | recipes(count: 2) { 13 | title 14 | ratingsCount 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/errors/ReflectMetadataMissingError.ts: -------------------------------------------------------------------------------- 1 | export class ReflectMetadataMissingError extends Error { 2 | constructor() { 3 | super( 4 | "Looks like you've forgot to provide experimental metadata API polyfill. " + 5 | "Please read the installation instruction for more details: " + 6 | "https://github.com/19majkel94/type-graphql#installation", 7 | ); 8 | 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/errors/UnionResolveTypeError.ts: -------------------------------------------------------------------------------- 1 | import { UnionMetadata } from "../metadata/definitions"; 2 | 3 | export class UnionResolveTypeError extends Error { 4 | constructor(unionMetadata: UnionMetadata) { 5 | super( 6 | `Cannot resolve type for union ${unionMetadata.name}! ` + 7 | `You need to return instance of object type class, not a plain object!`, 8 | ); 9 | 10 | Object.setPrototypeOf(this, new.target.prototype); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /website/pages/snippets/object-type.md: -------------------------------------------------------------------------------- 1 | ```typescript 2 | @ObjectType() 3 | class Recipe { 4 | @Field() 5 | title: string; 6 | 7 | @Field({ nullable: true }) 8 | description?: string; 9 | 10 | @Field(type => [Rate]) 11 | ratings: Rate[]; 12 | 13 | @Field(type => Float, { nullable: true }) 14 | get averageRating() { 15 | const sum = this.ratings.reduce((a, b) => a + b, 0); 16 | return sum / this.ratings.length; 17 | } 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "examples": "docusaurus-examples", 5 | "start": "docusaurus-start", 6 | "build": "docusaurus-build", 7 | "publish-gh-pages": "docusaurus-publish", 8 | "write-translations": "docusaurus-write-translations", 9 | "version": "docusaurus-version", 10 | "rename-version": "docusaurus-rename-version" 11 | }, 12 | "devDependencies": { 13 | "docusaurus": "^1.6.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/enums-and-unions/cook.samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Cook } from "./cook.type"; 4 | 5 | export const sampleCooks = [ 6 | createCook({ 7 | name: "Gordon Ramsay", 8 | yearsOfExperience: 21, 9 | }), 10 | createCook({ 11 | name: "Kim Kardashian", 12 | yearsOfExperience: 1, 13 | }), 14 | ]; 15 | 16 | function createCook(cookData: Partial): Cook { 17 | return plainToClass(Cook, cookData); 18 | } 19 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/person/person.type.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, Int } from "../../../src"; 2 | 3 | import { Resource } from "../resource/resource"; 4 | import { PersonRole } from "./person.role"; 5 | 6 | @ObjectType() 7 | export class Person implements Resource { 8 | @Field() 9 | id: number; 10 | 11 | @Field() 12 | name: string; 13 | 14 | @Field(type => Int) 15 | age: number; 16 | 17 | @Field(type => PersonRole) 18 | role: PersonRole; 19 | } 20 | -------------------------------------------------------------------------------- /examples/using-scoped-container/logger.ts: -------------------------------------------------------------------------------- 1 | import { Service, Inject } from "typedi"; 2 | 3 | import { Context } from "./types"; 4 | 5 | // this service will be recreated for each request (scoped) 6 | @Service() 7 | export class Logger { 8 | constructor(@Inject("context") private readonly context: Context) { 9 | console.log("Logger created!"); 10 | } 11 | 12 | log(...messages: any[]) { 13 | console.log(`(ID ${this.context.requestId}):`, ...messages); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/query-complexity/recipe-samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe-type"; 4 | 5 | export function createRecipeSamples() { 6 | return plainToClass(Recipe, [ 7 | { 8 | title: "Recipe 1", 9 | ratings: [0, 3, 1], 10 | }, 11 | { 12 | title: "Recipe 2", 13 | ratings: [4, 2, 3, 1], 14 | }, 15 | { 16 | title: "Recipe 3", 17 | ratings: [5, 4], 18 | }, 19 | ]); 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/buildTypeDefsAndResolvers.ts: -------------------------------------------------------------------------------- 1 | import { printSchema } from "graphql"; 2 | 3 | import { BuildSchemaOptions, buildSchema } from "./buildSchema"; 4 | import { createResolversMap } from "./createResolversMap"; 5 | 6 | export async function buildTypeDefsAndResolvers(options: BuildSchemaOptions) { 7 | const schema = await buildSchema(options); 8 | const typeDefs = printSchema(schema); 9 | const resolvers = createResolversMap(schema); 10 | return { typeDefs, resolvers }; 11 | } 12 | -------------------------------------------------------------------------------- /website/pages/snippets/typeorm.md: -------------------------------------------------------------------------------- 1 | ```typescript 2 | @Entity() 3 | @ObjectType() 4 | export class Rate { 5 | @PrimaryGeneratedColumn() 6 | readonly id: number; 7 | 8 | @Field(type => Int) 9 | @Column({ type: "int" }) 10 | value: number; 11 | 12 | @Field(type => User) 13 | @ManyToOne(type => User) 14 | user: Promise; 15 | 16 | @Field() 17 | @CreateDateColumn() 18 | date: Date; 19 | 20 | @ManyToOne(type => Recipe) 21 | recipe: Promise; 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /tests/helpers/subscriptions/subscribeToPromise.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode, ExecutionResult } from "graphql"; 2 | import { ApolloClient } from "apollo-client"; 3 | 4 | export interface Options { 5 | query: DocumentNode; 6 | apollo: ApolloClient; 7 | } 8 | export function apolloSubscribeToPromise({ apollo, query }: Options): Promise { 9 | return new Promise((resolve, reject) => { 10 | apollo.subscribe({ query }).subscribe({ next: resolve, error: reject }); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/CannotDetermineTypeError.ts: -------------------------------------------------------------------------------- 1 | export class CannotDetermineTypeError extends Error { 2 | constructor(typeName: string, propertyKey: string, parameterIndex?: number) { 3 | let errorMessage = `Cannot determine type for ${typeName}#${propertyKey} `; 4 | if (parameterIndex !== undefined) { 5 | errorMessage += `parameter #${parameterIndex} `; 6 | } 7 | errorMessage += "!"; 8 | super(errorMessage); 9 | 10 | Object.setPrototypeOf(this, new.target.prototype); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/NoExplicitTypeError.ts: -------------------------------------------------------------------------------- 1 | export class NoExplicitTypeError extends Error { 2 | constructor(typeName: string, propertyKey: string, parameterIndex?: number) { 3 | let errorMessage = `You need to provide explicit type for ${typeName}#${propertyKey} `; 4 | if (parameterIndex !== undefined) { 5 | errorMessage += `parameter #${parameterIndex} `; 6 | } 7 | errorMessage += "!"; 8 | super(errorMessage); 9 | 10 | Object.setPrototypeOf(this, new.target.prototype); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/WrongNullableListOptionError.ts: -------------------------------------------------------------------------------- 1 | import { NullableListOptions } from "../decorators/types"; 2 | 3 | export class WrongNullableListOptionError extends Error { 4 | constructor(typeOwnerName: string, nullable: boolean | NullableListOptions | undefined) { 5 | super( 6 | `Wrong nullable option set for ${typeOwnerName}. ` + 7 | `You cannot combine non-list type with nullable '${nullable}'.`, 8 | ); 9 | 10 | Object.setPrototypeOf(this, new.target.prototype); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/middlewares/recipe/recipe.samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | 5 | export default plainToClass(Recipe, [ 6 | { 7 | description: "Desc 1", 8 | title: "Recipe 1", 9 | ratings: [0, 3, 1], 10 | }, 11 | { 12 | description: "Desc 2", 13 | title: "Recipe 2", 14 | ratings: [4, 2, 3, 1], 15 | }, 16 | { 17 | description: "Desc 3", 18 | title: "Recipe 3", 19 | ratings: [4, 5, 3, 1, 5], 20 | }, 21 | ]); 22 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/entities/user.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from "../../../src"; 2 | import { PrimaryGeneratedColumn, Column, Entity } from "typeorm"; 3 | 4 | @ObjectType() 5 | @Entity() 6 | export class User { 7 | @Field(type => ID) 8 | @PrimaryGeneratedColumn() 9 | readonly id: number; 10 | 11 | @Field() 12 | @Column() 13 | email: string; 14 | 15 | @Field({ nullable: true }) 16 | @Column({ nullable: true }) 17 | nickname?: string; 18 | 19 | @Column() 20 | password: string; 21 | } 22 | -------------------------------------------------------------------------------- /examples/enums-and-unions/recipe.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../src"; 2 | 3 | import { Difficulty } from "./difficulty.enum"; 4 | import { Cook } from "./cook.type"; 5 | 6 | @ObjectType() 7 | export class Recipe { 8 | @Field() 9 | title: string; 10 | 11 | @Field({ nullable: true }) 12 | description?: string; 13 | 14 | @Field(type => [String]) 15 | ingredients: string[]; 16 | 17 | @Field(type => Difficulty) 18 | preparationDifficulty: Difficulty; 19 | 20 | @Field() 21 | cook: Cook; 22 | } 23 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | export function getId(): string { 4 | const randomNumber = Math.random(); 5 | const hash = crypto.createHash("sha256"); 6 | hash.update(randomNumber.toString()); 7 | return hash.digest("hex"); 8 | } 9 | 10 | export function calculateAge(birthday: Date) { 11 | const ageDiffMs = Date.now() - birthday.getTime(); 12 | const ageDate = new Date(ageDiffMs); // miliseconds from epoch 13 | return Math.abs(ageDate.getUTCFullYear() - 1970); 14 | } 15 | -------------------------------------------------------------------------------- /examples/simple-usage/examples.gql: -------------------------------------------------------------------------------- 1 | query GetRecipe1 { 2 | recipe(title: "Recipe 1") { 3 | title 4 | description 5 | ratings 6 | creationDate 7 | ratingsCount(minRate: 2) 8 | averageRating 9 | } 10 | } 11 | 12 | query GetRecipes { 13 | recipes { 14 | title 15 | description 16 | creationDate 17 | averageRating 18 | } 19 | } 20 | 21 | mutation AddRecipe { 22 | addRecipe(recipe: { 23 | title: "New recipe" 24 | description: "Simple description" 25 | }) { 26 | creationDate 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/errors/ConflictingDefaultWithNullableError.ts: -------------------------------------------------------------------------------- 1 | import { NullableListOptions } from "../decorators/types"; 2 | 3 | export class ConflictingDefaultWithNullableError extends Error { 4 | constructor(typeOwnerName: string, defaultValue: any, nullable: boolean | NullableListOptions) { 5 | super( 6 | `Wrong nullable option set for ${typeOwnerName}. ` + 7 | `You cannot combine default value '${defaultValue}' with nullable '${nullable}'.`, 8 | ); 9 | 10 | Object.setPrototypeOf(this, new.target.prototype); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/helpers/circular-refs/good/CircularRef1.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../../../src"; 2 | 3 | import { CircularRef2 } from "./CircularRef2"; 4 | 5 | let hasModuleFinishedInitialLoad = false; 6 | 7 | @ObjectType() 8 | export class CircularRef1 { 9 | @Field(type => { 10 | if (!hasModuleFinishedInitialLoad) { 11 | throw new Error("Field type function was called synchronously during module load"); 12 | } 13 | return [CircularRef2]; 14 | }) 15 | ref2Field: any[]; 16 | } 17 | 18 | hasModuleFinishedInitialLoad = true; 19 | -------------------------------------------------------------------------------- /tests/helpers/circular-refs/good/CircularRef2.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "../../../../src"; 2 | 3 | import { CircularRef1 } from "./CircularRef1"; 4 | 5 | let hasModuleFinishedInitialLoad = false; 6 | 7 | @ObjectType() 8 | export class CircularRef2 { 9 | @Field(type => { 10 | if (!hasModuleFinishedInitialLoad) { 11 | throw new Error("Field type function was called synchronously during module load"); 12 | } 13 | return [CircularRef1]; 14 | }) 15 | ref1Field: any[]; 16 | } 17 | 18 | hasModuleFinishedInitialLoad = true; 19 | -------------------------------------------------------------------------------- /examples/using-container/examples.gql: -------------------------------------------------------------------------------- 1 | query GetRecipe1 { 2 | recipe(recipeId: "1") { 3 | title 4 | description 5 | ingredients 6 | numberInCollection 7 | } 8 | } 9 | 10 | query GetRecipes { 11 | recipes { 12 | title 13 | description 14 | ingredientsLength 15 | numberInCollection 16 | } 17 | } 18 | 19 | mutation AddRecipe { 20 | addRecipe(recipe: { 21 | title: "New recipe", 22 | ingredients: [ 23 | "One", 24 | "Two", 25 | "Three", 26 | ], 27 | }) { 28 | id 29 | numberInCollection 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/using-container/recipe-type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType, Int } from "../../src"; 2 | 3 | @ObjectType() 4 | export class Recipe { 5 | @Field(type => ID) 6 | id: string; 7 | 8 | @Field() 9 | title: string; 10 | 11 | @Field({ nullable: true }) 12 | description?: string; 13 | 14 | @Field(type => [String]) 15 | ingredients: string[]; 16 | 17 | @Field(type => Int) 18 | protected numberInCollection: number; 19 | 20 | @Field(type => Int) 21 | protected get ingredientsLength(): number { 22 | return this.ingredients.length; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/interfaces/Middleware.ts: -------------------------------------------------------------------------------- 1 | import { ResolverData } from "./ResolverData"; 2 | 3 | export type NextFn = () => Promise; 4 | 5 | export type MiddlewareFn = ( 6 | action: ResolverData, 7 | next: NextFn, 8 | ) => Promise; 9 | 10 | export interface MiddlewareInterface { 11 | use: MiddlewareFn; 12 | } 13 | export interface MiddlewareClass { 14 | new (...args: any[]): MiddlewareInterface; 15 | } 16 | 17 | export type Middleware = MiddlewareFn | MiddlewareClass; 18 | -------------------------------------------------------------------------------- /examples/using-scoped-container/examples.gql: -------------------------------------------------------------------------------- 1 | query GetRecipe1 { 2 | recipe(recipeId: "1") { 3 | title 4 | description 5 | } 6 | } 7 | 8 | query GetNotExistingRecipe10 { 9 | recipe(recipeId: "10") { 10 | title 11 | description 12 | } 13 | } 14 | 15 | query GetRecipes { 16 | recipes { 17 | title 18 | description 19 | } 20 | } 21 | 22 | mutation AddRecipe { 23 | addRecipe(recipe: { 24 | title: "New recipe", 25 | ingredients: [ 26 | "One", 27 | "Two", 28 | "Three", 29 | ], 30 | }) { 31 | id 32 | title 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | transform: { 4 | "^.+\\.tsx?$": "ts-jest", 5 | }, 6 | testMatch: ["**/functional/**/*.ts", "**/units/**/*.ts"], 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 8 | rootDir: "./", 9 | roots: ["/tests", "/src"], 10 | collectCoverage: false, 11 | collectCoverageFrom: [ 12 | "/src/**/*.ts", 13 | "!/src/**/*.d.ts", 14 | "!/src/browser-shim.ts", 15 | ], 16 | coverageDirectory: "/coverage", 17 | testEnvironment: "node", 18 | }; 19 | -------------------------------------------------------------------------------- /tests/functional/errors/metadata-polyfill.ts: -------------------------------------------------------------------------------- 1 | import { ReflectMetadataMissingError } from "../../../src"; 2 | import { getMetadataStorage } from "../../../src/metadata/getMetadataStorage"; 3 | 4 | describe("Reflect metadata", () => { 5 | it("should throw ReflectMetadataMissingError when no polyfill provided", async () => { 6 | try { 7 | getMetadataStorage(); 8 | } catch (err) { 9 | expect(err).toBeInstanceOf(ReflectMetadataMissingError); 10 | expect(err.message).toContain("metadata"); 11 | expect(err.message).toContain("polyfill"); 12 | } 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/decorators/Info.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { SymbolKeysNotSupportedError } from "../errors"; 3 | 4 | export function Info(): ParameterDecorator { 5 | return (prototype, propertyKey, parameterIndex) => { 6 | if (typeof propertyKey === "symbol") { 7 | throw new SymbolKeysNotSupportedError(); 8 | } 9 | 10 | getMetadataStorage().collectHandlerParamMetadata({ 11 | kind: "info", 12 | target: prototype.constructor, 13 | methodName: propertyKey, 14 | index: parameterIndex, 15 | }); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/errors/ConflictingDefaultValuesError.ts: -------------------------------------------------------------------------------- 1 | export class ConflictingDefaultValuesError extends Error { 2 | constructor( 3 | typeName: string, 4 | fieldName: string, 5 | defaultValueFromDecorator: any, 6 | defaultValueFromInitializer: any, 7 | ) { 8 | super( 9 | `The '${fieldName}' field of '${typeName}' has conflicting default values. ` + 10 | `Default value from decorator ('${defaultValueFromDecorator}') ` + 11 | `is not equal to the property initializer value ('${defaultValueFromInitializer}').`, 12 | ); 13 | 14 | Object.setPrototypeOf(this, new.target.prototype); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/errors/helpers/formatArgumentValidationError.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from "graphql"; 2 | 3 | import { ArgumentValidationError } from "../ArgumentValidationError"; 4 | 5 | export function formatArgumentValidationError(err: GraphQLError) { 6 | const formattedError: { [key: string]: any } = {}; 7 | 8 | formattedError.message = err.message; 9 | formattedError.locations = err.locations; 10 | formattedError.path = err.path; 11 | 12 | if (err.originalError instanceof ArgumentValidationError) { 13 | formattedError.validationErrors = err.originalError.validationErrors; 14 | } 15 | 16 | return formattedError; 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or help request 3 | about: You don't know how do to something in your code or why it's not working 4 | 5 | --- 6 | 7 | If you want to achieve something in your code but you don't know how to do it, or you have some problems with configuration and your code is not working, gitter chat is a better place to ask your question: 8 | https://gitter.im/type-graphql/Lobby 9 | 10 | If your question is too complex, you will be redirected back to GitHub to create an issue but in correct category. 11 | 12 | **Please don't post issues with questions here, use gitter or StackOverflow. Thanks!** 13 | -------------------------------------------------------------------------------- /src/decorators/Ctx.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { SymbolKeysNotSupportedError } from "../errors"; 3 | 4 | export function Ctx(propertyName?: string): ParameterDecorator { 5 | return (prototype, propertyKey, parameterIndex) => { 6 | if (typeof propertyKey === "symbol") { 7 | throw new SymbolKeysNotSupportedError(); 8 | } 9 | 10 | getMetadataStorage().collectHandlerParamMetadata({ 11 | kind: "context", 12 | target: prototype.constructor, 13 | methodName: propertyKey, 14 | index: parameterIndex, 15 | propertyName, 16 | }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/decorators/PubSub.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { SymbolKeysNotSupportedError } from "../errors"; 3 | 4 | export function PubSub(triggerKey?: string): ParameterDecorator { 5 | return (prototype, propertyKey, parameterIndex) => { 6 | if (typeof propertyKey === "symbol") { 7 | throw new SymbolKeysNotSupportedError(); 8 | } 9 | 10 | getMetadataStorage().collectHandlerParamMetadata({ 11 | kind: "pubSub", 12 | target: prototype.constructor, 13 | methodName: propertyKey, 14 | index: parameterIndex, 15 | triggerKey, 16 | }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/errors/UnmetGraphQLPeerDependencyError.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getPeerDependencyGraphQLRequirement, 3 | getInstalledGraphQLVersion, 4 | } from "../utils/graphql-version"; 5 | 6 | export class UnmetGraphQLPeerDependencyError extends Error { 7 | constructor() { 8 | super( 9 | `Looks like you use an incorrect version of the 'graphql' package: "${getInstalledGraphQLVersion()}". ` + 10 | `Please ensure that you have installed a version ` + 11 | `that meets TypeGraphQL's requirement: "${getPeerDependencyGraphQLRequirement()}".`, 12 | ); 13 | 14 | Object.setPrototypeOf(this, new.target.prototype); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/helpers/customScalar.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from "graphql"; 2 | 3 | export const CustomScalar = new GraphQLScalarType({ 4 | name: "Custom", 5 | parseLiteral: () => "TypeGraphQL parseLiteral", 6 | parseValue: () => "TypeGraphQL parseValue", 7 | serialize: () => "TypeGraphQL serialize", 8 | }); 9 | export class CustomType {} 10 | 11 | export const ObjectScalar = new GraphQLScalarType({ 12 | name: "ObjectScalar", 13 | parseLiteral: () => ({ 14 | value: "TypeGraphQL parseLiteral", 15 | }), 16 | parseValue: () => ({ 17 | value: "TypeGraphQL parseValue", 18 | }), 19 | serialize: obj => obj.value, 20 | }); 21 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/resolvers/rate-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, FieldResolver, Root } from "../../../src"; 2 | import { Repository } from "typeorm"; 3 | import { InjectRepository } from "typeorm-typedi-extensions"; 4 | 5 | import { Rate } from "../entities/rate"; 6 | import { User } from "../entities/user"; 7 | 8 | @Resolver(of => Rate) 9 | export class RateResolver { 10 | constructor(@InjectRepository(User) private readonly userRepository: Repository) {} 11 | 12 | @FieldResolver() 13 | async user(@Root() rate: Rate): Promise { 14 | return (await this.userRepository.findOne(rate.userId, { cache: 1000 }))!; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/scalars/isodate.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, Kind } from "graphql"; 2 | 3 | export const GraphQLISODateTime = new GraphQLScalarType({ 4 | name: "DateTime", 5 | description: 6 | "The javascript `Date` as string. Type represents date and time as the ISO Date string.", 7 | parseValue(value: string) { 8 | return new Date(value); 9 | }, 10 | serialize(value: Date) { 11 | if (value instanceof Date) { 12 | return value.toISOString(); 13 | } 14 | return null; 15 | }, 16 | parseLiteral(ast) { 17 | if (ast.kind === Kind.STRING) { 18 | return new Date(ast.value); 19 | } 20 | return null; 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /examples/enums-and-unions/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { buildSchema } from "../../src"; 4 | 5 | import { ExampleResolver } from "./resolver"; 6 | 7 | async function bootstrap() { 8 | // build TypeGraphQL executable schema 9 | const schema = await buildSchema({ 10 | resolvers: [ExampleResolver], 11 | }); 12 | 13 | // Create GraphQL server 14 | const server = new ApolloServer({ schema }); 15 | 16 | // Start the server 17 | const { url } = await server.listen(4000); 18 | console.log(`Server is running, GraphQL Playground available at ${url}`); 19 | } 20 | 21 | bootstrap(); 22 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { buildSchema } from "../../src"; 4 | 5 | import { MultiResolver } from "./resolver"; 6 | 7 | async function bootstrap() { 8 | // build TypeGraphQL executable schema 9 | const schema = await buildSchema({ 10 | resolvers: [MultiResolver], 11 | }); 12 | 13 | // Create GraphQL server 14 | const server = new ApolloServer({ schema }); 15 | 16 | // Start the server 17 | const { url } = await server.listen(4000); 18 | console.log(`Server is running, GraphQL Playground available at ${url}`); 19 | } 20 | 21 | bootstrap(); 22 | -------------------------------------------------------------------------------- /examples/authorization/examples.gql: -------------------------------------------------------------------------------- 1 | query GetPublicRecipes { 2 | recipes { 3 | title 4 | description 5 | averageRating 6 | } 7 | } 8 | 9 | query GetRecipesForAuthedUser { 10 | recipes { 11 | title 12 | description 13 | ingredients 14 | averageRating 15 | } 16 | } 17 | 18 | query GetRecipesForAdmin { 19 | recipes { 20 | title 21 | description 22 | ingredients 23 | averageRating 24 | ratings 25 | } 26 | } 27 | 28 | mutation AddRecipeByAuthedUser { 29 | addRecipe(title: "Sample Recipe") { 30 | averageRating 31 | } 32 | } 33 | 34 | mutation DeleteRecipeByAdmin { 35 | deleteRecipe(title: "Recipe 1") 36 | } 37 | -------------------------------------------------------------------------------- /examples/middlewares/middlewares/log-access.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "typedi"; 2 | import { MiddlewareInterface, NextFn, ResolverData } from "../../../src"; 3 | 4 | import { Context } from "../context"; 5 | import { Logger } from "../logger"; 6 | 7 | @Service() 8 | export class LogAccessMiddleware implements MiddlewareInterface { 9 | constructor(private readonly logger: Logger) {} 10 | 11 | async use({ context, info }: ResolverData, next: NextFn) { 12 | const username: string = context.username || "guest"; 13 | this.logger.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`); 14 | return next(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple-subscriptions/examples.gql: -------------------------------------------------------------------------------- 1 | subscription AllNotifications { 2 | normalSubscription { 3 | id 4 | message 5 | date 6 | } 7 | } 8 | 9 | subscription EvenNotifications { 10 | subscriptionWithFilter { 11 | id 12 | message 13 | date 14 | } 15 | } 16 | 17 | mutation PublishMessage { 18 | pubSubMutation(message: "Hello") 19 | } 20 | 21 | # dynamic topics 22 | 23 | subscription DynamicTopic { 24 | subscriptionWithFilterToDynamicTopic(topic: "FOO_MESSAGES") { 25 | id 26 | message 27 | } 28 | } 29 | 30 | mutation PublishMessageToDynamicTopic { 31 | pubSubMutationToDynamicTopic(topic: "FOO_MESSAGES", message: "Hi Foo!") 32 | } 33 | -------------------------------------------------------------------------------- /src/metadata/definitions/field-metadata.ts: -------------------------------------------------------------------------------- 1 | import { ParamMetadata } from "./param-metadata"; 2 | import { TypeValueThunk, TypeOptions } from "../../decorators/types"; 3 | import { Middleware } from "../../interfaces/Middleware"; 4 | import { Complexity } from "../../interfaces"; 5 | 6 | export interface FieldMetadata { 7 | target: Function; 8 | schemaName: string; 9 | name: string; 10 | getType: TypeValueThunk; 11 | typeOptions: TypeOptions; 12 | description: string | undefined; 13 | deprecationReason: string | undefined; 14 | complexity: Complexity | undefined; 15 | params?: ParamMetadata[]; 16 | roles?: any[]; 17 | middlewares?: Array>; 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation-issue-or-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation issue or request 3 | about: There's something wrong in docs or something is missing 4 | 5 | --- 6 | 7 | **Describe the issue** 8 | A clear and concise description of what is wrong or what feature is missing. 9 | You may ask here for guides, e.g. "How to run TypeGraphQL on AWS Lamda?" if nobody helped you on gitter or StackOverflow. 10 | 11 | **Are you able to make a PR that fix this?** 12 | 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. 13 | 14 | **Additional context** 15 | Add any other context about the problem here. 16 | -------------------------------------------------------------------------------- /examples/enums-and-unions/examples.gql: -------------------------------------------------------------------------------- 1 | query AllRecipes { 2 | recipes { 3 | title 4 | description 5 | preparationDifficulty 6 | cook { 7 | name 8 | } 9 | } 10 | } 11 | 12 | query EasyRecipes { 13 | recipes(difficulty: Easy) { 14 | title 15 | description 16 | ingredients 17 | cook { 18 | name 19 | } 20 | } 21 | } 22 | 23 | query SearchByCookName { 24 | search(cookName: "Gordon") { 25 | __typename 26 | ... on Recipe { 27 | title 28 | preparationDifficulty 29 | cook { 30 | name 31 | } 32 | } 33 | ... on Cook { 34 | name 35 | yearsOfExperience 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/automatic-validation/examples.gql: -------------------------------------------------------------------------------- 1 | query GetRecipes { 2 | recipes { 3 | title 4 | description 5 | creationDate 6 | } 7 | } 8 | 9 | mutation CorrectAddRecipe { 10 | addRecipe(input: { 11 | title: "Correct title" 12 | description: "Very very very very very very very very long description" 13 | }) { 14 | creationDate 15 | } 16 | } 17 | 18 | mutation AddRecipeWithoutDesc { 19 | addRecipe(input: { 20 | title: "Correct title" 21 | }) { 22 | creationDate 23 | } 24 | } 25 | 26 | mutation IncorrectAddRecipe { 27 | addRecipe(input: { 28 | title: "Correct title" 29 | description: "Too short description" 30 | }) { 31 | creationDate 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/examples.gql: -------------------------------------------------------------------------------- 1 | query GetPersons { 2 | persons { 3 | __typename 4 | id 5 | name 6 | age 7 | ... on Student { 8 | universityName 9 | } 10 | ... on Employee { 11 | companyName 12 | } 13 | } 14 | } 15 | 16 | mutation AddStudent { 17 | addStudent(input: { 18 | name: "Student 1" 19 | dateOfBirth: "1991-11-30T00:00:00.000Z" 20 | universityName: "Uni 1" 21 | }) { 22 | id 23 | age 24 | } 25 | } 26 | 27 | mutation AddEmployee { 28 | addEmployee(input: { 29 | name: "Employee 1" 30 | dateOfBirth: "1995-07-23T00:00:00.000Z" 31 | companyName: "Company 1" 32 | }) { 33 | id 34 | age 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/examples.gql: -------------------------------------------------------------------------------- 1 | query FirstRecipe { 2 | recipe(id:"1") { 3 | title 4 | description 5 | comments { 6 | nickname 7 | content 8 | date 9 | } 10 | } 11 | } 12 | 13 | mutation AddCommentToRecipe1 { 14 | addNewComment(comment: { 15 | recipeId: "1", 16 | nickname: "19majkel94", 17 | content: "Nice one!" 18 | }) 19 | } 20 | 21 | mutation AddCommentToRecipe2 { 22 | addNewComment(comment: { 23 | recipeId: "2", 24 | nickname: "19majkel94", 25 | content: "Nice two!" 26 | }) 27 | } 28 | 29 | subscription NewCommentsForRecipe2 { 30 | newComments(recipeId: "2") { 31 | nickname 32 | content 33 | date 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/helpers/getSampleObjectFieldType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IntrospectionNamedTypeRef, 3 | IntrospectionObjectType, 4 | IntrospectionNonNullTypeRef, 5 | IntrospectionSchema, 6 | } from "graphql"; 7 | 8 | export function getSampleObjectFieldType(schemaIntrospection: IntrospectionSchema) { 9 | const sampleObject = schemaIntrospection.types.find( 10 | type => type.name === "SampleObject", 11 | ) as IntrospectionObjectType; 12 | return (fieldName: string) => { 13 | const field = sampleObject.fields.find(it => it.name === fieldName)!; 14 | const fieldType = (field.type as IntrospectionNonNullTypeRef) 15 | .ofType as IntrospectionNamedTypeRef; 16 | return fieldType; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Introduction": ["introduction"], 4 | "Beginner guides": ["installation", "getting-started", "types-and-fields", "resolvers", "bootstrap"], 5 | "Advanced guides": [ 6 | "scalars", 7 | "enums", 8 | "unions", 9 | "subscriptions", 10 | "interfaces-and-inheritance" 11 | ], 12 | "Features": [ 13 | "dependency-injection", 14 | "authorization", 15 | "validation", 16 | "middlewares", 17 | "complexity" 18 | ], 19 | "Others": ["emit-schema", "browser-usage"] 20 | }, 21 | "examples": { 22 | "Examples": ["examples"] 23 | }, 24 | "others": { 25 | "Others": ["faq"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/apollo-engine/recipe-samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe-type"; 4 | 5 | export function createRecipeSamples() { 6 | return plainToClass(Recipe, [ 7 | { 8 | title: "Recipe 1", 9 | description: "Desc 1", 10 | ratings: [0, 3, 1], 11 | creationDate: new Date("2018-04-11"), 12 | }, 13 | { 14 | title: "Recipe 2", 15 | description: "Desc 2", 16 | ratings: [4, 2, 3, 1], 17 | creationDate: new Date("2018-04-15"), 18 | }, 19 | { 20 | title: "Recipe 3", 21 | description: "Desc 3", 22 | ratings: [5, 4], 23 | creationDate: new Date(), 24 | }, 25 | ]); 26 | } 27 | -------------------------------------------------------------------------------- /examples/simple-usage/recipe-samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe-type"; 4 | 5 | export function createRecipeSamples() { 6 | return plainToClass(Recipe, [ 7 | { 8 | description: "Desc 1", 9 | title: "Recipe 1", 10 | ratings: [0, 3, 1], 11 | creationDate: new Date("2018-04-11"), 12 | }, 13 | { 14 | description: "Desc 2", 15 | title: "Recipe 2", 16 | ratings: [4, 2, 3, 1], 17 | creationDate: new Date("2018-04-15"), 18 | }, 19 | { 20 | description: "Desc 3", 21 | title: "Recipe 3", 22 | ratings: [5, 4], 23 | creationDate: new Date(), 24 | }, 25 | ]); 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want to suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | You can also propose how the new API should looks like. 13 | 14 | **Describe alternatives you've considered** 15 | A clear and concise description of any alternative solutions or features you've considered. 16 | 17 | **Additional context** 18 | Add any other context or screenshots about the feature request here. 19 | -------------------------------------------------------------------------------- /examples/authorization/auth-checker.ts: -------------------------------------------------------------------------------- 1 | import { AuthChecker } from "../../src"; 2 | 3 | import { Context } from "./context.interface"; 4 | 5 | // create auth checker function 6 | export const authChecker: AuthChecker = ({ context: { user } }, roles) => { 7 | if (roles.length === 0) { 8 | // if `@Authorized()`, check only is user exist 9 | return user !== undefined; 10 | } 11 | // there are some roles defined now 12 | 13 | if (!user) { 14 | // and if no user, restrict access 15 | return false; 16 | } 17 | if (user.roles.some(role => roles.includes(role))) { 18 | // grant access if the roles overlap 19 | return true; 20 | } 21 | 22 | // no roles matched, restrict access 23 | return false; 24 | }; 25 | -------------------------------------------------------------------------------- /examples/authorization/recipe.type.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, Int, Authorized, Float } from "../../src"; 2 | 3 | @ObjectType() 4 | export class Recipe { 5 | @Field() 6 | title: string; 7 | 8 | @Field({ nullable: true }) 9 | description?: string; 10 | 11 | @Authorized() // restrict access to ingredients only for logged users (paid subscription?) 12 | @Field(type => [String]) 13 | ingredients: string[]; 14 | 15 | @Authorized("ADMIN") // restrict access to rates details for admin only 16 | @Field(type => [Int]) 17 | ratings: number[]; 18 | 19 | @Field(type => Float, { nullable: true }) 20 | get averageRating(): number | null { 21 | return this.ratings.reduce((a, b) => a + b, 0) / this.ratings.length; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/using-container/sample-recipes.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe-type"; 4 | 5 | export const sampleRecipes = [ 6 | createRecipe({ 7 | id: "1", 8 | title: "Recipe 1", 9 | description: "Desc 1", 10 | ingredients: ["one", "two", "three"], 11 | }), 12 | createRecipe({ 13 | id: "2", 14 | title: "Recipe 2", 15 | description: "Desc 2", 16 | ingredients: ["four", "five", "six"], 17 | }), 18 | createRecipe({ 19 | id: "3", 20 | title: "Recipe 3", 21 | ingredients: ["seven", "eight", "nine"], 22 | }), 23 | ]; 24 | 25 | function createRecipe(recipeData: Partial): Recipe { 26 | return plainToClass(Recipe, recipeData); 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/auth-middleware.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFn } from "../interfaces/Middleware"; 2 | import { AuthChecker, AuthMode } from "../interfaces"; 3 | import { UnauthorizedError, ForbiddenError } from "../errors"; 4 | 5 | export function AuthMiddleware( 6 | authChecker: AuthChecker, 7 | authMode: AuthMode, 8 | roles: any[], 9 | ): MiddlewareFn { 10 | return async (action, next) => { 11 | const accessGranted = await authChecker(action, roles); 12 | if (!accessGranted) { 13 | if (authMode === "null") { 14 | return null; 15 | } else if (authMode === "error") { 16 | throw roles.length === 0 ? new UnauthorizedError() : new ForbiddenError(); 17 | } 18 | } 19 | return next(); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./helpers/formatArgumentValidationError"; 2 | 3 | export * from "./ArgumentValidationError"; 4 | export * from "./CannotDetermineTypeError"; 5 | export * from "./ForbiddenError"; 6 | export * from "./GeneratingSchemaError"; 7 | export * from "./ConflictingDefaultValuesError"; 8 | export * from "./ConflictingDefaultWithNullableError"; 9 | export * from "./MissingSubscriptionTopicsError"; 10 | export * from "./NoExplicitTypeError"; 11 | export * from "./ReflectMetadataMissingError"; 12 | export * from "./SymbolKeysNotSupportedError"; 13 | export * from "./UnauthorizedError"; 14 | export * from "./UnionResolveTypeError"; 15 | export * from "./UnmetGraphQLPeerDependencyError"; 16 | export * from "./WrongNullableListOptionError"; 17 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/entities/user.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from "../../../src"; 2 | import { PrimaryGeneratedColumn, Column, Entity, OneToMany } from "typeorm"; 3 | 4 | import { Recipe } from "./recipe"; 5 | import { Lazy } from "../helpers"; 6 | 7 | @ObjectType() 8 | @Entity() 9 | export class User { 10 | @Field(type => ID) 11 | @PrimaryGeneratedColumn() 12 | readonly id: number; 13 | 14 | @Field() 15 | @Column() 16 | email: string; 17 | 18 | @Field({ nullable: true }) 19 | @Column({ nullable: true }) 20 | nickname?: string; 21 | 22 | @Column() 23 | password: string; 24 | 25 | @OneToMany(type => Recipe, recipe => recipe.author, { lazy: true }) 26 | @Field(type => [Recipe]) 27 | recipes: Lazy; 28 | } 29 | -------------------------------------------------------------------------------- /examples/middlewares/decorators/validate-args.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | import { validate } from "class-validator"; 3 | import { ClassType, ArgumentValidationError, UseMiddleware } from "../../../src"; 4 | 5 | // sample implementation of custom validation decorator 6 | // this example use `class-validator` however you can plug-in `joi` or any other lib 7 | export function ValidateArgs(type: ClassType) { 8 | return UseMiddleware(async ({ args }, next) => { 9 | const instance = plainToClass(type, args); 10 | const validationErrors = await validate(instance); 11 | if (validationErrors.length > 0) { 12 | throw new ArgumentValidationError(validationErrors); 13 | } 14 | return next(); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/entities/rate.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, Int } from "../../../src"; 2 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, CreateDateColumn } from "typeorm"; 3 | 4 | import { User } from "./user"; 5 | import { Recipe } from "./recipe"; 6 | import { Lazy } from "../helpers"; 7 | 8 | @Entity() 9 | @ObjectType() 10 | export class Rate { 11 | @PrimaryGeneratedColumn() 12 | readonly id: number; 13 | 14 | @Field(type => Int) 15 | @Column({ type: "int" }) 16 | value: number; 17 | 18 | @Field(type => User) 19 | @ManyToOne(type => User, { lazy: true }) 20 | user: Lazy; 21 | 22 | @Field() 23 | @CreateDateColumn() 24 | date: Date; 25 | 26 | @ManyToOne(type => Recipe, { lazy: true }) 27 | recipe: Lazy; 28 | } 29 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/recipe/recipe.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, FieldResolver, Root } from "../../../src"; 2 | 3 | import { createResourceResolver } from "../resource/resource.resolver"; 4 | import { Recipe } from "./recipe.type"; 5 | 6 | const recipes: Recipe[] = [ 7 | { 8 | id: 1, 9 | title: "Recipe 1", 10 | ratings: [1, 3, 4], 11 | }, 12 | ]; 13 | 14 | export const ResourceResolver = createResourceResolver(Recipe, recipes); 15 | 16 | @Resolver(of => Recipe) 17 | export class RecipeResolver extends ResourceResolver { 18 | // here you can add resource-specific operations 19 | 20 | @FieldResolver() 21 | averageRating(@Root() recipe: Recipe): number { 22 | return recipe.ratings.reduce((a, b) => a + b, 0) / recipe.ratings.length; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/authorization/recipe.helpers.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | 5 | export function createRecipe(recipeData: Partial): Recipe { 6 | return plainToClass(Recipe, recipeData); 7 | } 8 | 9 | export const sampleRecipes = [ 10 | createRecipe({ 11 | title: "Recipe 1", 12 | description: "Desc 1", 13 | ingredients: ["one", "two", "three"], 14 | ratings: [3, 4, 5, 5, 5], 15 | }), 16 | createRecipe({ 17 | title: "Recipe 2", 18 | description: "Desc 2", 19 | ingredients: ["four", "five", "six"], 20 | ratings: [3, 4, 5, 3, 2], 21 | }), 22 | createRecipe({ 23 | title: "Recipe 3", 24 | ingredients: ["seven", "eight", "nine"], 25 | ratings: [4, 4, 5, 5, 4], 26 | }), 27 | ]; 28 | -------------------------------------------------------------------------------- /examples/middlewares/recipe/recipe.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Args, UseMiddleware } from "../../../src"; 2 | 3 | import recipeSamples from "./recipe.samples"; 4 | import { Recipe } from "./recipe.type"; 5 | import { RecipesArgs } from "./recipe.args"; 6 | import { ValidateArgs } from "../decorators/validate-args"; 7 | 8 | @Resolver(of => Recipe) 9 | export class RecipeResolver { 10 | private readonly items: Recipe[] = recipeSamples; 11 | 12 | @Query(returns => [Recipe]) 13 | @ValidateArgs(RecipesArgs) 14 | async recipes( 15 | @Args({ validate: false }) // disable built-in validation here 16 | options: RecipesArgs, 17 | ): Promise { 18 | const start = options.skip; 19 | const end = options.skip + options.take; 20 | return await this.items.slice(start, end); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { Container } from "typedi"; 4 | import { buildSchema } from "../../src"; 5 | 6 | import { RecipeResolver } from "./recipe/recipe.resolver"; 7 | import { PersonResolver } from "./person/person.resolver"; 8 | 9 | async function bootstrap() { 10 | // build TypeGraphQL executable schema 11 | const schema = await buildSchema({ 12 | resolvers: [RecipeResolver, PersonResolver], 13 | container: Container, 14 | }); 15 | 16 | // Create GraphQL server 17 | const server = new ApolloServer({ schema }); 18 | 19 | // Start the server 20 | const { url } = await server.listen(4000); 21 | console.log(`Server is running, GraphQL Playground available at ${url}`); 22 | } 23 | 24 | bootstrap(); 25 | -------------------------------------------------------------------------------- /website/pages/snippets/testability.md: -------------------------------------------------------------------------------- 1 | ```typescript 2 | @Resolver(of => Recipe) 3 | export class RecipeResolver { 4 | constructor( 5 | private readonly recipeRepository: Repository, 6 | private readonly rateRepository: Repository, 7 | ) {} 8 | 9 | @Query(returns => Recipe) 10 | async recipe(@Arg("recipeId") recipeId: string) { 11 | return this.recipeRepository.findOneById(recipeId); 12 | } 13 | 14 | @Mutation(returns => Recipe) 15 | async addRecipe(@Arg("recipe") recipeInput: RecipeInput) { 16 | const newRecipe = this.recipeRepository.create(recipeInput); 17 | return this.recipeRepository.save(newRecipe); 18 | } 19 | 20 | @FieldResolver() 21 | ratings(@Root() recipe: Recipe) { 22 | return this.ratingsRepository.find({ recipeId: recipe.id }); 23 | } 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/static/css/prism-theme.css: -------------------------------------------------------------------------------- 1 | .token.function { 2 | color: #dcdcaa; 3 | } 4 | 5 | .token.punctuation, .token.operator, .token.constant, .hljs.graphql { 6 | color: #d4d4d4; 7 | } 8 | 9 | .token.keyword, .token.boolean { 10 | color: #569cd6; 11 | } 12 | 13 | .token.class-name, .token.builtin { 14 | color: #4ec9b0; 15 | } 16 | 17 | .token.number { 18 | color: #b5cea8; 19 | } 20 | 21 | .token.comment { 22 | color: #6a9955; 23 | } 24 | 25 | .token.string { 26 | color: #ce9178; 27 | } 28 | 29 | .token.regex { 30 | color: #d16969; 31 | } 32 | 33 | .hljs, .token.attr-name, .token.property { 34 | color: #9cdcfe; 35 | } 36 | 37 | .hljs.graphql .token.attr-name { 38 | color: #9cdcfe; 39 | } 40 | 41 | .hljs a { 42 | color: inherit !important; 43 | } 44 | 45 | .hljs { 46 | background: rgb(30, 30, 30); 47 | } 48 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/entities/rate.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, Int } from "../../../src"; 2 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, CreateDateColumn } from "typeorm"; 3 | 4 | import { User } from "./user"; 5 | import { Recipe } from "./recipe"; 6 | import { RelationColumn } from "../helpers"; 7 | 8 | @Entity() 9 | @ObjectType() 10 | export class Rate { 11 | @PrimaryGeneratedColumn() 12 | readonly id: number; 13 | 14 | @Field(type => Int) 15 | @Column({ type: "int" }) 16 | value: number; 17 | 18 | @Field(type => User) 19 | @ManyToOne(type => User) 20 | user: User; 21 | @RelationColumn() 22 | userId: number; 23 | 24 | @Field() 25 | @CreateDateColumn() 26 | date: Date; 27 | 28 | @ManyToOne(type => Recipe) 29 | recipe: Recipe; 30 | @RelationColumn() 31 | recipeId: number; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/graphql-version.ts: -------------------------------------------------------------------------------- 1 | import * as semVer from "semver"; 2 | 3 | import { UnmetGraphQLPeerDependencyError } from "../errors"; 4 | 5 | export function getInstalledGraphQLVersion(): string { 6 | const graphqlPackageJson = require("graphql/package.json"); 7 | return graphqlPackageJson.version; 8 | } 9 | 10 | export function getPeerDependencyGraphQLRequirement(): string { 11 | const ownPackageJson = require("../../package.json"); 12 | return ownPackageJson.peerDependencies.graphql; 13 | } 14 | 15 | export function ensureInstalledCorrectGraphQLPackage() { 16 | const installedVersion = getInstalledGraphQLVersion(); 17 | const versionRequirement = getPeerDependencyGraphQLRequirement(); 18 | 19 | if (!semVer.satisfies(installedVersion, versionRequirement)) { 20 | throw new UnmetGraphQLPeerDependencyError(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | include: 3 | - stage: test 4 | language: node_js 5 | node_js: 6 | - 11 7 | - 10 8 | - 8 9 | - 6 10 | before_install: 11 | - npm i -g codecov 12 | script: 13 | - npm run verify 14 | - npm run test:ci 15 | after_success: 16 | - codecov 17 | notifications: 18 | email: 19 | on_success: never 20 | on_failure: always 21 | - stage: deploy 22 | branches: 23 | only: 24 | - master 25 | language: node_js 26 | node_js: 10 27 | install: true 28 | before_script: 29 | - chmod +x publish-website.sh 30 | script: 31 | - ./publish-website.sh 32 | notifications: 33 | email: 34 | on_success: never 35 | on_failure: always 36 | -------------------------------------------------------------------------------- /examples/simple-subscriptions/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { buildSchema } from "../../src"; 4 | 5 | import { SampleResolver } from "./resolver"; 6 | 7 | async function bootstrap() { 8 | // Build the TypeGraphQL schema 9 | const schema = await buildSchema({ 10 | resolvers: [SampleResolver], 11 | }); 12 | 13 | // Create GraphQL server 14 | const server = new ApolloServer({ 15 | schema, 16 | playground: true, 17 | // you can pass the endpoint path for subscriptions 18 | // otherwise it will be the same as main graphql endpoint 19 | // subscriptions: "/subscriptions", 20 | }); 21 | 22 | // Start the server 23 | const { url } = await server.listen(4000); 24 | console.log(`Server is running, GraphQL Playground available at ${url}`); 25 | } 26 | 27 | bootstrap(); 28 | -------------------------------------------------------------------------------- /src/decorators/InputType.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { getNameDecoratorParams } from "../helpers/decorators"; 3 | import { DescriptionOptions } from "./types"; 4 | 5 | export function InputType(): ClassDecorator; 6 | export function InputType(options: DescriptionOptions): ClassDecorator; 7 | export function InputType(name: string, options?: DescriptionOptions): ClassDecorator; 8 | export function InputType( 9 | nameOrOptions?: string | DescriptionOptions, 10 | maybeOptions?: DescriptionOptions, 11 | ): ClassDecorator { 12 | const { name, options } = getNameDecoratorParams(nameOrOptions, maybeOptions); 13 | return target => { 14 | getMetadataStorage().collectInputMetadata({ 15 | name: name || target.name, 16 | target, 17 | description: options.description, 18 | }); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/resource/resource.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "typedi"; 2 | 3 | import { Resource } from "./resource"; 4 | 5 | // we need to use factory as we need separate instance of service for each generic 6 | @Service() 7 | export class ResourceServiceFactory { 8 | create(resources?: TResource[]) { 9 | return new ResourceService(resources); 10 | } 11 | } 12 | 13 | export class ResourceService { 14 | constructor(protected resources: TResource[] = []) {} 15 | 16 | getOne(id: number): TResource | undefined { 17 | return this.resources.find(res => res.id === id); 18 | } 19 | 20 | getAll(skip: number, take: number): TResource[] { 21 | const start: number = skip; 22 | const end: number = skip + take; 23 | return this.resources.slice(start, end); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export { Arg } from "./Arg"; 2 | export { Args } from "./Args"; 3 | export { ArgsType } from "./ArgsType"; 4 | export { Authorized } from "./Authorized"; 5 | export { Ctx } from "./Ctx"; 6 | export { registerEnumType } from "./enums"; 7 | export { Field } from "./Field"; 8 | export { FieldResolver } from "./FieldResolver"; 9 | export { Info } from "./Info"; 10 | export { InputType } from "./InputType"; 11 | export { InterfaceType } from "./InterfaceType"; 12 | export { Mutation } from "./Mutation"; 13 | export { ObjectType } from "./ObjectType"; 14 | export { PubSub } from "./PubSub"; 15 | export { Query } from "./Query"; 16 | export { Resolver } from "./Resolver"; 17 | export { Root } from "./Root"; 18 | export { Subscription } from "./Subscription"; 19 | export { createUnionType } from "./unions"; 20 | export { UseMiddleware } from "./UseMiddleware"; 21 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/entities/recipe.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from "../../../src"; 2 | import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne } from "typeorm"; 3 | 4 | import { Rate } from "./rate"; 5 | import { User } from "./user"; 6 | import { Lazy } from "../helpers"; 7 | 8 | @Entity() 9 | @ObjectType() 10 | export class Recipe { 11 | @Field(type => ID) 12 | @PrimaryGeneratedColumn() 13 | readonly id: number; 14 | 15 | @Field() 16 | @Column() 17 | title: string; 18 | 19 | @Field({ nullable: true }) 20 | @Column({ nullable: true }) 21 | description?: string; 22 | 23 | @Field(type => [Rate]) 24 | @OneToMany(type => Rate, rate => rate.recipe, { lazy: true, cascade: ["insert"] }) 25 | ratings: Lazy; 26 | 27 | @Field(type => User) 28 | @ManyToOne(type => User, { lazy: true }) 29 | author: Lazy; 30 | } 31 | -------------------------------------------------------------------------------- /examples/query-complexity/recipe-type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, Int, Float } from "../../src"; 2 | 3 | @ObjectType() 4 | export class Recipe { 5 | /* 6 | By default, every field gets a complexity of 1. 7 | */ 8 | @Field() 9 | title: string; 10 | 11 | /* 12 | Which can be customized by passing the complexity parameter 13 | */ 14 | @Field(type => Int, { complexity: 2 }) 15 | ratingsCount: number; 16 | 17 | @Field(type => Float, { 18 | nullable: true, 19 | complexity: 10, 20 | }) 21 | get averageRating(): number | null { 22 | const ratingsCount = this.ratings.length; 23 | if (ratingsCount === 0) { 24 | return null; 25 | } 26 | const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); 27 | return ratingsSum / ratingsCount; 28 | } 29 | 30 | // internal property, not exposed in schema 31 | ratings: number[]; 32 | } 33 | -------------------------------------------------------------------------------- /src/decorators/InterfaceType.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { getNameDecoratorParams } from "../helpers/decorators"; 3 | import { DescriptionOptions } from "./types"; 4 | 5 | export function InterfaceType(): ClassDecorator; 6 | export function InterfaceType(options: DescriptionOptions): ClassDecorator; 7 | export function InterfaceType(name: string, options?: DescriptionOptions): ClassDecorator; 8 | export function InterfaceType( 9 | nameOrOptions?: string | DescriptionOptions, 10 | maybeOptions?: DescriptionOptions, 11 | ): ClassDecorator { 12 | const { name, options } = getNameDecoratorParams(nameOrOptions, maybeOptions); 13 | return target => { 14 | getMetadataStorage().collectInterfaceMetadata({ 15 | name: name || target.name, 16 | target, 17 | description: options.description, 18 | }); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /examples/automatic-validation/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { buildSchema, formatArgumentValidationError } from "../../src"; 4 | 5 | import { RecipeResolver } from "./recipe-resolver"; 6 | 7 | async function bootstrap() { 8 | // build TypeGraphQL executable schema 9 | const schema = await buildSchema({ 10 | resolvers: [RecipeResolver], 11 | }); 12 | 13 | // Create GraphQL server 14 | const server = new ApolloServer({ 15 | schema, 16 | // remember to pass `formatArgumentValidationError` 17 | // otherwise validation errors won't be returned to a client 18 | formatError: formatArgumentValidationError, 19 | }); 20 | 21 | // Start the server 22 | const { url } = await server.listen(4000); 23 | console.log(`Server is running, GraphQL Playground available at ${url}`); 24 | } 25 | 26 | bootstrap(); 27 | -------------------------------------------------------------------------------- /dev.js: -------------------------------------------------------------------------------- 1 | require("ts-node/register/transpile-only"); 2 | // require("./examples/apollo-engine/index.ts"); 3 | // require("./examples/authorization/index.ts"); 4 | // require("./examples/automatic-validation/index.ts"); 5 | // require("./examples/enums-and-unions/index.ts"); 6 | // require("./examples/interfaces-inheritance/index.ts"); 7 | // require("./examples/middlewares/index.ts"); 8 | // require("./examples/redis-subscriptions/index.ts"); 9 | // require("./examples/resolvers-inheritance/index.ts"); 10 | // require("./examples/simple-subscriptions/index.ts"); 11 | // require("./examples/query-complexity/index.ts"); 12 | // require("./examples/simple-usage/index.ts"); 13 | // require("./examples/using-container/index.ts"); 14 | // require("./examples/using-scoped-container/index.ts"); 15 | // require("./examples/typeorm-basic-usage/index.ts"); 16 | // require("./examples/typeorm-lazy-relations/index.ts"); 17 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/entities/recipe.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from "../../../src"; 2 | import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne } from "typeorm"; 3 | 4 | import { Rate } from "./rate"; 5 | import { User } from "./user"; 6 | import { RelationColumn } from "../helpers"; 7 | 8 | @Entity() 9 | @ObjectType() 10 | export class Recipe { 11 | @Field(type => ID) 12 | @PrimaryGeneratedColumn() 13 | readonly id: number; 14 | 15 | @Field() 16 | @Column() 17 | title: string; 18 | 19 | @Field({ nullable: true }) 20 | @Column({ nullable: true }) 21 | description?: string; 22 | 23 | @Field(type => [Rate]) 24 | @OneToMany(type => Rate, rate => rate.recipe, { cascade: ["insert"] }) 25 | ratings: Rate[]; 26 | 27 | @Field(type => User) 28 | @ManyToOne(type => User) 29 | author: User; 30 | @RelationColumn() 31 | authorId: number; 32 | } 33 | -------------------------------------------------------------------------------- /examples/middlewares/recipe/recipe.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType, Int, Float, UseMiddleware } from "../../../src"; 2 | 3 | import { LogAccessMiddleware } from "../middlewares/log-access"; 4 | import { NumberInterceptor } from "../middlewares/number-interceptor"; 5 | 6 | @ObjectType() 7 | export class Recipe { 8 | @Field() 9 | title: string; 10 | 11 | @Field({ nullable: true }) 12 | description?: string; 13 | 14 | @Field(type => [Int]) 15 | @UseMiddleware(LogAccessMiddleware) 16 | ratings: number[]; 17 | 18 | @Field(type => Float, { nullable: true }) 19 | @UseMiddleware(NumberInterceptor(3)) 20 | get averageRating(): number | null { 21 | const ratingsCount = this.ratings.length; 22 | if (ratingsCount === 0) { 23 | return null; 24 | } 25 | const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); 26 | return ratingsSum / ratingsCount; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/decorators/unions.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from "../interfaces"; 2 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 3 | import { InstanceSideOfClass, ArrayElementTypes } from "../helpers/utils"; 4 | 5 | export interface UnionTypeConfig { 6 | name: string; 7 | description?: string; 8 | types: ObjectTypes; 9 | } 10 | 11 | export type UnionFromClasses = InstanceSideOfClass>; 12 | 13 | export function createUnionType({ 14 | types, 15 | name, 16 | description, 17 | }: UnionTypeConfig): UnionFromClasses; 18 | export function createUnionType({ types, name, description }: UnionTypeConfig): any { 19 | const unionMetadataSymbol = getMetadataStorage().collectUnionMetadata({ 20 | types, 21 | name, 22 | description, 23 | }); 24 | 25 | return unionMetadataSymbol; 26 | } 27 | -------------------------------------------------------------------------------- /examples/simple-usage/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import * as path from "path"; 4 | import { buildSchema } from "../../src"; 5 | 6 | import { RecipeResolver } from "./recipe-resolver"; 7 | 8 | async function bootstrap() { 9 | // build TypeGraphQL executable schema 10 | const schema = await buildSchema({ 11 | resolvers: [RecipeResolver], 12 | // automatically create `schema.gql` file with schema definition in current folder 13 | emitSchemaFile: path.resolve(__dirname, "schema.gql"), 14 | }); 15 | 16 | // Create GraphQL server 17 | const server = new ApolloServer({ 18 | schema, 19 | // enable GraphQL Playground 20 | playground: true, 21 | }); 22 | 23 | // Start the server 24 | const { url } = await server.listen(4000); 25 | console.log(`Server is running, GraphQL Playground available at ${url}`); 26 | } 27 | 28 | bootstrap(); 29 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/examples.gql: -------------------------------------------------------------------------------- 1 | query GetRecipes { 2 | recipes { 3 | id 4 | title 5 | author { 6 | email 7 | } 8 | ratings { 9 | value 10 | } 11 | } 12 | } 13 | 14 | query GetRecipe { 15 | recipe(recipeId: 1) { 16 | id 17 | title 18 | ratings { 19 | value 20 | user { 21 | nickname 22 | } 23 | date 24 | } 25 | author { 26 | id 27 | nickname 28 | email 29 | } 30 | } 31 | } 32 | 33 | mutation AddRecipe { 34 | addRecipe(recipe: { 35 | title: "New Recipe" 36 | }) { 37 | id 38 | ratings { 39 | value 40 | } 41 | author { 42 | nickname 43 | } 44 | } 45 | } 46 | 47 | mutation RateRecipe { 48 | rate(rate: { 49 | recipeId: 3 50 | value: 4 51 | }) { 52 | id 53 | ratings { 54 | value 55 | user { 56 | email 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/examples.gql: -------------------------------------------------------------------------------- 1 | query GetRecipes { 2 | recipes { 3 | id 4 | title 5 | author { 6 | id 7 | email 8 | nickname 9 | } 10 | ratings { 11 | value 12 | } 13 | } 14 | } 15 | 16 | query GetRecipe { 17 | recipe(recipeId: 1) { 18 | id 19 | title 20 | ratings { 21 | value 22 | user { 23 | nickname 24 | } 25 | date 26 | } 27 | author { 28 | nickname 29 | recipes { 30 | title 31 | } 32 | } 33 | } 34 | } 35 | 36 | mutation AddRecipe { 37 | addRecipe(recipe: { 38 | title: "New Recipe" 39 | }) { 40 | id 41 | ratings { 42 | value 43 | } 44 | author { 45 | nickname 46 | } 47 | } 48 | } 49 | 50 | mutation RateRecipe { 51 | rate(rate: { 52 | recipeId: 3 53 | value: 4 54 | }) { 55 | id 56 | ratings { 57 | value 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/using-container/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { Container } from "typedi"; 4 | import { buildSchema } from "../../src"; 5 | 6 | import { RecipeResolver } from "./recipe-resolver"; 7 | import { sampleRecipes } from "./sample-recipes"; 8 | 9 | // put sample recipes in container 10 | Container.set({ id: "SAMPLE_RECIPES", factory: () => sampleRecipes.slice() }); 11 | 12 | async function bootstrap() { 13 | // build TypeGraphQL executable schema 14 | const schema = await buildSchema({ 15 | resolvers: [RecipeResolver], 16 | // register 3rd party IOC container 17 | container: Container, 18 | }); 19 | 20 | // Create GraphQL server 21 | const server = new ApolloServer({ schema }); 22 | 23 | // Start the server 24 | const { url } = await server.listen(4000); 25 | console.log(`Server is running, GraphQL Playground available at ${url}`); 26 | } 27 | 28 | bootstrap(); 29 | -------------------------------------------------------------------------------- /src/interfaces/resolvers-map.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLScalarType, 3 | GraphQLFieldResolver, 4 | GraphQLTypeResolver, 5 | GraphQLIsTypeOfFn, 6 | } from "graphql"; 7 | 8 | export interface ResolversMap { 9 | [key: string]: 10 | | ResolverObject 11 | | ResolverOptions 12 | | GraphQLScalarType 13 | | EnumResolver; 14 | } 15 | 16 | export interface ResolverObject { 17 | [key: string]: ResolverOptions; 18 | } 19 | 20 | export interface EnumResolver { 21 | [key: string]: string | number; 22 | } 23 | 24 | export interface ResolverOptions { 25 | fragment?: string; 26 | resolve?: GraphQLFieldResolver; 27 | subscribe?: GraphQLFieldResolver; 28 | __resolveType?: GraphQLTypeResolver; 29 | __isTypeOf?: GraphQLIsTypeOfFn; 30 | } 31 | -------------------------------------------------------------------------------- /src/scalars/timestamp.ts: -------------------------------------------------------------------------------- 1 | import { Kind, GraphQLScalarType } from "graphql"; 2 | 3 | function parseValue(value: string | null) { 4 | if (value === null) { 5 | return null; 6 | } 7 | try { 8 | return new Date(value); 9 | } catch (err) { 10 | return null; 11 | } 12 | } 13 | 14 | export const GraphQLTimestamp = new GraphQLScalarType({ 15 | name: "Timestamp", 16 | description: 17 | "The javascript `Date` as integer. " + 18 | "Type represents date and time as number of milliseconds from start of UNIX epoch.", 19 | serialize(value: Date) { 20 | if (value instanceof Date) { 21 | return value.getTime(); 22 | } 23 | return null; 24 | }, 25 | parseValue, 26 | parseLiteral(ast) { 27 | if (ast.kind === Kind.INT) { 28 | const num = parseInt(ast.value, 10); 29 | return new Date(num); 30 | } else if (ast.kind === Kind.STRING) { 31 | return parseValue(ast.value); 32 | } 33 | return null; 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/decorators/Query.ts: -------------------------------------------------------------------------------- 1 | import { ReturnTypeFunc, AdvancedOptions } from "./types"; 2 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 3 | import { getResolverMetadata } from "../helpers/resolver-metadata"; 4 | import { getTypeDecoratorParams } from "../helpers/decorators"; 5 | 6 | export function Query(): MethodDecorator; 7 | export function Query(options: AdvancedOptions): MethodDecorator; 8 | export function Query(returnTypeFunc: ReturnTypeFunc, options?: AdvancedOptions): MethodDecorator; 9 | export function Query( 10 | returnTypeFuncOrOptions?: ReturnTypeFunc | AdvancedOptions, 11 | maybeOptions?: AdvancedOptions, 12 | ): MethodDecorator { 13 | const { options, returnTypeFunc } = getTypeDecoratorParams(returnTypeFuncOrOptions, maybeOptions); 14 | return (prototype, methodName) => { 15 | const metadata = getResolverMetadata(prototype, methodName, returnTypeFunc, options); 16 | getMetadataStorage().collectQueryHandlerMetadata(metadata); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /examples/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import Container from "typedi"; 3 | import { ApolloServer } from "apollo-server"; 4 | import { buildSchema, formatArgumentValidationError } from "../../src"; 5 | 6 | import { RecipeResolver } from "./recipe/recipe.resolver"; 7 | import { ResolveTimeMiddleware } from "./middlewares/resolve-time"; 8 | import { ErrorLoggerMiddleware } from "./middlewares/error-logger"; 9 | 10 | async function bootstrap() { 11 | // build TypeGraphQL executable schema 12 | const schema = await buildSchema({ 13 | resolvers: [RecipeResolver], 14 | globalMiddlewares: [ErrorLoggerMiddleware, ResolveTimeMiddleware], 15 | container: Container, 16 | }); 17 | 18 | // Create GraphQL server 19 | const server = new ApolloServer({ schema, formatError: formatArgumentValidationError }); 20 | 21 | // Start the server 22 | const { url } = await server.listen(4000); 23 | console.log(`Server is running, GraphQL Playground available at ${url}`); 24 | } 25 | 26 | bootstrap(); 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something works incorrectly or doesn't work at all 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | A quick guide how to reproduce the bug. 12 | You can paste here code snippets or even better, provide a link to the repository with minimal reproducible code example. 13 | 14 | **Expected behavior** 15 | A clear and concise description of what you expected to happen. 16 | 17 | **Logs** 18 | If applicable, add some console logs to help explain your problem. 19 | You can paste the errors with stack trace that were printed when the error occured. 20 | 21 | **Enviorment (please complete the following information):** 22 | - OS: [e.g. Windows] 23 | - Node (e.g. 10.5.0) 24 | - Package version [e.g. 0.12.2] (please check if the bug still exist in newest release) 25 | - TypeScript version (e.g. 2.8.2) 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /src/decorators/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { ReturnTypeFunc, AdvancedOptions } from "./types"; 2 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 3 | import { getResolverMetadata } from "../helpers/resolver-metadata"; 4 | import { getTypeDecoratorParams } from "../helpers/decorators"; 5 | 6 | export function Mutation(): MethodDecorator; 7 | export function Mutation(options: AdvancedOptions): MethodDecorator; 8 | export function Mutation( 9 | returnTypeFunc: ReturnTypeFunc, 10 | options?: AdvancedOptions, 11 | ): MethodDecorator; 12 | export function Mutation( 13 | returnTypeFuncOrOptions?: ReturnTypeFunc | AdvancedOptions, 14 | maybeOptions?: AdvancedOptions, 15 | ): MethodDecorator { 16 | const { options, returnTypeFunc } = getTypeDecoratorParams(returnTypeFuncOrOptions, maybeOptions); 17 | return (prototype, methodName) => { 18 | const metadata = getResolverMetadata(prototype, methodName, returnTypeFunc, options); 19 | getMetadataStorage().collectMutationHandlerMetadata(metadata); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/decorators/Args.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { getParamInfo } from "../helpers/params"; 3 | import { ValidateOptions, ReturnTypeFunc } from "./types"; 4 | import { getTypeDecoratorParams } from "../helpers/decorators"; 5 | 6 | export function Args(): ParameterDecorator; 7 | export function Args(options: ValidateOptions): ParameterDecorator; 8 | export function Args( 9 | paramTypeFunction: ReturnTypeFunc, 10 | options?: ValidateOptions, 11 | ): ParameterDecorator; 12 | export function Args( 13 | paramTypeFnOrOptions?: ReturnTypeFunc | ValidateOptions, 14 | maybeOptions?: ValidateOptions, 15 | ): ParameterDecorator { 16 | const { options, returnTypeFunc } = getTypeDecoratorParams(paramTypeFnOrOptions, maybeOptions); 17 | return (prototype, propertyKey, parameterIndex) => { 18 | getMetadataStorage().collectHandlerParamMetadata({ 19 | kind: "args", 20 | ...getParamInfo({ prototype, propertyKey, parameterIndex, returnTypeFunc, options }), 21 | }); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /examples/simple-usage/schema.gql: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- 2 | # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! 3 | # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! 4 | # ----------------------------------------------- 5 | 6 | # The javascript `Date` as string. Type represents date and time as the ISO Date string. 7 | scalar DateTime 8 | 9 | type Mutation { 10 | addRecipe(recipe: RecipeInput!): Recipe! 11 | } 12 | 13 | type Query { 14 | recipe(title: String!): Recipe 15 | 16 | # Get all the recipes from around the world 17 | recipes: [Recipe!]! 18 | } 19 | 20 | # Object representing cooking recipe 21 | type Recipe { 22 | title: String! 23 | specification: String @deprecated(reason: "Use `description` field instead") 24 | 25 | # The recipe description with preparation info 26 | description: String 27 | ratings: [Int!]! 28 | creationDate: DateTime! 29 | ratingsCount(minRate: Int = 0): Int! 30 | averageRating: Float 31 | } 32 | 33 | input RecipeInput { 34 | title: String! 35 | description: String 36 | } 37 | -------------------------------------------------------------------------------- /examples/simple-usage/recipe-type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, Int, Float } from "../../src"; 2 | 3 | @ObjectType({ description: "Object representing cooking recipe" }) 4 | export class Recipe { 5 | @Field() 6 | title: string; 7 | 8 | @Field(type => String, { nullable: true, deprecationReason: "Use `description` field instead" }) 9 | get specification(): string | undefined { 10 | return this.description; 11 | } 12 | 13 | @Field({ nullable: true, description: "The recipe description with preparation info" }) 14 | description?: string; 15 | 16 | @Field(type => [Int]) 17 | ratings: number[]; 18 | 19 | @Field() 20 | creationDate: Date; 21 | 22 | @Field(type => Int) 23 | ratingsCount: number; 24 | 25 | @Field(type => Float, { nullable: true }) 26 | get averageRating(): number | null { 27 | const ratingsCount = this.ratings.length; 28 | if (ratingsCount === 0) { 29 | return null; 30 | } 31 | const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); 32 | return ratingsSum / ratingsCount; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/middlewares/middlewares/error-logger.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "typedi"; 2 | import { MiddlewareInterface, NextFn, ResolverData, ArgumentValidationError } from "../../../src"; 3 | 4 | import { Context } from "../context"; 5 | import { Middleware } from "../../../src/interfaces/Middleware"; 6 | import { Logger } from "../logger"; 7 | 8 | @Service() 9 | export class ErrorLoggerMiddleware implements MiddlewareInterface { 10 | constructor(private readonly logger: Logger) {} 11 | 12 | async use({ context, info }: ResolverData, next: NextFn) { 13 | try { 14 | return await next(); 15 | } catch (err) { 16 | this.logger.log({ 17 | message: err.message, 18 | operation: info.operation.operation, 19 | fieldName: info.fieldName, 20 | userName: context.username, 21 | }); 22 | if (!(err instanceof ArgumentValidationError)) { 23 | // hide errors from db like printing sql query 24 | throw new Error("Unknown error occurred. Try again later!"); 25 | } 26 | throw err; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/apollo-engine/recipe-type.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, Int, Float } from "../../src"; 2 | 3 | import { CacheControl } from "./cache-control"; 4 | 5 | @ObjectType() 6 | export class Recipe { 7 | @Field() 8 | title: string; 9 | 10 | @Field({ nullable: true }) 11 | description?: string; 12 | 13 | @Field(type => [Int]) 14 | ratings: number[]; 15 | 16 | @Field() 17 | creationDate: Date; 18 | 19 | @Field(type => Float, { nullable: true }) 20 | // will invalidate `cachedRecipe` cache with maxAge of 60 to 10 21 | // if the field is requested 22 | @CacheControl({ maxAge: 10 }) 23 | get cachedAverageRating() { 24 | console.log(`Called 'cachedAverageRating' for recipe '${this.title}'`); 25 | return this.averageRating; 26 | } 27 | 28 | @Field(type => Float, { nullable: true }) 29 | get averageRating(): number | null { 30 | const ratingsCount = this.ratings.length; 31 | if (ratingsCount === 0) { 32 | return null; 33 | } 34 | const ratingsSum = this.ratings.reduce((a, b) => a + b, 0); 35 | return ratingsSum / ratingsCount; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/automatic-validation/recipe-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Arg, Mutation, Args } from "../../src"; 2 | 3 | import { Recipe } from "./recipe-type"; 4 | import { RecipeInput } from "./recipe-input"; 5 | import { RecipesArguments } from "./recipes-arguments"; 6 | import { generateRecipes } from "./helpers"; 7 | 8 | @Resolver(of => Recipe) 9 | export class RecipeResolver { 10 | private readonly items: Recipe[] = generateRecipes(100); 11 | 12 | @Query(returns => [Recipe]) 13 | async recipes(@Args() options: RecipesArguments): Promise { 14 | const start: number = options.skip; 15 | const end: number = options.skip + options.take; 16 | return await this.items.slice(start, end); 17 | } 18 | 19 | @Mutation(returns => Recipe) 20 | async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise { 21 | const recipe = new Recipe(); 22 | recipe.description = recipeInput.description; 23 | recipe.title = recipeInput.title; 24 | recipe.creationDate = new Date(); 25 | 26 | await this.items.push(recipe); 27 | return recipe; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/decorators/Authorized.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { SymbolKeysNotSupportedError } from "../errors"; 3 | import { getArrayFromOverloadedRest } from "../helpers/decorators"; 4 | import { MethodAndPropDecorator } from "./types"; 5 | 6 | export function Authorized(): MethodAndPropDecorator; 7 | export function Authorized(roles: RoleType[]): MethodAndPropDecorator; 8 | export function Authorized(...roles: RoleType[]): MethodAndPropDecorator; 9 | export function Authorized( 10 | ...rolesOrRolesArray: Array 11 | ): MethodDecorator | PropertyDecorator { 12 | const roles = getArrayFromOverloadedRest(rolesOrRolesArray); 13 | 14 | return (prototype, propertyKey, descriptor) => { 15 | if (typeof propertyKey === "symbol") { 16 | throw new SymbolKeysNotSupportedError(); 17 | } 18 | 19 | getMetadataStorage().collectAuthorizedFieldMetadata({ 20 | target: prototype.constructor, 21 | fieldName: propertyKey, 22 | roles, 23 | }); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/decorators/Root.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { findType } from "../helpers/findType"; 3 | import { TypeValueThunk } from "./types"; 4 | import { SymbolKeysNotSupportedError } from "../errors"; 5 | 6 | export function Root(propertyName?: string): ParameterDecorator { 7 | return (prototype, propertyKey, parameterIndex) => { 8 | if (typeof propertyKey === "symbol") { 9 | throw new SymbolKeysNotSupportedError(); 10 | } 11 | 12 | let getType: TypeValueThunk | undefined; 13 | try { 14 | const typeInfo = findType({ 15 | metadataKey: "design:paramtypes", 16 | prototype, 17 | propertyKey, 18 | parameterIndex, 19 | }); 20 | getType = typeInfo.getType; 21 | } catch { 22 | // tslint:disable-next-line:no-empty 23 | } 24 | 25 | getMetadataStorage().collectHandlerParamMetadata({ 26 | kind: "root", 27 | target: prototype.constructor, 28 | methodName: propertyKey, 29 | index: parameterIndex, 30 | propertyName, 31 | getType, 32 | }); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michał Lytek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/decorators/UseMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { SymbolKeysNotSupportedError } from "../errors"; 2 | import { Middleware } from "../interfaces/Middleware"; 3 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 4 | import { getArrayFromOverloadedRest } from "../helpers/decorators"; 5 | import { MethodAndPropDecorator } from "./types"; 6 | 7 | export function UseMiddleware(middlewares: Array>): MethodAndPropDecorator; 8 | export function UseMiddleware(...middlewares: Array>): MethodAndPropDecorator; 9 | export function UseMiddleware( 10 | ...middlewaresOrMiddlewareArray: Array | Array>> 11 | ): MethodDecorator | PropertyDecorator { 12 | const middlewares = getArrayFromOverloadedRest(middlewaresOrMiddlewareArray); 13 | 14 | return (prototype, propertyKey, descriptor) => { 15 | if (typeof propertyKey === "symbol") { 16 | throw new SymbolKeysNotSupportedError(); 17 | } 18 | 19 | getMetadataStorage().collectMiddlewareMetadata({ 20 | target: prototype.constructor, 21 | fieldName: propertyKey, 22 | middlewares, 23 | }); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/resolvers/validate-arg.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorOptions } from "class-validator"; 2 | 3 | import { ArgumentValidationError } from "../errors/ArgumentValidationError"; 4 | 5 | export async function validateArg( 6 | arg: T | undefined, 7 | globalValidate: boolean | ValidatorOptions, 8 | argValidate?: boolean | ValidatorOptions, 9 | ): Promise { 10 | const validate = argValidate !== undefined ? argValidate : globalValidate; 11 | if (validate === false || arg == null || typeof arg !== "object") { 12 | return arg; 13 | } 14 | 15 | const validatorOptions: ValidatorOptions = Object.assign( 16 | {}, 17 | typeof globalValidate === "object" ? globalValidate : {}, 18 | typeof argValidate === "object" ? argValidate : {}, 19 | ); 20 | if (validatorOptions.skipMissingProperties !== false) { 21 | validatorOptions.skipMissingProperties = true; 22 | } 23 | 24 | const { validateOrReject } = await import("class-validator"); 25 | try { 26 | await validateOrReject(arg, validatorOptions); 27 | return arg; 28 | } catch (err) { 29 | throw new ArgumentValidationError(err); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/using-scoped-container/recipe/recipe.samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | import Container from "typedi"; 3 | 4 | import { Recipe } from "./recipe.type"; 5 | 6 | export const sampleRecipes = [ 7 | createRecipe({ 8 | id: "1", 9 | title: "Recipe 1", 10 | description: "Desc 1", 11 | ingredients: ["one", "two", "three"], 12 | }), 13 | createRecipe({ 14 | id: "2", 15 | title: "Recipe 2", 16 | description: "Desc 2", 17 | ingredients: ["four", "five", "six"], 18 | }), 19 | createRecipe({ 20 | id: "3", 21 | title: "Recipe 3", 22 | ingredients: ["seven", "eight", "nine"], 23 | }), 24 | ]; 25 | 26 | function createRecipe(recipeData: Partial): Recipe { 27 | return plainToClass(Recipe, recipeData); 28 | } 29 | 30 | export function setSamplesInContainer() { 31 | // add sample recipes to container 32 | Container.set({ 33 | id: "SAMPLE_RECIPES", 34 | transient: true, // create a fresh copy for each `get` of samples 35 | factory: () => { 36 | console.log("sampleRecipes copy created!"); 37 | return sampleRecipes.slice(); 38 | }, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /examples/using-container/recipe-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, FieldResolver, Arg, Root, Mutation } from "../../src"; 2 | 3 | import { Recipe } from "./recipe-type"; 4 | import { RecipeService } from "./recipe-service"; 5 | import { RecipeInput } from "./recipe-input"; 6 | 7 | @Resolver(of => Recipe) 8 | export class RecipeResolver { 9 | constructor( 10 | // constructor injection of service 11 | private readonly recipeService: RecipeService, 12 | ) {} 13 | 14 | @Query(returns => Recipe, { nullable: true }) 15 | async recipe(@Arg("recipeId") recipeId: string) { 16 | return this.recipeService.getOne(recipeId); 17 | } 18 | 19 | @Query(returns => [Recipe]) 20 | async recipes(): Promise { 21 | return this.recipeService.getAll(); 22 | } 23 | 24 | @Mutation(returns => Recipe) 25 | async addRecipe(@Arg("recipe") recipe: RecipeInput): Promise { 26 | return this.recipeService.add(recipe); 27 | } 28 | 29 | @FieldResolver() 30 | async numberInCollection(@Root() recipe: Recipe): Promise { 31 | const index = await this.recipeService.findIndex(recipe); 32 | return index + 1; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/decorators/ObjectType.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { getNameDecoratorParams } from "../helpers/decorators"; 3 | import { DescriptionOptions } from "./types"; 4 | import { ClassType } from "../interfaces"; 5 | 6 | export type ObjectOptions = DescriptionOptions & { 7 | implements?: Function | Function[]; 8 | }; 9 | 10 | export function ObjectType(): ClassDecorator; 11 | export function ObjectType(options: ObjectOptions): ClassDecorator; 12 | export function ObjectType(name: string, options?: ObjectOptions): ClassDecorator; 13 | export function ObjectType( 14 | nameOrOptions?: string | ObjectOptions, 15 | maybeOptions?: ObjectOptions, 16 | ): ClassDecorator { 17 | const { name, options } = getNameDecoratorParams(nameOrOptions, maybeOptions); 18 | const interfaceClasses: ClassType[] | undefined = 19 | options.implements && [].concat(options.implements as any); 20 | 21 | return target => { 22 | getMetadataStorage().collectObjectMetadata({ 23 | name: name || target.name, 24 | target, 25 | description: options.description, 26 | interfaceClasses, 27 | }); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /examples/authorization/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { buildSchema } from "../../src"; 4 | 5 | import { ExampleResolver } from "./resolver"; 6 | import { Context } from "./context.interface"; 7 | import { authChecker } from "./auth-checker"; 8 | 9 | void (async function bootstrap() { 10 | // build TypeGraphQL executable schema 11 | const schema = await buildSchema({ 12 | resolvers: [ExampleResolver], 13 | authChecker, // register auth checking function 14 | }); 15 | 16 | // Create GraphQL server 17 | const server = new ApolloServer({ 18 | schema, 19 | context: () => { 20 | const ctx: Context = { 21 | // create mocked user in context 22 | // in real app you would be mapping user from `req.user` or sth 23 | user: { 24 | id: 1, 25 | name: "Sample user", 26 | roles: ["REGULAR"], 27 | }, 28 | }; 29 | return ctx; 30 | }, 31 | }); 32 | 33 | // Start the server 34 | const { url } = await server.listen(4000); 35 | console.log(`Server is running, GraphQL Playground available at ${url}`); 36 | })(); 37 | -------------------------------------------------------------------------------- /website/versioned_sidebars/version-0.16.0-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version-0.16.0-docs": { 3 | "Introduction": [ 4 | "version-0.16.0-introduction" 5 | ], 6 | "Beginner guides": [ 7 | "version-0.16.0-getting-started", 8 | "version-0.16.0-types-and-fields", 9 | "version-0.16.0-resolvers", 10 | "version-0.16.0-bootstrap" 11 | ], 12 | "Advanced guides": [ 13 | "version-0.16.0-scalars", 14 | "version-0.16.0-enums", 15 | "version-0.16.0-unions", 16 | "version-0.16.0-subscriptions", 17 | "version-0.16.0-interfaces-and-inheritance" 18 | ], 19 | "Features": [ 20 | "version-0.16.0-dependency-injection", 21 | "version-0.16.0-authorization", 22 | "version-0.16.0-validation", 23 | "version-0.16.0-middlewares", 24 | "version-0.16.0-complexity" 25 | ], 26 | "Others": [ 27 | "version-0.16.0-emit-schema", 28 | "version-0.16.0-browser-usage" 29 | ] 30 | }, 31 | "version-0.16.0-examples": { 32 | "Examples": [ 33 | "version-0.16.0-examples" 34 | ] 35 | }, 36 | "version-0.16.0-others": { 37 | "Others": [ 38 | "version-0.16.0-faq" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers/resolver-metadata.ts: -------------------------------------------------------------------------------- 1 | import { ResolverMetadata } from "../metadata/definitions"; 2 | import { ReturnTypeFunc, AdvancedOptions } from "../decorators/types"; 3 | import { findType } from "./findType"; 4 | import { SymbolKeysNotSupportedError } from "../errors"; 5 | 6 | export function getResolverMetadata( 7 | prototype: object, 8 | propertyKey: string | symbol, 9 | returnTypeFunc?: ReturnTypeFunc, 10 | options: AdvancedOptions = {}, 11 | ): ResolverMetadata { 12 | if (typeof propertyKey === "symbol") { 13 | throw new SymbolKeysNotSupportedError(); 14 | } 15 | 16 | const { getType, typeOptions } = findType({ 17 | metadataKey: "design:returntype", 18 | prototype, 19 | propertyKey, 20 | returnTypeFunc, 21 | typeOptions: options, 22 | }); 23 | 24 | const methodName = propertyKey as keyof typeof prototype; 25 | 26 | return { 27 | methodName, 28 | schemaName: options.name || methodName, 29 | target: prototype.constructor, 30 | getReturnType: getType, 31 | returnTypeOptions: typeOptions, 32 | description: options.description, 33 | deprecationReason: options.deprecationReason, 34 | complexity: options.complexity, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /tests/functional/peer-dependency.ts: -------------------------------------------------------------------------------- 1 | import { ensureInstalledCorrectGraphQLPackage } from "../../src/utils/graphql-version"; 2 | import { UnmetGraphQLPeerDependencyError } from "../../src/errors"; 3 | 4 | describe("`graphql` package peer dependency", () => { 5 | it("should have installed correct version", async () => { 6 | ensureInstalledCorrectGraphQLPackage(); 7 | expect(true).toBe(true); 8 | }); 9 | 10 | it("should throw error when the installed version doesn't fulfill requirement", async () => { 11 | expect.assertions(5); 12 | jest.mock("graphql/package.json", () => ({ 13 | version: "14.0.2", 14 | })); 15 | const graphqlVersion = require("../../src/utils/graphql-version"); 16 | 17 | try { 18 | graphqlVersion.ensureInstalledCorrectGraphQLPackage(); 19 | } catch (err) { 20 | expect(err).toBeInstanceOf(Error); 21 | expect(err).toBeInstanceOf(UnmetGraphQLPeerDependencyError); 22 | const error = err as UnmetGraphQLPeerDependencyError; 23 | expect(error.message).toContain("incorrect version"); 24 | expect(error.message).toContain("graphql"); 25 | expect(error.message).toContain("requirement"); 26 | } 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/helpers/subscriptions/createWebSocketGQL.ts: -------------------------------------------------------------------------------- 1 | import * as NodeWebSocket from "ws"; 2 | import { GraphQLSchema } from "graphql"; 3 | import { ApolloServer } from "apollo-server"; 4 | import { SubscriptionClient } from "subscriptions-transport-ws"; 5 | import { ApolloClient } from "apollo-client"; 6 | import { WebSocketLink } from "apollo-link-ws"; 7 | import { InMemoryCache } from "apollo-cache-inmemory"; 8 | 9 | export interface WebSocketUtils { 10 | apolloClient: ApolloClient; 11 | apolloServer: ApolloServer; 12 | } 13 | 14 | export default async function createWebSocketUtils(schema: GraphQLSchema): Promise { 15 | const apolloServer = new ApolloServer({ 16 | schema, 17 | playground: false, 18 | }); 19 | const { subscriptionsUrl } = await apolloServer.listen({ 20 | port: 0, 21 | }); 22 | 23 | const subscriptionClient = new SubscriptionClient( 24 | subscriptionsUrl, 25 | { reconnect: true }, 26 | NodeWebSocket, 27 | ); 28 | const apolloClient = new ApolloClient({ 29 | link: new WebSocketLink(subscriptionClient), 30 | cache: new InMemoryCache(), 31 | }); 32 | return { 33 | apolloClient, 34 | apolloServer, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /docs/browser-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Browser usage 3 | --- 4 | 5 | ## Using classes in client app 6 | Sometimes you might want to use the classes, that you've created and annotated with TypeGraphQL decorators, in your client app that works in browser. For example, you may want to reuse the args or input classes with `class-validator` decorators or the object type classes with some helpful custom methods. 7 | 8 | As TypeGraphQL is a Node.js framework, it doesn't work in browser environment, so you may quickly got an error, e.g. `ERROR in ./node_modules/fs.realpath/index.js`, while trying to build your app with Webpack. To fix that, you have to configure Webpack to use the decorators shim instead of normal module. All you need is to add this plugin code to your webpack config: 9 | ```js 10 | plugins: [ 11 | ..., // here any other existing plugins that you already have 12 | new webpack.NormalModuleReplacementPlugin(/type-graphql$/, function (result) { 13 | result.request = result.request.replace(/type-graphql/, "type-graphql/browser-shim"); 14 | }), 15 | ] 16 | ``` 17 | 18 | Also, thanks to this your bundle will be much lighter as you don't embedded the whole TypeGraphQL library code in your app. 19 | 20 | -------------------------------------------------------------------------------- /examples/using-container/recipe-service.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | import { Service, Inject } from "typedi"; 3 | 4 | import { Recipe } from "./recipe-type"; 5 | import { RecipeInput } from "./recipe-input"; 6 | 7 | @Service() 8 | export class RecipeService { 9 | private autoIncrementValue: number; 10 | 11 | constructor(@Inject("SAMPLE_RECIPES") private readonly items: Recipe[]) { 12 | this.autoIncrementValue = this.items.length; 13 | } 14 | 15 | async getAll() { 16 | return this.items; 17 | } 18 | 19 | async getOne(id: string) { 20 | return this.items.find(it => it.id === id); 21 | } 22 | 23 | async add(data: RecipeInput) { 24 | const recipe = this.createRecipe(data); 25 | this.items.push(recipe); 26 | return recipe; 27 | } 28 | 29 | async findIndex(recipe: Recipe) { 30 | return this.items.findIndex(it => it.id === recipe.id); 31 | } 32 | 33 | private createRecipe(recipeData: Partial): Recipe { 34 | const recipe = plainToClass(Recipe, recipeData); 35 | recipe.id = this.getId(); 36 | return recipe; 37 | } 38 | 39 | private getId(): string { 40 | return (++this.autoIncrementValue).toString(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/helpers/params.ts: -------------------------------------------------------------------------------- 1 | import { findType } from "./findType"; 2 | import { ReturnTypeFunc, TypeOptions, ValidateOptions } from "../decorators/types"; 3 | import { CommonArgMetadata } from "../metadata/definitions"; 4 | import { SymbolKeysNotSupportedError } from "../errors"; 5 | 6 | export interface ParamInfo { 7 | prototype: Object; 8 | propertyKey: string | symbol; 9 | parameterIndex: number; 10 | returnTypeFunc?: ReturnTypeFunc; 11 | options?: TypeOptions & ValidateOptions; 12 | } 13 | export function getParamInfo({ 14 | prototype, 15 | propertyKey, 16 | parameterIndex, 17 | returnTypeFunc, 18 | options = {}, 19 | }: ParamInfo): CommonArgMetadata { 20 | if (typeof propertyKey === "symbol") { 21 | throw new SymbolKeysNotSupportedError(); 22 | } 23 | 24 | const { getType, typeOptions } = findType({ 25 | metadataKey: "design:paramtypes", 26 | prototype, 27 | propertyKey, 28 | parameterIndex, 29 | returnTypeFunc, 30 | typeOptions: options, 31 | }); 32 | 33 | return { 34 | target: prototype.constructor, 35 | methodName: propertyKey, 36 | index: parameterIndex, 37 | getType, 38 | typeOptions, 39 | validate: options.validate, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /tests/units/formatArgumentValidationError.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from "graphql"; 2 | import { MinLength, validate } from "class-validator"; 3 | 4 | import { formatArgumentValidationError, ArgumentValidationError } from "../../src"; 5 | 6 | describe("formatArgumentValidationError", () => { 7 | class SampleClass { 8 | @MinLength(8) 9 | min8lengthProperty: string; 10 | } 11 | 12 | it("should properly transform GraphQLError into validation errors object", async () => { 13 | const sample = new SampleClass(); 14 | sample.min8lengthProperty = "12345"; 15 | 16 | const validationErrors = await validate(sample); 17 | const argumentValidationError = new ArgumentValidationError(validationErrors); 18 | const error = new GraphQLError( 19 | "error message", 20 | undefined, 21 | undefined, 22 | undefined, 23 | undefined, 24 | argumentValidationError, 25 | ); 26 | 27 | const formattedError = formatArgumentValidationError(error); 28 | expect(formattedError.message).toEqual("error message"); 29 | expect(formattedError.validationErrors).toHaveLength(1); 30 | expect(formattedError.validationErrors).toEqual(validationErrors); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch example", 11 | "program": "${workspaceFolder}/dev.js", 12 | "protocol": "inspector", 13 | "sourceMaps": true, 14 | "smartStep": true, 15 | "internalConsoleOptions": "neverOpen", 16 | "console": "integratedTerminal" 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Debug tests", 22 | "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", 23 | "protocol": "inspector", 24 | "sourceMaps": true, 25 | "smartStep": true, 26 | "console": "internalConsole", 27 | "internalConsoleOptions": "openOnSessionStart", 28 | "args": [ 29 | "-i" 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/decorators/Arg.ts: -------------------------------------------------------------------------------- 1 | import { ReturnTypeFunc, DecoratorTypeOptions, DescriptionOptions, ValidateOptions } from "./types"; 2 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 3 | import { getParamInfo } from "../helpers/params"; 4 | import { getTypeDecoratorParams } from "../helpers/decorators"; 5 | 6 | export type Options = DecoratorTypeOptions & DescriptionOptions & ValidateOptions; 7 | 8 | export function Arg(name: string, options?: Options): ParameterDecorator; 9 | export function Arg( 10 | name: string, 11 | returnTypeFunc: ReturnTypeFunc, 12 | options?: Options, 13 | ): ParameterDecorator; 14 | export function Arg( 15 | name: string, 16 | returnTypeFuncOrOptions?: ReturnTypeFunc | Options, 17 | maybeOptions?: Options, 18 | ): ParameterDecorator { 19 | return (prototype, propertyKey, parameterIndex) => { 20 | const { options, returnTypeFunc } = getTypeDecoratorParams( 21 | returnTypeFuncOrOptions, 22 | maybeOptions, 23 | ); 24 | getMetadataStorage().collectHandlerParamMetadata({ 25 | kind: "arg", 26 | name, 27 | description: options.description, 28 | ...getParamInfo({ prototype, propertyKey, parameterIndex, returnTypeFunc, options }), 29 | }); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /examples/using-scoped-container/recipe/recipe.service.ts: -------------------------------------------------------------------------------- 1 | import { Service, Inject } from "typedi"; 2 | import { plainToClass } from "class-transformer"; 3 | 4 | import { Recipe } from "./recipe.type"; 5 | import { RecipeInput } from "./recipe.input"; 6 | 7 | // this service will be global - shared by every request 8 | @Service({ global: true }) 9 | export class RecipeService { 10 | private autoIncrementValue: number; 11 | 12 | constructor(@Inject("SAMPLE_RECIPES") private readonly items: Recipe[]) { 13 | console.log("RecipeService created!"); 14 | this.autoIncrementValue = items.length; 15 | } 16 | 17 | async getAll() { 18 | return this.items; 19 | } 20 | 21 | async getOne(id: string) { 22 | return this.items.find(it => it.id === id); 23 | } 24 | 25 | async add(data: RecipeInput) { 26 | const recipe = this.createRecipe(data); 27 | this.items.push(recipe); 28 | return recipe; 29 | } 30 | 31 | private createRecipe(recipeData: Partial): Recipe { 32 | const recipe = plainToClass(Recipe, { 33 | ...recipeData, 34 | id: this.getId(), 35 | }); 36 | return recipe; 37 | } 38 | 39 | private getId(): string { 40 | return (++this.autoIncrementValue).toString(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/enums-and-unions/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Arg } from "../../src"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | import { sampleRecipes } from "./recipe.samples"; 5 | import { Difficulty } from "./difficulty.enum"; 6 | import { SearchResult } from "./search-result.union"; 7 | import { Cook } from "./cook.type"; 8 | import { sampleCooks } from "./cook.samples"; 9 | 10 | @Resolver() 11 | export class ExampleResolver { 12 | private recipesData: Recipe[] = sampleRecipes; 13 | private cooks: Cook[] = sampleCooks; 14 | 15 | @Query(returns => [Recipe]) 16 | async recipes( 17 | @Arg("difficulty", type => Difficulty, { nullable: true }) difficulty?: Difficulty, 18 | ): Promise { 19 | if (!difficulty) { 20 | return this.recipesData; 21 | } 22 | 23 | return this.recipesData.filter(recipe => recipe.preparationDifficulty === difficulty); 24 | } 25 | 26 | @Query(returns => [SearchResult]) 27 | async search(@Arg("cookName") cookName: string): Promise> { 28 | const recipes = this.recipesData.filter(recipe => recipe.cook.name.match(cookName)); 29 | const cooks = this.cooks.filter(cook => cook.name.match(cookName)); 30 | 31 | return [...recipes, ...cooks]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/resolvers-inheritance/person/person.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Arg, Int, Mutation } from "../../../src"; 2 | 3 | import { createResourceResolver } from "../resource/resource.resolver"; 4 | import { Person } from "./person.type"; 5 | import { PersonRole } from "./person.role"; 6 | 7 | const persons: Person[] = [ 8 | { 9 | id: 1, 10 | name: "Person 1", 11 | age: 23, 12 | role: PersonRole.Normal, 13 | }, 14 | { 15 | id: 2, 16 | name: "Person 2", 17 | age: 48, 18 | role: PersonRole.Admin, 19 | }, 20 | ]; 21 | 22 | export const ResourceResolver = createResourceResolver(Person, persons); 23 | 24 | @Resolver() 25 | export class PersonResolver extends ResourceResolver { 26 | // here you can add resource-specific operations 27 | 28 | @Mutation() 29 | promote(@Arg("personId", type => Int) personId: number): boolean { 30 | // you have full access to base resolver class fields and methods 31 | 32 | const person = this.resourceService.getOne(personId); 33 | if (!person) { 34 | throw new Error("Person not found!"); 35 | } 36 | 37 | if (person.role === PersonRole.Normal) { 38 | person.role = PersonRole.Pro; 39 | return true; 40 | } 41 | 42 | return false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/apollo-engine/recipe-resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | FieldResolver, 5 | Arg, 6 | Root, 7 | Mutation, 8 | Float, 9 | Int, 10 | ResolverInterface, 11 | } from "../../src"; 12 | import { plainToClass } from "class-transformer"; 13 | 14 | import { Recipe } from "./recipe-type"; 15 | import { createRecipeSamples } from "./recipe-samples"; 16 | import { CacheControl } from "./cache-control"; 17 | 18 | @Resolver(of => Recipe) 19 | export class RecipeResolver { 20 | private readonly items: Recipe[] = createRecipeSamples(); 21 | 22 | @Query(returns => Recipe, { nullable: true }) 23 | async recipe(@Arg("title") title: string): Promise { 24 | return await this.items.find(recipe => recipe.title === title); 25 | } 26 | 27 | @Query(returns => Recipe, { nullable: true }) 28 | // here we declare that ApolloEngine will cache the query for 60s 29 | @CacheControl({ maxAge: 60 }) 30 | async cachedRecipe(@Arg("title") title: string): Promise { 31 | console.log(`Called 'cachedRecipe' with title '${title}'`); 32 | return await this.items.find(recipe => recipe.title === title); 33 | } 34 | 35 | @Query(returns => [Recipe]) 36 | async recipes(): Promise { 37 | return await this.items; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.16.0/browser-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Browser usage 3 | id: version-0.16.0-browser-usage 4 | original_id: browser-usage 5 | --- 6 | 7 | ## Using classes in client app 8 | Sometimes you might want to use the classes, that you've created and annotated with TypeGraphQL decorators, in your client app that works in browser. For example, you may want to reuse the args or input classes with `class-validator` decorators or the object type classes with some helpful custom methods. 9 | 10 | As TypeGraphQL is a Node.js framework, it doesn't work in browser environment, so you may quickly got an error, e.g. `ERROR in ./node_modules/fs.realpath/index.js`, while trying to build your app with Webpack. To fix that, you have to configure Webpack to use the decorators shim instead of normal module. All you need is to add this plugin code to your webpack config: 11 | ```js 12 | plugins: [ 13 | ..., // here any other existing plugins that you already have 14 | new webpack.NormalModuleReplacementPlugin(/type-graphql$/, function (result) { 15 | result.request = result.request.replace(/type-graphql/, "type-graphql/browser-shim"); 16 | }), 17 | ] 18 | ``` 19 | 20 | Also, thanks to this your bundle will be much lighter as you don't embedded the whole TypeGraphQL library code in your app. 21 | 22 | -------------------------------------------------------------------------------- /examples/query-complexity/recipe-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, FieldResolver, Root, ResolverInterface, Arg } from "../../src"; 2 | 3 | import { Recipe } from "./recipe-type"; 4 | import { createRecipeSamples } from "./recipe-samples"; 5 | 6 | @Resolver(of => Recipe) 7 | export class RecipeResolver implements ResolverInterface { 8 | private readonly items: Recipe[] = createRecipeSamples(); 9 | 10 | @Query(returns => [Recipe], { 11 | /* 12 | You can also pass a calculation function in the complexity option 13 | to determine a custom complexity. This function will provide the 14 | complexity of the child nodes as well as the field input arguments. 15 | That way you can make a more realistic estimation of individual field 16 | complexity values, e.g. by multiplying childComplexity by the number of items in array 17 | */ 18 | complexity: ({ childComplexity, args }) => args.count * childComplexity, 19 | }) 20 | async recipes(@Arg("count") count: number): Promise { 21 | return await this.items.slice(0, count); 22 | } 23 | 24 | /* Complexity in field resolver overrides complexity of equivalent field type */ 25 | @FieldResolver({ complexity: 5 }) 26 | ratingsCount(@Root() recipe: Recipe): number { 27 | return recipe.ratings.length; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import * as Redis from "ioredis"; 4 | import { RedisPubSub } from "graphql-redis-subscriptions"; 5 | import { buildSchema } from "../../src"; 6 | 7 | import { RecipeResolver } from "./recipe.resolver"; 8 | 9 | const REDIS_HOST = "192.168.99.100"; // replace with own IP 10 | const REDIS_PORT = 6379; 11 | 12 | async function bootstrap() { 13 | // configure Redis connection options 14 | const options: Redis.RedisOptions = { 15 | host: REDIS_HOST, 16 | port: REDIS_PORT, 17 | retryStrategy: times => Math.max(times * 100, 3000), 18 | }; 19 | 20 | // create Redis-based pub-sub 21 | const pubSub = new RedisPubSub({ 22 | publisher: new Redis(options), 23 | subscriber: new Redis(options), 24 | }); 25 | 26 | // Build the TypeGraphQL schema 27 | const schema = await buildSchema({ 28 | resolvers: [RecipeResolver], 29 | validate: false, 30 | pubSub, // provide redis-based instance of PubSub 31 | }); 32 | 33 | // Create GraphQL server 34 | const server = new ApolloServer({ schema }); 35 | 36 | // Start the server 37 | const { url } = await server.listen(4000); 38 | console.log(`Server is running, GraphQL Playground available at ${url}`); 39 | } 40 | 41 | bootstrap(); 42 | -------------------------------------------------------------------------------- /examples/authorization/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Authorized, Mutation, Arg } from "../../src"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | import { createRecipe, sampleRecipes } from "./recipe.helpers"; 5 | 6 | @Resolver() 7 | export class ExampleResolver { 8 | private recipesData: Recipe[] = sampleRecipes.slice(); 9 | 10 | // anyone can read recipes collection 11 | @Query(returns => [Recipe]) 12 | async recipes(): Promise { 13 | return await this.recipesData; 14 | } 15 | 16 | @Authorized() // only logged users can add new recipe 17 | @Mutation() 18 | addRecipe( 19 | @Arg("title") title: string, 20 | @Arg("description", { nullable: true }) description?: string, 21 | ): Recipe { 22 | const newRecipe = createRecipe({ 23 | title, 24 | description, 25 | ratings: [], 26 | }); 27 | this.recipesData.push(newRecipe); 28 | return newRecipe; 29 | } 30 | 31 | @Authorized("ADMIN") // only admin can remove the published recipe 32 | @Mutation() 33 | deleteRecipe(@Arg("title") title: string): boolean { 34 | const foundRecipeIndex = this.recipesData.findIndex(it => it.title === title); 35 | if (!foundRecipeIndex) { 36 | return false; 37 | } 38 | this.recipesData.splice(foundRecipeIndex, 1); 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/recipe.samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | import { Comment } from "./comment.type"; 5 | 6 | export const sampleRecipes = [ 7 | createRecipe({ 8 | id: "1", 9 | title: "Recipe 1", 10 | description: "Desc 1", 11 | comments: createComments([ 12 | { 13 | date: new Date("2018-03-21"), 14 | content: "Very tasty!", 15 | nickname: "Anonymous", 16 | }, 17 | { 18 | date: new Date("2018-01-12"), 19 | content: "Not so tasty!", 20 | nickname: "Anonymous again", 21 | }, 22 | ]), 23 | }), 24 | createRecipe({ 25 | id: "2", 26 | title: "Recipe 2", 27 | description: "Desc 2", 28 | comments: createComments([ 29 | { 30 | date: new Date(), 31 | content: "Very good, very cheap!", 32 | nickname: "Master of cooking", 33 | }, 34 | ]), 35 | }), 36 | createRecipe({ 37 | id: "3", 38 | title: "Recipe 3", 39 | comments: [], 40 | }), 41 | ]; 42 | 43 | function createRecipe(recipeData: Partial): Recipe { 44 | return plainToClass(Recipe, recipeData); 45 | } 46 | 47 | function createComments(commentData: Array>): Comment[] { 48 | return plainToClass(Comment, commentData); 49 | } 50 | -------------------------------------------------------------------------------- /tests/helpers/getInnerFieldType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IntrospectionObjectType, 3 | IntrospectionInterfaceType, 4 | IntrospectionNonNullTypeRef, 5 | IntrospectionNamedTypeRef, 6 | IntrospectionInputObjectType, 7 | IntrospectionTypeRef, 8 | IntrospectionEnumType, 9 | IntrospectionScalarType, 10 | IntrospectionUnionType, 11 | } from "graphql"; 12 | 13 | export function getInnerFieldType( 14 | type: IntrospectionObjectType | IntrospectionInterfaceType, 15 | name: string, 16 | ) { 17 | return getInnerTypeOfNonNullableType(type.fields.find(field => field.name === name)!); 18 | } 19 | 20 | export function getInnerInputFieldType(type: IntrospectionInputObjectType, name: string) { 21 | return getInnerTypeOfNonNullableType(type.inputFields.find(field => field.name === name)!); 22 | } 23 | 24 | export function getInnerTypeOfNonNullableType(definition: { type: IntrospectionTypeRef }) { 25 | return (definition.type as IntrospectionNonNullTypeRef).ofType! as IntrospectionNamedTypeRef; 26 | } 27 | 28 | export function getItemTypeOfList(definition: { type: IntrospectionTypeRef }) { 29 | const listType = (definition.type as IntrospectionNonNullTypeRef) 30 | .ofType! as IntrospectionNonNullTypeRef; 31 | const itemType = (listType.ofType! as IntrospectionNonNullTypeRef) 32 | .ofType as IntrospectionNamedTypeRef; 33 | return itemType; 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-eslint-rules", "tslint-config-prettier"], 4 | "rules": { 5 | "adjacent-overload-signatures": true, 6 | "ban-types": false, 7 | "no-console": [false], 8 | "no-namespace": false, 9 | "object-literal-sort-keys": false, 10 | "max-classes-per-file": false, 11 | "member-access": [true, "no-public"], 12 | "member-ordering": [ 13 | true, 14 | { 15 | "order": [ 16 | "public-static-field", 17 | "protected-static-field", 18 | "private-static-field", 19 | 20 | "public-instance-field", 21 | "protected-instance-field", 22 | "private-instance-field", 23 | 24 | "public-static-method", 25 | "protected-static-method", 26 | "private-static-method", 27 | 28 | "public-constructor", 29 | "protected-constructor", 30 | "private-constructor", 31 | 32 | "public-instance-method", 33 | "protected-instance-method", 34 | "private-instance-method" 35 | ] 36 | } 37 | ], 38 | "unified-signatures": false, 39 | "interface-name": [true, "never-prefix"], 40 | "ordered-imports": false, 41 | "no-unused-expression": false, 42 | "callable-types": false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | This folder consist of simple examples showing how to use different `TypeGraphQL` features and how well it integrates with 3rd party libraries. 3 | 4 | All examples has a `examples.gql` file with sample queries/mutations/subscriptions that you can execute. 5 | 6 | ## Basics 7 | - [Simple usage of fields, basic types and resolvers](./simple-usage) 8 | 9 | ## Advanced 10 | - [Enums and unions](./enums-and-unions) 11 | - [Interfaces and types inheritance](./interfaces-inheritance) 12 | - [Subscriptions (simple)](./simple-subscriptions) 13 | - [Subscriptions (using Redis)](./redis-subscriptions) 14 | - [Resolvers inheritance](./resolvers-inheritance) 15 | 16 | ## Features usage 17 | - [Dependency injection (IoC container)](./using-container) 18 | - [(scoped container)](./using-scoped-container) 19 | - [Authorization](./authorization) 20 | - [Validation](./automatic-validation) 21 | - [Middlewares](./middlewares) 22 | 23 | ## 3rd party libs integration 24 | - [TypeORM (manual, synchronous) *](./typeorm-basic-usage) 25 | - [TypeORM (automatic, lazy relations) *](./typeorm-lazy-relations) 26 | - [Apollo Engine (Apollo Cache Control) **](./apollo-engine) 27 | 28 | _* Note that you need to edit the TypeORM examples `index.ts` with credentials to your local database_ 29 | 30 | _** Note that you need to provide `APOLLO_ENGINE_API_KEY` env variable with your own API key_ 31 | -------------------------------------------------------------------------------- /src/helpers/decorators.ts: -------------------------------------------------------------------------------- 1 | import { ReturnTypeFunc, DescriptionOptions } from "../decorators/types"; 2 | 3 | export interface TypeDecoratorParams { 4 | options: Partial; 5 | returnTypeFunc?: ReturnTypeFunc; 6 | } 7 | export function getTypeDecoratorParams( 8 | returnTypeFuncOrOptions: ReturnTypeFunc | T | undefined, 9 | maybeOptions: T | undefined, 10 | ): TypeDecoratorParams { 11 | if (typeof returnTypeFuncOrOptions === "function") { 12 | return { 13 | returnTypeFunc: returnTypeFuncOrOptions as ReturnTypeFunc, 14 | options: maybeOptions || {}, 15 | }; 16 | } else { 17 | return { 18 | options: returnTypeFuncOrOptions || {}, 19 | }; 20 | } 21 | } 22 | 23 | export function getNameDecoratorParams( 24 | nameOrOptions: string | T | undefined, 25 | maybeOptions: T | undefined, 26 | ) { 27 | if (typeof nameOrOptions === "string") { 28 | return { 29 | name: nameOrOptions, 30 | options: maybeOptions || ({} as T), 31 | }; 32 | } else { 33 | return { 34 | options: nameOrOptions || ({} as T), 35 | }; 36 | } 37 | } 38 | 39 | export function getArrayFromOverloadedRest(overloadedArray: Array): T[] { 40 | let items: T[]; 41 | if (Array.isArray(overloadedArray[0])) { 42 | items = overloadedArray[0] as T[]; 43 | } else { 44 | items = overloadedArray as T[]; 45 | } 46 | return items; 47 | } 48 | -------------------------------------------------------------------------------- /examples/enums-and-unions/recipe.samples.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from "class-transformer"; 2 | 3 | import { Recipe } from "./recipe.type"; 4 | import { Difficulty } from "./difficulty.enum"; 5 | import { sampleCooks } from "./cook.samples"; 6 | 7 | export const sampleRecipes = [ 8 | createRecipe({ 9 | title: "Recipe 1", 10 | description: "Desc 1", 11 | preparationDifficulty: Difficulty.Easy, 12 | ingredients: ["one", "two", "three"], 13 | cook: sampleCooks[1], 14 | }), 15 | createRecipe({ 16 | title: "Recipe 2", 17 | description: "Desc 2", 18 | preparationDifficulty: Difficulty.Easy, 19 | ingredients: ["four", "five", "six"], 20 | cook: sampleCooks[0], 21 | }), 22 | createRecipe({ 23 | title: "Recipe 3", 24 | preparationDifficulty: Difficulty.Beginner, 25 | ingredients: ["seven", "eight", "nine"], 26 | cook: sampleCooks[1], 27 | }), 28 | createRecipe({ 29 | title: "Recipe 4", 30 | description: "Desc 4", 31 | preparationDifficulty: Difficulty.MasterChef, 32 | ingredients: ["ten", "eleven", "twelve"], 33 | cook: sampleCooks[0], 34 | }), 35 | createRecipe({ 36 | title: "Recipe 5", 37 | preparationDifficulty: Difficulty.Hard, 38 | ingredients: ["thirteen", "fourteen", "fifteen"], 39 | cook: sampleCooks[0], 40 | }), 41 | ]; 42 | 43 | function createRecipe(recipeData: Partial): Recipe { 44 | return plainToClass(Recipe, recipeData); 45 | } 46 | -------------------------------------------------------------------------------- /src/metadata/definitions/param-metadata.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorOptions } from "class-validator"; 2 | 3 | import { TypeValueThunk, TypeOptions } from "../../decorators/types"; 4 | 5 | export interface BasicParamMetadata { 6 | target: Function; 7 | methodName: string; 8 | index: number; 9 | } 10 | export interface InfoParamMetadata extends BasicParamMetadata { 11 | kind: "info"; 12 | } 13 | export interface PubSubParamMetadata extends BasicParamMetadata { 14 | kind: "pubSub"; 15 | triggerKey?: string; 16 | } 17 | export interface ContextParamMetadata extends BasicParamMetadata { 18 | kind: "context"; 19 | propertyName: string | undefined; 20 | } 21 | export interface RootParamMetadata extends BasicParamMetadata { 22 | kind: "root"; 23 | propertyName: string | undefined; 24 | getType: TypeValueThunk | undefined; 25 | } 26 | export interface CommonArgMetadata extends BasicParamMetadata { 27 | getType: TypeValueThunk; 28 | typeOptions: TypeOptions; 29 | validate: boolean | ValidatorOptions | undefined; 30 | } 31 | export interface ArgParamMetadata extends CommonArgMetadata { 32 | kind: "arg"; 33 | name: string; 34 | description: string | undefined; 35 | } 36 | export interface ArgsParamMetadata extends CommonArgMetadata { 37 | kind: "args"; 38 | } 39 | // prettier-ignore 40 | export type ParamMetadata = 41 | | InfoParamMetadata 42 | | PubSubParamMetadata 43 | | ContextParamMetadata 44 | | RootParamMetadata 45 | | ArgParamMetadata 46 | | ArgsParamMetadata 47 | ; 48 | -------------------------------------------------------------------------------- /src/utils/emitSchemaDefinitionFile.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, writeFileSync } from "fs"; 2 | import { GraphQLSchema, printSchema } from "graphql"; 3 | import { Options as PrintSchemaOptions } from "graphql/utilities/schemaPrinter"; 4 | import * as path from "path"; 5 | 6 | export const defaultSchemaFilePath = path.resolve(process.cwd(), "schema.gql"); 7 | 8 | export const defaultPrintSchemaOptions: PrintSchemaOptions = { commentDescriptions: false }; 9 | 10 | const generatedSchemaWarning = /* graphql */ `\ 11 | # ----------------------------------------------- 12 | # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! 13 | # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! 14 | # ----------------------------------------------- 15 | 16 | `; 17 | 18 | export function emitSchemaDefinitionFileSync( 19 | schemaFilePath: string, 20 | schema: GraphQLSchema, 21 | options: PrintSchemaOptions = defaultPrintSchemaOptions, 22 | ) { 23 | const schemaFileContent = generatedSchemaWarning + printSchema(schema, options); 24 | writeFileSync(schemaFilePath, schemaFileContent); 25 | } 26 | 27 | export async function emitSchemaDefinitionFile( 28 | schemaFilePath: string, 29 | schema: GraphQLSchema, 30 | options: PrintSchemaOptions = defaultPrintSchemaOptions, 31 | ) { 32 | const schemaFileContent = generatedSchemaWarning + printSchema(schema, options); 33 | return new Promise((resolve, reject) => 34 | writeFile(schemaFilePath, schemaFileContent, err => (err ? reject(err) : resolve())), 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /examples/apollo-engine/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server-express"; 3 | import * as express from "express"; 4 | import { ApolloEngine } from "apollo-engine"; 5 | import { buildSchema } from "../../src"; 6 | 7 | import { RecipeResolver } from "./recipe-resolver"; 8 | 9 | async function bootstrap() { 10 | // build TypeGraphQL executable schema 11 | const schema = await buildSchema({ 12 | resolvers: [RecipeResolver], 13 | }); 14 | 15 | // create an express app 16 | const expressApp = express(); 17 | // create apollo server 18 | const server = new ApolloServer({ 19 | schema, 20 | tracing: true, 21 | cacheControl: true, 22 | engine: false, // we will provide our own ApolloEngine 23 | }); 24 | // apply apollo server to the express app 25 | server.applyMiddleware({ app: expressApp }); 26 | 27 | // configure shared config settings 28 | const port = 4000; 29 | const graphqlEndpointPath = "/graphql"; 30 | 31 | // create an Apollo Engine 32 | const engine = new ApolloEngine({ 33 | // set `APOLLO_ENGINE_API_KEY` env variable or put here your own API key 34 | apiKey: process.env.APOLLO_ENGINE_API_KEY, 35 | }); 36 | 37 | // launch the Apollo Engine 38 | engine.listen( 39 | { 40 | port, 41 | expressApp, 42 | graphqlPaths: [graphqlEndpointPath], 43 | }, 44 | () => console.log(`Server with Apollo Engine is running on http://localhost:${port}`), 45 | ); 46 | } 47 | 48 | bootstrap(); 49 | -------------------------------------------------------------------------------- /src/browser-shim.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This "shim" can be used on the frontend to prevent from errors on undefined decorators, 3 | when you are sharing same classes across backend and frontend. 4 | To use this shim, simply configure your Webpack configuration to use this file 5 | instead of normal TypeGraphQL module. 6 | 7 | plugins: [ 8 | ..., // any existing plugins that you already have 9 | new webpack.NormalModuleReplacementPlugin(/type-graphql$/, function (result) { 10 | result.request = result.request.replace(/type-graphql/, "type-graphql/browser-shim"); 11 | }), 12 | ] 13 | */ 14 | 15 | const dummyFn = () => void 0; 16 | const dummyDecorator = () => dummyFn; 17 | 18 | export const Arg = dummyDecorator; 19 | export const Args = dummyDecorator; 20 | export const ArgsType = dummyDecorator; 21 | export const Authorized = dummyDecorator; 22 | export const Ctx = dummyDecorator; 23 | export const registerEnumType = dummyFn; 24 | export const Field = dummyDecorator; 25 | export const FieldResolver = dummyDecorator; 26 | export const Info = dummyDecorator; 27 | export const InputType = dummyDecorator; 28 | export const InterfaceType = dummyDecorator; 29 | export const Mutation = dummyDecorator; 30 | export const ObjectType = dummyDecorator; 31 | export const PubSub = dummyDecorator; 32 | export const Query = dummyDecorator; 33 | export const Resolver = dummyDecorator; 34 | export const Root = dummyDecorator; 35 | export const Subscription = dummyDecorator; 36 | export const createUnionType = dummyFn; 37 | export const UseMiddleware = dummyDecorator; 38 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/helpers.ts: -------------------------------------------------------------------------------- 1 | import { getRepository, Column, ColumnOptions } from "typeorm"; 2 | 3 | import { Recipe } from "./entities/recipe"; 4 | import { Rate } from "./entities/rate"; 5 | import { User } from "./entities/user"; 6 | 7 | export async function seedDatabase() { 8 | const recipeRepository = getRepository(Recipe); 9 | const ratingsRepository = getRepository(Rate); 10 | const userRepository = getRepository(User); 11 | 12 | const defaultUser = userRepository.create({ 13 | email: "test@github.com", 14 | nickname: "19majkel94", 15 | password: "s3cr3tp4ssw0rd", 16 | }); 17 | await userRepository.save(defaultUser); 18 | 19 | const recipes = recipeRepository.create([ 20 | { 21 | title: "Recipe 1", 22 | description: "Desc 1", 23 | author: defaultUser, 24 | ratings: ratingsRepository.create([ 25 | { value: 2, user: defaultUser }, 26 | { value: 4, user: defaultUser }, 27 | { value: 5, user: defaultUser }, 28 | { value: 3, user: defaultUser }, 29 | { value: 4, user: defaultUser }, 30 | ]), 31 | }, 32 | { 33 | title: "Recipe 2", 34 | author: defaultUser, 35 | ratings: ratingsRepository.create([ 36 | { value: 2, user: defaultUser }, 37 | { value: 4, user: defaultUser }, 38 | ]), 39 | }, 40 | ]); 41 | await recipeRepository.save(recipes); 42 | 43 | return { 44 | defaultUser, 45 | }; 46 | } 47 | 48 | export function RelationColumn(options?: ColumnOptions) { 49 | return Column({ nullable: true, ...options }); 50 | } 51 | -------------------------------------------------------------------------------- /examples/using-scoped-container/recipe/recipe.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "typedi"; 2 | import { Resolver, Query, Arg, Ctx, Mutation } from "../../../src"; 3 | 4 | import { Recipe } from "./recipe.type"; 5 | import { RecipeService } from "./recipe.service"; 6 | import { Logger } from "../logger"; 7 | import { Context } from "../types"; 8 | import { RecipeInput } from "./recipe.input"; 9 | 10 | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 11 | 12 | // this resolver will be recreated for each request (scoped) 13 | @Service() 14 | @Resolver(of => Recipe) 15 | export class RecipeResolver { 16 | constructor(private readonly recipeService: RecipeService, private readonly logger: Logger) { 17 | console.log("RecipeResolver created!"); 18 | } 19 | 20 | @Query(returns => Recipe, { nullable: true }) 21 | async recipe(@Arg("recipeId") recipeId: string, @Ctx() { requestId }: Context) { 22 | const recipe = await this.recipeService.getOne(recipeId); 23 | if (!recipe) { 24 | console.log("request ID:", requestId); // the same requestId that logger has 25 | this.logger.log(`Recipe ${recipeId} not found!`); 26 | } 27 | return recipe; 28 | } 29 | 30 | @Query(returns => [Recipe]) 31 | async recipes(): Promise { 32 | await delay(5000); // simulate delay to allow for manual concurrent requests 33 | return this.recipeService.getAll(); 34 | } 35 | 36 | @Mutation(returns => Recipe) 37 | async addRecipe(@Arg("recipe") recipe: RecipeInput): Promise { 38 | return this.recipeService.add(recipe); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/helpers.ts: -------------------------------------------------------------------------------- 1 | import { getRepository } from "typeorm"; 2 | 3 | import { Recipe } from "./entities/recipe"; 4 | import { Rate } from "./entities/rate"; 5 | import { User } from "./entities/user"; 6 | 7 | export async function seedDatabase() { 8 | const recipeRepository = getRepository(Recipe); 9 | const ratingsRepository = getRepository(Rate); 10 | const userRepository = getRepository(User); 11 | 12 | const defaultUser = userRepository.create({ 13 | email: "test@github.com", 14 | nickname: "19majkel94", 15 | password: "s3cr3tp4ssw0rd", 16 | }); 17 | await userRepository.save(defaultUser); 18 | 19 | const [recipe1, recipe2] = recipeRepository.create([ 20 | { 21 | title: "Recipe 1", 22 | description: "Desc 1", 23 | author: defaultUser, 24 | }, 25 | { 26 | title: "Recipe 2", 27 | author: defaultUser, 28 | }, 29 | ]); 30 | await recipeRepository.save([recipe1, recipe2]); 31 | 32 | const ratings = ratingsRepository.create([ 33 | { value: 2, user: defaultUser, recipe: recipe1 }, 34 | { value: 4, user: defaultUser, recipe: recipe1 }, 35 | { value: 5, user: defaultUser, recipe: recipe1 }, 36 | { value: 3, user: defaultUser, recipe: recipe1 }, 37 | { value: 4, user: defaultUser, recipe: recipe1 }, 38 | 39 | { value: 2, user: defaultUser, recipe: recipe2 }, 40 | { value: 4, user: defaultUser, recipe: recipe2 }, 41 | ]); 42 | await ratingsRepository.save(ratings); 43 | 44 | return { 45 | defaultUser, 46 | }; 47 | } 48 | 49 | export type Lazy = Promise | T; 50 | -------------------------------------------------------------------------------- /src/metadata/definitions/resolver-metadata.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TypeValueThunk, 3 | TypeOptions, 4 | ClassTypeResolver, 5 | SubscriptionFilterFunc, 6 | SubscriptionTopicFunc, 7 | } from "../../decorators/types"; 8 | import { ParamMetadata } from "./param-metadata"; 9 | import { Middleware } from "../../interfaces/Middleware"; 10 | import { Complexity } from "../../interfaces"; 11 | 12 | export interface BaseResolverMetadata { 13 | methodName: string; 14 | schemaName: string; 15 | target: Function; 16 | complexity: Complexity | undefined; 17 | resolverClassMetadata?: ResolverClassMetadata; 18 | params?: ParamMetadata[]; 19 | roles?: any[]; 20 | middlewares?: Array>; 21 | } 22 | 23 | export interface ResolverMetadata extends BaseResolverMetadata { 24 | getReturnType: TypeValueThunk; 25 | returnTypeOptions: TypeOptions; 26 | description?: string; 27 | deprecationReason?: string; 28 | } 29 | 30 | export interface FieldResolverMetadata extends BaseResolverMetadata { 31 | kind: "internal" | "external"; 32 | description?: string; 33 | deprecationReason?: string; 34 | getType?: TypeValueThunk; 35 | typeOptions?: TypeOptions; 36 | getObjectType?: ClassTypeResolver; 37 | } 38 | 39 | export interface SubscriptionResolverMetadata extends ResolverMetadata { 40 | topics: string | string[] | SubscriptionTopicFunc; 41 | filter: SubscriptionFilterFunc | undefined; 42 | } 43 | 44 | export interface ResolverClassMetadata { 45 | target: Function; 46 | getObjectType: ClassTypeResolver; 47 | isAbstract?: boolean; 48 | superResolver?: ResolverClassMetadata; 49 | } 50 | -------------------------------------------------------------------------------- /website/pages/en/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | const Container = CompLibrary.Container; 12 | 13 | const siteConfig = require(process.cwd() + '/siteConfig.js'); 14 | 15 | class Users extends React.Component { 16 | render() { 17 | if ((siteConfig.users || []).length === 0) { 18 | return null; 19 | } 20 | const showcase = siteConfig.users.map((user, i) => { 21 | return ( 22 | 23 | 24 | 25 | ); 26 | }); 27 | 28 | return ( 29 |
30 | 31 |
32 |
33 |

Who's Using This?

34 |

This project is used by many folks

35 |
36 |
{showcase}
37 |

Are you using this project?

38 | 41 | Add your company 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | module.exports = Users; 51 | -------------------------------------------------------------------------------- /website/pages/en/help.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | const Container = CompLibrary.Container; 12 | const GridBlock = CompLibrary.GridBlock; 13 | 14 | const siteConfig = require(process.cwd() + '/siteConfig.js'); 15 | 16 | class Help extends React.Component { 17 | render() { 18 | const supportLinks = [ 19 | { 20 | content: 21 | 'Learn more using the [documentation on this site.](/test-site/docs/en/doc1.html)', 22 | title: 'Browse Docs', 23 | }, 24 | { 25 | content: 'Ask questions about the documentation and project', 26 | title: 'Join the community', 27 | }, 28 | { 29 | content: "Find out what's new with this project", 30 | title: 'Stay up to date', 31 | }, 32 | ]; 33 | 34 | return ( 35 |
36 | 37 |
38 |
39 |

Need help?

40 |
41 |

This project is maintained by a dedicated group of people.

42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | module.exports = Help; 51 | -------------------------------------------------------------------------------- /examples/interfaces-inheritance/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Arg, Mutation } from "../../src"; 2 | import { plainToClass } from "class-transformer"; 3 | 4 | import { getId, calculateAge } from "./helpers"; 5 | import { Student } from "./student/student.type"; 6 | import { Employee } from "./employee/employee.type"; 7 | import { StudentInput } from "./student/student.input"; 8 | import { EmployeeInput } from "./employee/employee.input"; 9 | import { IPerson } from "./person/person.interface"; 10 | 11 | @Resolver() 12 | export class MultiResolver { 13 | private readonly personsRegistry: IPerson[] = []; 14 | 15 | @Query(returns => [IPerson]) 16 | persons(): IPerson[] { 17 | // this one returns interfaces 18 | // so GraphQL has to be able to resolve type of the item 19 | return this.personsRegistry; 20 | } 21 | 22 | @Mutation() 23 | addStudent(@Arg("input") input: StudentInput): Student { 24 | // be sure to create real instances of classes 25 | const student = plainToClass(Student, { 26 | id: getId(), 27 | name: input.name, 28 | universityName: input.universityName, 29 | age: calculateAge(input.dateOfBirth), 30 | }); 31 | this.personsRegistry.push(student); 32 | return student; 33 | } 34 | 35 | @Mutation() 36 | addEmployee(@Arg("input") input: EmployeeInput): Employee { 37 | const employee = plainToClass(Employee, { 38 | id: getId(), 39 | name: input.name, 40 | companyName: input.companyName, 41 | age: calculateAge(input.dateOfBirth), 42 | }); 43 | this.personsRegistry.push(employee); 44 | return employee; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/simple-usage/recipe-resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | FieldResolver, 5 | Arg, 6 | Root, 7 | Mutation, 8 | Float, 9 | Int, 10 | ResolverInterface, 11 | } from "../../src"; 12 | import { plainToClass } from "class-transformer"; 13 | 14 | import { Recipe } from "./recipe-type"; 15 | import { RecipeInput } from "./recipe-input"; 16 | import { createRecipeSamples } from "./recipe-samples"; 17 | 18 | @Resolver(of => Recipe) 19 | export class RecipeResolver implements ResolverInterface { 20 | private readonly items: Recipe[] = createRecipeSamples(); 21 | 22 | @Query(returns => Recipe, { nullable: true }) 23 | async recipe(@Arg("title") title: string): Promise { 24 | return await this.items.find(recipe => recipe.title === title); 25 | } 26 | 27 | @Query(returns => [Recipe], { description: "Get all the recipes from around the world " }) 28 | async recipes(): Promise { 29 | return await this.items; 30 | } 31 | 32 | @Mutation(returns => Recipe) 33 | async addRecipe(@Arg("recipe") recipeInput: RecipeInput): Promise { 34 | const recipe = plainToClass(Recipe, { 35 | description: recipeInput.description, 36 | title: recipeInput.title, 37 | ratings: [], 38 | creationDate: new Date(), 39 | }); 40 | await this.items.push(recipe); 41 | return recipe; 42 | } 43 | 44 | @FieldResolver() 45 | ratingsCount( 46 | @Root() recipe: Recipe, 47 | @Arg("minRate", type => Int, { defaultValue: 0.0 }) minRate: number, 48 | ): number { 49 | return recipe.ratings.filter(rating => rating >= minRate).length; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/schema/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLInputFieldConfigMap, 4 | GraphQLInputFieldConfig, 5 | GraphQLObjectType, 6 | GraphQLInterfaceType, 7 | GraphQLFieldConfigMap, 8 | GraphQLFieldConfigArgumentMap, 9 | } from "graphql"; 10 | 11 | export function getFieldMetadataFromInputType(type: GraphQLInputObjectType) { 12 | const fieldInfo = type.getFields(); 13 | const typeFields = Object.keys(fieldInfo).reduce( 14 | (fieldsMap, fieldName) => { 15 | const superField = fieldInfo[fieldName]; 16 | fieldsMap[fieldName] = { 17 | type: superField.type, 18 | description: superField.description, 19 | defaultValue: superField.defaultValue, 20 | }; 21 | return fieldsMap; 22 | }, 23 | {}, 24 | ); 25 | return typeFields; 26 | } 27 | 28 | export function getFieldMetadataFromObjectType(type: GraphQLObjectType | GraphQLInterfaceType) { 29 | const fieldInfo = type.getFields(); 30 | const typeFields = Object.keys(fieldInfo).reduce>( 31 | (fieldsMap, fieldName) => { 32 | const superField = fieldInfo[fieldName]; 33 | fieldsMap[fieldName] = { 34 | type: superField.type, 35 | args: superField.args.reduce((argMap, { name, ...arg }) => { 36 | argMap[name] = arg; 37 | return argMap; 38 | }, {}), 39 | resolve: superField.resolve, 40 | description: superField.description, 41 | deprecationReason: superField.deprecationReason, 42 | }; 43 | return fieldsMap; 44 | }, 45 | {}, 46 | ); 47 | return typeFields; 48 | } 49 | -------------------------------------------------------------------------------- /src/decorators/Subscription.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReturnTypeFunc, 3 | AdvancedOptions, 4 | SubscriptionFilterFunc, 5 | SubscriptionTopicFunc, 6 | } from "./types"; 7 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 8 | import { getResolverMetadata } from "../helpers/resolver-metadata"; 9 | import { getTypeDecoratorParams } from "../helpers/decorators"; 10 | import { MissingSubscriptionTopicsError } from "../errors"; 11 | 12 | export interface SubscriptionOptions extends AdvancedOptions { 13 | topics: string | string[] | SubscriptionTopicFunc; 14 | filter?: SubscriptionFilterFunc; 15 | } 16 | 17 | export function Subscription(options: SubscriptionOptions): MethodDecorator; 18 | export function Subscription( 19 | returnTypeFunc: ReturnTypeFunc, 20 | options: SubscriptionOptions, 21 | ): MethodDecorator; 22 | export function Subscription( 23 | returnTypeFuncOrOptions: ReturnTypeFunc | SubscriptionOptions, 24 | maybeOptions?: SubscriptionOptions, 25 | ): MethodDecorator { 26 | const { options, returnTypeFunc } = getTypeDecoratorParams(returnTypeFuncOrOptions, maybeOptions); 27 | return (prototype, methodName) => { 28 | const metadata = getResolverMetadata(prototype, methodName, returnTypeFunc, options); 29 | const subscriptionOptions = options as SubscriptionOptions; 30 | if (Array.isArray(options.topics) && options.topics.length === 0) { 31 | throw new MissingSubscriptionTopicsError(metadata.target, metadata.methodName); 32 | } 33 | getMetadataStorage().collectSubscriptionHandlerMetadata({ 34 | ...metadata, 35 | topics: subscriptionOptions.topics, 36 | filter: subscriptionOptions.filter, 37 | }); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /tests/helpers/getSchemaInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | graphql, 3 | getIntrospectionQuery, 4 | IntrospectionObjectType, 5 | IntrospectionSchema, 6 | } from "graphql"; 7 | 8 | import { buildSchema, BuildSchemaOptions } from "../../src"; 9 | 10 | export async function getSchemaInfo(options: BuildSchemaOptions) { 11 | // build schema from definitions 12 | const schema = await buildSchema(options); 13 | 14 | // get builded schema info from retrospection 15 | const result = await graphql(schema, getIntrospectionQuery()); 16 | expect(result.errors).toBeUndefined(); 17 | 18 | const schemaIntrospection = result.data!.__schema as IntrospectionSchema; 19 | expect(schemaIntrospection).toBeDefined(); 20 | 21 | const queryType = schemaIntrospection.types.find( 22 | type => type.name === schemaIntrospection.queryType.name, 23 | ) as IntrospectionObjectType; 24 | 25 | const mutationTypeNameRef = schemaIntrospection.mutationType; 26 | let mutationType: IntrospectionObjectType | undefined; 27 | if (mutationTypeNameRef) { 28 | mutationType = schemaIntrospection.types.find( 29 | type => type.name === mutationTypeNameRef.name, 30 | ) as IntrospectionObjectType; 31 | } 32 | 33 | const subscriptionTypeNameRef = schemaIntrospection.subscriptionType; 34 | let subscriptionType: IntrospectionObjectType | undefined; 35 | if (subscriptionTypeNameRef) { 36 | subscriptionType = schemaIntrospection.types.find( 37 | type => type.name === subscriptionTypeNameRef.name, 38 | ) as IntrospectionObjectType; 39 | } 40 | 41 | return { 42 | schema, 43 | schemaIntrospection, 44 | queryType, 45 | mutationType, 46 | subscriptionType, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/decorators/Resolver.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { ClassTypeResolver, ResolverClassOptions } from "./types"; 3 | import { ClassType } from "../interfaces"; 4 | 5 | export function Resolver(): ClassDecorator; 6 | export function Resolver(options: ResolverClassOptions): ClassDecorator; 7 | export function Resolver( 8 | typeFunc: ClassTypeResolver, 9 | options?: ResolverClassOptions, 10 | ): ClassDecorator; 11 | export function Resolver(objectType: ClassType, options?: ResolverClassOptions): ClassDecorator; 12 | export function Resolver( 13 | objectTypeOrTypeFuncOrMaybeOptions?: Function | ResolverClassOptions, 14 | maybeOptions?: ResolverClassOptions, 15 | ): ClassDecorator { 16 | const objectTypeOrTypeFunc: Function | undefined = 17 | typeof objectTypeOrTypeFuncOrMaybeOptions === "function" 18 | ? objectTypeOrTypeFuncOrMaybeOptions 19 | : undefined; 20 | const options: ResolverClassOptions = 21 | (typeof objectTypeOrTypeFuncOrMaybeOptions === "function" 22 | ? maybeOptions 23 | : objectTypeOrTypeFuncOrMaybeOptions) || {}; 24 | 25 | return target => { 26 | const getObjectType = objectTypeOrTypeFunc 27 | ? objectTypeOrTypeFunc.prototype 28 | ? () => objectTypeOrTypeFunc as ClassType 29 | : (objectTypeOrTypeFunc as ClassTypeResolver) 30 | : () => { 31 | throw new Error( 32 | `No provided object type in '@Resolver' decorator for class '${target.name}!'`, 33 | ); 34 | }; 35 | getMetadataStorage().collectResolverClassMetadata({ 36 | target, 37 | getObjectType, 38 | isAbstract: options.isAbstract || false, 39 | }); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { Container } from "typedi"; 4 | import * as TypeORM from "typeorm"; 5 | import * as TypeGraphQL from "../../src"; 6 | 7 | import { RecipeResolver } from "./resolvers/recipe-resolver"; 8 | import { Recipe } from "./entities/recipe"; 9 | import { Rate } from "./entities/rate"; 10 | import { User } from "./entities/user"; 11 | import { seedDatabase } from "./helpers"; 12 | import { Context } from "./resolvers/types/context"; 13 | 14 | // register 3rd party IOC container 15 | TypeORM.useContainer(Container); 16 | 17 | async function bootstrap() { 18 | try { 19 | // create TypeORM connection 20 | await TypeORM.createConnection({ 21 | type: "mysql", 22 | database: "type-graphql", 23 | username: "root", // fill this with your username 24 | password: "qwerty123", // and password 25 | port: 3306, 26 | host: "localhost", 27 | entities: [Recipe, Rate, User], 28 | synchronize: true, 29 | logger: "advanced-console", 30 | logging: "all", 31 | dropSchema: true, 32 | cache: true, 33 | }); 34 | 35 | // seed database with some data 36 | const { defaultUser } = await seedDatabase(); 37 | 38 | // build TypeGraphQL executable schema 39 | const schema = await TypeGraphQL.buildSchema({ 40 | resolvers: [RecipeResolver], 41 | container: Container, 42 | }); 43 | 44 | // create mocked context 45 | const context: Context = { user: defaultUser }; 46 | 47 | // Create GraphQL server 48 | const server = new ApolloServer({ schema, context }); 49 | 50 | // Start the server 51 | const { url } = await server.listen(4000); 52 | console.log(`Server is running, GraphQL Playground available at ${url}`); 53 | } catch (err) { 54 | console.error(err); 55 | } 56 | } 57 | 58 | bootstrap(); 59 | -------------------------------------------------------------------------------- /examples/typeorm-basic-usage/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { Container } from "typedi"; 4 | import * as TypeORM from "typeorm"; 5 | import * as TypeGraphQL from "../../src"; 6 | 7 | import { RecipeResolver } from "./resolvers/recipe-resolver"; 8 | import { RateResolver } from "./resolvers/rate-resolver"; 9 | import { Recipe } from "./entities/recipe"; 10 | import { Rate } from "./entities/rate"; 11 | import { User } from "./entities/user"; 12 | import { seedDatabase } from "./helpers"; 13 | 14 | export interface Context { 15 | user: User; 16 | } 17 | 18 | // register 3rd party IOC container 19 | TypeORM.useContainer(Container); 20 | 21 | async function bootstrap() { 22 | try { 23 | // create TypeORM connection 24 | await TypeORM.createConnection({ 25 | type: "mysql", 26 | database: "type-graphql", 27 | username: "root", // fill this with your username 28 | password: "qwerty123", // and password 29 | port: 3306, 30 | host: "localhost", 31 | entities: [Recipe, Rate, User], 32 | synchronize: true, 33 | logger: "advanced-console", 34 | logging: "all", 35 | dropSchema: true, 36 | cache: true, 37 | }); 38 | 39 | // seed database with some data 40 | const { defaultUser } = await seedDatabase(); 41 | 42 | // build TypeGraphQL executable schema 43 | const schema = await TypeGraphQL.buildSchema({ 44 | resolvers: [RecipeResolver, RateResolver], 45 | container: Container, 46 | }); 47 | 48 | // create mocked context 49 | const context: Context = { user: defaultUser }; 50 | 51 | // Create GraphQL server 52 | const server = new ApolloServer({ schema, context }); 53 | 54 | // Start the server 55 | const { url } = await server.listen(4000); 56 | console.log(`Server is running, GraphQL Playground available at ${url}`); 57 | } catch (err) { 58 | console.error(err); 59 | } 60 | } 61 | 62 | bootstrap(); 63 | -------------------------------------------------------------------------------- /src/metadata/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResolverClassMetadata, 3 | BaseResolverMetadata, 4 | MiddlewareMetadata, 5 | FieldResolverMetadata, 6 | } from "./definitions"; 7 | import { Middleware } from "../interfaces/Middleware"; 8 | import { isThrowing } from "../helpers/isThrowing"; 9 | import { ReflectMetadataMissingError } from "../errors"; 10 | 11 | export function mapSuperResolverHandlers( 12 | definitions: T[], 13 | superResolver: Function, 14 | resolverMetadata: ResolverClassMetadata, 15 | ): T[] { 16 | const superMetadata = definitions.filter(subscription => subscription.target === superResolver); 17 | 18 | return superMetadata.map(metadata => ({ 19 | ...(metadata as any), 20 | target: resolverMetadata.target, 21 | resolverClassMetadata: resolverMetadata, 22 | })); 23 | } 24 | 25 | export function mapSuperFieldResolverHandlers( 26 | definitions: FieldResolverMetadata[], 27 | superResolver: Function, 28 | resolverMetadata: ResolverClassMetadata, 29 | ): FieldResolverMetadata[] { 30 | const superMetadata = mapSuperResolverHandlers(definitions, superResolver, resolverMetadata); 31 | 32 | return superMetadata.map(metadata => ({ 33 | ...metadata, 34 | getObjectType: isThrowing(metadata.getObjectType!) 35 | ? resolverMetadata.getObjectType! 36 | : metadata.getObjectType!, 37 | })); 38 | } 39 | 40 | export function mapMiddlewareMetadataToArray( 41 | metadata: MiddlewareMetadata[], 42 | ): Array> { 43 | return metadata 44 | .map(m => m.middlewares) 45 | .reduce>>( 46 | (middlewares, resultArray) => resultArray.concat(middlewares), 47 | [], 48 | ); 49 | } 50 | 51 | export function ensureReflectMetadataExists() { 52 | if ( 53 | typeof Reflect !== "object" || 54 | typeof Reflect.decorate !== "function" || 55 | typeof Reflect.metadata !== "function" 56 | ) { 57 | throw new ReflectMetadataMissingError(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | --- 4 | 5 | Before getting started with TypeGraphQL we need to install some additional dependencies and properly configure TypeScript configuration for our project. 6 | 7 | > #### Prerequisites 8 | > Before you begin, make sure your development environment includes Node.js and npm. 9 | 10 | ## Packages installation 11 | 12 | First, you have to install the main package, as well as the [`graphql-js`](https://github.com/graphql/graphql-js) (and it's typings) which is a peer dependency of TypeGraphQL: 13 | ```sh 14 | npm i graphql @types/graphql type-graphql 15 | ``` 16 | 17 | Also, the `reflect-metadata` shim is required to make the type reflection works: 18 | ```sh 19 | npm i reflect-metadata 20 | ``` 21 | 22 | Please make sure to import it on top of your entry file (before you use/import `type-graphql` or your resolvers): 23 | ```typescript 24 | import "reflect-metadata"; 25 | ``` 26 | 27 | ## TypeScript configuration 28 | 29 | It's important to set these options in `tsconfig.json` file of your project: 30 | ```json 31 | { 32 | "emitDecoratorMetadata": true, 33 | "experimentalDecorators": true 34 | } 35 | ``` 36 | 37 | `TypeGraphQL` is designed to work with Node.js 6, 8 and latest stable. It uses features from ES7 (ES2016) so you should set your `tsconfig.json` appropriately: 38 | ```js 39 | { 40 | "target": "es2016" // or newer if your node.js version supports this 41 | } 42 | ``` 43 | 44 | Due to using `graphql-subscription` dependency that rely on an `AsyncIterator`, you may also have to provide the `esnext.asynciterable` to the `lib` option: 45 | ```json 46 | { 47 | "lib": ["es2016", "esnext.asynciterable"] 48 | } 49 | ``` 50 | 51 | All in all, the minimal `tsconfig.json` file example looks like this: 52 | ```json 53 | { 54 | "compilerOptions": { 55 | "target": "es2016", 56 | "module": "commonjs", 57 | "lib": ["es2016", "esnext.asynciterable"], 58 | "experimentalDecorators": true, 59 | "emitDecoratorMetadata": true 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /src/decorators/types.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from "graphql"; 2 | import { ValidatorOptions } from "class-validator"; 3 | 4 | import { ResolverFilterData, ClassType, ResolverTopicData, Complexity } from "../interfaces"; 5 | 6 | export type TypeValue = ClassType | GraphQLScalarType | Function | object | symbol; 7 | export type ReturnTypeFuncValue = TypeValue | [TypeValue]; 8 | 9 | export type TypeValueThunk = (type?: void) => TypeValue; 10 | export type ClassTypeResolver = (of?: void) => ClassType; 11 | 12 | export type ReturnTypeFunc = (returns?: void) => ReturnTypeFuncValue; 13 | 14 | export type SubscriptionFilterFunc = ( 15 | resolverFilterData: ResolverFilterData, 16 | ) => boolean | Promise; 17 | 18 | export type SubscriptionTopicFunc = ( 19 | resolverTopicData: ResolverTopicData, 20 | ) => string | string[]; 21 | 22 | export interface DecoratorTypeOptions { 23 | nullable?: boolean | NullableListOptions; 24 | defaultValue?: any; 25 | } 26 | 27 | export type NullableListOptions = "items" | "itemsAndList"; 28 | 29 | export interface TypeOptions extends DecoratorTypeOptions { 30 | array?: boolean; 31 | } 32 | export interface DescriptionOptions { 33 | description?: string; 34 | } 35 | export interface DepreciationOptions { 36 | deprecationReason?: string; 37 | } 38 | export interface ValidateOptions { 39 | validate?: boolean | ValidatorOptions; 40 | } 41 | export interface ComplexityOptions { 42 | complexity?: Complexity; 43 | } 44 | export interface SchemaNameOptions { 45 | name?: string; 46 | } 47 | export type BasicOptions = DecoratorTypeOptions & DescriptionOptions; 48 | export type AdvancedOptions = BasicOptions & 49 | DepreciationOptions & 50 | SchemaNameOptions & 51 | ComplexityOptions; 52 | 53 | export interface EnumConfig { 54 | name: string; 55 | description?: string; 56 | } 57 | 58 | export type MethodAndPropDecorator = PropertyDecorator & MethodDecorator; 59 | 60 | export interface ResolverClassOptions { 61 | isAbstract?: boolean; 62 | } 63 | -------------------------------------------------------------------------------- /src/decorators/FieldResolver.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { ReturnTypeFunc, AdvancedOptions, TypeValueThunk, TypeOptions } from "./types"; 3 | import { SymbolKeysNotSupportedError } from "../errors"; 4 | import { getTypeDecoratorParams } from "../helpers/decorators"; 5 | import { findType } from "../helpers/findType"; 6 | 7 | export function FieldResolver(): MethodDecorator; 8 | export function FieldResolver(options: AdvancedOptions): MethodDecorator; 9 | export function FieldResolver( 10 | returnTypeFunction?: ReturnTypeFunc, 11 | options?: AdvancedOptions, 12 | ): MethodDecorator; 13 | export function FieldResolver( 14 | returnTypeFuncOrOptions?: ReturnTypeFunc | AdvancedOptions, 15 | maybeOptions?: AdvancedOptions, 16 | ): MethodDecorator { 17 | return (prototype, propertyKey) => { 18 | if (typeof propertyKey === "symbol") { 19 | throw new SymbolKeysNotSupportedError(); 20 | } 21 | 22 | let getType: TypeValueThunk | undefined; 23 | let typeOptions: TypeOptions | undefined; 24 | 25 | const { options, returnTypeFunc } = getTypeDecoratorParams( 26 | returnTypeFuncOrOptions, 27 | maybeOptions, 28 | ); 29 | 30 | // try to get return type info 31 | try { 32 | const typeInfo = findType({ 33 | metadataKey: "design:returntype", 34 | prototype, 35 | propertyKey, 36 | returnTypeFunc, 37 | typeOptions: options, 38 | }); 39 | typeOptions = typeInfo.typeOptions; 40 | getType = typeInfo.getType; 41 | } catch { 42 | // tslint:disable-next-line:no-empty 43 | } 44 | 45 | getMetadataStorage().collectFieldResolverMetadata({ 46 | kind: "external", 47 | methodName: propertyKey, 48 | schemaName: options.name || propertyKey, 49 | target: prototype.constructor, 50 | getType, 51 | typeOptions, 52 | complexity: options.complexity, 53 | description: options.description, 54 | deprecationReason: options.deprecationReason, 55 | }); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/container.ts: -------------------------------------------------------------------------------- 1 | import { ResolverData } from "../interfaces"; 2 | 3 | export type SupportedType = { new (...args: any[]): T } | Function; 4 | 5 | export interface ContainerType { 6 | get(someClass: any, resolverData: ResolverData): any; 7 | } 8 | 9 | export type ContainerGetter = ( 10 | resolverData: ResolverData, 11 | ) => ContainerType; 12 | 13 | /** 14 | * Container to be used by this library for inversion control. 15 | * If container was not implicitly set then by default 16 | * container simply creates a new instance of the given class. 17 | */ 18 | class DefaultContainer { 19 | private instances: Array<{ type: Function; object: any }> = []; 20 | 21 | get(someClass: SupportedType): T { 22 | let instance = this.instances.find(it => it.type === someClass); 23 | if (!instance) { 24 | instance = { type: someClass, object: new (someClass as any)() }; 25 | this.instances.push(instance); 26 | } 27 | 28 | return instance.object; 29 | } 30 | } 31 | 32 | export class IOCContainer { 33 | private container: ContainerType | undefined; 34 | private containerGetter: ContainerGetter | undefined; 35 | private defaultContainer = new DefaultContainer(); 36 | 37 | constructor(iocContainerOrContainerGetter?: ContainerType | ContainerGetter) { 38 | if ( 39 | iocContainerOrContainerGetter && 40 | "get" in iocContainerOrContainerGetter && 41 | typeof iocContainerOrContainerGetter.get === "function" 42 | ) { 43 | this.container = iocContainerOrContainerGetter; 44 | } else if (typeof iocContainerOrContainerGetter === "function") { 45 | this.containerGetter = iocContainerOrContainerGetter; 46 | } 47 | } 48 | 49 | getInstance(someClass: SupportedType, resolverData: ResolverData): T { 50 | const container = this.containerGetter ? this.containerGetter(resolverData) : this.container; 51 | if (!container) { 52 | return this.defaultContainer.get(someClass); 53 | } 54 | return container.get(someClass, resolverData); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/typeorm-lazy-relations/resolvers/recipe-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Arg, Mutation, Ctx, Int } from "../../../src/"; 2 | import { Repository } from "typeorm"; 3 | import { InjectRepository } from "typeorm-typedi-extensions"; 4 | 5 | import { Recipe } from "../entities/recipe"; 6 | import { Rate } from "../entities/rate"; 7 | import { RecipeInput } from "./types/recipe-input"; 8 | import { RateInput } from "./types/rate-input"; 9 | import { Context } from "./types/context"; 10 | 11 | @Resolver(Recipe) 12 | export class RecipeResolver { 13 | constructor( 14 | @InjectRepository(Recipe) private readonly recipeRepository: Repository, 15 | @InjectRepository(Rate) private readonly ratingsRepository: Repository, 16 | ) {} 17 | 18 | @Query(returns => Recipe, { nullable: true }) 19 | recipe(@Arg("recipeId", type => Int) recipeId: number) { 20 | return this.recipeRepository.findOne(recipeId); 21 | } 22 | 23 | @Query(returns => [Recipe]) 24 | recipes(): Promise { 25 | return this.recipeRepository.find(); 26 | } 27 | 28 | @Mutation(returns => Recipe) 29 | addRecipe(@Arg("recipe") recipeInput: RecipeInput, @Ctx() { user }: Context): Promise { 30 | const recipe = this.recipeRepository.create({ 31 | ...recipeInput, 32 | author: user, 33 | }); 34 | return this.recipeRepository.save(recipe); 35 | } 36 | 37 | @Mutation(returns => Recipe) 38 | async rate(@Ctx() { user }: Context, @Arg("rate") rateInput: RateInput): Promise { 39 | // find the recipe 40 | const recipe = await this.recipeRepository.findOne(rateInput.recipeId, { 41 | relations: ["ratings"], // preload the relation as we will modify it 42 | }); 43 | if (!recipe) { 44 | throw new Error("Invalid recipe ID"); 45 | } 46 | 47 | // add the new recipe rate 48 | (await recipe.ratings).push( 49 | this.ratingsRepository.create({ 50 | recipe, 51 | user, 52 | value: rateInput.value, 53 | }), 54 | ); 55 | 56 | // return updated recipe 57 | return await this.recipeRepository.save(recipe); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/decorators/Field.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataStorage } from "../metadata/getMetadataStorage"; 2 | import { ReturnTypeFunc, AdvancedOptions, MethodAndPropDecorator } from "./types"; 3 | import { findType } from "../helpers/findType"; 4 | import { getTypeDecoratorParams } from "../helpers/decorators"; 5 | import { SymbolKeysNotSupportedError } from "../errors"; 6 | 7 | export function Field(): MethodAndPropDecorator; 8 | export function Field(options: AdvancedOptions): MethodAndPropDecorator; 9 | export function Field( 10 | returnTypeFunction?: ReturnTypeFunc, 11 | options?: AdvancedOptions, 12 | ): MethodAndPropDecorator; 13 | export function Field( 14 | returnTypeFuncOrOptions?: ReturnTypeFunc | AdvancedOptions, 15 | maybeOptions?: AdvancedOptions, 16 | ): MethodDecorator | PropertyDecorator { 17 | return (prototype, propertyKey, descriptor) => { 18 | if (typeof propertyKey === "symbol") { 19 | throw new SymbolKeysNotSupportedError(); 20 | } 21 | 22 | const { options, returnTypeFunc } = getTypeDecoratorParams( 23 | returnTypeFuncOrOptions, 24 | maybeOptions, 25 | ); 26 | const isResolver = Boolean(descriptor); 27 | const isResolverMethod = Boolean(descriptor && descriptor.value); 28 | 29 | const { getType, typeOptions } = findType({ 30 | metadataKey: isResolverMethod ? "design:returntype" : "design:type", 31 | prototype, 32 | propertyKey, 33 | returnTypeFunc, 34 | typeOptions: options, 35 | }); 36 | 37 | getMetadataStorage().collectClassFieldMetadata({ 38 | name: propertyKey, 39 | schemaName: options.name || propertyKey, 40 | getType, 41 | typeOptions, 42 | complexity: options.complexity, 43 | target: prototype.constructor, 44 | description: options.description, 45 | deprecationReason: options.deprecationReason, 46 | }); 47 | 48 | if (isResolver) { 49 | getMetadataStorage().collectFieldResolverMetadata({ 50 | kind: "internal", 51 | methodName: propertyKey, 52 | schemaName: options.name || propertyKey, 53 | target: prototype.constructor, 54 | complexity: options.complexity, 55 | }); 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /examples/using-scoped-container/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "apollo-server"; 3 | import Container, { ContainerInstance } from "typedi"; 4 | import { buildSchema, ResolverData } from "../../src"; 5 | 6 | import { RecipeResolver } from "./recipe/recipe.resolver"; 7 | import { Context } from "./types"; 8 | import { setSamplesInContainer } from "./recipe/recipe.samples"; 9 | 10 | async function bootstrap() { 11 | setSamplesInContainer(); 12 | 13 | // build TypeGraphQL executable schema 14 | const schema = await buildSchema({ 15 | resolvers: [RecipeResolver], 16 | // register our custom, scoped IOC container by passing a extracting from resolver data function 17 | container: ({ context }: ResolverData) => context.container, 18 | }); 19 | 20 | // Create GraphQL server 21 | const server = new ApolloServer({ 22 | schema, 23 | // we need to provide unique context with `requestId` for each request 24 | context: (): Context => { 25 | const requestId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); // uuid-like 26 | const container = Container.of(requestId); // get scoped container 27 | const context = { requestId, container }; // create our context 28 | container.set("context", context); // place context or other data in container 29 | return context; 30 | }, 31 | formatResponse: (response: any, { context }: ResolverData) => { 32 | // remember to dispose the scoped container to prevent memory leaks 33 | Container.reset(context.requestId); 34 | 35 | // for developers curiosity purpose, here is the logging of current scoped container instances 36 | // you can make multiple parallel requests to see in console how this works 37 | const instancesIds = ((Container as any).instances as ContainerInstance[]).map( 38 | instance => instance.id, 39 | ); 40 | console.log("instances left in memory:", instancesIds); 41 | 42 | return response; 43 | }, 44 | }); 45 | 46 | // Start the server 47 | const { url } = await server.listen(4000); 48 | console.log(`Server is running, GraphQL Playground available at ${url}`); 49 | } 50 | 51 | bootstrap(); 52 | -------------------------------------------------------------------------------- /examples/redis-subscriptions/recipe.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | Mutation, 5 | Arg, 6 | PubSub, 7 | Publisher, 8 | Subscription, 9 | Root, 10 | ID, 11 | ResolverFilterData, 12 | Args, 13 | } from "../../src"; 14 | 15 | import { Recipe } from "./recipe.type"; 16 | import { CommentInput } from "./comment.input"; 17 | import { Comment } from "./comment.type"; 18 | import { NewCommentPayload } from "./newComment.interface"; 19 | import { Topic } from "./topics"; 20 | import { sampleRecipes } from "./recipe.samples"; 21 | import { NewCommentsArgs } from "./recipe.resolver.args"; 22 | 23 | @Resolver() 24 | export class RecipeResolver { 25 | private readonly recipes: Recipe[] = sampleRecipes.slice(); 26 | 27 | @Query(returns => Recipe, { nullable: true }) 28 | async recipe(@Arg("id", type => ID) id: string) { 29 | return this.recipes.find(recipe => recipe.id === id); 30 | } 31 | 32 | @Mutation(returns => Boolean) 33 | async addNewComment( 34 | @Arg("comment") input: CommentInput, 35 | @PubSub(Topic.NewComment) notifyAboutNewComment: Publisher, 36 | ): Promise { 37 | const recipe = this.recipes.find(r => r.id === input.recipeId); 38 | if (!recipe) { 39 | return false; 40 | } 41 | const comment: Comment = { 42 | content: input.content, 43 | nickname: input.nickname, 44 | date: new Date(), 45 | }; 46 | recipe.comments.push(comment); 47 | await notifyAboutNewComment({ 48 | content: comment.content, 49 | nickname: comment.nickname, 50 | dateString: comment.date.toISOString(), 51 | recipeId: input.recipeId, 52 | }); 53 | return true; 54 | } 55 | 56 | @Subscription(returns => Comment, { 57 | topics: Topic.NewComment, 58 | filter: ({ payload, args }: ResolverFilterData) => { 59 | return payload.recipeId === args.recipeId; 60 | }, 61 | }) 62 | newComments( 63 | @Root() newComment: NewCommentPayload, 64 | @Args() { recipeId }: NewCommentsArgs, 65 | ): Comment { 66 | return { 67 | content: newComment.content, 68 | date: new Date(newComment.dateString), // limitation of Redis payload serialization 69 | nickname: newComment.nickname, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/helpers/findType.ts: -------------------------------------------------------------------------------- 1 | import { ReturnTypeFunc, TypeOptions, TypeValueThunk, TypeValue } from "../decorators/types"; 2 | import { bannedTypes } from "./returnTypes"; 3 | import { NoExplicitTypeError, CannotDetermineTypeError } from "../errors"; 4 | 5 | export type MetadataKey = "design:type" | "design:returntype" | "design:paramtypes"; 6 | 7 | export interface TypeInfo { 8 | getType: TypeValueThunk; 9 | typeOptions: TypeOptions; 10 | } 11 | 12 | export interface GetTypeParams { 13 | metadataKey: MetadataKey; 14 | prototype: Object; 15 | propertyKey: string; 16 | returnTypeFunc?: ReturnTypeFunc; 17 | typeOptions?: TypeOptions; 18 | parameterIndex?: number; 19 | } 20 | export function findType({ 21 | metadataKey, 22 | prototype, 23 | propertyKey, 24 | returnTypeFunc, 25 | typeOptions = {}, 26 | parameterIndex, 27 | }: GetTypeParams): TypeInfo { 28 | const options: TypeOptions = { ...typeOptions }; 29 | let metadataDesignType: Function | undefined; 30 | const reflectedType: Function[] | Function | undefined = Reflect.getMetadata( 31 | metadataKey, 32 | prototype, 33 | propertyKey, 34 | ); 35 | if (metadataKey === "design:paramtypes") { 36 | metadataDesignType = (reflectedType as Function[])[parameterIndex!]; 37 | } else { 38 | metadataDesignType = reflectedType as Function | undefined; 39 | } 40 | 41 | if ( 42 | !returnTypeFunc && 43 | (!metadataDesignType || (metadataDesignType && bannedTypes.includes(metadataDesignType))) 44 | ) { 45 | throw new NoExplicitTypeError(prototype.constructor.name, propertyKey, parameterIndex); 46 | } 47 | if (metadataDesignType === Array) { 48 | options.array = true; 49 | } 50 | 51 | if (returnTypeFunc) { 52 | const getType = () => { 53 | if (Array.isArray(returnTypeFunc())) { 54 | options.array = true; 55 | return (returnTypeFunc() as [TypeValue])[0]; 56 | } 57 | return returnTypeFunc(); 58 | }; 59 | return { 60 | getType, 61 | typeOptions: options, 62 | }; 63 | } else if (metadataDesignType) { 64 | return { 65 | getType: () => metadataDesignType!, 66 | typeOptions: options, 67 | }; 68 | } else { 69 | throw new CannotDetermineTypeError(prototype.constructor.name, propertyKey, parameterIndex); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | sidebar_label: List of examples 4 | --- 5 | 6 | On the [GitHub repository](https://github.com/19majkel94/type-graphql) there are a few simple examples showing how to use different TypeGraphQL features and how well it integrates with 3rd party libraries. 7 | 8 | All examples has a `examples.gql` file with sample queries/mutations/subscriptions that you can execute. 9 | 10 | ## Basics 11 | - [Simple usage of fields, basic types and resolvers](https://github.com/19majkel94/type-graphql/tree/master/examples/simple-usage) 12 | 13 | ## Advanced 14 | - [Enums and unions](https://github.com/19majkel94/type-graphql/tree/master/examples/enums-and-unions) 15 | - [Interfaces and types inheritance](https://github.com/19majkel94/type-graphql/tree/master/examples/interfaces-inheritance) 16 | - [Subscriptions (simple)](https://github.com/19majkel94/type-graphql/tree/master/examples/simple-subscriptions) 17 | - [Subscriptions (using Redis)](https://github.com/19majkel94/type-graphql/tree/master/examples/redis-subscriptions) 18 | - [Resolvers inheritance](https://github.com/19majkel94/type-graphql/tree/master/examples/resolvers-inheritance) 19 | 20 | ## Features usage 21 | - [Dependency injection (IoC container)](https://github.com/19majkel94/type-graphql/tree/master/examples/using-container) 22 | - [scoped container](https://github.com/19majkel94/type-graphql/tree/master/examples/using-scoped-container) 23 | - [Authorization](https://github.com/19majkel94/type-graphql/tree/master/examples/authorization) 24 | - [Validation](https://github.com/19majkel94/type-graphql/tree/master/examples/automatic-validation) 25 | - [Middlewares](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares) 26 | 27 | ## 3rd party libs integration 28 | - [TypeORM (manual, synchronous) *](https://github.com/19majkel94/type-graphql/tree/master/examples/typeorm-basic-usage) 29 | - [TypeORM (automatic, lazy relations) *](https://github.com/19majkel94/type-graphql/tree/master/examples/typeorm-lazy-relations) 30 | - [Apollo Engine (Apollo Cache Control) **](https://github.com/19majkel94/type-graphql/tree/master/examples/apollo-engine) 31 | 32 | _* Note that you need to edit the TypeORM examples `index.ts` with credentials to your local database_ 33 | 34 | _** Note that you need to provide `APOLLO_ENGINE_API_KEY` env variable with your own API key_ 35 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.16.0/emit-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Emitting schema SDL 3 | id: version-0.16.0-emit-schema 4 | original_id: emit-schema 5 | --- 6 | 7 | The TypeGraphQL main feature is creating schema using only TypeScript's classes and decorators. However, sometimes we might need the schema to be printed into a `schema.gql` file and there are a plenty of reasons for that. Mainly, 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 kinda 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 GraphiQL or GraphQL Playground. 8 | 9 | To accomplish this demand, TypeGraphQL allows you to create a schema definition file in two ways. The first one is automatically on every build of the schema - just pass `emitSchemaFile: true` to the `buildSchema` options in order to emit the `schema.gql` in the root of the project's working directory. You can also manually specify a path and the file name where the schema definition should be written. 10 | 11 | ```typescript 12 | const schema = await buildSchema({ 13 | resolvers: [ExampleResolver], 14 | // automatically create `schema.gql` file with schema definition 15 | // in project's working directory 16 | emitSchemaFile: true, 17 | // or create the file with schema in selected path 18 | emitSchemaFile: path.resolve(__dirname, "snapshots/schema", "schema.gql"), 19 | }); 20 | ``` 21 | 22 | Second way to emit schema definition file is by doing it manually in a programmatic way. All you need to do is to use `emitSchemaDefinitionFile` function or it's sync version (`emitSchemaDefinitionFileSync`) and pass the selected path to it, along with the schema object. You can use this i.a. 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. 23 | 24 | ```typescript 25 | import { emitSchemaDefinitionFile, buildSchema } from "type-graphql"; 26 | // ... 27 | hypotheticalFileWatcher.watch("./src/**/*.{resolver,type,input,arg}.ts", async () => { 28 | const schema = getSchemaNotFromBuildSchemaFunction(); 29 | await emitSchemaDefinitionFile("/path/to/folder/schema.gql", schema); 30 | }); 31 | ``` 32 | --------------------------------------------------------------------------------