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 |
|
8 | Create GraphQL resolvers and schemas with TypeScript, using classes and decorators!
|
9 |
|
10 | ## Design Goals
|
11 | We all love GraphQL but creating GraphQL API with TypeScript is a bit of pain.
|
12 | We 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 |
|
14 | What if I told you that you can have only one source of truth thanks to a little addition of decorators magic?
|
15 | Interested? So take a look at the quick intro to TypeGraphQL!
|
16 |
|
17 | ## Getting started
|
18 | Let's start at the begining with an example.
|
19 | We have API for cooking recipes and we love using GraphQL for it.
|
20 | At first we will create the `Recipe` type, which is the foundations of our API:
|
21 |
|
22 | ```js
|
23 | @GraphQLObjectType()
|
24 | class 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 | ```
|
41 | Take 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 |
|
49 | This will generate GraphQL type corresponding to this:
|
50 | ```graphql
|
51 | type Recipe {
|
52 | id: ID!
|
53 | title: String!
|
54 | description: String
|
55 | ratings: [Rate]!
|
56 | averageRating: Float
|
57 | }
|
58 | ```
|
59 |
|
60 | Next, we need to define what is the `Rate` type:
|
61 |
|
62 | ```js
|
63 | @GraphQLObjectType()
|
64 | class Rate {
|
65 | @Field(type => Int)
|
66 | value: number;
|
67 |
|
68 | @Field()
|
69 | date: Date;
|
70 |
|
71 | @Field()
|
72 | user: User;
|
73 | }
|
74 | ```
|
75 | Again, 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 |
|
79 | So, as we have the base of our recipe related types, let's create a resolver!
|
80 |
|
81 | We will start by creating a class with apropiate decorator:
|
82 | ```js
|
83 | @GraphQLResolver(objectType => Recipe)
|
84 | export 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 |
|
90 | Now let's create our first query:
|
91 | ```js
|
92 | @GraphQLResolver(objectType => Recipe)
|
93 | export 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 |
|
109 | So, how the `FindRecipeArgs` looks like?
|
110 | ```js
|
111 | @GraphQLArgumentType()
|
112 | class FindRecipeArgs {
|
113 | @Field(type => ID)
|
114 | recipeId: string;
|
115 | }
|
116 | ```
|
117 |
|
118 | This two will generate corresponding graphql schema:
|
119 | ```graphql
|
120 | type Query {
|
121 | recipe(recipeId: ID!): Recipe
|
122 | }
|
123 | ```
|
124 | It is great, isn't it? :smiley:
|
125 |
|
126 | Ok, let's add another query:
|
127 | ```js
|
128 | class RecipeResolver {
|
129 | // ...
|
130 | @Query(() => Recipe, { array: true })
|
131 | recipes(): Promise<Array<Recipe>> {
|
132 | return this.recipeRepository.find();
|
133 | }
|
134 | }
|
135 | ```
|
136 | As 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 |
|
138 | Also, remember to declare `{ array: true }` when your method is async or returns the `Promise<Array<T>>`.
|
139 |
|
140 | So now we have two queries in our schema:
|
141 | ```graphql
|
142 | type Query {
|
143 | recipe(recipeId: ID!): Recipe
|
144 | recipes: [Recipe]!
|
145 | }
|
146 | ```
|
147 |
|
148 | Now let's move to the mutations:
|
149 | ```js
|
150 | class 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 |
|
166 | Here's how `RateInput` type looks:
|
167 | ```js
|
168 | @GraphQLInputType()
|
169 | class 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 |
|
179 | The corresponding GraphQL schema:
|
180 | ```graphql
|
181 | input RateInput {
|
182 | recipeId: ID!
|
183 | value: Int!
|
184 | }
|
185 | ```
|
186 |
|
187 | And the rate mutation definition:
|
188 | ```graphql
|
189 | type Mutation {
|
190 | rate(rate: RateInput!): Recipe!
|
191 | }
|
192 | ```
|
193 |
|
194 | The 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 |
|
196 | So 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
|
199 | class RecipeResolver {
|
200 | // ...
|
201 | @FieldResolver()
|
202 | averageRating(@Root() recipe: Recipe) {
|
203 | // implementation...
|
204 | }
|
205 | }
|
206 | ```
|
207 |
|
208 | The whole `RecipeResolver` we discussed above with sample implementation of methods looks like this:
|
209 | ```js
|
210 | @GraphQLResolver(objectType => Recipe)
|
211 | export 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 |
|
263 | As I mentioned, in real life we want to reuse as much TypeScript definition as we can.
|
264 | So the GQL type classes would be also reused by ORM or validation lib:
|
265 |
|
266 | ```js
|
267 | import { Entity, ObjectIdColumn, Column, OneToMany, CreateDateColumn } from "typeorm";
|
268 |
|
269 | @Entity()
|
270 | @GraphQLObjectType()
|
271 | export 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
|
299 | import { Length, Min, Max } from "class-validator";
|
300 |
|
301 | @GraphQLInputType()
|
302 | class 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 |
|
314 | Of 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 |
|
320 | 1. Install module:
|
321 | ```
|
322 | npm i type-graphql
|
323 | ```
|
324 |
|
325 | 2. reflect-metadata shim is required:
|
326 | ```
|
327 | npm i reflect-metadata
|
328 | ```
|
329 |
|
330 | and make sure to import it on top of your entry file (before you use/import `type-graphql` or your resolvers):
|
331 | ```js
|
332 | import "reflect-metadata";
|
333 | ```
|
334 |
|
335 | 3. 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
|
344 | All you need to do is to import your resolvers and register them in schema builder:
|
345 | ```js
|
346 | import { buildSchema } from "type-graphql";
|
347 |
|
348 | import { SampleResolver } from "./resolvers";
|
349 |
|
350 | const schema = buildSchema({
|
351 | resolvers: [SampleResolver],
|
352 | });
|
353 |
|
354 | ```
|
355 | And that's it! You can also create a HTTP-based GraphQL API server:
|
356 | ```js
|
357 | // remember to install "express" and "express-graphql" modules!
|
358 | const app = express();
|
359 | app.use(
|
360 | "/graphql",
|
361 | graphqlHTTP({
|
362 | schema, // this is our schema from TypeGraphQL
|
363 | graphiql: true,
|
364 | }),
|
365 | );
|
366 | app.listen(4000, () => {
|
367 | console.log("Running a GraphQL API server at localhost:4000/graphql");
|
368 | });
|
369 | ```
|
370 |
|
371 | ## Examples
|
372 | You 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 |
|
378 | Currently released version is an early alpha. However it's working quite well, so please feel free to test it and experiment with it.
|
379 |
|
380 | More feedback = less bugs thanks to you! :smiley:
|
381 |
|
382 | ## Roadmap
|
383 | You can keep track of [development's progress on project board](https://github.com/19majkel94/type-graphql/projects/1).
|
384 |
|
385 | Stay tuned and come back later for more! :wink:
|