├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── assets └── demo.gif ├── book.json ├── docs ├── README.md ├── SUMMARY.md ├── explore │ ├── arg.md │ ├── enum │ │ ├── index.md │ │ └── register-enum.md │ ├── hooks │ │ ├── authorization-example.md │ │ ├── before-and-after.md │ │ └── index.md │ ├── inject │ │ ├── context-source-info.md │ │ └── index.md │ ├── input-type-and-input-field.md │ ├── object-type-and-field.md │ ├── schema │ │ ├── compile-schema.md │ │ ├── index.md │ │ └── query-and-mutation.md │ └── union.md ├── getting-started │ ├── minimal-demo.md │ ├── setup.md │ └── what-is-typegql.md └── reference │ └── index.md ├── examples ├── basic-express-server │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── schema.ts │ ├── tsconfig.json │ └── yarn.lock ├── custom-decorators │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── schema.ts │ ├── tsconfig.json │ └── yarn.lock ├── forward-resolution │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── yarn.lock ├── merge-schemas │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── schemaA.ts │ ├── schemaB.ts │ ├── tsconfig.json │ └── yarn.lock ├── nested-mutation-or-query │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── schema.ts │ ├── tsconfig.json │ └── yarn.lock ├── serverless │ ├── .gitignore │ ├── README.md │ ├── handler.ts │ ├── package.json │ ├── serverless.yml │ ├── tsconfig.json │ └── yarn.lock └── typeorm-basic-integration │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── yarn.lock ├── jest.config.js ├── package.json ├── preprocessor.js ├── publish-docs.js ├── rollup.config.js ├── src ├── domains │ ├── arg │ │ ├── compiler.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── options.ts │ │ └── registry.ts │ ├── enum │ │ ├── error.ts │ │ ├── index.ts │ │ ├── registry.ts │ │ └── services.ts │ ├── field │ │ ├── compiler │ │ │ ├── fieldType.ts │ │ │ ├── index.ts │ │ │ ├── resolver.ts │ │ │ └── services.ts │ │ ├── error.ts │ │ ├── index.ts │ │ └── registry.ts │ ├── hooks │ │ ├── error.ts │ │ ├── index.ts │ │ └── registry.ts │ ├── index.ts │ ├── inject │ │ ├── index.ts │ │ └── registry.ts │ ├── inputField │ │ ├── compiler │ │ │ ├── fieldType.ts │ │ │ └── index.ts │ │ ├── error.ts │ │ ├── index.ts │ │ └── registry.ts │ ├── inputObjectType │ │ ├── compiler │ │ │ ├── index.ts │ │ │ └── objectType.ts │ │ ├── error.ts │ │ ├── index.ts │ │ └── registry.ts │ ├── objectType │ │ ├── compiler │ │ │ ├── index.ts │ │ │ └── objectType.ts │ │ ├── error.ts │ │ ├── index.ts │ │ └── registry.ts │ ├── schema │ │ ├── compiler.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── registry.ts │ │ ├── rootFields.ts │ │ └── services.ts │ └── union │ │ ├── compiler.ts │ │ ├── error.ts │ │ ├── index.ts │ │ └── registry.ts ├── index.ts ├── services │ ├── error.ts │ ├── types │ │ └── index.ts │ └── utils │ │ ├── cachedThunk.ts │ │ ├── deepWeakMap │ │ └── index.ts │ │ ├── deprecation │ │ └── index.ts │ │ ├── getParameterNames.ts │ │ ├── gql │ │ ├── index.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── parseNative.ts │ │ │ └── resolve.ts │ │ └── validators.ts │ │ ├── index.ts │ │ └── inheritance │ │ └── index.ts └── test │ ├── arg │ ├── __snapshots__ │ │ ├── complex.spec.ts.snap │ │ └── infering.spec.ts.snap │ ├── basics.spec.ts │ ├── complex.spec.ts │ └── infering.spec.ts │ ├── enum │ ├── __snapshots__ │ │ └── index.spec.ts.snap │ └── index.spec.ts │ ├── field │ ├── __snapshots__ │ │ ├── index.spec.ts.snap │ │ └── special-fields.spec.ts.snap │ ├── getters.spec.ts │ ├── index.spec.ts │ └── special-fields.spec.ts │ ├── functional │ ├── __snapshots__ │ │ ├── enum.spec.ts.snap │ │ ├── fieldArguments.spec.ts.snap │ │ ├── mutation.spec.ts.snap │ │ └── query.spec.ts.snap │ ├── enum.spec.ts │ ├── fieldArguments.spec.ts │ ├── inputOutput.spec.ts │ ├── mutation.spec.ts │ └── query.spec.ts │ ├── hooks │ ├── __snapshots__ │ │ └── index.spec.ts.snap │ └── index.spec.ts │ ├── inject │ ├── __snapshots__ │ │ └── index.spec.ts.snap │ └── index.spec.ts │ ├── inputObjectType │ ├── __snapshots__ │ │ └── index.spec.ts.snap │ ├── index.spec.ts │ └── inheritance.spec.ts │ ├── misc │ └── index.spec.ts │ ├── objectType │ ├── __snapshots__ │ │ └── index.spec.ts.snap │ ├── index.spec.ts │ └── inheritance.spec.ts │ ├── schema │ ├── __snapshots__ │ │ └── index.spec.ts.snap │ └── index.spec.ts │ ├── setup.ts │ ├── union │ └── index.spec.ts │ └── utils │ └── index.ts ├── tsconfig.json ├── ttsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | types 4 | .rpt2_cache 5 | _book 6 | docs/_book 7 | _book 8 | 9 | .DS_Store 10 | 11 | coverage 12 | yarn-error.log 13 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | lib/test -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | before_install: 5 | - npm i -g codecov 6 | scripts: 7 | - npm run test --ci 8 | after_success: 9 | - codecov -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@prismake.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Prismake. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/typegql.svg)](https://badge.fury.io/js/typegql) 2 | [![npm version](https://david-dm.org/prismake/typegql.svg)](https://david-dm.org/prismake/typegql) 3 | [![codecov](https://codecov.io/gh/prismake/typegql/branch/master/graph/badge.svg)](https://codecov.io/gh/prismake/typegql) 4 | [![Build Status](https://api.travis-ci.org/prismake/typegql.svg?branch=master)](https://travis-ci.org/prismake/typegql) 5 | 6 | ### What is `typegql`? 7 | 8 | ![demo](assets/demo.gif) 9 | 10 | typegql is set of decorators allowing creating GraphQL APIs quickly and in type-safe way. 11 | 12 | * [Documentation](https://prismake.github.io/typegql/) 13 | 14 | ### Examples: 15 | 16 | * [Basic Express example](examples/basic-express-server) 17 | * [Typeorm integration example](examples/typeorm-basic-integration) 18 | * [Forward resolution - eg. query only needed db fields](examples/forward-resolution) 19 | * [Nested mutations or queries](examples/nested-mutation-or-query) 20 | * [Custom decorators / Higher order decorators](examples/custom-decorators) 21 | * [Serverless eg. AWS Lambda](examples/serverless) 22 | * [Merge schemas](examples/merge-schemas) 23 | 24 | ## Basic example 25 | 26 | Example below is able to resolve such query 27 | 28 | ```graphql 29 | query { 30 | hello(name: "Bob") # will resolve to 'Hello, Bob!' 31 | } 32 | ``` 33 | 34 | ```typescript 35 | import { compileSchema, SchemaRoot, Query } from 'typegql'; 36 | 37 | @SchemaRoot() 38 | class SuperSchema { 39 | @Query() 40 | hello(name: string): string { 41 | return `Hello, ${name}!`; 42 | } 43 | } 44 | 45 | const compiledSchema = compileSchema({ roots: [SuperSchema] }); 46 | ``` 47 | 48 | `compiledSchema` is regular executable schema compatible with `graphql-js` library. 49 | 50 | To use it with `express`, you'd have to simply: 51 | 52 | ```typescript 53 | import * as express from 'express'; 54 | import * as graphqlHTTP from 'express-graphql'; 55 | 56 | const app = express(); 57 | 58 | app.use( 59 | '/graphql', 60 | graphqlHTTP({ 61 | schema: compiledSchema, 62 | graphiql: true, 63 | }), 64 | ); 65 | app.listen(3000, () => console.log('Graphql API ready on http://localhost:3000/graphql')); 66 | ``` 67 | 68 | ## Adding nested types 69 | 70 | For now, our query field returned scalar (string). Let's return something more complex. Schema will look like: 71 | 72 | ```graphql 73 | mutation { 74 | createProduct(name: "Chair", price: 99.99) { 75 | name 76 | price 77 | isExpensive 78 | } 79 | } 80 | ``` 81 | 82 | Such query will have a bit more code and here it is: 83 | 84 | ```typescript 85 | import { Schema, Query, ObjectType, Field, Mutation, compileSchema } from 'typegql'; 86 | 87 | @ObjectType({ description: 'Simple product object type' }) 88 | class Product { 89 | @Field() name: string; 90 | 91 | @Field() price: number; 92 | 93 | @Field() 94 | isExpensive() { 95 | return this.price > 50; 96 | } 97 | } 98 | 99 | @Schema() 100 | class SuperSchema { 101 | @Mutation() 102 | createProduct(name: string, price: number): Product { 103 | const product = new Product(); 104 | product.name = name; 105 | product.price = price; 106 | return product; 107 | } 108 | } 109 | 110 | const compiledSchema = compileSchema(SuperSchema); 111 | ``` 112 | 113 | ## Forcing field type. 114 | 115 | Until now, `typegql` was able to guess type of every field from typescript type definitions. 116 | 117 | There are, however, some cases where we'd have to define them explicitly. 118 | 119 | * We want to strictly tell if field is nullable or not 120 | * We want to be explicit about if some `number` type is `Float` or `Int` (`GraphQLFloat` or `GraphQLInt`) etc 121 | * Function we use returns type of `Promise` while field itself is typed as `SomeType` 122 | * List (Array) type is used. (For now, typescript `Reflect` api is not able to guess type of single array item. This might change in the future) 123 | 124 | Let's modify our `Product` so it has additional `categories` field that will return array of strings. For sake of readibility, I'll ommit all fields we've defined previously. 125 | 126 | ```typescript 127 | @ObjectType() 128 | class Product { 129 | @Field({ type: [String] }) // note we can use any native type like GraphQLString! 130 | categories(): string[] { 131 | return ['Tables', 'Furniture']; 132 | } 133 | } 134 | ``` 135 | 136 | We've added `{ type: [String] }` as `@Field` options. Type can be anything that is resolvable to `GraphQL` type 137 | 138 | * Native JS scalars: `String`, `Number`, `Boolean`. 139 | * Any type that is already compiled to `graphql` eg. `GraphQLFloat` or any type from external graphql library etc 140 | * Every class decorated with `@ObjectType` 141 | * One element array of any of above for list types eg. `[String]` or `[GraphQLFloat]` 142 | 143 | ## Writing Asynchronously 144 | 145 | Every field function we write can be `async` and return `Promise`. Let's say, instead of hard-coding our categories, we want to fetch it from some external API: 146 | 147 | ```typescript 148 | @ObjectType() 149 | class Product { 150 | @Field({ type: [String] }) // note we can use any native type like GraphQLString! 151 | async categories(): Promise { 152 | const categories = await api.fetchCategories(); 153 | return categories.map(cat => cat.name); 154 | } 155 | } 156 | ``` 157 | 158 | ## Before `1.0.0` 159 | 160 | Before version `1.0.0` consider APIs of `typegql` to be subject to change. We encourage you to try this library out and provide us feedback so we can polish it to be as usable and efficent as possible. 161 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismake/typegql/313c4b196a2dde0d2dfe55969503a0a4f829b614/assets/demo.gif -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.2.3", 3 | "root": "./docs" 4 | } 5 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ### What is `typegql`? 4 | 5 | typegql is set of decorators allowing creating GraphQL APIs quickly and in type-safe way. 6 | 7 | ## Creating very simple schema 8 | 9 | Schema is main building block of any `graphql` schema. It'll join all parts of our api together. 10 | 11 | In `typegql` to create schema, we need to pass class decorated with `@Schema` to `compileSchema` function 12 | 13 | ```ts 14 | import { Schema, compileSchema} from 'typegql 15 | 16 | @Schema() 17 | class SuperSchema { 18 | // fields implementation 19 | } 20 | 21 | const compiledSchema = compileSchema(SuperSchema); 22 | ``` 23 | 24 | `compiledSchema` from example above is standard, regular `graphql` schema. 25 | 26 | ## Adding Query and Mutation fields 27 | 28 | Any working schema requires at least one Query field. There are special decorators - `@Query` and `@Mutation` used to register root fields of schema. 29 | 30 | Very simple fully working schema like 31 | 32 | ```graphql 33 | { 34 | hello # will resolve to 'world' 35 | } 36 | ``` 37 | 38 | Could be implemented as: 39 | 40 | ```typescript 41 | import { Schema, Query, compileSchema} from 'typegql 42 | 43 | @Schema() 44 | class SuperSchema { 45 | @Query() 46 | hello(): string { 47 | return 'world' 48 | } 49 | } 50 | 51 | const compiledSchema = compileSchema(SuperSchema); 52 | ``` 53 | 54 | ## Adding parameters 55 | 56 | Let's add some customization to our schema: 57 | 58 | ```graphql 59 | { 60 | hello(name: "Bob") # will resolve to 'Hello, Bob!' 61 | } 62 | ``` 63 | 64 | With tiny change in our code: 65 | 66 | ```typescript 67 | import { Schema, Query, compileSchema} from 'typegql 68 | 69 | @Schema() 70 | class SuperSchema { 71 | @Query() 72 | hello(name: string): string { 73 | return `Hello, ${name}!`; 74 | } 75 | } 76 | 77 | const compiledSchema = compileSchema(SuperSchema); 78 | ``` 79 | 80 | ## Adding nested types 81 | 82 | For now, our query field returned scalar (string). Let's return something more complex. Schema will look like: 83 | 84 | ```graphql 85 | mutation { 86 | createProduct(name: "Chair", price: 99.99) { 87 | name 88 | price 89 | isExpensive 90 | } 91 | } 92 | ``` 93 | 94 | Such query will have a bit more code and here it is: 95 | 96 | ```typescript 97 | import { Schema, Query, ObjectType, Field, Mutation, compileSchema} from 'typegql; 98 | 99 | @ObjectType({ description: 'Simple product object type' }) 100 | class Product { 101 | @Field() 102 | name: string; 103 | 104 | @Field() 105 | price: number; 106 | 107 | @Field() 108 | isExpensive() { 109 | return this.price > 50; 110 | } 111 | } 112 | 113 | @Schema() 114 | class SuperSchema { 115 | @Mutation() 116 | createProduct(name: string, price: number): Product { 117 | const product = new Product(); 118 | product.name = name; 119 | product.price = price; 120 | return product; 121 | } 122 | } 123 | 124 | const compiledSchema = compileSchema(SuperSchema); 125 | ``` 126 | 127 | ## Forcing field type. 128 | 129 | Since now, `typegql` was able to guess type of every field from typescript type definitions. 130 | 131 | There are, however, some cases where we'd have to define them explicitly. 132 | 133 | * We want to strictly tell if field is nullable or not 134 | * Function we use returns type of `Promise` while field itself is typed as `SomeType` 135 | * List (Array) type is used. (For now, typescript `Reflect` api is not able to guess type of single array item. This might change in the future) 136 | 137 | Let's modify our `Product` so it has additional `categories` field that will return array of strings. For sake of readibility, I'll ommit all fields we've defined previously. 138 | 139 | ```typescript 140 | @ObjectType() 141 | class Product { 142 | @Field({ type: [String] }) // note we can use any native type like GraphQLString! 143 | categories(): string[] { 144 | return ['Tables', 'Furniture']; 145 | } 146 | } 147 | ``` 148 | 149 | We've added `{ type: [String] }` as `@Field` options. Type can be anything that is resolvable to `GraphQL` type 150 | 151 | * Native JS scalars: `String`, `Number`, `Boolean`. 152 | * Any type that is already compiled to `graphql` eg. `GraphQLFloat` or any type from external graphql library etc 153 | * Every class decorated with `@ObjectType` 154 | * One element array of any of above for list types eg. `[String]` or `[GraphQLFloat]` 155 | 156 | ## Writing Asynchroniously 157 | 158 | Every field function we write can be `async` and return `Promise`. Let's say, instead of hard-coding our categories, we want to fetch it from some external API: 159 | 160 | ```typescript 161 | @ObjectType() 162 | class Product { 163 | @Field({ type: [String] }) // note we can use any native type like GraphQLString! 164 | async categories(): Promise { 165 | const categories = await api.fetchCategories(); 166 | return categories.map(cat => cat.name); 167 | } 168 | } 169 | ``` 170 | 171 | ## Adding to your project 172 | 173 | [Installation and setup](getting-started/setup.md) 174 | 175 | **Important!** setup steps are simple, but required. Make sure to check [setup](getting-started/setup.md) section. 176 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | ### Get started 4 | 5 | * [Getting started](README.md) 6 | * [Installation and setup](getting-started/setup.md) 7 | 8 | ### Explore in details 9 | 10 | * [@ObjectType and @Field](explore/object-type-and-field.md) 11 | * [@Arg and field arguments](explore/arg.md) 12 | * [@InputObjectType and @InputField](explore/input-type-and-input-field.md) 13 | * [Enums](explore/enum/index.md) 14 | * [Hooks](explore/hooks/index.md) 15 | * [@Before and @After](explore/hooks/before-and-after.md) 16 | * [Authorization example](explore/hooks/authorization-example.md) 17 | * [Injecting values to resolver](explore/inject/index.md) 18 | * [@Union](explore/union.md) 19 | 20 | ### Reference 21 | 22 | * [Api Reference](reference/index.md) 23 | * [@ObjectType](reference/index.md#objecttype) 24 | * [@Field](reference/index.md#field) 25 | * [@InputObjectType](reference/index.md#inputobjecttype) 26 | * [@InputField](reference/index.md#inputfield) 27 | * [@Arg](reference/index.md#arg) 28 | * [@Inject](reference/index.md#inject) 29 | * [@Context](reference/index.md#context) 30 | * [@Source](reference/index.md#source) 31 | * [@Info](reference/index.md#info) 32 | * [@Before](reference/index.md#before) 33 | * [@After](reference/index.md#after) 34 | * [@Schema](reference/index.md#schema) 35 | * [@Query](reference/index.md#query) 36 | * [@Mutation](reference/index.md#mutation) 37 | * [@Union](reference/index.md#union) 38 | * [registerEnum](reference/index.md#registerenum) 39 | * [compileSchema](reference/index.md#compileschema) 40 | * [compileObjectType](reference/index.md#compileobjecttype) 41 | * [compileInputObjectType](reference/index.md#compileinputobjecttype) 42 | -------------------------------------------------------------------------------- /docs/explore/arg.md: -------------------------------------------------------------------------------- 1 | # Field arguments 2 | 3 | Many of fields in graphql queries use arguments. eg 4 | 5 | ```graphql 6 | query { 7 | calculator { 8 | add(a: 2, b: 3) 9 | } 10 | } 11 | ``` 12 | 13 | Let's create such `Calculator` object type. 14 | 15 | ```ts 16 | import { ObjectType, Field } from 'typegql'; 17 | 18 | @ObjectType() 19 | class Calculator { 20 | @Field() 21 | add(a: number, b: number): number { 22 | return a + b; 23 | } 24 | } 25 | ``` 26 | 27 | Note we didn't even have to use `@Arg` decorator yet. In case of simple, scalar argument types like `string`, `number`, `boolean` it's not required. By default arguments not decorated with `@Arg` are **required**. 28 | 29 | ## Customizing argumens (eg. adding default value) 30 | 31 | Let's say we're building another calculator metod, called `pow` which returns `base` to the `exponent` power (base^exponent). By default, we want `exponent` to be `2`. 32 | 33 | ```ts 34 | import { ObjectType, Field, Arg } from 'typegql'; 35 | 36 | @ObjectType() 37 | class Calculator { 38 | @Field() 39 | pow( 40 | base: number, 41 | @Arg({ nullable: true, defaultValue: 2 }) 42 | exponent: number, 43 | ): number { 44 | return Math.pow(base, exponent); 45 | } 46 | } 47 | ``` 48 | 49 | Right now `exponent` argument is optional so we can use both: 50 | 51 | ```graphql 52 | framgnet A on Calculator { 53 | pow(2) # 4 54 | } 55 | 56 | framgnet B on Calculator { 57 | pow(2, 5) # 32 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/explore/enum/index.md: -------------------------------------------------------------------------------- 1 | # Enums 2 | 3 | For enums we can work with native Typescript enum keyword like 4 | 5 | ```ts 6 | enum TaskType { 7 | Done, 8 | InProgress, 9 | Finished, 10 | Cancelled, 11 | } 12 | ``` 13 | 14 | The only thing is required is registering such enum with it's name, so schema compiler is aware of it (as under the hood, enum is plain key-value object). 15 | 16 | ```ts 17 | import { registerEnum } from 'typegql'; 18 | 19 | enum TaskType { 20 | Done, 21 | InProgress, 22 | Finished, 23 | Cancelled, 24 | } 25 | 26 | registerEnum(TaskType, { name: 'TaskType' }); 27 | ``` 28 | 29 | Now, to use such enum in query like: 30 | 31 | ```graphql 32 | query { 33 | currentUser { 34 | hasAnyTaskOfType(type: Done) 35 | } 36 | } 37 | ``` 38 | 39 | `User` type would be defined as: 40 | 41 | ```ts 42 | import { ObjectType, Field, registerEnum } from 'typegql'; 43 | 44 | enum TaskType { 45 | Done, 46 | InProgress, 47 | Finished, 48 | Cancelled, 49 | } 50 | 51 | registerEnum(TaskType, { name: 'TaskType' }); 52 | 53 | @ObjectType() 54 | class User { 55 | @Field() id: number; 56 | 57 | @Field() 58 | hasAnyTaskOfType(type: TaskType): boolean { 59 | return tasksService.hasTasksOfType(type); 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/explore/enum/register-enum.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismake/typegql/313c4b196a2dde0d2dfe55969503a0a4f829b614/docs/explore/enum/register-enum.md -------------------------------------------------------------------------------- /docs/explore/hooks/authorization-example.md: -------------------------------------------------------------------------------- 1 | # Authorization example with @Before hook 2 | 3 | `@Before` hook is great for use-cases like authorization because: 4 | 5 | * it is executed before field resolver itself 6 | * if any of `@Before` hooks will return promise - it will be resolved before field itself is resolved 7 | * if any of `@Before` will throw error, resolving will be instantly stopped 8 | 9 | Very basic authorization model could look like: 10 | 11 | ```ts 12 | import { ObjectType, Field, Before } from 'typegql'; 13 | 14 | @ObjectType() 15 | class User { 16 | @Field() name: string; 17 | 18 | @Before(({ context }) => { 19 | if (!context.isUserAuthorized()) { 20 | throw new Error('Unauthorized to know this'); 21 | } 22 | }) 23 | @Field() 24 | isBanned: boolean; 25 | } 26 | ``` 27 | 28 | In such case, we're checking graphql context before we'll allow access to field itself. 29 | 30 | ## Creating reusable hooks 31 | 32 | Authorization like in example above is very likely to be required in many places in any graphql schema. 33 | 34 | We could simply create custom hook that can be easily re-used: 35 | 36 | ```ts 37 | import { ObjectType, Field, Before } from 'typegql'; 38 | 39 | function AdminOnly(errorMessage: string = 'Unauthorized') { 40 | return Before(({ context }) => { 41 | if (!context.isAdmin()) { 42 | throw new Error(errorMessage); 43 | } 44 | }); 45 | } 46 | 47 | @ObjectType() 48 | class User { 49 | @Field() name: string; 50 | 51 | @AdminOnly() 52 | @Field() 53 | isBanned: boolean; 54 | 55 | @AdminOnly('Only admin can see this information') 56 | @Field() 57 | isEmailConfirmed: boolean; 58 | } 59 | ``` 60 | 61 | ## Object aware authorization (eg. only I can see my email address) 62 | 63 | Quite often access level for every given object depends on who is seeing it. Let's say we want only admin or owner of account to be able to see email address of `User`: 64 | 65 | ```ts 66 | import { ObjectType, Field, Before } from 'typegql'; 67 | 68 | function AdminOrAccountOwner(errorMessage: string = 'Unauthorized') { 69 | return Before(async ({ context, source }) => { 70 | if (context.isCurrentUserAdmin()) { 71 | return; // allow for admin 72 | } 73 | 74 | // note that we can use async functions inside hooks 75 | if (source.id === (await context.getCurrentUserId())) { 76 | return; // allow if accessing user id is the same as accessed user id 77 | } 78 | 79 | throw new Error(errorMessage); 80 | }); 81 | } 82 | 83 | @ObjectType() 84 | class User { 85 | @Field() id: string; 86 | 87 | @AdminOrAccountOwner() 88 | @Field() 89 | email: string; 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/explore/hooks/before-and-after.md: -------------------------------------------------------------------------------- 1 | # `@Before` and `@After` hooks 2 | 3 | Let's say we want to send informations about useage of some field of our graphql api to analitycs server. We could achieve that with: 4 | 5 | ```ts 6 | import { ObjectType, Field, After } from 'typegql'; 7 | 8 | @ObjectType() 9 | class Person { 10 | @Field() id: number; 11 | 12 | @Field() 13 | @After(() => { 14 | loggingService.sendLog(`User was removed`); 15 | }) 16 | remove(): boolean { 17 | const isDeleted = userService.removeById(this.id); 18 | return isDeleted; 19 | } 20 | } 21 | ``` 22 | 23 | ## Using resolver informations inside hook function 24 | 25 | Arguments passed to resolver are exactly the same, as ones passed to native graphql resolver funtion. Signature of hook function is 26 | 27 | `(source, arguments, context, info) => void` 28 | 29 | In case, we'd like to add id of removed user to logs, we'd change our code to: 30 | 31 | ```ts 32 | @ObjectType() 33 | class Person { 34 | @Field() id: number; 35 | 36 | @Field() 37 | @After(user => { 38 | loggingService.sendLog(`User with id ${user.id} was removed`); 39 | // note we could as well use `this` keyword like: 40 | // loggingService.sendLog(`User with id ${this.id} was removed`); 41 | }) 42 | remove(): boolean { 43 | const isDeleted = userService.removeById(this.id); 44 | return isDeleted; 45 | } 46 | } 47 | ``` 48 | 49 | ## Notes 50 | 51 | * If one field has many hooks of the same type - they're executed in parallel. 52 | * Field resolver is not called until all @Before hooks are resolved 53 | -------------------------------------------------------------------------------- /docs/explore/hooks/index.md: -------------------------------------------------------------------------------- 1 | # Field resolution hooks 2 | 3 | In many cases, it might be desired to perform some aditional action before or after some field is resolved. Most common use-case could be authorization of user. 4 | 5 | Hooks are special kind of functions added to field with `@Before` or `@After` decorator. 6 | 7 | * [@Before and @After](before-and-after.md) 8 | * [Authorization example](authorization-example.md) 9 | -------------------------------------------------------------------------------- /docs/explore/inject/context-source-info.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismake/typegql/313c4b196a2dde0d2dfe55969503a0a4f829b614/docs/explore/inject/context-source-info.md -------------------------------------------------------------------------------- /docs/explore/inject/index.md: -------------------------------------------------------------------------------- 1 | # Injecting value to resolver 2 | 3 | Quite often we need to have access to graphql context inside resolver. Common example could be field like `viewer` 4 | 5 | ```graphql 6 | { 7 | viewer { 8 | me { 9 | username 10 | } 11 | } 12 | } 13 | ``` 14 | 15 | ## Built-in injectors `@Context`, `@Source`, `@Info` 16 | 17 | There are 3 argument decorators in `typegql` that allow passing values to resolvers - `@Context`, `@Source`, `@Info` 18 | 19 | Implementation of `Viewer` type would use `@Context` decorator to get user id from graphql context and could look like: 20 | 21 | ```ts 22 | import { ObjectType, Field, Context } from 'typegql'; 23 | 24 | @ObjectType() 25 | class Viewer { 26 | @Field({ type: () => Person }) 27 | me(@Context context) { 28 | return db.findUserById(context.currentUser.id); 29 | } 30 | @Field() id: number; 31 | } 32 | ``` 33 | 34 | ## Custom injector with `@Inject` 35 | 36 | Beside built-in injectors you can use `@Inject` decorator to pass any value to given argument. 37 | 38 | `@Inject` decorator requires getter function that converts resolve info to any arbitral value. **Note you can return promise of any value, too**. 39 | 40 | ```ts 41 | import { ObjectType, Field, Inject } from 'typegql'; 42 | 43 | @ObjectType() 44 | class Viewer { 45 | @Field({ type: () => Person }) 46 | me(@Inject({source, args, context, info} => 42) someNumber: number): number { 47 | return someNumber; // it will be 42; 48 | } 49 | @Field() id: number; 50 | } 51 | ``` 52 | 53 | ## Notes 54 | 55 | * Decorated arguments are not present in `args` list of field schema definition. 56 | -------------------------------------------------------------------------------- /docs/explore/input-type-and-input-field.md: -------------------------------------------------------------------------------- 1 | # Complex input types with `@InputObjectType` and `@InputField` 2 | 3 | Sometimes we need arguments to have complex structure (eg. user have some todos and want to delete few of them using filter) like 4 | 5 | ```graphql 6 | query { 7 | currentUser { 8 | deleteManyTodos(where: { isDone: true, nameContains: "call my friend" }) { 9 | deletedCount 10 | } 11 | } 12 | } 13 | ``` 14 | 15 | Let's create such `User` type that allows complex `where` argument for `deleteManyTodos` field. 16 | 17 | ```ts 18 | import { ObjectType, Field, Arg, InputObjectType } from 'typegql'; 19 | 20 | @InputObjectType() 21 | class WhereFilter { 22 | @InputField({ defaultValue: true }) 23 | isDone: boolean; 24 | @InputField() nameContains: string; 25 | } 26 | 27 | @ObjectType() 28 | class DeleteTodosResult { 29 | constructor(public deletedCount: number) {} 30 | @Field() deletedCount: number; 31 | } 32 | 33 | @ObjectType() 34 | class User { 35 | @Field() 36 | deleteManyTodos(where: WhereFilter): DeleteTodosResult { 37 | const deletedCount = todosService.remove(where); 38 | return new DeleteTodosResult(deletedCount); 39 | } 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/explore/object-type-and-field.md: -------------------------------------------------------------------------------- 1 | # Object types 2 | 3 | Object types are main building block of any graphql schema. In `typegql` - `ObjectType` is equivalent to `GraphQLObjectType`. 4 | 5 | To define new object type, decorate any class with `@ObjectType` decorator. 6 | 7 | ```ts 8 | import { ObjectType } from 'typegql'; 9 | 10 | @ObjectType() 11 | class MyType { 12 | // fields will be defined here 13 | } 14 | ``` 15 | 16 | ## Fields 17 | 18 | Every `ObjectType` must have at least one field. To create new field, decorate any property or method of `ObjectType` class with `@Field` decorator: 19 | 20 | ```ts 21 | import { ObjectType, Field } from 'typegql'; 22 | 23 | @ObjectType() 24 | class MyType { 25 | @Field() foo: string; 26 | 27 | @Field() 28 | fooMethod(): string { 29 | return 'fooMethodResult'; 30 | } 31 | } 32 | ``` 33 | 34 | ## Nested Sub-types 35 | 36 | To build nested structure of objects - every field can use another `ObjectType` as it's type 37 | 38 | ```ts 39 | import { ObjectType, Field } from 'typegql'; 40 | 41 | @ObjectType() 42 | class Location { 43 | constructor(lat: number, lng: number) { 44 | this.lat = lat; 45 | this.lng = lng; 46 | } 47 | 48 | @Field() lat: number; 49 | @Field() lng: number; 50 | } 51 | 52 | @ObjectType() 53 | class Restaurant { 54 | @Field() name: string; 55 | 56 | @Field() 57 | location(): Location { 58 | return new Location(50, 50); 59 | } 60 | } 61 | ``` 62 | 63 | ## Circular type references 64 | 65 | In other to define circular references. Eg. car have owner, owner has cars which have owner. it's required to assign types in lazy way (with getter functions) 66 | 67 | To do so, we'll use `@Field` with `type` option. 68 | 69 | ```ts 70 | import { ObjectType, Field } from 'typegql'; 71 | 72 | @ObjectType() 73 | class Car { 74 | @Field({ type: () => Person }) 75 | owner() { 76 | return db.findPersonByCarId(this.id); 77 | } 78 | @Field() id: number; 79 | } 80 | 81 | @ObjectType() 82 | class Person { 83 | @Field() id: number; 84 | @Field() name: string; 85 | @Field({ type: () => Car }) 86 | car() { 87 | return db.findCarByOwnerId(this.id); 88 | } 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/explore/schema/compile-schema.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismake/typegql/313c4b196a2dde0d2dfe55969503a0a4f829b614/docs/explore/schema/compile-schema.md -------------------------------------------------------------------------------- /docs/explore/schema/index.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | ### What is `typegql`? 4 | 5 | typegql is set of decorators allowing creating GraphQL APIs quickly and in type-safe way. 6 | 7 | ## Creating very simple schema 8 | 9 | Schema is main building block of any `graphql` schema. It'll join all parts of our api together. 10 | 11 | In `typegql` to create schema, we need to pass class decorated with `@Schema` to `compileSchema` function 12 | 13 | ```ts 14 | import { Schema, compileSchema} from 'typegql 15 | 16 | @Schema() 17 | class SuperSchema { 18 | // fields implementation 19 | } 20 | 21 | const compiledSchema = compileSchema(SuperSchema); 22 | ``` 23 | 24 | `compiledSchema` from example above is standard, regular `graphql` schema. 25 | 26 | ## Adding Query and Mutation fields 27 | 28 | Any working schema requires at least one Query field. There are special decorators - `@Query` and `@Mutation` used to register root fields of schema. 29 | 30 | Very simple fully working schema like 31 | 32 | ```graphql 33 | { 34 | hello # will resolve to 'world' 35 | } 36 | ``` 37 | 38 | Could be implemented as: 39 | 40 | ```typescript 41 | import { Schema, Query, compileSchema} from 'typegql 42 | 43 | @Schema() 44 | class SuperSchema { 45 | @Query() 46 | hello(): string { 47 | return 'world' 48 | } 49 | } 50 | 51 | const compiledSchema = compileSchema(SuperSchema); 52 | ``` 53 | 54 | ## Adding parameters 55 | 56 | Let's add some customization to our schema: 57 | 58 | ```graphql 59 | { 60 | hello(name: "Bob") # will resolve to 'Hello, Bob!' 61 | } 62 | ``` 63 | 64 | With tiny change in our code: 65 | 66 | ```typescript 67 | import { Schema, Query, compileSchema} from 'typegql 68 | 69 | @Schema() 70 | class SuperSchema { 71 | @Query() 72 | hello(name: string): string { 73 | return `Hello, ${name}!`; 74 | } 75 | } 76 | 77 | const compiledSchema = compileSchema(SuperSchema); 78 | ``` 79 | 80 | ## Adding nested types 81 | 82 | For now, our query field returned scalar (string). Let's return something more complex. Schema will look like: 83 | 84 | ```graphql 85 | mutation { 86 | createProduct(name: "Chair", price: 99.99) { 87 | name 88 | price 89 | isExpensive 90 | } 91 | } 92 | ``` 93 | 94 | Such query will have a bit more code and here it is: 95 | 96 | ```typescript 97 | import { Schema, Query, ObjectType, Field, Mutation, compileSchema} from 'typegql; 98 | 99 | @ObjectType({ description: 'Simple product object type' }) 100 | class Product { 101 | @Field() 102 | name: string; 103 | 104 | @Field() 105 | price: number; 106 | 107 | @Field() 108 | isExpensive() { 109 | return this.price > 50; 110 | } 111 | } 112 | 113 | @Schema() 114 | class SuperSchema { 115 | @Mutation() 116 | createProduct(name: string, price: number): Product { 117 | const product = new Product(); 118 | product.name = name; 119 | product.price = price; 120 | return product; 121 | } 122 | } 123 | 124 | const compiledSchema = compileSchema(SuperSchema); 125 | ``` 126 | 127 | ## Forcing field type. 128 | 129 | Since now, `typegql` was able to guess type of every field from typescript type definitions. 130 | 131 | There are, however, some cases where we'd have to define them explicitly. 132 | 133 | * We want to strictly tell if field is nullable or not 134 | * Function we use returns type of `Promise` while field itself is typed as `SomeType` 135 | * List (Array) type is used. (For now, typescript `Reflect` api is not able to guess type of single array item. This might change in the future) 136 | 137 | Let's modify our `Product` so it has additional `categories` field that will return array of strings. For sake of readibility, I'll ommit all fields we've defined previously. 138 | 139 | ```typescript 140 | @ObjectType() 141 | class Product { 142 | @Field({ type: [String] }) // note we can use any native type like GraphQLString! 143 | categories(): string[] { 144 | return ['Tables', 'Furniture']; 145 | } 146 | } 147 | ``` 148 | 149 | We've added `{ type: [String] }` as `@Field` options. Type can be anything that is resolvable to `GraphQL` type 150 | 151 | * Native JS scalars: `String`, `Number`, `Boolean`. 152 | * Any type that is already compiled to `graphql` eg. `GraphQLFloat` or any type from external graphql library etc 153 | * Every class decorated with `@ObjectType` 154 | * One element array of any of above for list types eg. `[String]` or `[GraphQLFloat]` 155 | 156 | ## Writing Asynchroniously 157 | 158 | Every field function we write can be `async` and return `Promise`. Let's say, instead of hard-coding our categories, we want to fetch it from some external API: 159 | 160 | ```typescript 161 | @ObjectType() 162 | class Product { 163 | @Field({ type: [String] }) // note we can use any native type like GraphQLString! 164 | async categories(): Promise { 165 | const categories = await api.fetchCategories(); 166 | return categories.map(cat => cat.name); 167 | } 168 | } 169 | ``` 170 | -------------------------------------------------------------------------------- /docs/explore/schema/query-and-mutation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismake/typegql/313c4b196a2dde0d2dfe55969503a0a4f829b614/docs/explore/schema/query-and-mutation.md -------------------------------------------------------------------------------- /docs/explore/union.md: -------------------------------------------------------------------------------- 1 | # Union types 2 | 3 | Considering such query: 4 | 5 | ```graphql 6 | { 7 | search(keyword: "car") { 8 | ... on Product { 9 | name 10 | price 11 | } 12 | ... on Category { 13 | name 14 | itemsCount 15 | avgProductPrice 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | Search field return union of `Product` and `Category`. To define such union we need to use `@Union` decorator 22 | 23 | ```ts 24 | import { ObjectType, Field, Union } from 'typegql'; 25 | 26 | @ObjectType() 27 | class Product { 28 | @Field() name: string; 29 | @Field() price: number; 30 | } 31 | 32 | @ObjectType() 33 | class Category { 34 | @Field() name: string; 35 | @Field() itemsCount: number; 36 | @Field() avgProductPrice: number; 37 | } 38 | 39 | @Union({ types: [Product, Category] }) 40 | class SearchResult {} 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/getting-started/minimal-demo.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismake/typegql/313c4b196a2dde0d2dfe55969503a0a4f829b614/docs/getting-started/minimal-demo.md -------------------------------------------------------------------------------- /docs/getting-started/setup.md: -------------------------------------------------------------------------------- 1 | # Installation and setup 2 | 3 | Firstly, add `typegql` to your project 4 | 5 | `yarn add typegql` 6 | 7 | **Important!** To work with `typescript`, you'll need `reflect-medatada` so `typegql` can infer types from your code. 8 | 9 | Add `import "reflect-metadata";` somewhere in bootstrap (before any `typegql` decorator is used) of your app eg `app/index.ts`. 10 | 11 | ## Modify `tsconfig.json` 12 | 13 | `typegql` will try to infer types of your fields, when possible. To allow this, you'll have to add following to your `tsconfig.json` `compilerOptions` section: 14 | 15 | ``` 16 | "emitDecoratorMetadata": true, 17 | "experimentalDecorators": true, 18 | ``` 19 | 20 | ## Does `typegql` work without typescript? 21 | 22 | It absolutely does. Keep in mind, however - without typescript all types will have to be defined explicitly. 23 | -------------------------------------------------------------------------------- /docs/getting-started/what-is-typegql.md: -------------------------------------------------------------------------------- 1 | `typegql` is set of decorators allowing creating GraphQL APIs quickly and in type-safe way. 2 | 3 | ## Minimal Example 4 | 5 | Schema able to handle such query: 6 | 7 | ```graphql 8 | { 9 | hello { 10 | world(name: "Bob") # will resolve with `Hello world, Bob` 11 | } 12 | } 13 | ``` 14 | 15 | Can be created with: 16 | 17 | ```typescript 18 | import { Schema, Query, ObjectType, Field, compileSchema } from 'typegql'; 19 | 20 | @ObjectType() 21 | class Hello { 22 | @Field() 23 | world(name: string): string { 24 | return `Hello world, ${name}!`; 25 | } 26 | } 27 | 28 | @Schema() 29 | class MyFirstSchema { 30 | @Query() 31 | hello(): Hello { 32 | return new Hello(); 33 | } 34 | } 35 | 36 | const schema = compileSchema(MyFirstSchema); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/reference/index.md: -------------------------------------------------------------------------------- 1 | # Api Reference 2 | 3 | ### @ObjectType 4 | 5 | ```typescript 6 | interface ObjectTypeOptions { 7 | name?: string; // infered from class name 8 | description?: string; 9 | } 10 | 11 | @ObjectType(options?: ObjectTypeOptions) 12 | ``` 13 | 14 | ### @Field 15 | 16 | ```typescript 17 | interface FieldOptions { 18 | name?: string; 19 | description?: string; 20 | type?: any | () => any; 21 | isNullable?: boolean; 22 | } 23 | 24 | @Field(options?: FieldOptions) 25 | ``` 26 | 27 | ### @InputObjectType 28 | 29 | ```typescript 30 | interface InputObjectTypeOptions { 31 | name?: string; 32 | description?: string; 33 | } 34 | 35 | @InputObjectType(options?: InputObjectTypeOptions) 36 | ``` 37 | 38 | ### @InputField 39 | 40 | ```typescript 41 | interface InputFieldOptions { 42 | description?: string; 43 | defaultValue?: any; 44 | type?: any | () => any; 45 | name?: string; 46 | isNullable?: boolean; 47 | } 48 | 49 | @InputField(options?: InputFieldOptions) 50 | ``` 51 | 52 | ### @Arg 53 | 54 | ```typescript 55 | interface ArgOptions { 56 | description?: string; 57 | type?: any; 58 | isNullable?: boolean; 59 | } 60 | @Arg(options?: ArgOptions) 61 | ``` 62 | 63 | ### @Inject 64 | 65 | ```typescript 66 | type InjectorResolver = ( 67 | source: any, 68 | args: any, 69 | context: any, 70 | info: GraphQLResolveInfo, 71 | ) => any; 72 | 73 | @Inject(resolver: InjectorResolver): ParameterDecorator { 74 | ``` 75 | 76 | ### @Context 77 | 78 | No decorator options avaliable 79 | 80 | ```typescript 81 | @Context: ParameterDecorator 82 | ``` 83 | 84 | ### @Source 85 | 86 | No decorator options avaliable 87 | 88 | ```typescript 89 | @Source: ParameterDecorator 90 | ``` 91 | 92 | ### @Info 93 | 94 | No decorator options avaliable 95 | 96 | ```typescript 97 | @Info: ParameterDecorator 98 | ``` 99 | 100 | ### @Before 101 | 102 | ```typescript 103 | interface HookExecutorResolverArgs { 104 | source: any; 105 | args: { [argName: string]: any }; 106 | context: any; 107 | info: GraphQLResolveInfo; 108 | } 109 | 110 | type HookExecutor = (data: HookExecutorResolverArgs) => any | Promise; 111 | 112 | @Before(hook: HookExecutor); 113 | ``` 114 | 115 | ### @After 116 | 117 | ```typescript 118 | interface HookExecutorResolverArgs { 119 | source: any; 120 | args: { [argName: string]: any }; 121 | context: any; 122 | info: GraphQLResolveInfo; 123 | } 124 | 125 | type HookExecutor = (data: HookExecutorResolverArgs) => any | Promise; 126 | 127 | @After(hook: HookExecutor); 128 | ``` 129 | 130 | ### @Schema 131 | 132 | ```typescript 133 | @Schema(): ClassDecorator; 134 | ``` 135 | 136 | ### @Query 137 | 138 | Has same interface as [@Field](#field) decorator. Can be used only inside @Schema class 139 | 140 | ### @Mutation 141 | 142 | Has same interface as [@Field](#field) decorator. Can be used only inside @Schema class 143 | 144 | ### @Union 145 | 146 | ```typescript 147 | interface UnionOptions { 148 | name?: string; 149 | resolveTypes?: (value: any, context: any, info: GraphQLResolveInfo): any; // must return type resolvable to one of defined in `types` option 150 | types: any[] | () => any[]; 151 | } 152 | 153 | interface UnionTypeResolver { 154 | (value: any, context: any, info: GraphQLResolveInfo): any; 155 | } 156 | 157 | @Union(options: UnionOptions): ClassDecorator; 158 | ``` 159 | 160 | ### registerEnum 161 | 162 | ```typescript 163 | interface EnumOptions { 164 | name: string; 165 | description?: string; 166 | } 167 | 168 | registerEnum(enumDef: Object, options: EnumOptions | string): void; 169 | ``` 170 | 171 | ### compileSchema 172 | 173 | ```typescript 174 | compileSchema(schemaTarget: Function): GraphQLSchema 175 | ``` 176 | 177 | ### compileObjectType, 178 | 179 | ```typescript 180 | compileObjectType(schemaTarget: Function): GraphQLObjectType 181 | ``` 182 | 183 | ### compileInputObjectType, 184 | 185 | ```typescript 186 | compileInputObjectType(schemaTarget: Function): GraphQLInputObjectType 187 | ``` 188 | -------------------------------------------------------------------------------- /examples/basic-express-server/README.md: -------------------------------------------------------------------------------- 1 | ## Basic express example 2 | 3 | Example of basic graphql api able to resolve such query 4 | 5 | ```graphql 6 | query { 7 | hello(name: "Bob") # will resolve to 'Hello, Bob!' 8 | } 9 | ``` 10 | 11 | Here is all the server code required: 12 | 13 | ```typescript 14 | import * as express from 'express'; 15 | import { Schema, Query, compileSchema } from 'typegql'; 16 | import * as graphqlHTTP from 'express-graphql'; 17 | 18 | @Schema() 19 | class SuperSchema { 20 | @Query() 21 | hello(name: string): string { 22 | return `Hello, ${name}!`; 23 | } 24 | } 25 | 26 | const compiledSchema = compileSchema(SuperSchema); 27 | 28 | const app = express(); 29 | 30 | app.use( 31 | '/graphql', 32 | graphqlHTTP({ 33 | schema: compiledSchema, 34 | graphiql: true, 35 | }), 36 | ); 37 | app.listen(3000); 38 | ``` 39 | 40 | To start this example, in this folder run `yarn install` and `yarn start`. Server will be running under `http://localhost:3000/graphql` 41 | -------------------------------------------------------------------------------- /examples/basic-express-server/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Schema, Query, compileSchema } from 'typegql'; 3 | import * as graphqlHTTP from 'express-graphql'; 4 | 5 | import { schema } from './schema'; 6 | 7 | const app = express(); 8 | 9 | app.use( 10 | '/graphql', 11 | graphqlHTTP({ 12 | schema, 13 | graphiql: true, 14 | }), 15 | ); 16 | app.listen(3000, () => { 17 | console.log('Api ready on port 3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/basic-express-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-express-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.16.3", 8 | "express-graphql": "^0.6.12", 9 | "ts-node": "^6.0.0", 10 | "typegql": "^0.4.0", 11 | "typescript": "^2.8.1" 12 | }, 13 | "scripts": { 14 | "start": "ts-node index.ts" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/basic-express-server/schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Query, compileSchema } from 'typegql'; 2 | 3 | @Schema() 4 | class MySchema { 5 | @Query() 6 | hello(name: string): string { 7 | return `hello, ${name}!`; 8 | } 9 | } 10 | 11 | export const schema = compileSchema(MySchema); 12 | -------------------------------------------------------------------------------- /examples/basic-express-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"], 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic-express-server/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@^1.3.0, accepts@~1.3.5: 6 | version "1.3.5" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 8 | dependencies: 9 | mime-types "~2.1.18" 10 | negotiator "0.6.1" 11 | 12 | ansi-styles@^3.2.1: 13 | version "3.2.1" 14 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 15 | dependencies: 16 | color-convert "^1.9.0" 17 | 18 | array-flatten@1.1.1: 19 | version "1.1.1" 20 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 21 | 22 | arrify@^1.0.0: 23 | version "1.0.1" 24 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 25 | 26 | body-parser@1.18.2: 27 | version "1.18.2" 28 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 29 | dependencies: 30 | bytes "3.0.0" 31 | content-type "~1.0.4" 32 | debug "2.6.9" 33 | depd "~1.1.1" 34 | http-errors "~1.6.2" 35 | iconv-lite "0.4.19" 36 | on-finished "~2.3.0" 37 | qs "6.5.1" 38 | raw-body "2.3.2" 39 | type-is "~1.6.15" 40 | 41 | bytes@3.0.0: 42 | version "3.0.0" 43 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 44 | 45 | chalk@^2.3.0: 46 | version "2.4.0" 47 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52" 48 | dependencies: 49 | ansi-styles "^3.2.1" 50 | escape-string-regexp "^1.0.5" 51 | supports-color "^5.3.0" 52 | 53 | color-convert@^1.9.0: 54 | version "1.9.1" 55 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" 56 | dependencies: 57 | color-name "^1.1.1" 58 | 59 | color-name@^1.1.1: 60 | version "1.1.3" 61 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 62 | 63 | content-disposition@0.5.2: 64 | version "0.5.2" 65 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 66 | 67 | content-type@^1.0.4, content-type@~1.0.4: 68 | version "1.0.4" 69 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 70 | 71 | cookie-signature@1.0.6: 72 | version "1.0.6" 73 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 74 | 75 | cookie@0.3.1: 76 | version "0.3.1" 77 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 78 | 79 | debug@2.6.9: 80 | version "2.6.9" 81 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 82 | dependencies: 83 | ms "2.0.0" 84 | 85 | depd@1.1.1: 86 | version "1.1.1" 87 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 88 | 89 | depd@~1.1.1, depd@~1.1.2: 90 | version "1.1.2" 91 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 92 | 93 | destroy@~1.0.4: 94 | version "1.0.4" 95 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 96 | 97 | diff@^3.1.0: 98 | version "3.5.0" 99 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" 100 | 101 | ee-first@1.1.1: 102 | version "1.1.1" 103 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 104 | 105 | encodeurl@~1.0.2: 106 | version "1.0.2" 107 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 108 | 109 | escape-html@~1.0.3: 110 | version "1.0.3" 111 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 112 | 113 | escape-string-regexp@^1.0.5: 114 | version "1.0.5" 115 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 116 | 117 | etag@~1.8.1: 118 | version "1.8.1" 119 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 120 | 121 | express-graphql@^0.6.12: 122 | version "0.6.12" 123 | resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.6.12.tgz#dfcb2058ca72ed5190b140830ad8cdbf76a9128a" 124 | dependencies: 125 | accepts "^1.3.0" 126 | content-type "^1.0.4" 127 | http-errors "^1.3.0" 128 | raw-body "^2.3.2" 129 | 130 | express@^4.16.3: 131 | version "4.16.3" 132 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" 133 | dependencies: 134 | accepts "~1.3.5" 135 | array-flatten "1.1.1" 136 | body-parser "1.18.2" 137 | content-disposition "0.5.2" 138 | content-type "~1.0.4" 139 | cookie "0.3.1" 140 | cookie-signature "1.0.6" 141 | debug "2.6.9" 142 | depd "~1.1.2" 143 | encodeurl "~1.0.2" 144 | escape-html "~1.0.3" 145 | etag "~1.8.1" 146 | finalhandler "1.1.1" 147 | fresh "0.5.2" 148 | merge-descriptors "1.0.1" 149 | methods "~1.1.2" 150 | on-finished "~2.3.0" 151 | parseurl "~1.3.2" 152 | path-to-regexp "0.1.7" 153 | proxy-addr "~2.0.3" 154 | qs "6.5.1" 155 | range-parser "~1.2.0" 156 | safe-buffer "5.1.1" 157 | send "0.16.2" 158 | serve-static "1.13.2" 159 | setprototypeof "1.1.0" 160 | statuses "~1.4.0" 161 | type-is "~1.6.16" 162 | utils-merge "1.0.1" 163 | vary "~1.1.2" 164 | 165 | finalhandler@1.1.1: 166 | version "1.1.1" 167 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" 168 | dependencies: 169 | debug "2.6.9" 170 | encodeurl "~1.0.2" 171 | escape-html "~1.0.3" 172 | on-finished "~2.3.0" 173 | parseurl "~1.3.2" 174 | statuses "~1.4.0" 175 | unpipe "~1.0.0" 176 | 177 | forwarded@~0.1.2: 178 | version "0.1.2" 179 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 180 | 181 | fresh@0.5.2: 182 | version "0.5.2" 183 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 184 | 185 | graphql@^0.13.1: 186 | version "0.13.2" 187 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" 188 | dependencies: 189 | iterall "^1.2.1" 190 | 191 | has-flag@^3.0.0: 192 | version "3.0.0" 193 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 194 | 195 | http-errors@1.6.2: 196 | version "1.6.2" 197 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 198 | dependencies: 199 | depd "1.1.1" 200 | inherits "2.0.3" 201 | setprototypeof "1.0.3" 202 | statuses ">= 1.3.1 < 2" 203 | 204 | http-errors@^1.3.0, http-errors@~1.6.2: 205 | version "1.6.3" 206 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 207 | dependencies: 208 | depd "~1.1.2" 209 | inherits "2.0.3" 210 | setprototypeof "1.1.0" 211 | statuses ">= 1.4.0 < 2" 212 | 213 | iconv-lite@0.4.19: 214 | version "0.4.19" 215 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 216 | 217 | inherits@2.0.3: 218 | version "2.0.3" 219 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 220 | 221 | ipaddr.js@1.6.0: 222 | version "1.6.0" 223 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b" 224 | 225 | iterall@^1.2.1: 226 | version "1.2.2" 227 | resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" 228 | 229 | make-error@^1.1.1: 230 | version "1.3.4" 231 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.4.tgz#19978ed575f9e9545d2ff8c13e33b5d18a67d535" 232 | 233 | media-typer@0.3.0: 234 | version "0.3.0" 235 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 236 | 237 | merge-descriptors@1.0.1: 238 | version "1.0.1" 239 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 240 | 241 | methods@~1.1.2: 242 | version "1.1.2" 243 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 244 | 245 | mime-db@~1.33.0: 246 | version "1.33.0" 247 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 248 | 249 | mime-types@~2.1.18: 250 | version "2.1.18" 251 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 252 | dependencies: 253 | mime-db "~1.33.0" 254 | 255 | mime@1.4.1: 256 | version "1.4.1" 257 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 258 | 259 | minimist@0.0.8: 260 | version "0.0.8" 261 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 262 | 263 | minimist@^1.2.0: 264 | version "1.2.0" 265 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 266 | 267 | mkdirp@^0.5.1: 268 | version "0.5.1" 269 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 270 | dependencies: 271 | minimist "0.0.8" 272 | 273 | ms@2.0.0: 274 | version "2.0.0" 275 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 276 | 277 | negotiator@0.6.1: 278 | version "0.6.1" 279 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 280 | 281 | object-path@^0.11.4: 282 | version "0.11.4" 283 | resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" 284 | 285 | on-finished@~2.3.0: 286 | version "2.3.0" 287 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 288 | dependencies: 289 | ee-first "1.1.1" 290 | 291 | parseurl@~1.3.2: 292 | version "1.3.2" 293 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 294 | 295 | path-to-regexp@0.1.7: 296 | version "0.1.7" 297 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 298 | 299 | proxy-addr@~2.0.3: 300 | version "2.0.3" 301 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" 302 | dependencies: 303 | forwarded "~0.1.2" 304 | ipaddr.js "1.6.0" 305 | 306 | qs@6.5.1: 307 | version "6.5.1" 308 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 309 | 310 | range-parser@~1.2.0: 311 | version "1.2.0" 312 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 313 | 314 | raw-body@2.3.2, raw-body@^2.3.2: 315 | version "2.3.2" 316 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 317 | dependencies: 318 | bytes "3.0.0" 319 | http-errors "1.6.2" 320 | iconv-lite "0.4.19" 321 | unpipe "1.0.0" 322 | 323 | reflect-metadata@^0.1.12: 324 | version "0.1.12" 325 | resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2" 326 | 327 | safe-buffer@5.1.1: 328 | version "5.1.1" 329 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 330 | 331 | send@0.16.2: 332 | version "0.16.2" 333 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" 334 | dependencies: 335 | debug "2.6.9" 336 | depd "~1.1.2" 337 | destroy "~1.0.4" 338 | encodeurl "~1.0.2" 339 | escape-html "~1.0.3" 340 | etag "~1.8.1" 341 | fresh "0.5.2" 342 | http-errors "~1.6.2" 343 | mime "1.4.1" 344 | ms "2.0.0" 345 | on-finished "~2.3.0" 346 | range-parser "~1.2.0" 347 | statuses "~1.4.0" 348 | 349 | serve-static@1.13.2: 350 | version "1.13.2" 351 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" 352 | dependencies: 353 | encodeurl "~1.0.2" 354 | escape-html "~1.0.3" 355 | parseurl "~1.3.2" 356 | send "0.16.2" 357 | 358 | setprototypeof@1.0.3: 359 | version "1.0.3" 360 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 361 | 362 | setprototypeof@1.1.0: 363 | version "1.1.0" 364 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 365 | 366 | source-map-support@^0.5.3: 367 | version "0.5.4" 368 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.4.tgz#54456efa89caa9270af7cd624cc2f123e51fbae8" 369 | dependencies: 370 | source-map "^0.6.0" 371 | 372 | source-map@^0.6.0: 373 | version "0.6.1" 374 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 375 | 376 | "statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": 377 | version "1.5.0" 378 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 379 | 380 | statuses@~1.4.0: 381 | version "1.4.0" 382 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" 383 | 384 | supports-color@^5.3.0: 385 | version "5.4.0" 386 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" 387 | dependencies: 388 | has-flag "^3.0.0" 389 | 390 | ts-node@^6.0.0: 391 | version "6.0.0" 392 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-6.0.0.tgz#46c25f8498593a9248eeea16906f1598fa098140" 393 | dependencies: 394 | arrify "^1.0.0" 395 | chalk "^2.3.0" 396 | diff "^3.1.0" 397 | make-error "^1.1.1" 398 | minimist "^1.2.0" 399 | mkdirp "^0.5.1" 400 | source-map-support "^0.5.3" 401 | yn "^2.0.0" 402 | 403 | type-is@~1.6.15, type-is@~1.6.16: 404 | version "1.6.16" 405 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 406 | dependencies: 407 | media-typer "0.3.0" 408 | mime-types "~2.1.18" 409 | 410 | typegql@^0.1.1: 411 | version "0.1.6" 412 | resolved "https://registry.yarnpkg.com/typegql/-/typegql-0.1.6.tgz#9bfdc1c1fdbe49e994588316f7712d964faf76c4" 413 | dependencies: 414 | graphql "^0.13.1" 415 | object-path "^0.11.4" 416 | reflect-metadata "^0.1.12" 417 | typegql "^0.1.1" 418 | 419 | typegql@^0.4.0: 420 | version "0.4.0" 421 | resolved "https://registry.yarnpkg.com/typegql/-/typegql-0.4.0.tgz#1a5f203b63743119e507a0dfa2bd3a3c5f6d0341" 422 | dependencies: 423 | graphql "^0.13.1" 424 | object-path "^0.11.4" 425 | reflect-metadata "^0.1.12" 426 | typegql "^0.1.1" 427 | 428 | typescript@^2.8.1: 429 | version "2.8.1" 430 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624" 431 | 432 | unpipe@1.0.0, unpipe@~1.0.0: 433 | version "1.0.0" 434 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 435 | 436 | utils-merge@1.0.1: 437 | version "1.0.1" 438 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 439 | 440 | vary@~1.1.2: 441 | version "1.1.2" 442 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 443 | 444 | yn@^2.0.0: 445 | version "2.0.0" 446 | resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" 447 | -------------------------------------------------------------------------------- /examples/custom-decorators/README.md: -------------------------------------------------------------------------------- 1 | ## Custom Decorators 2 | 3 | Sometimes you might want to re-use some field or type decorator settings. Creating custom decorator might be suitable solution then. 4 | 5 | Doing it is quite simple. You'd need to define custom function that returns call to original decorator. 6 | 7 | Let's say we want to define custom field decorator that always has `String` type and requires field description (that originally would be optional) 8 | 9 | ```typescript 10 | import { Schema, Query, Field, ObjectType, compileSchema } from 'typegql'; 11 | 12 | function StringWithDescription(fieldDescription: string) { 13 | if (!fieldDescription) { 14 | throw new Error( 15 | `Field description is required when decorated with @StringWithDescription`, 16 | ); 17 | } 18 | return Field({ 19 | type: String, 20 | description: fieldDescription, 21 | }); 22 | } 23 | ``` 24 | 25 | Later on, to use such `@StringWithDescription` field decorator you could simply: 26 | 27 | ```typescript 28 | import { ObjectType } from 'typegql'; 29 | 30 | @ObjectType() 31 | class CustomObject { 32 | @StringWithDescription('This is custom field') stringValue: string; 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /examples/custom-decorators/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Schema, Query, compileSchema } from 'typegql'; 3 | import * as graphqlHTTP from 'express-graphql'; 4 | 5 | import { schema } from './schema'; 6 | 7 | const app = express(); 8 | 9 | app.use( 10 | '/graphql', 11 | graphqlHTTP({ 12 | schema, 13 | graphiql: true, 14 | }), 15 | ); 16 | app.listen(3000, () => { 17 | console.log('Api ready on port 3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/custom-decorators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-express-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.16.3", 8 | "express-graphql": "^0.6.12", 9 | "ts-node": "^6.0.0", 10 | "typegql": "^0.4.0", 11 | "typescript": "^2.8.1" 12 | }, 13 | "scripts": { 14 | "start": "ts-node index.ts" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/custom-decorators/schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Query, Field, ObjectType, compileSchema } from 'typegql'; 2 | 3 | function StringWithDescription(additionalDescription: string) { 4 | return Field({ 5 | type: String, 6 | description: `This is custom field. Additional description: ${additionalDescription}`, 7 | }); 8 | } 9 | 10 | function ExplicitNameObjectType(name: string) { 11 | if (!name) { 12 | throw new Error( 13 | `Classes decorated with @ExplicitNameObjectType require explicit name instead of one guessed from class name.`, 14 | ); 15 | } 16 | return ObjectType({ name }); 17 | } 18 | 19 | @ExplicitNameObjectType('MyTypeName') 20 | class CustomObject { 21 | @StringWithDescription('This is custom field') stringValue: string; 22 | } 23 | 24 | @Schema() 25 | class MySchema { 26 | @Query() 27 | getCustomObject(stringValue: string): CustomObject { 28 | const object = new CustomObject(); 29 | object.stringValue = stringValue; 30 | return object; 31 | } 32 | } 33 | 34 | export const schema = compileSchema(MySchema); 35 | -------------------------------------------------------------------------------- /examples/custom-decorators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"], 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/forward-resolution/README.md: -------------------------------------------------------------------------------- 1 | ## Forward resolution 2 | 3 | Sometimes you want to know what fields are queried before you'll query your db etc. It often reduces number of db queries etc. 4 | 5 | To do that, we can use custom `@Inject` argument decorator that will resolve to list of needed subfields. Such information can be passed to any orm etc. 6 | 7 | I've based this decorator on great lib: https://github.com/jakepusateri/graphql-list-fields that is able to get `string[]` of needed fields out of `info` graphql variable. 8 | -------------------------------------------------------------------------------- /examples/forward-resolution/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { GraphQLResolveInfo } from 'graphql'; 3 | import * as getFieldNames from 'graphql-list-fields'; 4 | import { 5 | Schema, 6 | Query, 7 | ObjectType, 8 | Field, 9 | Inject, 10 | compileSchema, 11 | } from 'typegql'; 12 | import * as graphqlHTTP from 'express-graphql'; 13 | 14 | function NeededFields(filter: string[] = []) { 15 | return Inject(({ info }) => { 16 | const selectionFieldNames: string[] = getFieldNames(info); // let's get all needed fields 17 | 18 | // if there is no filter, return all selected field names 19 | if (!filter || !filter.length) { 20 | return selectionFieldNames; 21 | } 22 | 23 | // let's return only those allowed 24 | return selectionFieldNames.filter(fieldName => { 25 | return filter.includes(fieldName); 26 | }); 27 | }); 28 | } 29 | 30 | // let's create some object that is aware of fields that it needs to know about 31 | @ObjectType() 32 | class LazyObject { 33 | constructor(neededFields: string[]) { 34 | console.log( 35 | 'I will only perform expensive operations for fields:', 36 | neededFields, 37 | ); 38 | 39 | if (neededFields.includes['foo']) { 40 | this.foo = 'I have foo'; 41 | } 42 | 43 | if (neededFields.includes['bar']) { 44 | this.bar = 'I have bar'; 45 | } 46 | } 47 | @Field() foo: string; 48 | @Field() bar: string; 49 | } 50 | 51 | @Schema() 52 | class SuperSchema { 53 | @Query() 54 | foo( 55 | @NeededFields(['foo', 'bar']) 56 | neededFields: string[], // this arg will tell if `foo` and `bar` are needed 57 | ): LazyObject { 58 | console.log('Needed fields are: ', { neededFields }); 59 | return new LazyObject(neededFields); 60 | } 61 | } 62 | 63 | const compiledSchema = compileSchema(SuperSchema); 64 | 65 | const app = express(); 66 | 67 | app.use( 68 | '/graphql', 69 | graphqlHTTP({ 70 | schema: compiledSchema, 71 | graphiql: true, 72 | }), 73 | ); 74 | app.listen(3000); 75 | -------------------------------------------------------------------------------- /examples/forward-resolution/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forward-resolution-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.16.3", 8 | "express-graphql": "^0.6.12", 9 | "graphql-list-fields": "^2.0.2", 10 | "ts-node": "^6.0.0", 11 | "typegql": "^0.4.0", 12 | "typescript": "^2.8.1" 13 | }, 14 | "scripts": { 15 | "start": "ts-node index.ts" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/forward-resolution/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"], 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/merge-schemas/README.md: -------------------------------------------------------------------------------- 1 | ## Merge schemas 2 | 3 | It's simple example showing how to merge multiple schemas generated with `compileSchema` using `graphql-tools`. 4 | -------------------------------------------------------------------------------- /examples/merge-schemas/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Schema, Query, compileSchema } from 'typegql'; 3 | import * as graphqlHTTP from 'express-graphql'; 4 | import { mergeSchemas } from 'graphql-tools'; 5 | 6 | import { schemaA } from './schemaA'; 7 | import { schemaB } from './schemaB'; 8 | import { graphql } from 'graphql'; 9 | 10 | const mergedSchema = mergeSchemas({ schemas: [schemaA, schemaB] }); 11 | 12 | const app = express(); 13 | 14 | app.use( 15 | '/graphql', 16 | graphqlHTTP({ 17 | schema: mergedSchema, 18 | graphiql: true, 19 | }), 20 | ); 21 | app.listen(3000, () => { 22 | console.log('Api ready on port 3000'); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/merge-schemas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-express-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.16.3", 8 | "express-graphql": "^0.6.12", 9 | "graphql-tools": "^2.24.0", 10 | "ts-node": "^6.0.0", 11 | "typegql": "^0.4.0", 12 | "typescript": "^2.8.1" 13 | }, 14 | "scripts": { 15 | "start": "ts-node index.ts" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/merge-schemas/schemaA.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Query, compileSchema } from 'typegql'; 2 | 3 | @Schema() 4 | class SchemaA { 5 | @Query() 6 | foo(name: string): string { 7 | return `bar, ${name}!`; 8 | } 9 | } 10 | 11 | export const schemaA = compileSchema(SchemaA); 12 | -------------------------------------------------------------------------------- /examples/merge-schemas/schemaB.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Query, compileSchema } from 'typegql'; 2 | 3 | @Schema() 4 | class SchemaB { 5 | @Query() 6 | hello(name: string): string { 7 | return `hello, ${name}!`; 8 | } 9 | } 10 | 11 | export const schemaB = compileSchema(SchemaB); 12 | -------------------------------------------------------------------------------- /examples/merge-schemas/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"], 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/nested-mutation-or-query/README.md: -------------------------------------------------------------------------------- 1 | ## Nested mutations 2 | 3 | Sometimes, nested mutations and queries are very handy and can help to keep code more encapsulated 4 | 5 | This example will allow mutation like 6 | 7 | ```graphql 8 | mutation { 9 | book(bookId: 2) { 10 | edit(name: "Lord of the Rings") { 11 | id 12 | name 13 | } 14 | } 15 | } 16 | ``` 17 | 18 | instead of 19 | 20 | ```graphql 21 | mutation { 22 | editBook(bookId: 2, name: "Lord of the Rings") { 23 | id 24 | name 25 | } 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/nested-mutation-or-query/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Schema, Query, compileSchema } from 'typegql'; 3 | import * as graphqlHTTP from 'express-graphql'; 4 | 5 | import { schema } from './schema'; 6 | 7 | const app = express(); 8 | 9 | app.use( 10 | '/graphql', 11 | graphqlHTTP({ 12 | schema, 13 | graphiql: true, 14 | }), 15 | ); 16 | app.listen(3000, () => { 17 | console.log('Api ready on port 3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/nested-mutation-or-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-express-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.16.3", 8 | "express-graphql": "^0.6.12", 9 | "ts-node": "^6.0.0", 10 | "typegql": "^0.4.0", 11 | "typescript": "^2.8.1" 12 | }, 13 | "scripts": { 14 | "start": "ts-node index.ts" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/nested-mutation-or-query/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Schema, 3 | Query, 4 | Mutation, 5 | Field, 6 | ObjectType, 7 | compileSchema, 8 | } from 'typegql'; 9 | 10 | @ObjectType() 11 | class Book { 12 | @Field() id: number; 13 | @Field() name: string; 14 | } 15 | 16 | const booksDb: Book[] = [ 17 | { id: 1, name: 'Lord of the Rings' }, 18 | { id: 2, name: 'Harry Potter' }, 19 | ]; 20 | 21 | @ObjectType() 22 | class BookMutation { 23 | private readonly bookId: number; 24 | 25 | constructor(bookId: number) { 26 | this.bookId = bookId; 27 | } 28 | 29 | @Field() 30 | edit(name: string): Book { 31 | const book = new Book(); 32 | book.id = this.bookId; 33 | book.name = name; 34 | return book; 35 | } 36 | 37 | @Field() 38 | remove(name: string): string { 39 | return `Book with id ${this.bookId} removed.`; 40 | } 41 | } 42 | 43 | @Schema() 44 | class MySchema { 45 | @Mutation() 46 | book(bookId: number): BookMutation { 47 | return new BookMutation(bookId); 48 | } 49 | 50 | @Query() 51 | hello(): string { 52 | return 'World!'; 53 | } 54 | } 55 | 56 | export const schema = compileSchema(MySchema); 57 | -------------------------------------------------------------------------------- /examples/nested-mutation-or-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"], 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/serverless/.gitignore: -------------------------------------------------------------------------------- 1 | .build -------------------------------------------------------------------------------- /examples/serverless/README.md: -------------------------------------------------------------------------------- 1 | ## Serverless example 2 | 3 | This example presents how to deploy graphql schema based on typegql to serverless infrastructure like AWS lambda. 4 | 5 | Schema is very simple: 6 | 7 | ```graphql 8 | query { 9 | hello(name: "Bob") # will resolve to 'Hello, Bob!' 10 | } 11 | ``` 12 | 13 | As schema compiled by `compileSchema` is regular, normal GraphQLSchema, we can simply use it with `apollo-server-lambda` that will handle everything. 14 | 15 | ```typescript 16 | import { Schema, Query, compileSchema } from 'typegql'; 17 | import { graphqlLambda, graphiqlLambda } from 'apollo-server-lambda'; 18 | 19 | @Schema() 20 | class MySchema { 21 | @Query() 22 | hello(name: string): string { 23 | return `hello, ${name}!`; 24 | } 25 | } 26 | 27 | const schema = compileSchema(MySchema); 28 | 29 | export function graphqlHandler(event, context, callback) { 30 | function callbackFilter(error, output) { 31 | output.headers['Access-Control-Allow-Origin'] = '*'; 32 | callback(error, output); 33 | } 34 | const handler = graphqlLambda({ schema, tracing: true }); 35 | return handler(event, context, callbackFilter); 36 | } 37 | 38 | export const graphiqlHandler = graphiqlLambda({ endpointURL: '/graphql' }); 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/serverless/handler.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Query, compileSchema } from 'typegql'; 2 | import { graphqlLambda, graphiqlLambda } from 'apollo-server-lambda'; 3 | 4 | @Schema() 5 | class MySchema { 6 | @Query() 7 | hello(name: string): string { 8 | return `hello, ${name}!`; 9 | } 10 | } 11 | 12 | const schema = compileSchema(MySchema); 13 | 14 | export function graphqlHandler(event, context, callback) { 15 | function callbackFilter(error, output) { 16 | output.headers['Access-Control-Allow-Origin'] = '*'; 17 | callback(error, output); 18 | } 19 | const handler = graphqlLambda({ schema, tracing: true }); 20 | return handler(event, context, callbackFilter); 21 | } 22 | 23 | export const graphiqlHandler = graphiqlLambda({ endpointURL: '/graphql' }); 24 | -------------------------------------------------------------------------------- /examples/serverless/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "apollo-server-lambda": "^1.3.4", 8 | "graphql": "0.13.2", 9 | "serverless": "^1.26.1", 10 | "serverless-plugin-typescript": "^1.1.5", 11 | "typegql": "^0.4.0" 12 | }, 13 | "devDependencies": { 14 | "serverless-offline": "^3.20.2" 15 | }, 16 | "resolutions": { 17 | "graphql": "0.11.3" 18 | }, 19 | "scripts": { 20 | "dev": "sls offline" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/serverless/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-graphql-rest 2 | 3 | frameworkVersion: ">=1.21.0 <2.0.0" 4 | 5 | provider: 6 | name: aws 7 | runtime: nodejs6.10 8 | stage: dev 9 | region: us-east-1 10 | 11 | plugins: 12 | - serverless-offline 13 | - serverless-plugin-typescript 14 | 15 | 16 | functions: 17 | graphql: 18 | handler: handler.graphqlHandler 19 | events: 20 | - http: 21 | path: graphql 22 | method: post 23 | cors: true 24 | graphiql: 25 | handler: handler.graphiqlHandler 26 | events: 27 | - http: 28 | path: graphiql 29 | method: get 30 | cors: true 31 | -------------------------------------------------------------------------------- /examples/serverless/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"], 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/typeorm-basic-integration/README.md: -------------------------------------------------------------------------------- 1 | ## Basic typeorm database integration example 2 | 3 | Example is able to perform such graphql queries: 4 | 5 | ### Create user and save it to database: 6 | 7 | ```graphql 8 | mutation { 9 | createUser(name: "Bob", age: 30) { 10 | id 11 | name 12 | age 13 | } 14 | } 15 | ``` 16 | 17 | ### Get all users: 18 | 19 | ```graphql 20 | query { 21 | getAllUsers { 22 | id 23 | name 24 | } 25 | } 26 | ``` 27 | 28 | ### Get user by name 29 | 30 | ```graphql 31 | query { 32 | getUserByName(name: "Bob") { 33 | id 34 | name 35 | } 36 | } 37 | ``` 38 | 39 | For sake of simplicity and learning curve, all server code is included in single file. 40 | 41 | Idea behind: 42 | 43 | Decorators of `typeorm` and `typegql` works very well together. We've declared single `User` class like: 44 | 45 | ```typescript 46 | @Entity() // set this class to be database entity 47 | @ObjectType() // also, make it graphql api public type 48 | export class User extends BaseEntity { 49 | // define fields as db columns and as graphql fields at the same time 50 | @PrimaryGeneratedColumn() 51 | @Field() 52 | id: number; 53 | 54 | @Column() 55 | @Field() 56 | name: string; 57 | 58 | // most of fields will be present both in db and in api. 59 | // fields like password would use only @Column field so they're saved in db but not avaliable in API 60 | @Column() 61 | @Field() 62 | age: number; 63 | 64 | @Field() // computed field that is not present in database! 65 | isAdult(): boolean { 66 | return this.age > 21; 67 | } 68 | } 69 | ``` 70 | 71 | After model is ready, let's define some api interface with some mutation and queries 72 | 73 | ```typescript 74 | @Schema() 75 | class ApiSchema { 76 | @Query({ type: [User] }) // as function return type is Promise of user, we need to set type manually as array of users 77 | async getAllUsers(): Promise { 78 | // we can easily use async functions inside resolvers 79 | const allUsers = await User.find(); 80 | return allUsers; 81 | } 82 | 83 | @Query({ type: User }) 84 | async getUserByName(name: string): Promise { 85 | const user = await User.findOne({ where: { name } }); 86 | return user; 87 | } 88 | 89 | @Mutation({ type: User }) 90 | async createUser(name: string, age: number): Promise { 91 | const newUser = User.create({ age, name }); 92 | return await newUser.save(); 93 | } 94 | } 95 | ``` 96 | 97 | Now, with schema defined as well, we need to compile it and it's ready to use. `index.ts` file of this example is fully functional (it includes connecting to the database and setting up express api). 98 | 99 | Note for this example to run, you'd need to have some database running locally. 100 | -------------------------------------------------------------------------------- /examples/typeorm-basic-integration/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Entity, BaseEntity, ManyToOne, OneToMany } from 'typeorm'; 3 | import { 4 | Schema, 5 | Query, 6 | Mutation, 7 | ObjectType, 8 | Field, 9 | compileSchema, 10 | } from 'typegql'; 11 | import * as graphqlHTTP from 'express-graphql'; 12 | 13 | import { PrimaryGeneratedColumn, Column, createConnection } from 'typeorm'; 14 | 15 | @Entity() 16 | @ObjectType() 17 | export class Book extends BaseEntity { 18 | @PrimaryGeneratedColumn() 19 | @Field() 20 | id: number; 21 | 22 | @Column() 23 | @Field() 24 | title: string; 25 | 26 | @Column() 27 | @Field() 28 | pagesCount: number; 29 | 30 | @ManyToOne(type => User, user => user.books, { lazy: true }) 31 | @Field({ type: () => User }) 32 | author: User; 33 | } 34 | 35 | @Entity() 36 | @ObjectType() 37 | export class User extends BaseEntity { 38 | @PrimaryGeneratedColumn() 39 | @Field() 40 | id: number; 41 | 42 | @Column() 43 | @Field() 44 | name: string; 45 | 46 | @Column() 47 | @Field() 48 | age: number; 49 | 50 | @OneToMany(type => Book, book => book.author) 51 | @Field({ type: () => [Book] }) 52 | books: Book[]; 53 | 54 | @Field() 55 | isAdult(): boolean { 56 | return this.age > 21; 57 | } 58 | } 59 | 60 | @Schema() 61 | class ApiSchema { 62 | @Query({ type: [User] }) 63 | async getAllUsers(): Promise { 64 | const allUsers = await User.find(); 65 | return allUsers; 66 | } 67 | 68 | @Query({ type: User }) 69 | async getUserByName(name: string): Promise { 70 | const user = await User.findOne({ where: { name } }); 71 | return user; 72 | } 73 | 74 | @Query({ type: [Book] }) 75 | async getAllBooks(): Promise { 76 | const books = await Book.find(); 77 | return books; 78 | } 79 | 80 | @Mutation({ type: User }) 81 | async createUser(name: string, age: number): Promise { 82 | const newUser = User.create({ age, name }); 83 | return await newUser.save(); 84 | } 85 | 86 | @Mutation({ type: Book }) 87 | async createBook( 88 | title: string, 89 | pagesCount: number, 90 | authorId: number, 91 | ): Promise { 92 | const newBook = Book.create({ 93 | title, 94 | pagesCount, 95 | author: { id: authorId }, 96 | }); 97 | return await newBook.save(); 98 | } 99 | } 100 | 101 | const compiledSchema = compileSchema(ApiSchema); 102 | 103 | const app = express(); 104 | 105 | async function startApp() { 106 | console.log('Connecting to database'); 107 | const connection = await createConnection({ 108 | type: 'postgres', 109 | host: 'localhost', 110 | port: 5432, 111 | username: 'adam', 112 | password: '', 113 | database: 'test', 114 | entities: [User, Book], 115 | synchronize: true, 116 | }); 117 | console.log('Connected'); 118 | 119 | app.use( 120 | '/graphql', 121 | graphqlHTTP({ 122 | schema: compiledSchema, 123 | graphiql: true, 124 | }), 125 | ); 126 | app.listen(3000, () => console.log('API ready on port 3000')); 127 | } 128 | 129 | startApp(); 130 | -------------------------------------------------------------------------------- /examples/typeorm-basic-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-express-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.16.3", 8 | "express-graphql": "^0.6.12", 9 | "pg": "^7.4.1", 10 | "ts-node": "^6.0.0", 11 | "typegql": "^0.4.0", 12 | "typeorm": "^0.1.21", 13 | "typescript": "^2.8.1" 14 | }, 15 | "scripts": { 16 | "start": "ts-node index.ts" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/typeorm-basic-integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"], 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | modulePaths: ['/src/'], 3 | moduleNameMapper: { 4 | '~/(.*)': '/src/$1', 5 | }, 6 | moduleFileExtensions: ['ts', 'tsx', 'js'], 7 | transform: { 8 | '^.+\\.(t|j)sx?$': 'ts-jest', 9 | // '^.+\\.(ts|tsx)$': '/preprocessor.js', 10 | }, 11 | collectCoverage: false, 12 | testMatch: ['**/*.spec.(ts|tsx)'], 13 | setupFiles: ['/src/test/setup.ts'], 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typegql", 3 | "version": "0.7.0", 4 | "main": "lib/index.js", 5 | "types": "lib/index.d.ts", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "jest --coverage", 9 | "test:watch": "jest --watch", 10 | "b": "ncc build src/index.ts -o dist", 11 | "build:rollup": "rollup src/index.ts -c", 12 | "build": "rimraf lib && yarn run build:rollup", 13 | "prepublishOnly": "rimraf lib && yarn build", 14 | "docs": "gitbook serve", 15 | "docs:build": "gitbook build ./docs", 16 | "precommit": "pretty-quick --staged" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/prismake/typegql" 21 | }, 22 | "devDependencies": { 23 | "@types/graphql": "^14.0.7", 24 | "@types/jest": "^24.0.11", 25 | "@types/node": "^11.12.0", 26 | "@types/object-path": "^0.11.0", 27 | "@zeit/ncc": "^0.17.0", 28 | "@zerollup/ts-transform-paths": "^1.7.0", 29 | "gh-pages": "^2.0.1", 30 | "gitbook": "^3.2.3", 31 | "gitbook-cli": "^2.3.2", 32 | "husky": "^1.3.1", 33 | "jest": "^24.5.0", 34 | "prettier": "^1.16.4", 35 | "pretty-quick": "^1.10.0", 36 | "rimraf": "^2.6.3", 37 | "rollup": "^1.7.3", 38 | "rollup-plugin-typescript": "^1.0.1", 39 | "rollup-plugin-typescript2": "^0.20.1", 40 | "ts-jest": "^24.0.0", 41 | "ttypescript": "^1.5.6", 42 | "typescript": "^3.3.4000" 43 | }, 44 | "dependencies": { 45 | "graphql": "^14.2.0", 46 | "object-path": "^0.11.4", 47 | "reflect-metadata": "^0.1.13" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | const tsc = require('typescript'); 2 | const tsConfig = require('./tsconfig.json'); 3 | 4 | module.exports = { 5 | process(src, path) { 6 | if (path.endsWith('.ts') || path.endsWith('.tsx')) { 7 | return tsc.transpile(src, tsConfig.compilerOptions, path, []); 8 | } 9 | return src; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /publish-docs.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages'); 2 | 3 | ghpages.publish('docs/_book', () => { 4 | console.log('Done.'); 5 | }); 6 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | 3 | const pkg = require('./package.json'); 4 | 5 | export default { 6 | input: './src/index.ts', 7 | entry: './src/index.ts', 8 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 9 | output: { file: pkg.main, name: 'typegql', format: 'cjs' }, 10 | cacheRoot: './cache/.rpt2', 11 | plugins: [typescript()], 12 | }; 13 | -------------------------------------------------------------------------------- /src/domains/arg/compiler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldConfigArgumentMap, 3 | GraphQLType, 4 | GraphQLInputType, 5 | isInputType, 6 | GraphQLNonNull, 7 | } from 'graphql'; 8 | import { resolveType, getParameterNames } from '~/services/utils'; 9 | import { injectorRegistry } from '~/domains/inject'; 10 | import { ArgsIndex, argRegistry } from './registry'; 11 | import { ArgError } from './error'; 12 | import { defaultArgOptions } from './options'; 13 | 14 | function compileInferedAndRegisterdArgs( 15 | infered: any[], 16 | registeredArgs: ArgsIndex = {}, 17 | ) { 18 | const argsMerged = infered.map((inferedType, index) => { 19 | const registered = registeredArgs[index]; 20 | if (registered && registered.type) { 21 | return registered.type; 22 | } 23 | return inferedType; 24 | }); 25 | 26 | const resolvedArgs = argsMerged.map((rawType, index) => { 27 | return resolveType(rawType, true); 28 | }); 29 | 30 | return resolvedArgs; 31 | } 32 | 33 | function validateArgs( 34 | target: Function, 35 | fieldName: string, 36 | types: GraphQLType[], 37 | ): types is GraphQLInputType[] { 38 | types.forEach((argType, argIndex) => { 39 | const isInjectedArg = injectorRegistry.has(target, fieldName, argIndex); 40 | 41 | if (!isInjectedArg && !argType) { 42 | throw new ArgError( 43 | target, 44 | fieldName, 45 | argIndex, 46 | `Could not infer type of argument. Make sure to use native GraphQLInputType, native scalar like 'String' or class decorated with @InputObjectType`, 47 | ); 48 | } 49 | 50 | if (!isInjectedArg && !isInputType(argType)) { 51 | throw new ArgError( 52 | target, 53 | fieldName, 54 | argIndex, 55 | `Argument has incorrect type. Make sure to use native GraphQLInputType, native scalar like 'String' or class decorated with @InputObjectType`, 56 | ); 57 | } 58 | 59 | if (isInjectedArg && argRegistry.has(target, fieldName, argIndex)) { 60 | throw new ArgError( 61 | target, 62 | fieldName, 63 | argIndex, 64 | `Argument cannot be marked wiht both @Arg and @Inject or custom injector`, 65 | ); 66 | } 67 | }); 68 | return true; 69 | } 70 | 71 | function enhanceType(originalType: GraphQLInputType, isNullable: boolean) { 72 | let finalType = originalType; 73 | if (!isNullable) { 74 | finalType = new GraphQLNonNull(finalType); 75 | } 76 | return finalType; 77 | } 78 | 79 | function convertArgsArrayToArgsMap( 80 | target: Function, 81 | fieldName: string, 82 | argsTypes: GraphQLInputType[], 83 | registeredArgs: ArgsIndex = {}, 84 | ): GraphQLFieldConfigArgumentMap { 85 | const fieldDescriptor = Object.getOwnPropertyDescriptor( 86 | target.prototype, 87 | fieldName, 88 | ); 89 | 90 | // in case of getters, field arguments are not relevant 91 | if (fieldDescriptor.get) { 92 | return {}; 93 | } 94 | 95 | const functionDefinition = target.prototype[fieldName]; 96 | const argNames = getParameterNames(functionDefinition); 97 | 98 | if (!argNames || !argNames.length) { 99 | return {}; 100 | } 101 | 102 | const argsMap: GraphQLFieldConfigArgumentMap = {}; 103 | argNames.forEach((argName, index) => { 104 | const argConfig = registeredArgs[index] || { ...defaultArgOptions }; 105 | const argType = argsTypes[index]; 106 | 107 | // don't publish args marked as auto Injected 108 | if (injectorRegistry.has(target, fieldName, index)) { 109 | return; 110 | } 111 | 112 | let finalType = enhanceType(argType, argConfig.isNullable); 113 | 114 | argsMap[argName] = { 115 | type: finalType, 116 | description: argConfig.description, 117 | }; 118 | }); 119 | return argsMap; 120 | } 121 | 122 | export function compileFieldArgs( 123 | target: Function, 124 | fieldName: string, 125 | ): GraphQLFieldConfigArgumentMap { 126 | const registeredArgs = argRegistry.getAll(target)[fieldName]; 127 | const inferedRawArgs = Reflect.getMetadata( 128 | 'design:paramtypes', 129 | target.prototype, 130 | fieldName, 131 | ); 132 | 133 | // There are no arguments 134 | if (!inferedRawArgs) { 135 | return {}; 136 | } 137 | 138 | const argTypes = compileInferedAndRegisterdArgs( 139 | inferedRawArgs, 140 | registeredArgs, 141 | ); 142 | 143 | if (!validateArgs(target, fieldName, argTypes)) { 144 | return; 145 | } 146 | 147 | return convertArgsArrayToArgsMap(target, fieldName, argTypes, registeredArgs); 148 | } 149 | -------------------------------------------------------------------------------- /src/domains/arg/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | import { getParameterNames } from '~/services/utils'; 4 | 5 | export class ArgError extends BaseError { 6 | constructor( 7 | target: Function, 8 | fieldName: string, 9 | argIndex: number, 10 | msg: string, 11 | ) { 12 | const paramNames = getParameterNames(target.prototype[fieldName]); 13 | const paramName = paramNames[argIndex]; 14 | const fullMsg = `@Type ${ 15 | target.name 16 | }.${fieldName}(${paramName} <-------): ${msg}`; 17 | super(fullMsg); 18 | this.message = fullMsg; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/domains/arg/index.ts: -------------------------------------------------------------------------------- 1 | import { argRegistry } from './registry'; 2 | export { compileFieldArgs } from './compiler'; 3 | import { ArgOptions, defaultArgOptions } from './options'; 4 | 5 | export function Arg(options: ArgOptions = {}): ParameterDecorator { 6 | return (target: Object, fieldName: string, argIndex: number) => { 7 | const compiledOptions = { 8 | ...defaultArgOptions, 9 | ...options, 10 | } 11 | argRegistry.set(target.constructor, [fieldName, argIndex], compiledOptions); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/domains/arg/options.ts: -------------------------------------------------------------------------------- 1 | export interface ArgOptions { 2 | description?: string; 3 | type?: any; 4 | isNullable?: boolean; 5 | } 6 | 7 | export const defaultArgOptions: ArgOptions = { 8 | isNullable: false, 9 | }; 10 | -------------------------------------------------------------------------------- /src/domains/arg/registry.ts: -------------------------------------------------------------------------------- 1 | import { DeepWeakMap } from '~/services/utils'; 2 | 3 | export interface ArgInnerConfig { 4 | description?: string; 5 | isNullable?: boolean; 6 | type?: any; 7 | } 8 | 9 | export const argRegistry = new DeepWeakMap< 10 | Function, 11 | ArgInnerConfig, 12 | { 13 | [fieldName: string]: ArgsIndex; 14 | } 15 | >(); 16 | 17 | export interface ArgsIndex { 18 | [argIndex: number]: ArgInnerConfig; 19 | } 20 | -------------------------------------------------------------------------------- /src/domains/enum/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class EnumError extends BaseError { 4 | constructor(name: string, msg: string) { 5 | const fullMsg = `Enum ${name}: ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domains/enum/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLEnumType } from 'graphql'; 2 | import { EnumError } from './error'; 3 | 4 | import { enumsRegistry } from './registry'; 5 | export { enumsRegistry } from './registry'; 6 | import { convertNativeEnumToGraphQLEnumValues } from './services'; 7 | 8 | export interface EnumOptions { 9 | name: string; 10 | description?: string; 11 | } 12 | 13 | export function registerEnum(enumDef: Object, options: EnumOptions | string) { 14 | if (typeof options === 'string') { 15 | options = { name: options }; 16 | } 17 | const { name, description }: EnumOptions = options; 18 | 19 | if (enumsRegistry.has(enumDef)) { 20 | throw new EnumError(name, `Enum is already registered`); 21 | } 22 | 23 | const values = convertNativeEnumToGraphQLEnumValues(enumDef); 24 | const enumType = new GraphQLEnumType({ 25 | name, 26 | description, 27 | values, 28 | }); 29 | enumsRegistry.set(enumDef, enumType); 30 | return enumType; 31 | } 32 | -------------------------------------------------------------------------------- /src/domains/enum/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLEnumType } from 'graphql'; 2 | 3 | export const enumsRegistry = new WeakMap(); 4 | -------------------------------------------------------------------------------- /src/domains/enum/services.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLEnumValueConfigMap } from 'graphql'; 2 | 3 | export function convertNativeEnumToGraphQLEnumValues( 4 | enumDef: any, 5 | ): GraphQLEnumValueConfigMap { 6 | const valueConfigMap: GraphQLEnumValueConfigMap = {}; 7 | Object.keys(enumDef).map(key => { 8 | if (!isNaN(key as any)) { 9 | return; 10 | } 11 | const value = enumDef[key]; 12 | valueConfigMap[key] = { 13 | value, 14 | }; 15 | }); 16 | return valueConfigMap; 17 | } 18 | -------------------------------------------------------------------------------- /src/domains/field/compiler/fieldType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql'; 2 | import { inferTypeByTarget, resolveType } from '~/services/utils'; 3 | import { FieldError } from '../index'; 4 | 5 | export function resolveTypeOrThrow( 6 | type: any, 7 | target: Function, 8 | fieldName: string, 9 | ): GraphQLType { 10 | const resolvedType = resolveType(type); 11 | 12 | if (!resolvedType) { 13 | throw new FieldError( 14 | target, 15 | fieldName, 16 | `Forced type is incorrect. Make sure to use either native graphql type or class that is registered with @Type decorator`, 17 | ); 18 | } 19 | 20 | return resolvedType; 21 | } 22 | 23 | export function inferTypeOrThrow( 24 | target: Function, 25 | fieldName: string, 26 | ): GraphQLType { 27 | const inferedType = inferTypeByTarget(target.prototype, fieldName); 28 | if (!inferedType) { 29 | throw new FieldError( 30 | target, 31 | fieldName, 32 | `Could not infer return type and no type is forced. In case of circular dependencies make sure to force types of instead of infering them.`, 33 | ); 34 | } 35 | return resolveType(inferedType); 36 | } 37 | 38 | export function validateNotInferableField(target: Function, fieldName: string) { 39 | const inferedType = inferTypeByTarget(target.prototype, fieldName); 40 | if (inferedType === Array) { 41 | throw new FieldError( 42 | target, 43 | fieldName, 44 | `Field returns list so it's required to explicitly set list item type. You can set list type like: @Field({ type: [ItemType] })`, 45 | ); 46 | } 47 | 48 | if (inferedType === Promise) { 49 | throw new FieldError( 50 | target, 51 | fieldName, 52 | `Field returns Promise so it's required to explicitly set resolved type as it's not possible to guess it. You can set resolved type like: @Field({ type: ItemType })`, 53 | ); 54 | } 55 | 56 | if (inferedType === Object) { 57 | throw new FieldError( 58 | target, 59 | fieldName, 60 | `It was not possible to guess type of this field. It might be because it returns Promise, Array etc. In such case it's needed to explicitly declare type of field like: @Field({ type: ItemType })`, 61 | ); 62 | } 63 | return true; 64 | } 65 | -------------------------------------------------------------------------------- /src/domains/field/compiler/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfig, GraphQLFieldConfigMap } from 'graphql'; 2 | import { getClassWithAllParentClasses } from '~/services/utils/inheritance'; 3 | import { FieldError, fieldsRegistry } from '../index'; 4 | 5 | import { compileFieldResolver } from './resolver'; 6 | import { compileFieldArgs } from '~/domains/arg'; 7 | import { 8 | enhanceType, 9 | isRootFieldOnNonRootBase, 10 | resolveRegisteredOrInferedType, 11 | validateResolvedType, 12 | } from './services'; 13 | 14 | import { validateNotInferableField } from './fieldType'; 15 | 16 | export function compileFieldConfig( 17 | target: Function, 18 | fieldName: string, 19 | ): GraphQLFieldConfig { 20 | const { type, description, isNullable } = fieldsRegistry.get( 21 | target, 22 | fieldName, 23 | ); 24 | const args = compileFieldArgs(target, fieldName); 25 | 26 | const resolvedType = resolveRegisteredOrInferedType(target, fieldName, type); 27 | 28 | // if was not able to resolve type, try to show some helpful information about it 29 | if (!resolvedType && !validateNotInferableField(target, fieldName)) { 30 | return; 31 | } 32 | 33 | // show error about being not able to resolve field type 34 | if (!validateResolvedType(target, fieldName, resolvedType)) { 35 | validateNotInferableField(target, fieldName); 36 | return; 37 | } 38 | 39 | const finalType = enhanceType(resolvedType, isNullable); 40 | 41 | return { 42 | description, 43 | type: finalType, 44 | resolve: compileFieldResolver(target, fieldName), 45 | args, 46 | }; 47 | } 48 | 49 | function getAllFields(target: Function) { 50 | const fields = fieldsRegistry.getAll(target); 51 | const finalFieldsMap: GraphQLFieldConfigMap = {}; 52 | Object.keys(fields).forEach(fieldName => { 53 | if (isRootFieldOnNonRootBase(target, fieldName)) { 54 | throw new FieldError( 55 | target, 56 | fieldName, 57 | `Given field is root field (@Query or @Mutation) not registered inside @Schema type. `, 58 | ); 59 | } 60 | 61 | const config = fieldsRegistry.get(target, fieldName); 62 | finalFieldsMap[config.name] = compileFieldConfig(target, fieldName); 63 | }); 64 | return finalFieldsMap; 65 | } 66 | 67 | export function compileAllFields(target: Function) { 68 | const targetWithParents = getClassWithAllParentClasses(target); 69 | 70 | const finalFieldsMap: GraphQLFieldConfigMap = {}; 71 | 72 | targetWithParents.forEach(targetLevel => { 73 | Object.assign(finalFieldsMap, getAllFields(targetLevel)); 74 | }); 75 | return finalFieldsMap; 76 | } 77 | -------------------------------------------------------------------------------- /src/domains/field/compiler/resolver.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldResolver } from 'graphql'; 2 | import { 3 | InjectorsIndex, 4 | InjectorResolver, 5 | injectorRegistry, 6 | } from '~/domains/inject'; 7 | import { 8 | fieldAfterHooksRegistry, 9 | fieldBeforeHooksRegistry, 10 | HookExecutor, 11 | } from '~/domains/hooks'; 12 | import { getParameterNames } from '~/services/utils'; 13 | 14 | import { isSchemaRoot, getSchemaRootInstance } from '~/domains/schema'; 15 | 16 | interface ArgsMap { 17 | [argName: string]: any; 18 | } 19 | 20 | interface ComputeArgsOptions { 21 | args: ArgsMap; 22 | injectors: InjectorsIndex; 23 | injectorToValueMapper: (injector: InjectorResolver) => any; 24 | } 25 | 26 | async function performHooksExecution( 27 | hooks: HookExecutor[], 28 | source: any, 29 | args: any, 30 | context: any, 31 | info: any, 32 | ) { 33 | if (!hooks) { 34 | return; 35 | } 36 | // all hooks are executed in parrell instead of sequence. We wait for them all to be resolved before we continue 37 | return await Promise.all( 38 | hooks.map(hook => { 39 | return hook({ source, args, context, info }); 40 | }), 41 | ); 42 | } 43 | 44 | function computeFinalArgs( 45 | func: Function, 46 | { args, injectors, injectorToValueMapper }: ComputeArgsOptions, 47 | ) { 48 | const paramNames = getParameterNames(func); 49 | return paramNames.map((paramName, index) => { 50 | if (args && args.hasOwnProperty(paramName)) { 51 | return args[paramName]; 52 | } 53 | 54 | const injector = injectors[index]; 55 | 56 | if (!injector) { 57 | return null; 58 | } 59 | 60 | return injectorToValueMapper(injector); 61 | }); 62 | } 63 | 64 | function getFieldOfTarget(instance: any, prototype: any, fieldName: string) { 65 | if (!instance) { 66 | return prototype[fieldName]; 67 | } 68 | 69 | const instanceField = instance[fieldName]; 70 | 71 | if (instanceField !== undefined) { 72 | return instanceField; 73 | } 74 | 75 | return prototype[fieldName]; 76 | } 77 | 78 | export function compileFieldResolver( 79 | target: Function, 80 | fieldName: string, 81 | ): GraphQLFieldResolver { 82 | // const config = fieldsRegistry.get(target, fieldName); 83 | const injectors = injectorRegistry.getAll(target)[fieldName]; 84 | const beforeHooks = fieldBeforeHooksRegistry.get(target, fieldName); 85 | const afterHooks = fieldAfterHooksRegistry.get(target, fieldName); 86 | 87 | return async (source: any, args = null, context = null, info = null) => { 88 | if (isSchemaRoot(target)) { 89 | source = getSchemaRootInstance(target); 90 | } 91 | 92 | await performHooksExecution(beforeHooks, source, args, context, info); 93 | const instanceField = getFieldOfTarget(source, target.prototype, fieldName); 94 | 95 | if (typeof instanceField !== 'function') { 96 | await performHooksExecution(afterHooks, source, args, context, info); 97 | return instanceField; 98 | } 99 | 100 | const instanceFieldFunc = instanceField as Function; 101 | 102 | const params = computeFinalArgs(instanceFieldFunc, { 103 | args: args || {}, 104 | injectors: injectors || {}, 105 | injectorToValueMapper: injector => 106 | injector.apply(source, [{ source, args, context, info }]), 107 | }); 108 | 109 | const result = await instanceFieldFunc.apply(source, params); 110 | 111 | await performHooksExecution(afterHooks, source, args, context, info); // TODO: Consider adding resolve return to hook callback 112 | return result; 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /src/domains/field/compiler/services.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isOutputType, 3 | GraphQLType, 4 | GraphQLOutputType, 5 | GraphQLNonNull, 6 | } from 'graphql'; 7 | import { FieldError } from '../index'; 8 | 9 | import { resolveTypeOrThrow, inferTypeOrThrow } from './fieldType'; 10 | import { 11 | mutationFieldsRegistry, 12 | queryFieldsRegistry, 13 | isSchemaRoot, 14 | } from '~/domains/schema'; 15 | 16 | export function resolveRegisteredOrInferedType( 17 | target: Function, 18 | fieldName: string, 19 | forcedType?: any, 20 | ) { 21 | if (forcedType) { 22 | return resolveTypeOrThrow(forcedType, target, fieldName); 23 | } 24 | return inferTypeOrThrow(target, fieldName); 25 | } 26 | 27 | export function validateResolvedType( 28 | target: Function, 29 | fieldName: string, 30 | type: GraphQLType, 31 | ): type is GraphQLOutputType { 32 | if (!isOutputType(type)) { 33 | throw new FieldError( 34 | target, 35 | fieldName, 36 | `Validation of type failed. Resolved type for @Field must be GraphQLOutputType.`, 37 | ); 38 | } 39 | return true; 40 | } 41 | 42 | export function enhanceType( 43 | originalType: GraphQLOutputType, 44 | isNullable: boolean, 45 | ) { 46 | let finalType = originalType; 47 | if (!isNullable) { 48 | finalType = new GraphQLNonNull(finalType); 49 | } 50 | return finalType; 51 | } 52 | 53 | export function isRootFieldOnNonRootBase(base: Function, fieldName: string) { 54 | const isRoot = isSchemaRoot(base); 55 | if (isRoot) { 56 | return false; 57 | } 58 | if (mutationFieldsRegistry.has(base, fieldName)) { 59 | return true; 60 | } 61 | if (queryFieldsRegistry.has(base, fieldName)) { 62 | return true; 63 | } 64 | return false; 65 | } 66 | -------------------------------------------------------------------------------- /src/domains/field/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class FieldError extends BaseError { 4 | constructor(target: Function, fieldName: string, msg: string) { 5 | const fullMsg = `@ObjectType ${target.name}.${fieldName}: ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domains/field/index.ts: -------------------------------------------------------------------------------- 1 | import { fieldsRegistry, FieldInnerConfig } from './registry'; 2 | 3 | export { 4 | FieldInnerConfig, 5 | fieldsRegistry, 6 | queryFieldsRegistry, 7 | } from './registry'; 8 | export { compileAllFields, compileFieldConfig } from './compiler'; 9 | export { FieldError } from './error'; 10 | 11 | export interface FieldOptions { 12 | description?: string; 13 | type?: any; 14 | name?: string; 15 | isNullable?: boolean; 16 | } 17 | 18 | export function Field(options?: FieldOptions): PropertyDecorator { 19 | return (targetInstance: Object, fieldName: string) => { 20 | const finalConfig: FieldInnerConfig = { 21 | property: fieldName, 22 | name: fieldName, 23 | isNullable: true, 24 | ...options, 25 | }; 26 | 27 | fieldsRegistry.set(targetInstance.constructor, fieldName, { 28 | ...finalConfig, 29 | }); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/domains/field/registry.ts: -------------------------------------------------------------------------------- 1 | import { DeepWeakMap } from '~/services/utils'; 2 | 3 | export interface AllRegisteredFields { 4 | [fieldName: string]: FieldInnerConfig; 5 | } 6 | 7 | export const fieldsRegistry = new DeepWeakMap< 8 | Function, 9 | FieldInnerConfig, 10 | AllRegisteredFields 11 | >(); 12 | 13 | export interface FieldInnerConfig { 14 | name: string; 15 | property: string; 16 | description?: string; 17 | isNullable?: boolean; 18 | type?: any; 19 | } 20 | 21 | export interface AllQueryFields { 22 | [fieldName: string]: FieldInnerConfig; 23 | } 24 | 25 | export const queryFieldsRegistry = new DeepWeakMap< 26 | Function, 27 | FieldInnerConfig, 28 | AllQueryFields 29 | >(); 30 | -------------------------------------------------------------------------------- /src/domains/hooks/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class HookError extends BaseError { 4 | constructor(target: Function, fieldName: string, msg: string) { 5 | const fullMsg = `@HookError ${target.name}.${fieldName}: ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domains/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerFieldAfterHook, 3 | registerFieldBeforeHook, 4 | HookExecutor, 5 | } from './registry'; 6 | 7 | export { 8 | fieldAfterHooksRegistry, 9 | fieldBeforeHooksRegistry, 10 | HookExecutor, 11 | } from './registry'; 12 | export { HookError } from './error'; 13 | 14 | export function Before(hook: HookExecutor): PropertyDecorator { 15 | return (targetInstance: Object, fieldName: string) => { 16 | registerFieldBeforeHook(targetInstance.constructor, fieldName, hook); 17 | }; 18 | } 19 | 20 | export function After(hook: HookExecutor): PropertyDecorator { 21 | return (targetInstance: Object, fieldName: string) => { 22 | registerFieldAfterHook(targetInstance.constructor, fieldName, hook); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/domains/hooks/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | import { DeepWeakMap } from '~/services/utils'; 3 | import { HookError } from './error'; 4 | 5 | export interface HookExecutorResolverArgs { 6 | source: any; 7 | args: { [argName: string]: any }; 8 | context: any; 9 | info: GraphQLResolveInfo; 10 | } 11 | 12 | export type HookExecutor = ( 13 | data: HookExecutorResolverArgs, 14 | ) => Result | Promise; 15 | 16 | export interface AllRegisteredHooks { 17 | [fieldName: string]: HookExecutor; 18 | } 19 | 20 | export const fieldBeforeHooksRegistry = new DeepWeakMap< 21 | Function, 22 | HookExecutor[], 23 | AllRegisteredHooks 24 | >(); 25 | 26 | export const fieldAfterHooksRegistry = new DeepWeakMap< 27 | Function, 28 | HookExecutor[], 29 | AllRegisteredHooks 30 | >(); 31 | 32 | export function registerFieldBeforeHook( 33 | target: Function, 34 | fieldName: string, 35 | hook: HookExecutor, 36 | ) { 37 | if (!hook) { 38 | throw new HookError( 39 | target, 40 | fieldName, 41 | `Field @Before hook function must be supplied.`, 42 | ); 43 | } 44 | const currentHooks = fieldBeforeHooksRegistry.get(target, fieldName) || []; 45 | fieldBeforeHooksRegistry.set(target, fieldName, [hook, ...currentHooks]); 46 | } 47 | 48 | export function registerFieldAfterHook( 49 | target: Function, 50 | fieldName: string, 51 | hook: HookExecutor, 52 | ) { 53 | if (!hook) { 54 | throw new HookError( 55 | target, 56 | fieldName, 57 | `Field @After hook function must be supplied.`, 58 | ); 59 | } 60 | const currentHooks = fieldAfterHooksRegistry.get(target, fieldName) || []; 61 | fieldAfterHooksRegistry.set(target, fieldName, [hook, ...currentHooks]); 62 | } 63 | -------------------------------------------------------------------------------- /src/domains/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ObjectType, 3 | compileObjectType, 4 | objectTypeRegistry, 5 | } from './objectType'; 6 | export { 7 | InputObjectType, 8 | compileInputObjectType, 9 | inputObjectTypeRegistry, 10 | } from './inputObjectType'; 11 | export { Field } from './field'; 12 | export { InputField } from './inputField'; 13 | export { Arg } from './arg'; 14 | export { Inject, Context, Source, Info } from './inject'; 15 | export { registerEnum, enumsRegistry } from './enum'; 16 | export { Union, unionRegistry } from './union'; 17 | export { Before, After } from './hooks'; 18 | export { 19 | SchemaRoot, 20 | Schema, 21 | schemaRootsRegistry, 22 | compileSchema, 23 | Query, 24 | Mutation, 25 | } from './schema'; 26 | -------------------------------------------------------------------------------- /src/domains/inject/index.ts: -------------------------------------------------------------------------------- 1 | import { injectorRegistry, InjectorResolver } from './registry'; 2 | export { 3 | injectorRegistry, 4 | InjectorsIndex, 5 | InjectorResolver, 6 | InjectorResolverData, 7 | } from './registry'; 8 | 9 | export function Inject(resolver: InjectorResolver): ParameterDecorator { 10 | return (target: Object, fieldName: string, argIndex: number) => { 11 | injectorRegistry.set(target.constructor, [fieldName, argIndex], resolver); 12 | }; 13 | } 14 | 15 | export const Context = Inject(({ context }) => { 16 | return context; 17 | }); 18 | 19 | export const Info = Inject(({ info }) => { 20 | return info; 21 | }); 22 | 23 | export const Source = Inject(({ source }) => { 24 | return source; 25 | }); 26 | -------------------------------------------------------------------------------- /src/domains/inject/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | 3 | import { DeepWeakMap } from '~/services/utils'; 4 | 5 | export interface InjectorResolverData { 6 | source: any; 7 | args: { [key: string]: any }; 8 | context: any; 9 | info: GraphQLResolveInfo; 10 | } 11 | 12 | export type InjectorResolver = (data: InjectorResolverData) => any; 13 | 14 | export interface InjectorsIndex { 15 | [injectorIndex: number]: InjectorResolver; 16 | } 17 | 18 | export interface AllInjectors { 19 | [fieldName: string]: InjectorsIndex; 20 | } 21 | 22 | export const injectorRegistry = new DeepWeakMap< 23 | Function, 24 | InjectorResolver, 25 | AllInjectors 26 | >(); 27 | -------------------------------------------------------------------------------- /src/domains/inputField/compiler/fieldType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql'; 2 | import { inferTypeByTarget } from '~/services/utils'; 3 | import { InputFieldError } from '../index'; 4 | 5 | import { resolveType } from '~/services/utils'; 6 | 7 | export function resolveTypeOrThrow( 8 | type: any, 9 | target: Function, 10 | fieldName: string, 11 | ): GraphQLType { 12 | const resolvedType = resolveType(type, true); 13 | 14 | if (!resolvedType) { 15 | throw new InputFieldError( 16 | target, 17 | fieldName, 18 | `Forced type is incorrect. Make sure to use either native graphql type or class that is registered with @Type decorator`, 19 | ); 20 | } 21 | 22 | return resolvedType; 23 | } 24 | 25 | export function inferTypeOrThrow( 26 | target: Function, 27 | fieldName: string, 28 | ): GraphQLType { 29 | const inferedType = inferTypeByTarget(target.prototype, fieldName); 30 | if (!inferedType) { 31 | throw new InputFieldError( 32 | target, 33 | fieldName, 34 | `Could not infer return type and no type is forced. In case of circular dependencies make sure to force types of instead of infering them.`, 35 | ); 36 | } 37 | return resolveType(inferedType, true); 38 | } 39 | -------------------------------------------------------------------------------- /src/domains/inputField/compiler/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLType, 3 | isInputType, 4 | GraphQLInputType, 5 | GraphQLInputFieldConfig, 6 | GraphQLInputFieldConfigMap, 7 | GraphQLNonNull, 8 | } from 'graphql'; 9 | import { getClassWithAllParentClasses } from '~/services/utils'; 10 | import { InputFieldError, inputFieldsRegistry } from '../index'; 11 | 12 | import { resolveTypeOrThrow, inferTypeOrThrow } from './fieldType'; 13 | 14 | function getFinalInputFieldType( 15 | target: Function, 16 | fieldName: string, 17 | forcedType?: any, 18 | ) { 19 | if (forcedType) { 20 | return resolveTypeOrThrow(forcedType, target, fieldName); 21 | } 22 | return inferTypeOrThrow(target, fieldName); 23 | } 24 | 25 | function validateResolvedType( 26 | target: Function, 27 | fieldName: string, 28 | type: GraphQLType, 29 | ): type is GraphQLInputType { 30 | if (!isInputType(type)) { 31 | throw new InputFieldError( 32 | target, 33 | fieldName, 34 | `Validation of type failed. Resolved type for @Field must be GraphQLInputType.`, 35 | ); 36 | } 37 | return true; 38 | } 39 | 40 | function enhanceType(originalType: GraphQLInputType, isNullable: boolean) { 41 | let finalType = originalType; 42 | if (!isNullable) { 43 | finalType = new GraphQLNonNull(finalType); 44 | } 45 | return finalType; 46 | } 47 | 48 | export function compileInputFieldConfig( 49 | target: Function, 50 | fieldName: string, 51 | ): GraphQLInputFieldConfig { 52 | const { 53 | type, 54 | description, 55 | defaultValue, 56 | isNullable, 57 | } = inputFieldsRegistry.get(target, fieldName); 58 | 59 | const resolvedType = getFinalInputFieldType(target, fieldName, type); 60 | 61 | if (!validateResolvedType(target, fieldName, resolvedType)) { 62 | return; 63 | } 64 | 65 | resolvedType; 66 | const finalType = enhanceType(resolvedType, isNullable); 67 | 68 | return { 69 | description, 70 | defaultValue, 71 | type: finalType, 72 | }; 73 | } 74 | 75 | export function compileAllInputFieldsForSingleTarget(target: Function) { 76 | const fields = inputFieldsRegistry.getAll(target); 77 | const finalFieldsMap: GraphQLInputFieldConfigMap = {}; 78 | Object.keys(fields).forEach(fieldName => { 79 | const config = inputFieldsRegistry.get(target, fieldName); 80 | finalFieldsMap[config.name] = compileInputFieldConfig(target, fieldName); 81 | }); 82 | return finalFieldsMap; 83 | } 84 | 85 | export function compileAllInputFields(target: Function) { 86 | const targetWithParents = getClassWithAllParentClasses(target); 87 | const finalFieldsMap: GraphQLInputFieldConfigMap = {}; 88 | 89 | targetWithParents.forEach(targetLevel => { 90 | Object.assign( 91 | finalFieldsMap, 92 | compileAllInputFieldsForSingleTarget(targetLevel), 93 | ); 94 | }); 95 | return finalFieldsMap; 96 | } 97 | -------------------------------------------------------------------------------- /src/domains/inputField/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class InputFieldError extends BaseError { 4 | constructor(target: Function, fieldName: string, msg: string) { 5 | const fullMsg = `@InputField ${target.name}.${fieldName}: ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domains/inputField/index.ts: -------------------------------------------------------------------------------- 1 | import { inputFieldsRegistry, FieldInputInnerConfig } from './registry'; 2 | 3 | export { FieldInputInnerConfig, inputFieldsRegistry } from './registry'; 4 | export { compileAllInputFields, compileInputFieldConfig } from './compiler'; 5 | export { InputFieldError } from './error'; 6 | 7 | export interface InputFieldOptions { 8 | description?: string; 9 | defaultValue?: any; 10 | type?: any; 11 | name?: string; 12 | isNullable?: boolean; 13 | } 14 | 15 | export function InputField(options?: InputFieldOptions): PropertyDecorator { 16 | return (targetInstance: Object, fieldName: string) => { 17 | const finalConfig: FieldInputInnerConfig = { 18 | property: fieldName, 19 | name: fieldName, 20 | ...options, 21 | }; 22 | 23 | inputFieldsRegistry.set(targetInstance.constructor, fieldName, finalConfig); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/domains/inputField/registry.ts: -------------------------------------------------------------------------------- 1 | import { DeepWeakMap } from '~/services/utils'; 2 | 3 | export interface AllRegisteredInputFields { 4 | [fieldName: string]: FieldInputInnerConfig; 5 | } 6 | 7 | export const inputFieldsRegistry = new DeepWeakMap< 8 | Function, 9 | FieldInputInnerConfig, 10 | AllRegisteredInputFields 11 | >(); 12 | 13 | export interface FieldInputInnerConfig { 14 | name: string; 15 | defaultValue?: any; 16 | property: string; 17 | description?: string; 18 | type?: any; 19 | isNullable?: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /src/domains/inputObjectType/compiler/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | compileInputObjectType, 3 | compileInputObjectTypeWithConfig, 4 | } from './objectType'; 5 | -------------------------------------------------------------------------------- /src/domains/inputObjectType/compiler/objectType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLInputFieldConfigMap } from 'graphql'; 2 | import { InputObjectTypeError, inputObjectTypeRegistry } from '../index'; 3 | 4 | import { 5 | compileAllInputFields, 6 | inputFieldsRegistry, 7 | } from '~/domains/inputField'; 8 | import { 9 | createCachedThunk, 10 | getClassWithAllParentClasses, 11 | } from '~/services/utils'; 12 | 13 | const compileOutputTypeCache = new WeakMap(); 14 | 15 | export interface TypeOptions { 16 | name: string; 17 | description?: string; 18 | } 19 | 20 | function createTypeInputFieldsGetter( 21 | target: Function, 22 | ): () => GraphQLInputFieldConfigMap { 23 | const targetWithParents = getClassWithAllParentClasses(target); 24 | const hasFields = targetWithParents.some(ancestor => { 25 | return !inputFieldsRegistry.isEmpty(ancestor); 26 | }); 27 | 28 | if (!hasFields) { 29 | throw new InputObjectTypeError( 30 | target, 31 | `There are no fields inside this type.`, 32 | ); 33 | } 34 | 35 | return createCachedThunk(() => { 36 | return compileAllInputFields(target); 37 | }); 38 | } 39 | 40 | export function compileInputObjectTypeWithConfig( 41 | target: Function, 42 | config: TypeOptions, 43 | ): GraphQLInputObjectType { 44 | if (compileOutputTypeCache.has(target)) { 45 | return compileOutputTypeCache.get(target); 46 | } 47 | 48 | const compiled = new GraphQLInputObjectType({ 49 | ...config, 50 | fields: createTypeInputFieldsGetter(target), 51 | }); 52 | 53 | compileOutputTypeCache.set(target, compiled); 54 | return compiled; 55 | } 56 | 57 | export function compileInputObjectType(target: Function) { 58 | if (!inputObjectTypeRegistry.has(target)) { 59 | throw new InputObjectTypeError( 60 | target, 61 | `Class is not registered. Make sure it's decorated with @InputObjectType decorator`, 62 | ); 63 | } 64 | 65 | const compiler = inputObjectTypeRegistry.get(target); 66 | 67 | return compiler(); 68 | } 69 | -------------------------------------------------------------------------------- /src/domains/inputObjectType/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class InputObjectTypeError extends BaseError { 4 | constructor(target: Function, msg: string) { 5 | const fullMsg = `@InputObjectType '${target.name}': ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domains/inputObjectType/index.ts: -------------------------------------------------------------------------------- 1 | import { compileInputObjectTypeWithConfig } from './compiler'; 2 | import { inputObjectTypeRegistry } from './registry'; 3 | 4 | export { compileInputObjectType } from './compiler'; 5 | export { InputObjectTypeError } from './error'; 6 | export { inputObjectTypeRegistry } from './registry'; 7 | 8 | export interface InputObjectTypeOptions { 9 | name?: string; 10 | description?: string; 11 | } 12 | 13 | export function InputObjectType( 14 | options?: InputObjectTypeOptions, 15 | ): ClassDecorator { 16 | return (target: Function) => { 17 | const config = { name: target.name, ...options }; 18 | const inputTypeCompiler = () => 19 | compileInputObjectTypeWithConfig(target, config); 20 | inputObjectTypeRegistry.set(target, inputTypeCompiler); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/domains/inputObjectType/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType } from 'graphql'; 2 | 3 | type Getter = () => Result; 4 | 5 | export const inputObjectTypeRegistry = new WeakMap< 6 | Function, 7 | Getter 8 | >(); 9 | 10 | export interface TypeConfig { 11 | name: string; 12 | description: string; 13 | isNonNull?: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /src/domains/objectType/compiler/index.ts: -------------------------------------------------------------------------------- 1 | export { compileObjectType, compileObjectTypeWithConfig } from './objectType'; 2 | -------------------------------------------------------------------------------- /src/domains/objectType/compiler/objectType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import { ObjectTypeError, objectTypeRegistry } from '../index'; 3 | 4 | import { compileAllFields, fieldsRegistry } from '~/domains/field'; 5 | import { 6 | createCachedThunk, 7 | getClassWithAllParentClasses, 8 | } from '~/services/utils'; 9 | 10 | const compileOutputTypeCache = new WeakMap(); 11 | 12 | export interface TypeOptions { 13 | name: string; 14 | description?: string; 15 | } 16 | 17 | function createTypeFieldsGetter(target: Function) { 18 | const targetWithParents = getClassWithAllParentClasses(target); 19 | const hasFields = targetWithParents.some(ancestor => { 20 | return !fieldsRegistry.isEmpty(ancestor); 21 | }); 22 | 23 | if (!hasFields) { 24 | throw new ObjectTypeError(target, `There are no fields inside this type.`); 25 | } 26 | 27 | return createCachedThunk(() => { 28 | return compileAllFields(target); 29 | }); 30 | } 31 | 32 | export function compileObjectTypeWithConfig( 33 | target: Function, 34 | config: TypeOptions, 35 | ): GraphQLObjectType { 36 | if (compileOutputTypeCache.has(target)) { 37 | return compileOutputTypeCache.get(target); 38 | } 39 | 40 | const compiled = new GraphQLObjectType({ 41 | ...config, 42 | isTypeOf: value => value instanceof target, 43 | fields: createTypeFieldsGetter(target), 44 | }); 45 | 46 | compileOutputTypeCache.set(target, compiled); 47 | return compiled; 48 | } 49 | 50 | export function compileObjectType(target: Function) { 51 | if (!objectTypeRegistry.has(target)) { 52 | throw new ObjectTypeError( 53 | target, 54 | `Class is not registered. Make sure it's decorated with @ObjectType decorator`, 55 | ); 56 | } 57 | 58 | const compiler = objectTypeRegistry.get(target); 59 | return compiler(); 60 | } 61 | -------------------------------------------------------------------------------- /src/domains/objectType/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class ObjectTypeError extends BaseError { 4 | constructor(target: Function, msg: string) { 5 | const fullMsg = `@ObjectType '${target.name}': ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domains/objectType/index.ts: -------------------------------------------------------------------------------- 1 | import { compileObjectTypeWithConfig } from './compiler'; 2 | import { objectTypeRegistry } from './registry'; 3 | 4 | export { compileObjectType } from './compiler'; 5 | export { ObjectTypeError } from './error'; 6 | export { objectTypeRegistry, inputTypeRegistry } from './registry'; 7 | 8 | export interface ObjectTypeOptions { 9 | name?: string; 10 | description?: string; 11 | } 12 | 13 | export function ObjectType(options?: ObjectTypeOptions): ClassDecorator { 14 | return (target: Function) => { 15 | const config = { name: target.name, ...options }; 16 | const outputTypeCompiler = () => 17 | compileObjectTypeWithConfig(target, config); 18 | objectTypeRegistry.set(target, outputTypeCompiler); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/domains/objectType/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputType, GraphQLObjectType } from 'graphql'; 2 | 3 | type Getter = () => Result; 4 | 5 | export const objectTypeRegistry = new WeakMap< 6 | Function, 7 | Getter 8 | >(); 9 | export const inputTypeRegistry = new WeakMap< 10 | Function, 11 | Getter 12 | >(); 13 | 14 | export interface TypeConfig { 15 | name: string; 16 | description: string; 17 | isNonNull?: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/domains/schema/compiler.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLObjectType, GraphQLFieldConfig } from 'graphql'; 2 | import { 3 | queryFieldsRegistry, 4 | mutationFieldsRegistry, 5 | RootFieldsRegistry, 6 | } from './registry'; 7 | import { SchemaRootError } from './error'; 8 | import { showDeprecationWarning } from '~/services/utils'; 9 | 10 | import { validateSchemaRoots } from "./services"; 11 | 12 | export interface CompileSchemaOptions { 13 | roots: Function[]; 14 | } 15 | 16 | function getAllRootFieldsFromRegistry( 17 | roots: Function[], 18 | registry: RootFieldsRegistry, 19 | name: 'Query' | 'Mutation', 20 | ): GraphQLObjectType { 21 | const allRootFields: { [key: string]: GraphQLFieldConfig } = {}; 22 | for (let root of roots) { 23 | const rootFields = registry.getAll(root); 24 | Object.keys(rootFields).forEach(fieldName => { 25 | const fieldConfigGetter = rootFields[fieldName]; 26 | const fieldConfig = fieldConfigGetter(); 27 | 28 | // throw error if root field with this name is already registered 29 | if (!!allRootFields[fieldName]) { 30 | throw new SchemaRootError( 31 | root, 32 | `Duplicate of root field name: '${fieldName}'. Seems this name is also used inside other schema root.`, 33 | ); 34 | } 35 | allRootFields[fieldName] = fieldConfig; 36 | }); 37 | } 38 | 39 | const isEmpty = Object.keys(allRootFields).length < 1; 40 | 41 | if (isEmpty) { 42 | return null; 43 | } 44 | 45 | return new GraphQLObjectType({ 46 | name, 47 | fields: allRootFields, 48 | }); 49 | } 50 | 51 | export function compileSchema(config: CompileSchemaOptions | Function) { 52 | const roots = typeof config === 'function' ? [config] : config.roots; 53 | 54 | if (typeof config === 'function') { 55 | showDeprecationWarning( 56 | `Passing schema root to compileSchema is deprecated. Use config object with 'roots' field. compileSchema(SchemaRoot) --> compileSchema({ roots: [SchemaRoot] })`, 57 | config, 58 | ); 59 | } 60 | 61 | validateSchemaRoots(roots); 62 | 63 | const query = getAllRootFieldsFromRegistry( 64 | roots, 65 | queryFieldsRegistry, 66 | 'Query', 67 | ); 68 | const mutation = getAllRootFieldsFromRegistry( 69 | roots, 70 | mutationFieldsRegistry, 71 | 'Mutation', 72 | ); 73 | 74 | if (!query) { 75 | throw new Error( 76 | 'At least one of schema roots must have @Query root field.', 77 | ); 78 | } 79 | 80 | return new GraphQLSchema({ 81 | query: query || undefined, 82 | mutation: mutation || undefined, 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /src/domains/schema/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class SchemaRootError extends BaseError { 4 | constructor(target: Function, msg: string) { 5 | const fullMsg = `@Schema ${target.name}: ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | 11 | export class SchemaFieldError extends BaseError { 12 | constructor(target: Function, fieldName: string, msg: string) { 13 | const fullMsg = `@Schema ${target.name}.${fieldName}: ${msg}`; 14 | super(fullMsg); 15 | this.message = fullMsg; 16 | } 17 | } 18 | export class SchemaCompilationError extends BaseError { 19 | constructor(msg: string) { 20 | const fullMsg = `SchemaCompilationError: ${msg}`; 21 | super(fullMsg); 22 | this.message = fullMsg; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/domains/schema/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | schemaRootsRegistry, 3 | mutationFieldsRegistry, 4 | queryFieldsRegistry, 5 | } from './registry'; 6 | import { schemaRootsRegistry, SchemaRootConfig } from './registry'; 7 | import { showDeprecationWarning } from '~/services/utils'; 8 | // import { compileSchema } from './compiler'; 9 | export { compileSchema } from './compiler'; 10 | export { Query, Mutation } from './rootFields'; 11 | export { isSchemaRoot, getSchemaRootInstance } from './services' 12 | 13 | export function SchemaRoot(config: SchemaRootConfig = {}): ClassDecorator { 14 | return target => { 15 | schemaRootsRegistry.set(target, config); 16 | }; 17 | } 18 | 19 | export function Schema(config: SchemaRootConfig = {}) { 20 | showDeprecationWarning( 21 | 'Use @SchemaRoot instead and compile like: compileSchema({ roots: [RootA, RootB] })', 22 | Schema, 23 | ); 24 | return SchemaRoot(config); 25 | } 26 | -------------------------------------------------------------------------------- /src/domains/schema/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfig } from 'graphql'; 2 | import { DeepWeakMap } from '~/services/utils'; 3 | 4 | export type Getter = () => Result; 5 | 6 | export interface SchemaRootConfig {} 7 | 8 | export const schemaRootsRegistry = new WeakMap(); 9 | 10 | export type RootFieldsRegistry = DeepWeakMap< 11 | Function, 12 | Getter> 13 | >; 14 | 15 | export const queryFieldsRegistry = new DeepWeakMap< 16 | Function, 17 | Getter> 18 | >(); 19 | 20 | export const mutationFieldsRegistry = new DeepWeakMap< 21 | Function, 22 | Getter> 23 | >(); 24 | -------------------------------------------------------------------------------- /src/domains/schema/rootFields.ts: -------------------------------------------------------------------------------- 1 | import { Field, FieldOptions, compileFieldConfig } from '~/domains/field'; 2 | import { 3 | queryFieldsRegistry, 4 | mutationFieldsRegistry, 5 | schemaRootsRegistry, 6 | } from './registry'; 7 | import { SchemaFieldError } from './error'; 8 | 9 | function validateRootSchemaField(targetInstance: Object, fieldName: string) { 10 | if ( 11 | !(targetInstance as any)[fieldName] && 12 | !targetInstance.constructor.prototype[fieldName] 13 | ) { 14 | throw new SchemaFieldError( 15 | targetInstance.constructor, 16 | fieldName, 17 | `Every root schema field must regular class function`, 18 | ); 19 | } 20 | } 21 | 22 | function requireSchemaRoot(target: Function, fieldName: string) { 23 | if (schemaRootsRegistry.has(target)) { 24 | return; 25 | } 26 | throw new SchemaFieldError( 27 | target, 28 | fieldName, 29 | `Root field must be registered on class decorated with @Schema`, 30 | ); 31 | } 32 | 33 | function getFieldCompiler(target: Function, fieldName: string) { 34 | const fieldCompiler = () => { 35 | requireSchemaRoot(target, fieldName); 36 | const compiledField = compileFieldConfig( 37 | target, 38 | fieldName, 39 | ); 40 | return compiledField; 41 | }; 42 | 43 | return fieldCompiler; 44 | } 45 | 46 | // special fields 47 | export function Query(options?: FieldOptions): PropertyDecorator { 48 | return (targetInstance: Object, fieldName: string) => { 49 | validateRootSchemaField(targetInstance, fieldName); 50 | Field(options)(targetInstance, fieldName); 51 | const fieldCompiler = getFieldCompiler(targetInstance.constructor, fieldName); 52 | queryFieldsRegistry.set( 53 | targetInstance.constructor, 54 | fieldName, 55 | fieldCompiler, 56 | ); 57 | }; 58 | } 59 | 60 | export function Mutation(options?: FieldOptions): PropertyDecorator { 61 | return (targetInstance: Object, fieldName: string) => { 62 | validateRootSchemaField(targetInstance, fieldName); 63 | Field(options)(targetInstance, fieldName); 64 | const fieldCompiler = getFieldCompiler(targetInstance.constructor, fieldName); 65 | mutationFieldsRegistry.set( 66 | targetInstance.constructor, 67 | fieldName, 68 | fieldCompiler, 69 | ); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/domains/schema/services.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from "~/services/types"; 2 | import { 3 | schemaRootsRegistry, 4 | } from './registry'; 5 | import { SchemaRootError, SchemaCompilationError } from './error'; 6 | 7 | function hasDuplicates(arr: Function[]) { 8 | return (new Set(arr)).size !== arr.length; 9 | } 10 | 11 | export function isSchemaRoot(base: Function) { 12 | return schemaRootsRegistry.has(base); 13 | } 14 | 15 | export function validateSchemaRoots(roots: Function[]) { 16 | if (hasDuplicates(roots)) { 17 | throw new SchemaCompilationError(`At least one schema root is provided more than once in schema roots`); 18 | } 19 | for (let root of roots) { 20 | if (!schemaRootsRegistry.has(root)) { 21 | throw new SchemaRootError( 22 | root, 23 | `Schema root must be registered with @SchemaRoot`, 24 | ); 25 | } 26 | } 27 | } 28 | 29 | const schemaRootInstances = new WeakMap(); 30 | 31 | export function getSchemaRootInstance(schemaRootClass: Function) { 32 | if (!isSchemaRoot(schemaRootClass)) { 33 | return null; 34 | } 35 | 36 | if (schemaRootInstances.has(schemaRootClass)) { 37 | return schemaRootInstances.get(schemaRootClass); 38 | } 39 | const instance = new (schemaRootClass as Constructable)(); 40 | schemaRootInstances.set(schemaRootClass, instance); 41 | return instance; 42 | } -------------------------------------------------------------------------------- /src/domains/union/compiler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLUnionType, 3 | GraphQLObjectType, 4 | GraphQLResolveInfo, 5 | GraphQLType, 6 | } from 'graphql'; 7 | 8 | import { resolveTypesList, isObjectType, resolveType } from '~/services/utils'; 9 | import { Thunk } from '~/services/types'; 10 | import { UnionError } from './error'; 11 | 12 | export interface UnionTypeResolver { 13 | (value: any, context: any, info: GraphQLResolveInfo): any; 14 | } 15 | 16 | export interface UnionOptions { 17 | types: Thunk; 18 | name: string; 19 | resolveTypes?: UnionTypeResolver; 20 | } 21 | 22 | const compileUnionCache = new WeakMap(); 23 | 24 | function getDefaultResolver(types: GraphQLObjectType[]): UnionTypeResolver { 25 | return (value: any, context: any, info: any) => { 26 | for (let type of types) { 27 | if (type.isTypeOf && type.isTypeOf(value, context, info)) { 28 | return type; 29 | } 30 | } 31 | }; 32 | } 33 | 34 | /** 35 | * Resolves type, and if needed, tries to resolve it using typegql-aware types 36 | */ 37 | function enhanceTypeResolver( 38 | originalResolver: UnionTypeResolver, 39 | ): UnionTypeResolver { 40 | return (value, context, info) => { 41 | const rawResolvedType = originalResolver(value, context, info); 42 | return resolveType(rawResolvedType); 43 | }; 44 | } 45 | 46 | function validateResolvedTypes( 47 | target: Function, 48 | types: GraphQLType[], 49 | ): types is GraphQLObjectType[] { 50 | for (let type of types) { 51 | if (!isObjectType(type)) { 52 | throw new UnionError( 53 | target, 54 | `Every union type must be of type GraphQLObjectType. '${type}' is not.`, 55 | ); 56 | } 57 | } 58 | return true; 59 | } 60 | 61 | export function compileUnionType(target: Function, config: UnionOptions) { 62 | if (compileUnionCache.has(target)) { 63 | return compileUnionCache.get(target); 64 | } 65 | 66 | const { types, resolveTypes, name } = config; 67 | 68 | const resolvedTypes = resolveTypesList(types); 69 | 70 | if (!validateResolvedTypes(target, resolvedTypes)) { 71 | return; 72 | } 73 | 74 | const typeResolver = resolveTypes 75 | ? enhanceTypeResolver(resolveTypes) 76 | : getDefaultResolver(resolvedTypes); 77 | 78 | const compiled = new GraphQLUnionType({ 79 | name, 80 | resolveType: typeResolver, 81 | types: resolvedTypes as GraphQLObjectType[], 82 | }); 83 | 84 | compileUnionCache.set(target, compiled); 85 | return compiled; 86 | } 87 | -------------------------------------------------------------------------------- /src/domains/union/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '~/services/error'; 2 | 3 | export class UnionError extends BaseError { 4 | constructor(target: Function, msg: string) { 5 | const fullMsg = `@Union '${target.name}': ${msg}`; 6 | super(fullMsg); 7 | this.message = fullMsg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domains/union/index.ts: -------------------------------------------------------------------------------- 1 | import { Thunk } from '~/services/types'; 2 | import { unionRegistry } from './registry'; 3 | export { unionRegistry } from './registry'; 4 | 5 | import { compileUnionType, UnionTypeResolver } from './compiler'; 6 | 7 | export interface UnionOptions { 8 | name?: string; 9 | resolveTypes?: UnionTypeResolver; 10 | types: Thunk; 11 | } 12 | 13 | export function Union(config: UnionOptions): ClassDecorator { 14 | return target => { 15 | unionRegistry.set(target, () => { 16 | return compileUnionType(target, { name: target.name, ...config }); 17 | }); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/domains/union/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLUnionType } from 'graphql'; 2 | 3 | export const unionRegistry = new WeakMap GraphQLUnionType>(); 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | GraphQLFloat as Float, 3 | GraphQLInt as Int, 4 | GraphQLID as ID, 5 | } from 'graphql'; 6 | 7 | export { 8 | Arg, 9 | Field, 10 | Info, 11 | SchemaRoot, 12 | Context, 13 | ObjectType, 14 | Query, 15 | Mutation, 16 | InputField, 17 | InputObjectType, 18 | Union, 19 | Inject, 20 | Source, 21 | compileSchema, 22 | compileObjectType, 23 | compileInputObjectType, 24 | registerEnum, 25 | Schema, 26 | After, 27 | Before, 28 | } from './domains'; 29 | -------------------------------------------------------------------------------- /src/services/error.ts: -------------------------------------------------------------------------------- 1 | export abstract class BaseError extends Error {} 2 | -------------------------------------------------------------------------------- /src/services/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Thunk = Result | (() => Result); 2 | 3 | export type Constructable = { new (): any }; 4 | -------------------------------------------------------------------------------- /src/services/utils/cachedThunk.ts: -------------------------------------------------------------------------------- 1 | const cache = new WeakMap<() => any, any>(); 2 | 3 | export function createCachedThunk( 4 | thunk: () => ThunkResult, 5 | ): () => ThunkResult { 6 | return () => { 7 | if (cache.has(thunk)) { 8 | return cache.get(thunk); 9 | } 10 | const result = thunk(); 11 | cache.set(thunk, result); 12 | return result; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/services/utils/deepWeakMap/index.ts: -------------------------------------------------------------------------------- 1 | import * as objectPath from 'object-path'; 2 | 3 | export type DeepWeakMapPath = (string | number) | (string | number)[]; 4 | 5 | function flattenPaths(paths: DeepWeakMapPath[]): string[] { 6 | return paths.reduce((accumulatedPath: string[], nextPath) => { 7 | if (Array.isArray(nextPath)) { 8 | return [...accumulatedPath, ...nextPath.map(pathPart => `${pathPart}`)]; 9 | } 10 | return [...accumulatedPath, `${nextPath}`]; 11 | }, []) as string[]; 12 | } 13 | 14 | export class DeepWeakMap< 15 | Key extends Object, 16 | Value, 17 | Structure = { [key: string]: Value } 18 | > { 19 | private map: WeakMap; 20 | constructor() { 21 | this.map = new WeakMap(); 22 | } 23 | 24 | isEmpty(target: Key) { 25 | return !Object.keys(this.getAll(target)).length; 26 | } 27 | 28 | getAll(target: Key): Structure { 29 | const { map } = this; 30 | if (!map.has(target)) { 31 | map.set(target, {} as Structure); 32 | } 33 | return map.get(target); 34 | } 35 | 36 | set(target: Key, path: DeepWeakMapPath, value: Value) { 37 | objectPath.set(this.getAll(target) as any, path, value); 38 | } 39 | 40 | get(target: Key, ...paths: DeepWeakMapPath[]): Value { 41 | const path = flattenPaths(paths); 42 | return objectPath.get(this.getAll(target) as any, path); 43 | } 44 | 45 | has(target: Key, ...paths: DeepWeakMapPath[]): boolean { 46 | const path = flattenPaths(paths); 47 | return !!this.get(target, path); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/services/utils/deprecation/index.ts: -------------------------------------------------------------------------------- 1 | const shownRegistry = new WeakMap(); 2 | 3 | export type Logger = (msg: string) => void; 4 | 5 | export function showDeprecationWarning( 6 | message: string, 7 | uniqueIdentifier?: any, 8 | logger: Logger = console.log, 9 | ) { 10 | if (uniqueIdentifier && shownRegistry.has(uniqueIdentifier)) { 11 | return; 12 | } 13 | if (uniqueIdentifier) { 14 | shownRegistry.set(uniqueIdentifier, true); 15 | } 16 | logger(`@Deprecation warning: ${message}`); 17 | } 18 | -------------------------------------------------------------------------------- /src/services/utils/getParameterNames.ts: -------------------------------------------------------------------------------- 1 | var COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm; 2 | var DEFAULT_PARAMS = /=[^,]+/gm; 3 | var FAT_ARROWS = /=>.*$/gm; 4 | 5 | export function getParameterNames(fn: Function): string[] { 6 | var code = fn 7 | .toString() 8 | .replace(COMMENTS, '') 9 | .replace(FAT_ARROWS, '') 10 | .replace(DEFAULT_PARAMS, ''); 11 | 12 | var result = code 13 | .slice(code.indexOf('(') + 1, code.indexOf(')')) 14 | .match(/([^\s,]+)/g); 15 | 16 | return result === null ? [] : result; 17 | } 18 | -------------------------------------------------------------------------------- /src/services/utils/gql/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | parseNativeTypeToGraphQL, 3 | inferTypeByTarget, 4 | resolveType, 5 | resolveTypesList, 6 | } from './types'; 7 | 8 | export { isObjectType } from './validators'; 9 | -------------------------------------------------------------------------------- /src/services/utils/gql/types/index.ts: -------------------------------------------------------------------------------- 1 | export { parseNativeTypeToGraphQL, inferTypeByTarget } from './parseNative'; 2 | export { resolveType, resolveTypesList } from './resolve'; 3 | -------------------------------------------------------------------------------- /src/services/utils/gql/types/parseNative.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLString, 3 | GraphQLFloat, 4 | GraphQLBoolean, 5 | GraphQLScalarType, 6 | } from 'graphql'; 7 | import 'reflect-metadata'; 8 | export type ParsableScalar = String | Number | Boolean; 9 | 10 | export function isParsableScalar(input: any): input is ParsableScalar { 11 | return [String, Number, Boolean].includes(input); 12 | } 13 | 14 | export function parseNativeTypeToGraphQL(input: any): GraphQLScalarType { 15 | if (input === String) { 16 | return GraphQLString; 17 | } 18 | if (input === Number) { 19 | return GraphQLFloat; 20 | } 21 | if (input === Boolean) { 22 | return GraphQLBoolean; 23 | } 24 | } 25 | 26 | export function inferTypeByTarget(target: Function, key?: string) { 27 | if (!key) { 28 | return Reflect.getMetadata('design:type', target); 29 | } 30 | 31 | const returnType = Reflect.getMetadata('design:returntype', target, key); 32 | if (returnType) { 33 | return returnType; 34 | } 35 | 36 | const type = Reflect.getMetadata('design:type', target, key); 37 | return type; 38 | } 39 | -------------------------------------------------------------------------------- /src/services/utils/gql/types/resolve.ts: -------------------------------------------------------------------------------- 1 | import { isType, GraphQLType, GraphQLList, GraphQLNonNull } from 'graphql'; 2 | import { Thunk } from '~/services/types'; 3 | import { 4 | objectTypeRegistry, 5 | compileObjectType, 6 | inputObjectTypeRegistry, 7 | compileInputObjectType, 8 | enumsRegistry, 9 | unionRegistry, 10 | } from '~/domains'; 11 | import { parseNativeTypeToGraphQL, isParsableScalar } from './parseNative'; 12 | 13 | /** 14 | * 15 | * @param input 16 | * @param allowThunk 17 | * @param preferInputType We want to be able to have single class used both for output and input type - thats why we need to be able to set resolve priority in different scenarios 18 | */ 19 | export function resolveType( 20 | input: any, 21 | preferInputType = false, 22 | allowThunk = true, 23 | ): GraphQLType { 24 | if (isType(input)) { 25 | return input; 26 | } 27 | 28 | if (isParsableScalar(input)) { 29 | return parseNativeTypeToGraphQL(input); 30 | } 31 | 32 | if (Array.isArray(input)) { 33 | return resolveListType(input); 34 | } 35 | 36 | if (enumsRegistry.has(input)) { 37 | return enumsRegistry.get(input); 38 | } 39 | 40 | if (unionRegistry.has(input)) { 41 | return unionRegistry.get(input)(); 42 | } 43 | 44 | if (preferInputType && inputObjectTypeRegistry.has(input)) { 45 | return compileInputObjectType(input); 46 | } 47 | 48 | if (objectTypeRegistry.has(input)) { 49 | return compileObjectType(input); 50 | } 51 | 52 | if (inputObjectTypeRegistry.has(input)) { 53 | return compileInputObjectType(input); 54 | } 55 | 56 | if (input === Promise) { 57 | return; 58 | } 59 | 60 | if (!allowThunk || typeof input !== 'function') { 61 | return; 62 | } 63 | 64 | try { 65 | return resolveType(input(), preferInputType, false); 66 | } catch (error) { 67 | return; 68 | } 69 | } 70 | 71 | function resolveListType(input: any[], preferInputType = false): GraphQLType { 72 | if (input.length !== 1) { 73 | return; 74 | } 75 | const [itemType] = input; 76 | 77 | const resolvedItemType = resolveType(itemType, preferInputType); 78 | 79 | if (!resolvedItemType) { 80 | return; 81 | } 82 | return new GraphQLList(new GraphQLNonNull(resolvedItemType)); 83 | } 84 | 85 | export function resolveTypesList( 86 | types: Thunk, 87 | preferInputType = false, 88 | ): GraphQLType[] { 89 | if (Array.isArray(types)) { 90 | return types.map(type => { 91 | return resolveType(type, preferInputType); 92 | }); 93 | } 94 | return types().map(type => { 95 | return resolveType(type, preferInputType); 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /src/services/utils/gql/validators.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | 3 | export function isObjectType(input: any): input is GraphQLObjectType { 4 | return typeof input.getFields === 'function'; // TODO: More precise 5 | } 6 | -------------------------------------------------------------------------------- /src/services/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { createCachedThunk } from './cachedThunk'; 2 | export { getParameterNames } from './getParameterNames'; 3 | export { getClassWithAllParentClasses } from './inheritance'; 4 | export { DeepWeakMap } from './deepWeakMap'; 5 | export { 6 | parseNativeTypeToGraphQL, 7 | inferTypeByTarget, 8 | resolveType, 9 | resolveTypesList, 10 | isObjectType, 11 | } from './gql'; 12 | 13 | export { showDeprecationWarning } from './deprecation'; 14 | 15 | -------------------------------------------------------------------------------- /src/services/utils/inheritance/index.ts: -------------------------------------------------------------------------------- 1 | export function getClassWithAllParentClasses(target: Function) { 2 | const result = [target]; 3 | let currentNode = target; 4 | while (Object.getPrototypeOf(currentNode)) { 5 | const parent = Object.getPrototypeOf(currentNode); 6 | 7 | if (parent === Function.prototype) break; 8 | result.push(parent); 9 | currentNode = parent; 10 | } 11 | return result.reverse(); // reverse so we go from parents to children 12 | } 13 | -------------------------------------------------------------------------------- /src/test/arg/__snapshots__/complex.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Complex arguments should not allow complex argument type decorated with @ObjectType 1`] = `"@Type Foo.bar(input <-------): Argument has incorrect type. Make sure to use native GraphQLInputType, native scalar like 'String' or class decorated with @InputObjectType"`; 4 | 5 | exports[`Complex arguments should not allow complex argument type not decorated with @InputObjectType 1`] = `"@Type Foo.bar(input <-------): Could not infer type of argument. Make sure to use native GraphQLInputType, native scalar like 'String' or class decorated with @InputObjectType"`; 6 | -------------------------------------------------------------------------------- /src/test/arg/__snapshots__/infering.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Arguments Throws if is not able to infer arguemtn type without @Arg decorator 1`] = `"@Type Foo.bar(baz <-------): Could not infer type of argument. Make sure to use native GraphQLInputType, native scalar like 'String' or class decorated with @InputObjectType"`; 4 | -------------------------------------------------------------------------------- /src/test/arg/basics.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString, GraphQLNonNull } from 'graphql'; 2 | import { Field, ObjectType, compileObjectType, Arg } from '~/domains'; 3 | 4 | describe('Arguments with @Arg', () => { 5 | it('Allows setting argument with @Arg decorator', () => { 6 | @ObjectType() 7 | class Foo { 8 | @Field() 9 | bar(@Arg() baz: string): string { 10 | return baz; 11 | } 12 | } 13 | const { bar } = compileObjectType(Foo).getFields(); 14 | 15 | expect(bar.args.length).toEqual(1); 16 | const [bazArg] = bar.args; 17 | expect(bazArg.type).toEqual(new GraphQLNonNull(GraphQLString)); 18 | expect(bazArg.name).toBe('baz'); 19 | }); 20 | 21 | it('Allows setting custom @Arg description', () => { 22 | @ObjectType() 23 | class Foo { 24 | @Field() 25 | bar( 26 | @Arg({ description: 'test' }) 27 | baz: string, 28 | ): string { 29 | return baz; 30 | } 31 | } 32 | const [bazArg] = compileObjectType(Foo).getFields().bar.args; 33 | expect(bazArg.description).toBe('test'); 34 | }); 35 | 36 | it('Is passing argument value to resolver properly and in proper order', async () => { 37 | @ObjectType() 38 | class Foo { 39 | @Field() 40 | bar(aaa: string, zzz: string): string { 41 | return `${aaa}.${zzz}`; 42 | } 43 | } 44 | const { bar } = compileObjectType(Foo).getFields(); 45 | const resolvedValue = await bar.resolve( 46 | new Foo(), 47 | { zzz: 'zzz', aaa: 'aaa' }, 48 | null, 49 | null, 50 | ); 51 | expect(resolvedValue).toEqual('aaa.zzz'); 52 | }); 53 | 54 | it('Is properly passing `this` argument', async () => { 55 | @ObjectType() 56 | class Foo { 57 | private instanceVar = 'instance'; 58 | @Field() 59 | bar(param: string): string { 60 | return `${this.instanceVar}.${param}`; 61 | } 62 | } 63 | const { bar } = compileObjectType(Foo).getFields(); 64 | const resolvedValue = await bar.resolve( 65 | new Foo(), 66 | { param: 'param' }, 67 | null, 68 | null, 69 | ); 70 | expect(resolvedValue).toEqual('instance.param'); 71 | }); 72 | 73 | it('Respects isNullable @Arg option', () => { 74 | @ObjectType() 75 | class Foo { 76 | @Field() 77 | bar( 78 | @Arg({ isNullable: true }) 79 | baz: string, 80 | @Arg({ isNullable: false }) 81 | bazRequired: string, 82 | ): string { 83 | return baz; 84 | } 85 | } 86 | const [bazArg, bazRequiredArg] = compileObjectType( 87 | Foo, 88 | ).getFields().bar.args; 89 | expect(bazArg.type).toBe(GraphQLString); 90 | expect(bazRequiredArg.type).toEqual(new GraphQLNonNull(GraphQLString)); 91 | }); 92 | 93 | it('Will allow registering argument at runtime', () => { 94 | @ObjectType() 95 | class Foo { 96 | @Field() 97 | bar( 98 | baz: string, 99 | bazRequired: string, 100 | ): string { 101 | return baz; 102 | } 103 | } 104 | 105 | Arg({type: String, isNullable: true})(Foo.prototype, 'bar', 0); 106 | Arg({type: String, isNullable: false})(Foo.prototype, 'bar', 1); 107 | 108 | const [bazArg, bazRequiredArg] = compileObjectType( 109 | Foo, 110 | ).getFields().bar.args; 111 | 112 | expect(bazArg.type).toBe(GraphQLString); 113 | expect(bazRequiredArg.type).toEqual(new GraphQLNonNull(GraphQLString)); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/test/arg/complex.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, getNamedType, GraphQLString } from 'graphql'; 2 | import { 3 | Field, 4 | ObjectType, 5 | compileObjectType, 6 | compileInputObjectType, 7 | InputField, 8 | InputObjectType, 9 | Arg, 10 | } from '~/domains'; 11 | 12 | describe('Complex arguments', () => { 13 | it('should not allow complex argument type not decorated with @InputObjectType', async () => { 14 | class Input { 15 | @InputField() bar: string; 16 | } 17 | 18 | @ObjectType() 19 | class Foo { 20 | @Field() 21 | bar(input: Input): string { 22 | return 'ok'; 23 | } 24 | } 25 | expect(() => { 26 | compileObjectType(Foo).getFields(); 27 | }).toThrowErrorMatchingSnapshot(); 28 | }); 29 | 30 | it('should not allow complex argument type decorated with @ObjectType', async () => { 31 | @ObjectType() 32 | class Input { 33 | @Field() bar: string; 34 | } 35 | 36 | @ObjectType() 37 | class Foo { 38 | @Field() 39 | bar(input: Input): string { 40 | return 'ok'; 41 | } 42 | } 43 | 44 | expect(() => 45 | compileObjectType(Foo).getFields(), 46 | ).toThrowErrorMatchingSnapshot(); 47 | }); 48 | it('Supports complex input types', async () => { 49 | @InputObjectType() 50 | class Input { 51 | @InputField() bar: string; 52 | } 53 | 54 | @ObjectType() 55 | class Foo { 56 | @Field() 57 | bar(input: Input): string { 58 | return 'ok'; 59 | } 60 | } 61 | const { bar } = compileObjectType(Foo).getFields(); 62 | expect(bar.args[0].type).toEqual( 63 | new GraphQLNonNull(compileInputObjectType(Input)), 64 | ); 65 | }); 66 | 67 | it('Supports scalar list argument type', () => { 68 | @ObjectType() 69 | class Foo { 70 | @Field() 71 | bar( 72 | @Arg({ type: [String] }) 73 | input: string[], 74 | ): string { 75 | return 'ok'; 76 | } 77 | } 78 | const { bar } = compileObjectType(Foo).getFields(); 79 | const argType = bar.args[0].type; 80 | expect(argType.toString()).toEqual('[String!]!'); 81 | expect(getNamedType(argType)).toEqual(GraphQLString); 82 | }); 83 | 84 | it('Supports nested list argument type', () => { 85 | @InputObjectType() 86 | class Input { 87 | @InputField() bar: string; 88 | } 89 | 90 | @ObjectType() 91 | class Foo { 92 | @Field() 93 | bar( 94 | @Arg({ type: [Input] }) 95 | input: Input[], 96 | ): string { 97 | return 'ok'; 98 | } 99 | } 100 | const { bar } = compileObjectType(Foo).getFields(); 101 | const argType = bar.args[0].type; 102 | expect(argType.toString()).toEqual('[Input!]!'); 103 | expect(getNamedType(argType)).toEqual(compileInputObjectType(Input)); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/test/arg/infering.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString, GraphQLFloat, GraphQLNonNull } from 'graphql'; 2 | import { Field, ObjectType, compileObjectType } from '~/domains'; 3 | 4 | describe('Arguments', () => { 5 | it('Infers basic arguments without @Arg decorator', () => { 6 | @ObjectType() 7 | class Foo { 8 | @Field() 9 | bar(baz: string): string { 10 | return baz; 11 | } 12 | } 13 | const { bar } = compileObjectType(Foo).getFields(); 14 | 15 | expect(bar.args.length).toBeGreaterThan(0); 16 | const [bazArg] = bar.args; 17 | expect(bazArg.type).toEqual(new GraphQLNonNull(GraphQLString)); 18 | expect(bazArg.name).toBe('baz'); 19 | }); 20 | 21 | it('Throws if is not able to infer arguemtn type without @Arg decorator', () => { 22 | @ObjectType() 23 | class Foo { 24 | @Field() 25 | bar(baz: any): string { 26 | return baz; 27 | } 28 | } 29 | expect(() => 30 | compileObjectType(Foo).getFields(), 31 | ).toThrowErrorMatchingSnapshot(); 32 | }); 33 | 34 | it('Infers multiple basic arguments without @Arg decorator', () => { 35 | @ObjectType() 36 | class Foo { 37 | @Field() 38 | bar(baz: string, boo: number): string { 39 | return baz; 40 | } 41 | } 42 | const { bar } = compileObjectType(Foo).getFields(); 43 | 44 | expect(bar.args.length).toEqual(2); 45 | const [bazArg, booArg] = bar.args; 46 | expect(bazArg.type).toEqual(new GraphQLNonNull(GraphQLString)); 47 | expect(bazArg.name).toEqual('baz'); 48 | expect(booArg.name).toEqual('boo'); 49 | expect(booArg.type).toEqual(new GraphQLNonNull(GraphQLFloat)); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/test/enum/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Enums Throw when registering the same enum twice 1`] = `"Enum Foo2: Enum is already registered"`; 4 | -------------------------------------------------------------------------------- /src/test/enum/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { registerEnum } from '~/domains'; 2 | import { resolveType } from '~/services/utils'; 3 | 4 | describe('Enums', () => { 5 | it('Registers returns proper enum type', () => { 6 | enum Foo { 7 | Bar, 8 | Baz, 9 | } 10 | 11 | const enumType = registerEnum(Foo, 'Foo'); 12 | expect(enumType.name).toEqual('Foo'); 13 | expect(enumType.getValues().length).toEqual(2); 14 | expect(enumType.getValues()[0].name).toEqual('Bar'); 15 | expect(enumType.getValues()[0].value).toEqual(0); 16 | }); 17 | 18 | it('Registers returns proper enum type with string based enums', () => { 19 | enum Foo { 20 | Bar = 'Test', 21 | Baz = 'Test2', 22 | } 23 | 24 | const enumType = registerEnum(Foo, 'Foo'); 25 | expect(enumType.name).toEqual('Foo'); 26 | expect(enumType.getValues().length).toEqual(2); 27 | expect(enumType.getValues()[1].name).toEqual('Baz'); 28 | expect(enumType.getValues()[1].value).toEqual('Test2'); 29 | }); 30 | 31 | it('Throw when registering the same enum twice', () => { 32 | enum Foo { 33 | Bar = 'Test', 34 | Baz = 'Test2', 35 | } 36 | 37 | registerEnum(Foo, { name: 'Foo' }); 38 | expect(() => 39 | registerEnum(Foo, { name: 'Foo2' }), 40 | ).toThrowErrorMatchingSnapshot(); 41 | }); 42 | 43 | it('Will properly resolve registered enum', () => { 44 | enum Foo { 45 | Bar = 'Test', 46 | Baz = 'Test2', 47 | } 48 | 49 | const enumType = registerEnum(Foo, { name: 'Foo' }); 50 | expect(resolveType(Foo)).toEqual(enumType); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/test/field/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Field Shows proper error message when trying to use list type without being explicit about item type 1`] = `"@ObjectType Foo.bar: Field returns list so it's required to explicitly set list item type. You can set list type like: @Field({ type: [ItemType] })"`; 4 | 5 | exports[`Field Shows proper error message when trying to use promise type without being explicit about item type 1`] = `"@ObjectType Foo.bar: Field returns Promise so it's required to explicitly set resolved type as it's not possible to guess it. You can set resolved type like: @Field({ type: ItemType })"`; 6 | 7 | exports[`Field Throws if pointing to unregistered type 1`] = `"@ObjectType Bar.foo: Forced type is incorrect. Make sure to use either native graphql type or class that is registered with @Type decorator"`; 8 | 9 | exports[`Field Will not allow promise field without type addnotation 1`] = `"@ObjectType Foo.bar: Field returns Promise so it's required to explicitly set resolved type as it's not possible to guess it. You can set resolved type like: @Field({ type: ItemType })"`; 10 | -------------------------------------------------------------------------------- /src/test/field/__snapshots__/special-fields.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Special fields - @Query, @Mutation @Subscribe Will not allow registering special type on type that is not @Schema 1`] = `"@ObjectType Foo.bar: Given field is root field (@Query or @Mutation) not registered inside @Schema type. "`; 4 | -------------------------------------------------------------------------------- /src/test/field/getters.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFloat } from 'graphql'; 2 | import { ObjectType, Field, compileObjectType } from '~/domains'; 3 | 4 | describe('Fields based on getters', () => { 5 | it('Will work with getter field', async () => { 6 | @ObjectType({ description: 'Simple product object type' }) 7 | class Foo { 8 | @Field() 9 | get bar(): number { 10 | return 42; 11 | } 12 | } 13 | 14 | const { bar } = compileObjectType(Foo).getFields(); 15 | const resolvedValue = await bar.resolve(new Foo(), null, null, null); 16 | expect(resolvedValue).toEqual(42); 17 | expect(bar.type).toEqual(GraphQLFloat); 18 | }); 19 | 20 | it('Will not run getter when defining object type', async () => { 21 | const spy = jest.fn(); 22 | @ObjectType({ description: 'Simple product object type' }) 23 | class Foo { 24 | @Field() 25 | get bar(): number { 26 | console.log('spy'); 27 | spy(); 28 | return 42; 29 | } 30 | } 31 | 32 | const { bar } = compileObjectType(Foo).getFields(); 33 | expect(spy).not.toBeCalled(); 34 | expect(bar.type).toEqual(GraphQLFloat); 35 | }); 36 | 37 | it('Will run getter with proper context', async () => { 38 | @ObjectType({ description: 'Simple product object type' }) 39 | class Foo { 40 | constructor(public number: number) {} 41 | 42 | baz: number = 42; 43 | @Field() 44 | get bar(): number { 45 | return this.number + this.baz; 46 | } 47 | } 48 | 49 | const { bar } = compileObjectType(Foo).getFields(); 50 | const result = await bar.resolve(new Foo(100), null, null, null); 51 | 52 | expect(result).toEqual(142); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/test/field/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLString, 3 | GraphQLFloat, 4 | GraphQLBoolean, 5 | isNamedType, 6 | getNamedType, 7 | } from 'graphql'; 8 | import { ObjectType, Field, compileObjectType } from '~/domains'; 9 | 10 | import 'reflect-metadata'; 11 | 12 | describe('Field', () => { 13 | it('Resolves fields with default value', async () => { 14 | @ObjectType() 15 | class Foo { 16 | @Field() bar: string = 'baz'; 17 | } 18 | const compiled = compileObjectType(Foo); 19 | const barField = compiled.getFields().bar; 20 | 21 | expect(await barField.resolve(new Foo(), {}, null, null)).toEqual('baz'); 22 | }); 23 | 24 | it('Resolves fields with function resolver', async () => { 25 | @ObjectType() 26 | class Foo { 27 | @Field() 28 | bar(): string { 29 | return 'baz'; 30 | } 31 | } 32 | 33 | const compiled = compileObjectType(Foo); 34 | const barField = compiled.getFields().bar; 35 | 36 | expect(await barField.resolve(new Foo(), {}, null, null as any)).toEqual( 37 | 'baz', 38 | ); 39 | }); 40 | 41 | it('Handles description', () => { 42 | @ObjectType() 43 | class Foo { 44 | @Field({ description: 'test' }) 45 | bar: string = 'baz'; 46 | } 47 | expect(compileObjectType(Foo).getFields().bar.description).toEqual('test'); 48 | }); 49 | 50 | it('Handles custom name', async () => { 51 | @ObjectType() 52 | class Foo { 53 | @Field({ name: 'baz', description: 'test' }) 54 | bar: string = 'test'; 55 | } 56 | const compiled = compileObjectType(Foo); 57 | const bazField = compiled.getFields().baz; 58 | expect(compiled.getFields().bar).toBeFalsy(); 59 | expect(bazField).toBeTruthy(); 60 | expect(bazField.description).toEqual('test'); 61 | expect(await bazField.resolve(new Foo(), {}, null, null as any)).toBe( 62 | 'test', 63 | ); 64 | }); 65 | 66 | it('Properly infers basic scalar types', () => { 67 | @ObjectType() 68 | class Foo { 69 | @Field() bar: string; 70 | @Field() baz: number; 71 | @Field() foo: boolean; 72 | @Field() coo: boolean = false; 73 | @Field() 74 | boo(): boolean { 75 | return true; 76 | } 77 | } 78 | 79 | const { bar, baz, foo, boo, coo } = compileObjectType(Foo).getFields(); 80 | 81 | expect(bar.type).toEqual(GraphQLString); 82 | expect(baz.type).toEqual(GraphQLFloat); 83 | expect(foo.type).toEqual(GraphQLBoolean); 84 | expect(boo.type).toEqual(GraphQLBoolean); 85 | expect(coo.type).toEqual(GraphQLBoolean); 86 | }); 87 | 88 | it('Properly sets forced field type', () => { 89 | @ObjectType() 90 | class Foo { 91 | @Field({ type: () => GraphQLFloat }) 92 | bar: string; 93 | } 94 | 95 | const { bar } = compileObjectType(Foo).getFields(); 96 | expect(bar.type).toEqual(GraphQLFloat); 97 | }); 98 | 99 | it('Supports references to other types', () => { 100 | @ObjectType() 101 | class Foo { 102 | @Field() foo: string; 103 | } 104 | 105 | @ObjectType() 106 | class Bar { 107 | @Field() foo: Foo; 108 | } 109 | 110 | const { foo } = compileObjectType(Bar).getFields(); 111 | const compiledFoo = compileObjectType(Foo); 112 | expect(foo.type).toBe(compiledFoo); 113 | }); 114 | 115 | it('Supports references to itself', () => { 116 | @ObjectType() 117 | class Foo { 118 | @Field() fooNested: Foo; 119 | } 120 | 121 | const { fooNested } = compileObjectType(Foo).getFields(); 122 | expect(fooNested.type).toBe(compileObjectType(Foo)); 123 | }); 124 | 125 | it('Supports circular references', () => { 126 | @ObjectType() 127 | class Car { 128 | @Field({ type: () => Owner }) 129 | owner: any; 130 | } 131 | 132 | @ObjectType() 133 | class Owner { 134 | @Field({ type: () => Car }) 135 | car: any; 136 | } 137 | 138 | const { owner } = compileObjectType(Car).getFields(); 139 | const { car } = compileObjectType(Owner).getFields(); 140 | 141 | expect(owner.type).toBe(compileObjectType(Owner)); 142 | expect(car.type).toBe(compileObjectType(Car)); 143 | }); 144 | 145 | it('Throws if pointing to unregistered type', () => { 146 | class Foo {} 147 | 148 | @ObjectType() 149 | class Bar { 150 | @Field({ type: () => Foo }) 151 | foo: Foo; 152 | } 153 | 154 | expect(() => 155 | compileObjectType(Bar).getFields(), 156 | ).toThrowErrorMatchingSnapshot(); 157 | }); 158 | 159 | it('Properly resolves native scalar types', () => { 160 | @ObjectType() 161 | class Foo { 162 | @Field({ type: () => String }) 163 | bar: any; 164 | @Field({ type: () => Number }) 165 | baz: any; 166 | } 167 | 168 | const { bar, baz } = compileObjectType(Foo).getFields(); 169 | expect(bar.type).toBe(GraphQLString); 170 | expect(baz.type).toBe(GraphQLFloat); 171 | }); 172 | 173 | it('Shows proper error message when trying to use list type without being explicit about item type', () => { 174 | @ObjectType() 175 | class Foo { 176 | @Field() bar: string[]; 177 | } 178 | 179 | expect(() => 180 | compileObjectType(Foo).getFields(), 181 | ).toThrowErrorMatchingSnapshot(); 182 | }); 183 | 184 | it('Shows proper error message when trying to use promise type without being explicit about item type', () => { 185 | @ObjectType() 186 | class Foo { 187 | @Field() 188 | async bar() { 189 | return 'baz'; 190 | } 191 | } 192 | 193 | expect(() => 194 | compileObjectType(Foo).getFields(), 195 | ).toThrowErrorMatchingSnapshot(); 196 | }); 197 | 198 | it('Properly supports list type of field', () => { 199 | @ObjectType() 200 | class Foo { 201 | @Field({ type: [String] }) 202 | bar: string[]; 203 | } 204 | 205 | const { bar } = compileObjectType(Foo).getFields(); 206 | expect(isNamedType(bar.type)).toBe(false); 207 | expect(getNamedType(bar.type)).toBe(GraphQLString); 208 | }); 209 | 210 | it('Is properly passing `this` default values', async () => { 211 | @ObjectType() 212 | class Foo { 213 | private instanceVar = 'instance'; 214 | @Field() bar: string = this.instanceVar; 215 | } 216 | const { bar } = compileObjectType(Foo).getFields(); 217 | const resolvedValue = await bar.resolve(new Foo(), null, null, null); 218 | expect(resolvedValue).toEqual('instance'); 219 | }); 220 | 221 | it('Will not allow promise field without type addnotation', async () => { 222 | @ObjectType() 223 | class Foo { 224 | @Field() 225 | async bar(): Promise { 226 | return 10; 227 | } 228 | } 229 | 230 | expect(() => 231 | compileObjectType(Foo).getFields(), 232 | ).toThrowErrorMatchingSnapshot(); 233 | }); 234 | 235 | it('Properly resolves edge cases default values of fields', async () => { 236 | @ObjectType() 237 | class Foo { 238 | @Field() undef: boolean = undefined; 239 | @Field() falsy: boolean = false; 240 | @Field() truthy: boolean = true; 241 | @Field() nully: boolean = null; 242 | @Field() zero: number = 0; 243 | @Field() maxInt: number = Number.MAX_SAFE_INTEGER; 244 | } 245 | const compiled = compileObjectType(Foo); 246 | 247 | const { undef, falsy, truthy, nully, zero, maxInt } = compiled.getFields(); 248 | 249 | const foo = new Foo(); 250 | 251 | expect(await undef.resolve(foo, {}, null, null)).toEqual(undefined); 252 | expect(await falsy.resolve(foo, {}, null, null)).toEqual(false); 253 | expect(await truthy.resolve(foo, {}, null, null)).toEqual(true); 254 | expect(await nully.resolve(foo, {}, null, null)).toEqual(null); 255 | expect(await zero.resolve(foo, {}, null, null)).toEqual(0); 256 | expect(await maxInt.resolve(foo, {}, null, null)).toEqual(9007199254740991); 257 | }); 258 | }); 259 | -------------------------------------------------------------------------------- /src/test/field/special-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Query, compileObjectType } from '~/domains'; 2 | 3 | describe('Special fields - @Query, @Mutation @Subscribe', () => { 4 | it('Will not allow registering special type on type that is not @Schema', () => { 5 | @ObjectType() 6 | class Foo { 7 | @Query() 8 | bar(): string { 9 | return null; 10 | } 11 | } 12 | 13 | expect(() => 14 | compileObjectType(Foo).getFields(), 15 | ).toThrowErrorMatchingSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/test/functional/__snapshots__/enum.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Query with enums Will guard proper enum values 1`] = ` 4 | Array [ 5 | [GraphQLError: Expected type TestEnum, found Foob; Did you mean the enum value Foo?], 6 | ] 7 | `; 8 | -------------------------------------------------------------------------------- /src/test/functional/__snapshots__/fieldArguments.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Query args will have correct values even for "false" 1`] = ` 4 | Object { 5 | "boolTest": false, 6 | "boolTest2": false, 7 | "boolTest3": true, 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/test/functional/__snapshots__/mutation.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Mutation should not allow wrong argument types 1`] = ` 4 | Array [ 5 | [GraphQLError: Expected type String!, found 2.], 6 | ] 7 | `; 8 | -------------------------------------------------------------------------------- /src/test/functional/__snapshots__/query.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Query should not allow wrong argument types 1`] = ` 4 | Array [ 5 | [GraphQLError: Expected type String!, found 2.], 6 | ] 7 | `; 8 | -------------------------------------------------------------------------------- /src/test/functional/enum.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Query, 3 | SchemaRoot, 4 | compileSchema, 5 | ObjectType, 6 | Arg, 7 | Field, 8 | registerEnum, 9 | } from '~/domains'; 10 | import { graphql } from 'graphql'; 11 | 12 | enum TestEnum { 13 | Foo = 'Foo', 14 | Bar = 'Bar', 15 | Baz = 'Baz', 16 | } 17 | 18 | registerEnum(TestEnum, { name: 'TestEnum' }); 19 | 20 | @ObjectType() 21 | class Hello { 22 | @Field() 23 | world( 24 | @Arg({ type: TestEnum }) 25 | name: TestEnum, 26 | ): string { 27 | return `Hello, ${name}`; 28 | } 29 | } 30 | 31 | @SchemaRoot() 32 | class FooSchema { 33 | @Query() 34 | hello(): Hello { 35 | return new Hello(); 36 | } 37 | } 38 | 39 | const schema = compileSchema({ roots: [FooSchema] }); 40 | 41 | describe('Query with enums', () => { 42 | it('Will guard proper enum values', async () => { 43 | const result = await graphql( 44 | schema, 45 | ` 46 | { 47 | hello { 48 | world(name: Foob) 49 | } 50 | } 51 | `, 52 | ); 53 | 54 | expect(result.errors).toBeDefined(); 55 | expect(result.errors).toMatchSnapshot(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/test/functional/fieldArguments.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Query, 3 | SchemaRoot, 4 | compileSchema, 5 | ObjectType, 6 | Arg, 7 | Field, 8 | } from '~/domains'; 9 | import { graphql } from 'graphql'; 10 | 11 | @ObjectType() 12 | class Hello { 13 | @Field() 14 | boolTest(v1: boolean): boolean { 15 | return v1; 16 | } 17 | @Field() 18 | boolTest2(@Arg({ isNullable: true }) v2: boolean): boolean { 19 | return v2; 20 | } 21 | @Field() 22 | boolTest3(v3: boolean): boolean { 23 | return v3; 24 | } 25 | } 26 | 27 | @SchemaRoot() 28 | class FooSchema { 29 | @Query() 30 | hello(): Hello { 31 | return new Hello(); 32 | } 33 | } 34 | 35 | const schema = compileSchema({ roots: [FooSchema] }); 36 | 37 | describe('Query args', () => { 38 | it('will have correct values even for "false"', async () => { 39 | const result = await graphql( 40 | schema, 41 | ` 42 | { 43 | hello { 44 | boolTest(v1: false) 45 | boolTest2(v2: false) 46 | boolTest3(v3: true) 47 | } 48 | } 49 | `, 50 | ); 51 | 52 | expect(result.errors).toBeUndefined(); 53 | expect(result.data.hello).toMatchSnapshot(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/test/functional/inputOutput.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectType, 3 | Field, 4 | InputObjectType, 5 | InputField, 6 | compileObjectType, 7 | compileInputObjectType, 8 | } from '~/domains'; 9 | import { GraphQLString, GraphQLNonNull } from 'graphql'; 10 | 11 | @ObjectType() 12 | @InputObjectType() 13 | class InOut { 14 | @Field() 15 | @InputField() 16 | foo: string; 17 | } 18 | 19 | describe('Input & Output Object Type', () => { 20 | it('will properly compile object type that is input and output at once', async () => { 21 | const output = compileObjectType(InOut); 22 | const input = compileInputObjectType(InOut); 23 | 24 | expect(output.getFields().foo.type).toEqual(GraphQLString); 25 | expect(input.getFields().foo.type).toEqual( 26 | new GraphQLNonNull(GraphQLString), 27 | ); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/test/functional/mutation.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Query, 3 | SchemaRoot, 4 | compileSchema, 5 | ObjectType, 6 | Field, 7 | Mutation, 8 | InputField, 9 | InputObjectType, 10 | } from '~/domains'; 11 | import { graphql } from 'graphql'; 12 | 13 | @InputObjectType() 14 | class Input { 15 | @InputField() value: string; 16 | } 17 | 18 | @ObjectType() 19 | class Hello { 20 | constructor(private helloLabel = 'Hello') {} 21 | @Field() 22 | world(name: string): string { 23 | return `${this.helloLabel}, ${name}`; 24 | } 25 | } 26 | 27 | @SchemaRoot() 28 | class FooSchema { 29 | @Mutation() 30 | hello(): Hello { 31 | return new Hello(); 32 | } 33 | 34 | @Query() 35 | deepInput(input: Input): Hello { 36 | return new Hello(input.value); 37 | } 38 | } 39 | 40 | const schema = compileSchema({ roots: [FooSchema] }); 41 | 42 | describe('Mutation', () => { 43 | it('should not allow wrong argument types', async () => { 44 | const result = await graphql( 45 | schema, 46 | ` 47 | mutation { 48 | hello { 49 | world(name: 2) 50 | } 51 | } 52 | `, 53 | ); 54 | expect(result.errors).toBeDefined(); 55 | expect(result.errors).toMatchSnapshot(); 56 | }); 57 | 58 | it('should allow complex input types', async () => { 59 | const result = await graphql( 60 | schema, 61 | ` 62 | { 63 | deepInput(input: { value: "Hola" }) { 64 | world(name: "Bob") 65 | } 66 | } 67 | `, 68 | ); 69 | 70 | expect(result).toEqual({ data: { deepInput: { world: 'Hola, Bob' } } }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/test/functional/query.spec.ts: -------------------------------------------------------------------------------- 1 | import { Query, SchemaRoot, compileSchema, ObjectType, Field } from '~/domains'; 2 | import { graphql } from 'graphql'; 3 | 4 | @ObjectType() 5 | class Hello { 6 | @Field() 7 | world(name: string): string { 8 | return `Hello, ${name}`; 9 | } 10 | } 11 | 12 | @SchemaRoot() 13 | class FooSchema { 14 | @Query() 15 | hello(): Hello { 16 | return new Hello(); 17 | } 18 | 19 | @Query() 20 | foo(): string { 21 | return 'bar'; 22 | } 23 | } 24 | 25 | const schema = compileSchema({ roots: [FooSchema] }); 26 | 27 | describe('Query', () => { 28 | it('should support queries with simple arguments', async () => { 29 | const result = await graphql( 30 | schema, 31 | ` 32 | { 33 | hello { 34 | world(name: "Bob") 35 | } 36 | } 37 | `, 38 | ); 39 | 40 | expect(result).toEqual({ data: { hello: { world: 'Hello, Bob' } } }); 41 | }); 42 | 43 | it('should not allow wrong argument types', async () => { 44 | const result = await graphql( 45 | schema, 46 | ` 47 | { 48 | hello { 49 | world(name: 2) 50 | } 51 | } 52 | `, 53 | ); 54 | expect(result.errors).toBeDefined(); 55 | expect(result.errors).toMatchSnapshot(); 56 | }); 57 | 58 | it('will support flat root fields', async () => { 59 | const result = await graphql( 60 | schema, 61 | ` 62 | { 63 | foo 64 | } 65 | `, 66 | ); 67 | expect(result).toEqual({ data: { foo: 'bar' } }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/test/hooks/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hooks Will stop resolution when @Before hook throws an error 1`] = `[Error: Foo Error]`; 4 | -------------------------------------------------------------------------------- /src/test/hooks/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, Before, After, compileObjectType } from '~/domains'; 2 | 3 | describe('Hooks', () => { 4 | it('Will call @Before hook on field resolve', async () => { 5 | const callback = jest.fn(); 6 | @ObjectType() 7 | class Foo { 8 | @Field() 9 | @Before(callback) 10 | bar: string = 'done'; 11 | } 12 | 13 | const { bar } = compileObjectType(Foo).getFields(); 14 | 15 | await bar.resolve(null, null, null, null); 16 | 17 | expect(callback).toBeCalled(); 18 | }); 19 | 20 | it('Will properly pass data to @Before and @After hooks', async () => { 21 | const beforeCb = jest.fn(); 22 | const afterInnerCb = jest.fn(); 23 | const afterCb = jest.fn(({ context }) => { 24 | afterInnerCb(context); 25 | }); 26 | @ObjectType() 27 | class Foo { 28 | @Field() 29 | @Before(beforeCb) 30 | @Before(afterCb) 31 | bar: string = 'done'; 32 | } 33 | 34 | const { bar } = compileObjectType(Foo).getFields(); 35 | 36 | await bar.resolve('foo', { bar: 42 }, 'baz', null); 37 | 38 | expect(beforeCb).toBeCalledWith({ 39 | args: { bar: 42 }, 40 | context: 'baz', 41 | info: null, 42 | source: 'foo', 43 | }); 44 | 45 | expect(beforeCb).toBeCalledWith({ 46 | args: { bar: 42 }, 47 | context: 'baz', 48 | info: null, 49 | source: 'foo', 50 | }); 51 | 52 | expect(afterInnerCb).toBeCalledWith('baz'); 53 | }); 54 | 55 | it('Will stop resolution when @Before hook throws an error', async () => { 56 | const beforeCb = jest.fn(() => { 57 | throw new Error('Foo Error'); 58 | }); 59 | 60 | const afterCb = jest.fn(); 61 | const innerCb = jest.fn(); 62 | 63 | @ObjectType() 64 | class Foo { 65 | @Field() 66 | @Before(beforeCb) 67 | @After(afterCb) 68 | bar(): string { 69 | innerCb(); 70 | return 'foo'; 71 | } 72 | } 73 | 74 | const { bar } = compileObjectType(Foo).getFields(); 75 | 76 | async function exec() { 77 | await bar.resolve(null, null, null, null); 78 | } 79 | 80 | expect(exec()).rejects.toMatchSnapshot(); 81 | 82 | expect(beforeCb).toBeCalled(); 83 | expect(afterCb).not.toBeCalled(); 84 | expect(innerCb).not.toBeCalled(); 85 | }); 86 | 87 | it('Will call @After hook on field resolve', async () => { 88 | const callback = jest.fn(); 89 | @ObjectType() 90 | class Foo { 91 | @Field() 92 | @After(callback) 93 | bar: string = 'done'; 94 | } 95 | 96 | const { bar } = compileObjectType(Foo).getFields(); 97 | 98 | await bar.resolve(null, null, null, null); 99 | 100 | expect(callback).toBeCalled(); 101 | }); 102 | 103 | it('Will call @Before cb first, then resolver, and then @After cb', async () => { 104 | const beforeCb = jest.fn(); 105 | const resolverCb = jest.fn(); 106 | const afterCb = jest.fn(); 107 | 108 | let counter: number = 0; 109 | 110 | const incrementAndCall = (cb: Function) => () => { 111 | counter++; 112 | cb(counter); 113 | }; 114 | 115 | @ObjectType() 116 | class Foo { 117 | @Field() 118 | @Before(incrementAndCall(beforeCb)) 119 | @After(incrementAndCall(afterCb)) 120 | bar(): string { 121 | incrementAndCall(resolverCb)(); 122 | return 'done'; 123 | } 124 | } 125 | 126 | const { bar } = compileObjectType(Foo).getFields(); 127 | 128 | const result = await bar.resolve(null, null, null, null); 129 | 130 | expect(beforeCb).toBeCalledWith(1); 131 | expect(resolverCb).toBeCalledWith(2); 132 | expect(afterCb).toBeCalledWith(3); 133 | expect(result).toEqual('done'); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /src/test/inject/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`@Inject Will throw if trying to mark argument both with @Inject and @Arg 1`] = `"@Type Foo.bar(test <-------): Argument cannot be marked wiht both @Arg and @Inject or custom injector"`; 4 | -------------------------------------------------------------------------------- /src/test/inject/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Inject, 3 | ObjectType, 4 | Field, 5 | compileObjectType, 6 | Arg, 7 | Context, 8 | Source, 9 | Info, 10 | } from '~/domains'; 11 | 12 | import { wait } from '../utils'; 13 | 14 | describe('@Inject', () => { 15 | it('Properly injects any value', async () => { 16 | @ObjectType() 17 | class Foo { 18 | @Field() 19 | bar( 20 | @Inject(() => 'baz') 21 | test: string, 22 | ): string { 23 | return test; 24 | } 25 | } 26 | 27 | const { bar } = compileObjectType(Foo).getFields(); 28 | const result = await bar.resolve(new Foo(), null, null, null); 29 | 30 | expect(result).toEqual('baz'); 31 | }); 32 | 33 | it('Makes injected argument not visible in arguments list', async () => { 34 | @ObjectType() 35 | class Foo { 36 | @Field() 37 | bar( 38 | @Inject(() => 'baz') 39 | test: string, 40 | ): string { 41 | return test; 42 | } 43 | } 44 | 45 | const { bar } = compileObjectType(Foo).getFields(); 46 | expect(bar.args.length).toEqual(0); 47 | }); 48 | 49 | it('Will throw if trying to mark argument both with @Inject and @Arg', async () => { 50 | @ObjectType() 51 | class Foo { 52 | @Field() 53 | bar( 54 | @Arg() 55 | @Inject(() => 'baz') 56 | test: string, 57 | ): string { 58 | return test; 59 | } 60 | } 61 | expect(() => 62 | compileObjectType(Foo).getFields(), 63 | ).toThrowErrorMatchingSnapshot(); 64 | }); 65 | 66 | it('Will properly inject Context, Source and Info', async () => { 67 | @ObjectType() 68 | class Foo { 69 | @Field() 70 | bar( 71 | @Context context: string, 72 | @Source source: Foo, 73 | @Info info: any, 74 | ): number { 75 | if (context === 'context' && source === this && info === null) { 76 | return 42; 77 | } 78 | } 79 | } 80 | const { bar } = compileObjectType(Foo).getFields(); 81 | expect(await bar.resolve(new Foo(), null, 'context', null)).toEqual(42); 82 | }); 83 | 84 | it('Will properly mix Injected and normal Arguments', async () => { 85 | @ObjectType() 86 | class Foo { 87 | @Field() 88 | bar( 89 | @Arg() zzz: string, 90 | @Context context: string, 91 | @Inject(() => 42) 92 | answer: number, 93 | ): string { 94 | return `${zzz}.${context}.${answer}`; 95 | } 96 | } 97 | const { bar } = compileObjectType(Foo).getFields(); 98 | expect(bar.args.length).toEqual(1); 99 | expect( 100 | await bar.resolve(new Foo(), { zzz: 'zzz' }, 'context', null), 101 | ).toEqual('zzz.context.42'); 102 | }); 103 | 104 | it('Will allow `this` inside injectors', async () => { 105 | @ObjectType() 106 | class Foo { 107 | test = 'test'; 108 | @Field() 109 | bar( 110 | @Inject(function() { 111 | return this.test; 112 | }) 113 | baz: string, 114 | ): string { 115 | return baz; 116 | } 117 | } 118 | const { bar } = compileObjectType(Foo).getFields(); 119 | expect(await bar.resolve(new Foo(), null, null, null)).toEqual('test'); 120 | }); 121 | 122 | it('Will allow injecting async values', async () => { 123 | @ObjectType() 124 | class Foo { 125 | test = 'test'; 126 | @Field() 127 | bar( 128 | @Inject(async () => { 129 | await wait(1); 130 | return 'async'; 131 | }) 132 | baz: string, 133 | ): string { 134 | return baz; 135 | } 136 | } 137 | const { bar } = compileObjectType(Foo).getFields(); 138 | expect(await bar.resolve(new Foo(), null, null, null)).toEqual('async'); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/test/inputObjectType/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Type Throws when @ObjectType has no fields 1`] = `"@ObjectType 'NoFields': There are no fields inside this type."`; 4 | 5 | exports[`Type Throws when @ObjectType has no fields 2`] = `"@ObjectType 'NoDeclaredFields': There are no fields inside this type."`; 6 | 7 | exports[`Type Throws when trying to compile type without @ObjectType decorator 1`] = `"@ObjectType 'Bar': Class is not registered. Make sure it's decorated with @ObjectType decorator"`; 8 | -------------------------------------------------------------------------------- /src/test/inputObjectType/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import { ObjectType, compileObjectType } from '~/domains'; 3 | import { Field } from '~/domains/field'; 4 | 5 | describe('Type', () => { 6 | it('Throws when trying to compile type without @ObjectType decorator', () => { 7 | expect(() => 8 | compileObjectType(class Bar {}), 9 | ).toThrowErrorMatchingSnapshot(); 10 | }); 11 | 12 | it('Throws when @ObjectType has no fields', () => { 13 | @ObjectType() 14 | class NoFields {} 15 | 16 | @ObjectType() 17 | class NoDeclaredFields { 18 | foo: string; 19 | } 20 | expect(() => compileObjectType(NoFields)).toThrowErrorMatchingSnapshot(); 21 | expect(() => 22 | compileObjectType(NoDeclaredFields), 23 | ).toThrowErrorMatchingSnapshot(); 24 | }); 25 | 26 | it('Compiles basic type with field', () => { 27 | @ObjectType() 28 | class Foo { 29 | @Field() bar: string; 30 | } 31 | 32 | const compiled = compileObjectType(Foo); 33 | 34 | const fields = compiled.getFields(); 35 | const barField = fields.bar; 36 | 37 | expect(compiled).toBeInstanceOf(GraphQLObjectType); 38 | 39 | expect(barField).toBeTruthy(); 40 | expect(barField.name).toEqual('bar'); 41 | }); 42 | 43 | it('Sets proper options', () => { 44 | @ObjectType({ description: 'Baz' }) 45 | class Foo { 46 | @Field() bar: string; 47 | } 48 | 49 | const compiled = compileObjectType(Foo); 50 | 51 | expect(compiled.description).toEqual('Baz'); 52 | expect(compiled.name).toEqual('Foo'); 53 | 54 | @ObjectType({ name: 'Baz' }) 55 | class FooCustomName { 56 | @Field() bar: string; 57 | } 58 | 59 | const compiledCustomName = compileObjectType(FooCustomName); 60 | 61 | expect(compiledCustomName.name).toEqual('Baz'); 62 | }); 63 | 64 | it('Final type is compiled only once per class', () => { 65 | @ObjectType() 66 | class Foo { 67 | @Field() bar: string; 68 | } 69 | 70 | const compiledA = compileObjectType(Foo); 71 | const compiledB = compileObjectType(Foo); 72 | 73 | expect(compiledA).toBe(compiledB); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/test/inputObjectType/inheritance.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString, GraphQLNonNull } from 'graphql'; 2 | import { InputObjectType, InputField, compileInputObjectType } from '~/domains'; 3 | 4 | describe('Type inheritance', () => { 5 | it('Will pass input fields from parent class', () => { 6 | class Base { 7 | @InputField() baseField: string; 8 | } 9 | 10 | @InputObjectType() 11 | class Foo extends Base {} 12 | 13 | const { baseField } = compileInputObjectType(Foo).getFields(); 14 | 15 | expect(baseField.type).toEqual(new GraphQLNonNull(GraphQLString)); 16 | }); 17 | 18 | it('Will overwrite input fields in child class', () => { 19 | class Base { 20 | @InputField() foo: string; 21 | @InputField() bar: string; 22 | } 23 | 24 | @InputObjectType() 25 | class Foo extends Base { 26 | @InputField({ isNullable: true }) 27 | foo: string; 28 | } 29 | 30 | const { foo, bar } = compileInputObjectType(Foo).getFields(); 31 | 32 | expect(bar.type).toEqual(new GraphQLNonNull(GraphQLString)); 33 | expect(foo.type).toEqual(GraphQLString); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/test/misc/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { showDeprecationWarning } from '~/services/utils'; 2 | 3 | describe('showDeprecationWarning', () => { 4 | it('Will not show deprecation warning twice for the same object', async () => { 5 | const object = {}; 6 | const watcher = jest.fn(); 7 | showDeprecationWarning('Test', object, watcher); 8 | showDeprecationWarning('Test', object, watcher); 9 | 10 | expect(watcher).toHaveBeenCalledTimes(1); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/test/objectType/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Type Throws when @ObjectType has no fields 1`] = `"@ObjectType 'NoFields': There are no fields inside this type."`; 4 | 5 | exports[`Type Throws when @ObjectType has no fields 2`] = `"@ObjectType 'NoDeclaredFields': There are no fields inside this type."`; 6 | 7 | exports[`Type Throws when trying to compile type without @ObjectType decorator 1`] = `"@ObjectType 'Bar': Class is not registered. Make sure it's decorated with @ObjectType decorator"`; 8 | -------------------------------------------------------------------------------- /src/test/objectType/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import { ObjectType, compileObjectType } from '~/domains'; 3 | import { Field } from '~/domains/field'; 4 | 5 | describe('Type', () => { 6 | it('Throws when trying to compile type without @ObjectType decorator', () => { 7 | expect(() => 8 | compileObjectType(class Bar {}), 9 | ).toThrowErrorMatchingSnapshot(); 10 | }); 11 | 12 | it('Throws when @ObjectType has no fields', () => { 13 | @ObjectType() 14 | class NoFields {} 15 | 16 | @ObjectType() 17 | class NoDeclaredFields { 18 | foo: string; 19 | } 20 | expect(() => compileObjectType(NoFields)).toThrowErrorMatchingSnapshot(); 21 | expect(() => 22 | compileObjectType(NoDeclaredFields), 23 | ).toThrowErrorMatchingSnapshot(); 24 | }); 25 | 26 | it('Compiles basic type with field', () => { 27 | @ObjectType() 28 | class Foo { 29 | @Field() bar: string; 30 | } 31 | 32 | const compiled = compileObjectType(Foo); 33 | 34 | const fields = compiled.getFields(); 35 | const barField = fields.bar; 36 | 37 | expect(compiled).toBeInstanceOf(GraphQLObjectType); 38 | 39 | expect(barField).toBeTruthy(); 40 | expect(barField.name).toEqual('bar'); 41 | }); 42 | 43 | it('Sets proper options', () => { 44 | @ObjectType({ description: 'Baz' }) 45 | class Foo { 46 | @Field() bar: string; 47 | } 48 | 49 | const compiled = compileObjectType(Foo); 50 | 51 | expect(compiled.description).toEqual('Baz'); 52 | expect(compiled.name).toEqual('Foo'); 53 | 54 | @ObjectType({ name: 'Baz' }) 55 | class FooCustomName { 56 | @Field() bar: string; 57 | } 58 | 59 | const compiledCustomName = compileObjectType(FooCustomName); 60 | 61 | expect(compiledCustomName.name).toEqual('Baz'); 62 | }); 63 | 64 | it('Final type is compiled only once per class', () => { 65 | @ObjectType() 66 | class Foo { 67 | @Field() bar: string; 68 | } 69 | 70 | const compiledA = compileObjectType(Foo); 71 | const compiledB = compileObjectType(Foo); 72 | 73 | expect(compiledA).toBe(compiledB); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/test/objectType/inheritance.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString, GraphQLNonNull } from 'graphql'; 2 | import { ObjectType, compileObjectType, Field } from '~/domains'; 3 | import { getClassWithAllParentClasses } from '~/services/utils'; 4 | 5 | describe('Type inheritance', () => { 6 | it('Will pass fields from parent class', () => { 7 | class Base { 8 | @Field() baseField: string; 9 | } 10 | 11 | @ObjectType() 12 | class Foo extends Base {} 13 | 14 | const { baseField } = compileObjectType(Foo).getFields(); 15 | 16 | expect(baseField.type).toEqual(GraphQLString); 17 | }); 18 | 19 | it('Will overwrite fields in child class', () => { 20 | class Base { 21 | @Field() foo: string; 22 | @Field() bar: string; 23 | } 24 | 25 | @ObjectType() 26 | class Foo extends Base { 27 | @Field({ isNullable: false }) 28 | foo: string; 29 | } 30 | 31 | const { foo, bar } = compileObjectType(Foo).getFields(); 32 | 33 | expect(bar.type).toEqual(GraphQLString); 34 | expect(foo.type).toEqual(new GraphQLNonNull(GraphQLString)); 35 | }); 36 | 37 | it('picks up all the properties even for long chain of extended classes', async () => { 38 | @ObjectType() 39 | class Vehicle { 40 | @Field() passengers: string; 41 | } 42 | 43 | @ObjectType() 44 | class Car extends Vehicle { 45 | @Field() doorCount: number; 46 | } 47 | 48 | @ObjectType() 49 | class Lamborghini extends Car { 50 | @Field() speed: string; 51 | } 52 | const compiled = compileObjectType(Lamborghini); 53 | 54 | const fields = compiled.getFields(); 55 | 56 | expect(fields).toHaveProperty('passengers'); 57 | expect(fields).toHaveProperty('doorCount'); 58 | expect(fields).toHaveProperty('speed'); 59 | expect(getClassWithAllParentClasses(Lamborghini).length).toBe(3); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/test/schema/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Query, 3 | SchemaRoot, 4 | compileSchema, 5 | ObjectType, 6 | Field, 7 | Mutation, 8 | } from '~/domains'; 9 | import { 10 | graphql, 11 | introspectionQuery, 12 | GraphQLObjectType, 13 | GraphQLString, 14 | GraphQLFloat, 15 | } from 'graphql'; 16 | 17 | describe('@SchemaRoot', () => { 18 | it('should not allow compiling schema not decorated with @Schema', () => { 19 | class Foo {} 20 | 21 | expect(() => 22 | compileSchema({ roots: [Foo] }), 23 | ).toThrowErrorMatchingSnapshot(); 24 | }); 25 | 26 | it('should not allow @Schema without any @Query field', () => { 27 | @SchemaRoot() 28 | class Foo {} 29 | 30 | expect(() => 31 | compileSchema({ roots: [Foo] }), 32 | ).toThrowErrorMatchingSnapshot(); 33 | }); 34 | 35 | it('should generate all schema fields properly for valid schema', async () => { 36 | @ObjectType() 37 | class Hello { 38 | @Field() 39 | world(name: string): string { 40 | return `Hello, ${name}`; 41 | } 42 | } 43 | 44 | @SchemaRoot() 45 | class FooSchema { 46 | @Query() 47 | hello(): Hello { 48 | return new Hello(); 49 | } 50 | } 51 | 52 | const schema = compileSchema({ roots: [FooSchema] }); 53 | 54 | expect(await graphql(schema, introspectionQuery)).toMatchSnapshot(); 55 | }); 56 | 57 | it('should allow schema to be compiled from multiple roots', async () => { 58 | @SchemaRoot() 59 | class FooSchema { 60 | @Query() 61 | foo(): string { 62 | return 'foo'; 63 | } 64 | } 65 | 66 | @SchemaRoot() 67 | class BarSchema { 68 | @Query() 69 | bar(): number { 70 | return 42; 71 | } 72 | } 73 | 74 | const schema = compileSchema({ roots: [FooSchema, BarSchema] }); 75 | 76 | const queryType = schema.getQueryType() as GraphQLObjectType; 77 | 78 | const { foo, bar } = queryType.getFields(); 79 | 80 | expect(foo.name).toEqual('foo'); 81 | expect(foo.type).toEqual(GraphQLString); 82 | 83 | expect(bar.name).toEqual('bar'); 84 | expect(bar.type).toEqual(GraphQLFloat); 85 | }); 86 | 87 | it('should allow schema root with mutations only if there is other root with queries', async () => { 88 | @SchemaRoot() 89 | class FooSchema { 90 | @Query() 91 | foo(): string { 92 | return 'foo'; 93 | } 94 | } 95 | 96 | @SchemaRoot() 97 | class BarSchema { 98 | @Mutation() 99 | bar(): number { 100 | return 42; 101 | } 102 | } 103 | 104 | const schema = compileSchema({ roots: [FooSchema, BarSchema] }); 105 | 106 | const queryType = schema.getQueryType() as GraphQLObjectType; 107 | const mutationType = schema.getMutationType() as GraphQLObjectType; 108 | 109 | const { foo } = queryType.getFields(); 110 | const { bar } = mutationType.getFields(); 111 | 112 | expect(foo.name).toEqual('foo'); 113 | expect(foo.type).toEqual(GraphQLString); 114 | 115 | expect(bar.name).toEqual('bar'); 116 | expect(bar.type).toEqual(GraphQLFloat); 117 | }); 118 | 119 | it('should not allow schema that has only mutation fields', async () => { 120 | @SchemaRoot() 121 | class FooSchema { 122 | @Mutation() 123 | foo(): string { 124 | return 'foo'; 125 | } 126 | } 127 | 128 | @SchemaRoot() 129 | class BarSchema { 130 | @Mutation() 131 | bar(): number { 132 | return 42; 133 | } 134 | } 135 | 136 | expect(() => 137 | compileSchema({ roots: [FooSchema, BarSchema] }), 138 | ).toThrowErrorMatchingSnapshot(); 139 | }); 140 | 141 | it('will not allow multiple schema roots to have conflicting root field names', async () => { 142 | @SchemaRoot() 143 | class FooSchema { 144 | @Query() 145 | foo(): string { 146 | return 'foo'; 147 | } 148 | } 149 | 150 | @SchemaRoot() 151 | class BarSchema { 152 | @Query() 153 | foo(): number { 154 | return 42; 155 | } 156 | } 157 | 158 | expect(() => 159 | compileSchema({ roots: [FooSchema, BarSchema] }), 160 | ).toThrowErrorMatchingSnapshot(); 161 | }); 162 | 163 | it('will not allow schema with incorrect object types', async () => { 164 | @ObjectType() 165 | class Hello { 166 | @Field() 167 | async world(name: string): Promise { 168 | return `Hello, ${name}`; 169 | } 170 | } 171 | 172 | @SchemaRoot() 173 | class FooSchema { 174 | @Query() 175 | hello(): Hello { 176 | return new Hello(); 177 | } 178 | } 179 | 180 | expect(() => 181 | compileSchema({ roots: [FooSchema] }), 182 | ).toThrowErrorMatchingSnapshot(); 183 | }); 184 | 185 | it('should support schema root instance properties', async () => { 186 | @SchemaRoot() 187 | class FooSchema { 188 | private bar: number = 42; 189 | 190 | @Query() 191 | foo(): number { 192 | return this.bar; 193 | } 194 | } 195 | 196 | const schema = compileSchema({ roots: [FooSchema] }); 197 | 198 | const result = await graphql( 199 | schema, 200 | ` 201 | { 202 | foo 203 | } 204 | `, 205 | ); 206 | 207 | expect(result.data.foo).toEqual(42); 208 | }); 209 | 210 | it('should call schema root constructor', async () => { 211 | const constructorCall = jest.fn(); 212 | @SchemaRoot() 213 | class FooSchema { 214 | constructor() { 215 | constructorCall(); 216 | } 217 | 218 | @Query() 219 | foo(): number { 220 | return 42; 221 | } 222 | } 223 | 224 | const schema = compileSchema({ roots: [FooSchema] }); 225 | 226 | await graphql( 227 | schema, 228 | ` 229 | { 230 | foo 231 | } 232 | `, 233 | ); 234 | 235 | expect(constructorCall).toBeCalled(); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /src/test/union/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLUnionType } from 'graphql'; 2 | import { ObjectType, Union, Field, compileObjectType } from '~/domains'; 3 | import { resolveType } from '~/services/utils'; 4 | 5 | @ObjectType() 6 | class Sub1 { 7 | @Field() bar: string; 8 | } 9 | 10 | @ObjectType() 11 | class Sub2 { 12 | @Field() bar: number; 13 | } 14 | 15 | @Union({ types: [Sub1, Sub2] }) 16 | class UnionType {} 17 | 18 | const customTypeResolver = jest.fn(type => Sub1); 19 | 20 | @Union({ 21 | types: [Sub1, Sub2], 22 | resolveTypes: customTypeResolver, 23 | }) 24 | class CustomUnionType {} 25 | 26 | @ObjectType() 27 | class Foo { 28 | @Field({ type: UnionType }) 29 | bar: Sub1 | Sub2; 30 | 31 | @Field({ type: CustomUnionType }) 32 | baz: Sub1 | Sub2; 33 | } 34 | 35 | describe('Unions', () => { 36 | it('Registers returns proper enum type', () => { 37 | const { bar } = compileObjectType(Foo).getFields(); 38 | expect(bar.type).toEqual(resolveType(UnionType)); 39 | expect(bar.type).not.toEqual(UnionType); 40 | }); 41 | 42 | it('Properly resolves type of union', () => { 43 | const { bar } = compileObjectType(Foo).getFields(); 44 | 45 | const unionType = bar.type as GraphQLUnionType; 46 | 47 | expect( 48 | unionType.resolveType && unionType.resolveType(new Sub1(), null, null), 49 | ).toBe(resolveType(Sub1)); 50 | expect( 51 | unionType.resolveType && unionType.resolveType(new Sub2(), null, null), 52 | ).toBe(resolveType(Sub2)); 53 | }); 54 | 55 | it('Properly resolves with custom type resolver', () => { 56 | const { baz } = compileObjectType(Foo).getFields(); 57 | 58 | const unionType = baz.type as GraphQLUnionType; 59 | 60 | expect( 61 | unionType.resolveType && unionType.resolveType(new Sub2(), null, null), 62 | ).toBe(resolveType(Sub1)); 63 | expect(customTypeResolver).toBeCalled(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function wait(time: number) { 2 | return new Promise(resolve => setTimeout(resolve, time)); 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "moduleResolution": "node", 5 | "lib": ["es2017"], 6 | "target": "es2015", 7 | "noImplicitAny": true, 8 | "noImplicitUseStrict": true, 9 | "pretty": true, 10 | "baseUrl": ".", 11 | "rootDir": "./src", 12 | "outDir": "./lib", 13 | "paths": { 14 | "~/*": ["src/*"] 15 | }, 16 | "declaration": true, 17 | "esModuleInterop": true, 18 | "declarationDir": "./types", 19 | "emitDecoratorMetadata": true, 20 | "experimentalDecorators": true, 21 | "noUnusedLocals": true, 22 | "skipLibCheck": true, 23 | "stripInternal": true, 24 | "sourceMap": true 25 | }, 26 | "exclude": ["./examples"] 27 | } 28 | -------------------------------------------------------------------------------- /ttsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "plugins": [ 8 | { 9 | "transform": "@zerollup/ts-transform-paths" 10 | } 11 | ] 12 | } 13 | } --------------------------------------------------------------------------------