Skip to content

Latest commit

 

History

History
386 lines (303 loc) · 9.51 KB

File metadata and controls

386 lines (303 loc) · 9.51 KB

Advanced Tutorial

This guide is for JavaScript users. TypeScript version is here.

This guide extends getting started guide and teach you how to add relations and resolvers in your application.

Controller args

For the following GraphQL query args:

type Query {
    posts(limit: Int, offset: Int, sortBy: String): [Post]
}

Args are being passed as a first parameter in your controller action:

import {EntityManager} from "typeorm";
import {Post} from "../entity/Post";

export class PostController {

    constructor(container) {
        this.entityManager = container.get(EntityManager);
    }

    // serves "posts: [Post]" requests
    // "args" variable will contain all arguments - limit, offset and sortBy
    posts(args) {

        let findOptions = {};
        if (args.limit)
            findOptions.take = args.limit;
        if (args.offset)
            findOptions.skip = args.offset;
        if (args.sortBy === "last")
            findOptions.order = { "id": "DESC" };
        if (args.sortBy === "name")
            findOptions.order = { "name": "ASC" };

        return this.entityManager.find(Post, findOptions);
    }
    
    // ...
    
}

Adding relations

Using TypeORM you can create one-to-one, one-to-many, many-to-one and many-to-many relations between your entities. Vesper automatically resolves all your relations when you request them.

Let's create a new model called Category and add a many-to-many relation between Post and Category. First, create a new src/schemas/model/Category.graphql schema file:

type Category {
    id: Int
    name: String
    posts: [Post]
}

and change src/schemas/model/Post.graphql schema:

type Post {
    id: Int
    title: String
    text: String
    categories: [Category]
}

Second, we need to create a new database model src/entity/Category.js:

import {EntitySchema} from "typeorm";

export const Category = new EntitySchema({
    name: "Category",
    columns: {
        id: {
            type: Number,
            primary: true,
            generated: true
        },
        name: {
            type: String
        }
    },
    relations: {
        posts: {
            type: "many-to-many",
            target: "Post",
            inverseSide: "categories"
        }
    }
});

And change src/entity/Post.js entity:

import {EntitySchema} from "typeorm";

export const Post = new EntitySchema({
    name: "Post",
    columns: {
        id: {
            type: Number,
            primary: true,
            generated: true
        },
        title: {
            type: String
        },
        text: {
            type: String
        }
    },
    relations: {
        categories: {
            type: "many-to-many",
            joinTable: true,
            target: "Category",
            inverseSide: "posts"
        }
    }
});

And make sure to register entity in app bootstrap file (src/index.js):

bootstrap({
    entities: [
        Post,
        Category
    ]
    // ...
});

Now you'll be able to query post categories and category posts without any extra code written! Vesper and TypeORM does all the magic for you.

query {
    posts {
        id
        title
        text
        categories {
            id
            name
        }
    }
}
query {
    categories {
        id
        name
        posts {
            id
            title
            text
        }
    }
}

Creating a Resolver

Vesper provides you an elegant way to create resolvers for your models. Let's say you have categoryNames property in your Post schema:

type Post {
    id: Int
    title: String
    text: String
    categories: [Category]
    categoryNames: [String]
}

Unlike id, title and text this property is not stored in your database, and its value needs to be resolved only when client actually requests this data. It means we cannot simply set this property inside controller if its computation does affect performance. Here, resolvers come into the play.

Create a resolver class inside src/resolver/PostResolver.js file:

import {EntityManager} from "typeorm";
import {Category} from "../entity/Category";

export class PostResolver {

    constructor(container) {
        this.entityManager = container.get(EntityManager);
    }

    categoryNames(post) {
        return this.entityManager
            .createQueryBuilder(Category, "category")
            .innerJoin("category.posts", "post", "post.id = :postId", { postId: post.id })
            .getMany()
            .then(categories => categories.map(category => category.name));
    }

}

And register it in bootstrap file (src/index.js):

bootstrap({
    resolvers: [
        { resolver: PostResolver, model: Post, methods: ["categoryNames"] },
    ],
    // ...
});

Now you'll able to request it:

query {
    posts {
        id
        title
        text
        categoryNames
    }
}

And code inside your resolver will be executed only when client requested categoryNames property.

Resolver and DataLoader

In previous section we created a resolver that resolves categoryNames property. Code inside categoryNames method will be executed as many times as many posts we load. This can lead into performance issues if you have a costly operation inside your resolver method. To address this issue GraphQL suggests to use data-loader library. Vesper provides a powerful abstraction layer that prevents you to use it directly and reduce a boilerplate code.

Let's change our PostResolver.js file:

import {EntityManager} from "typeorm";
import {Category} from "../entity/Category";

export class PostResolver {

    constructor(container) {
        this.entityManager = container.get(EntityManager);
    }

    categoryNames(posts) {
        const postIds = posts.map(post => post.id);
        return this.entityManager
            .createQueryBuilder(Category, "category")
            .innerJoinAndSelect("category.posts", "post", "post.id IN (:...postIds)", { postIds })
            .getMany()
            .then(categories => {
                return posts.map(post => {
                    return categories
                        .filter(category => category.posts.some(categoryPost => categoryPost.id === post.id))
                        .map(category => category.name);
                });
            });
    }

}

And register this resolver method with many: true flag set:

bootstrap({
    resolvers: [
        { resolver: PostResolver, model: Post, methods: [{ methodName: "categoryNames", many: true }] },
    ],
    // ...
});

As you can see now categoryNames accepts an array of all posts for which we need to resolve the data. This allowed us to return category names within a single database query.

Using service container

Vesper provides you a powerful service container out of the box. This allows you to structure your code a better way and easily unit-test your code. Let's create a TextGenerator class in a src/service/TextGenerator.js file:

export class TextGenerator {

    generate() {
        // here you can generate text for your posts 
        // using any faker data library
        return "";
    }

}

Then you can inject this service in any controller, resolver or other service using constructor:

import {TextGenerator} from "../service/TextGenerator";

export class PostController {

    constructor(container) {
        this.textGenerator = container.get(TextGenerator);
    }
    
    // ...
    
}

Controllers and resolvers are services as well. All services have a request-scope by default.

Validating input arguments

All user input must be validated. Vesper provides you a way to validate all user input (args) elegant way. Create a src/validator/PostArgsValidator.js file with following contents:

export class PostsArgsValidator {
    
    validate(args) {
       if (args.limit !== undefined && args.limit > 100)
           throw new Error(`Limit cannot be more then 100.`);
    
       if (args.limit !== undefined && args.limit < 1)
           throw new Error(`Limit cannot be less then 1.`);
    
       if (args.offset !== undefined && args.offset < 0)
           throw new Error(`Offset cannot be less then zero.`);
    
       if (args.sortBy && args.sortBy !== "name" && args.sortBy !== "last")
           throw new Error(`Sort can only be by name or by last`);
    }

}

Then you need to register validator for action you need:

// ...
    controllers: [
        { controller: PostController, action: "posts", type: "query", validators: [PostsArgsValidator] },
    ]
// ...

And controller args will be validated before controller method is executed. Validators are regular services and you can inject any other service using constructor injection.

At this point you should already know a 90% of Vesper framework and you are ready to start creating amazing backends using it. Example repository for this sample is available here.