UNPKG

11.6 kBMarkdownView Raw
1![logo](https://github.com/19majkel94/type-graphql/blob/master/logo.png?raw=true)
2
3# TypeGraphQL
4[![npm version](https://badge.fury.io/js/type-graphql.svg)](https://badge.fury.io/js/type-graphql)
5[![dependencies](https://david-dm.org/19majkel94/type-graphql/status.svg)](https://david-dm.org/19majkel94/type-graphql)
6[![gitter](https://badges.gitter.im/type-graphql.svg)](https://gitter.im/type-graphql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7
8Create GraphQL resolvers and schemas with TypeScript, using classes and decorators!
9
10## Design Goals
11We all love GraphQL but creating GraphQL API with TypeScript is a bit of pain.
12We have to mantain separate GQL schemas using SDL or JS API and keep the related TypeScript interfaces in sync with them. We also have separate ORM classes representing our db entities. This duplication is a really bad developer experience.
13
14What if I told you that you can have only one source of truth thanks to a little addition of decorators magic?
15Interested? So take a look at the quick intro to TypeGraphQL!
16
17## Getting started
18Let's start at the begining with an example.
19We have API for cooking recipes and we love using GraphQL for it.
20At first we will create the `Recipe` type, which is the foundations of our API:
21
22```js
23@GraphQLObjectType()
24class Recipe {
25 @Field(type => ID)
26 readonly id: string;
27
28 @Field()
29 title: string;
30
31 @Field({ nullable: true })
32 description?: string;
33
34 @Field(type => Rate)
35 ratings: Rate[];
36
37 @Field({ nullable: true })
38 averageRating?: number;
39}
40```
41Take a look at the decorators:
42
43- `@GraphQLObjectType()` marks the class as the object shape known from GraphQL SDL as `type`
44- `@Field()` marks the property as the object's field - it is also used to collect type metadata from TypeScript reflection system
45- the parameter function in decorator `@Field(type => ID)` is used to declare the GraphQL scalar type like the builit-in `ID`
46- due to reflection limitation, optional (nullable) fields has to be annotated with `{ nullable: true }` decorator param
47- we also have to declare `(type => Rate)` because of limitation of type reflection - emited type of `ratings` property is `Array`, so we need to know what is the type of items in the array
48
49This will generate GraphQL type corresponding to this:
50```graphql
51type Recipe {
52 id: ID!
53 title: String!
54 description: String
55 ratings: [Rate]!
56 averageRating: Float
57}
58```
59
60Next, we need to define what is the `Rate` type:
61
62```js
63@GraphQLObjectType()
64class Rate {
65 @Field(type => Int)
66 value: number;
67
68 @Field()
69 date: Date;
70
71 @Field()
72 user: User;
73}
74```
75Again, take a look at `@Field(type => Int)` decorator - Javascript doesn't have integers so we have to mark that our number type will be `Int`, not `Float` (which is `number` by default).
76
77-----
78
79So, as we have the base of our recipe related types, let's create a resolver!
80
81We will start by creating a class with apropiate decorator:
82```js
83@GraphQLResolver(objectType => Recipe)
84export class RecipeResolver {
85 // we will implement this later
86}
87```
88`@GraphQLResolver` marks our class as a resolver of type `Recipe` (type info is needed for attaching field resolver to correct type).
89
90Now let's create our first query:
91```js
92@GraphQLResolver(objectType => Recipe)
93export class RecipeResolver {
94 constructor(
95 // declare to inject instance of our repository
96 private readonly recipeRepository: Repository<Recipe>,
97 ){}
98
99 @Query(returnType => Recipe, { nullable: true })
100 async recipe(@Args() { recipeId }: FindRecipeArgs): Promise<Recipe | undefined> {
101 return this.recipeRepository.findOneById(recipeId);
102 }
103```
104- our query needs to communicate with database, so we declare the repository in constructor and the DI framework will do the magic and injects the instance to our resolver
105- `@Query` decorator marks the class method as the query (who would have thought?)
106- our method is async, so we can't infer the return type from reflection system - we need to define it as `(returnType => Recipe)` and also mark it as nullable because `findOneById` might not return the recipe (no document with the id in DB)
107- `@Args()` marks the parameter as query arguments object, where `FindRecipeArgs` define it's fields - this will be injected in this place to this method
108
109So, how the `FindRecipeArgs` looks like?
110```js
111@GraphQLArgumentType()
112class FindRecipeArgs {
113 @Field(type => ID)
114 recipeId: string;
115}
116```
117
118This two will generate corresponding graphql schema:
119```graphql
120type Query {
121 recipe(recipeId: ID!): Recipe
122}
123```
124It is great, isn't it? :smiley:
125
126Ok, let's add another query:
127```js
128class RecipeResolver {
129 // ...
130 @Query(() => Recipe, { array: true })
131 recipes(): Promise<Array<Recipe>> {
132 return this.recipeRepository.find();
133 }
134}
135```
136As you can see, the function parameter name `@Query(returnType => Recipe)` is only the convention and if you want, you can use the shorthand syntax like `@Query(() => Recipe)` which might be quite less readable for someone. We need to declare it as a function to help resolve circular dependencies.
137
138Also, remember to declare `{ array: true }` when your method is async or returns the `Promise<Array<T>>`.
139
140So now we have two queries in our schema:
141```graphql
142type Query {
143 recipe(recipeId: ID!): Recipe
144 recipes: [Recipe]!
145}
146```
147
148Now let's move to the mutations:
149```js
150class RecipeResolver {
151 // ...
152 @Mutation(returnType => Recipe)
153 async rate(
154 @Arg("rate") rateInput: RateInput,
155 @Context() { user }: Context,
156 ) {
157 // implementation...
158 }
159}
160```
161- we declare the method as mutation using the `@Mutation()` with return type function syntax
162- the `@Arg()` decorator let's you declare single argument of the mutation
163- for complex arguments you can use as input types like `RateInput` in this case
164- injecting the context is also possible - using `@Context()` decorator, so you have an access to `request` or `user` data - whatever you define on server settings
165
166Here's how `RateInput` type looks:
167```js
168@GraphQLInputType()
169class RateInput {
170 @Field(type => ID)
171 recipeId: string;
172
173 @Field(type => Int)
174 value: number;
175}
176```
177`@GraphQLInputType()` marks the class as the `input` in SDL, in oposite to `type` or `scalar`
178
179The corresponding GraphQL schema:
180```graphql
181input RateInput {
182 recipeId: ID!
183 value: Int!
184}
185```
186
187And the rate mutation definition:
188```graphql
189type Mutation {
190 rate(rate: RateInput!): Recipe!
191}
192```
193
194The last one we discuss now is the field resolver. As we declared earlier, we store array of ratings in our recipe documents and we want to expose the average rating value.
195
196So all we need is to decorate the method with `@FieldResolver()` and the method parameter with `@Root()` decorator with the root value type of `Recipe` - as simple as that!
197
198```js
199class RecipeResolver {
200 // ...
201 @FieldResolver()
202 averageRating(@Root() recipe: Recipe) {
203 // implementation...
204 }
205}
206```
207
208The whole `RecipeResolver` we discussed above with sample implementation of methods looks like this:
209```js
210@GraphQLResolver(objectType => Recipe)
211export class RecipeResolver {
212 constructor(
213 // inject the repository (or other services)
214 private readonly recipeRepository: Repository<Recipe>,
215 ){}
216
217 @Query(returnType => Recipe, { nullable: true })
218 recipe(@Args() { recipeId }: FindRecipeParams) {
219 return this.recipeRepository.findOneById(recipeId);
220 }
221
222 @Query(() => Recipe, { array: true })
223 recipes(): Promise<Array<Recipe>> {
224 return this.recipeRepository.find();
225 }
226
227 @Mutation(Recipe)
228 async rate(
229 @Arg("rate") rateInput: RateInput,
230 @Context() { user }: Context,
231 ) {
232 // find the document
233 const recipe = await this.recipeRepository.findOneById(rateInput.recipeId);
234 if (!recipe) {
235 throw new Error("Invalid recipe ID");
236 }
237
238 // update the document
239 recipe.ratings.push({
240 date: new Date(),
241 value: rateInput.value,
242 user,
243 });
244
245 // and save it
246 return this.recipeRepository.save(recipe);
247 }
248
249 @FieldResolver()
250 averageRating(@Root() recipe: Recipe) {
251 const ratingsCount = recipe.ratings.length;
252 const ratingsSum = recipe.ratings
253 .map(rating => rating.value)
254 .reduce((a, b) => a + b, 0);
255
256 return ratingsCount ? ratingsSum / ratingsCount : null;
257 }
258}
259```
260
261### Real life example
262
263As I mentioned, in real life we want to reuse as much TypeScript definition as we can.
264So the GQL type classes would be also reused by ORM or validation lib:
265
266```js
267import { Entity, ObjectIdColumn, Column, OneToMany, CreateDateColumn } from "typeorm";
268
269@Entity()
270@GraphQLObjectType()
271export class Recipe {
272 @ObjectIdColumn()
273 @Field(type => ID)
274 readonly id: ObjectId;
275
276 @Column()
277 @Field()
278 title: string;
279
280 @Field()
281 @Column()
282 description: string;
283
284 @OneToMany(type => Rate, rate => rate.recipe)
285 @Field(type => Rate)
286 ratings: Rate[];
287
288 // note that this field is not stored in DB
289 @Field()
290 averageRating: number;
291
292 // and this one is not exposed by GraphQL
293 @CreateDateColumn()
294 creationDate: Date;
295}
296```
297
298```js
299import { Length, Min, Max } from "class-validator";
300
301@GraphQLInputType()
302class RateInput {
303 @Length(24)
304 @Field(type => ID)
305 recipeId: string;
306
307 @Min(1)
308 @Max(5)
309 @Field(type => Int)
310 value: number;
311}
312```
313
314Of course TypeGraphQL will automatically validate the input and params with `class-validator` for you too! (in near future :wink:)
315
316## How to use
317
318### Installation
319
3201. Install module:
321```
322npm i type-graphql
323```
324
3252. reflect-metadata shim is required:
326```
327npm i reflect-metadata
328```
329
330and make sure to import it on top of your entry file (before you use/import `type-graphql` or your resolvers):
331```js
332import "reflect-metadata";
333```
334
3353. Its important to set these options in tsconfig.json file of your project:
336```json
337{
338 "emitDecoratorMetadata": true,
339 "experimentalDecorators": true
340}
341```
342
343### Usage
344All you need to do is to import your resolvers and register them in schema builder:
345```js
346import { buildSchema } from "type-graphql";
347
348import { SampleResolver } from "./resolvers";
349
350const schema = buildSchema({
351 resolvers: [SampleResolver],
352});
353
354```
355And that's it! You can also create a HTTP-based GraphQL API server:
356```js
357// remember to install "express" and "express-graphql" modules!
358const app = express();
359app.use(
360 "/graphql",
361 graphqlHTTP({
362 schema, // this is our schema from TypeGraphQL
363 graphiql: true,
364 }),
365);
366app.listen(4000, () => {
367 console.log("Running a GraphQL API server at localhost:4000/graphql");
368});
369```
370
371## Examples
372You can also check the [examples](https://github.com/19majkel94/type-graphql/tree/master/examples) folder on the repo for more example of usage: simple fields resolvers, DI Container support, etc.
373
374[Tests](https://github.com/19majkel94/type-graphql/tree/master/tests) folder will also give you some tips how to make some things done.
375
376## Work in progress
377
378Currently released version is an early alpha. However it's working quite well, so please feel free to test it and experiment with it.
379
380More feedback = less bugs thanks to you! :smiley:
381
382## Roadmap
383You can keep track of [development's progress on project board](https://github.com/19majkel94/type-graphql/projects/1).
384
385Stay tuned and come back later for more! :wink: