vesper Framework Documentation

repository·master·Indexed 20 days ago

https://github.com/vesper-framework/vesper

A NodeJS GraphQL server-side framework for TypeScript and JavaScript designed for building scalable and extensible applications. Vesper provides a declarative structure using Controllers, Args, Models, Resolvers, and Services, featuring a built-in service container for dependency injection, automatic TypeORM entity relation resolution, and a DataLoader-inspired pattern for optimizing resolvers.

Tokens
33.2K
Snippets
98
Records
114
Agent score
67%

What's inside vesper

  1. Overview of Vesper core components

    master

    Vesper is a NodeJS framework for building scalable, declarative, and fast GraphQL-based server applications. When building with Vesper, your application is composed of the following core components:

    • Controllers: Handle your root queries.
    • Args: Represent GraphQL resolver arguments or user input.
    • Models: Define your data structures.
    • Resolvers: Implement the logic for fetching data.
    • Services: Contain business logic and are typically managed by the service container.
    • GraphQL schemas: Defined using the standard .graphql format.
  2. How Vesper handles entity relations with TypeORM

    master

    Vesper automatically resolves entity relations defined via TypeORM. When you define relationships such as one-to-one, one-to-many, many-to-one, or many-to-many in your TypeORM entities, Vesper allows you to request these related fields directly in your GraphQL queries. The framework handles the underlying data fetching so that nested related data is returned to the client automatically.

    query {
        posts {
            id
            title
            text
            categories {
                id
                name
            }
        }
    }
  3. Distinguish between Models and Entities in Vesper

    master

    In Vesper's TypeScript implementation, there is a distinction between a Model and an Entity:

    1. Model: Represents a GraphQL type. You typically define this as a TypeScript interface or class within a model directory to match your GraphQL schema.
    2. Entity: Represents the data structure that is actually stored in the database. Entities are implemented using TypeORM decorators (like @Entity(), @Column(), etc.) and are typically stored in an entity directory.

    While a Model defines the shape of the data for your API, the Entity defines how that data is persisted in your database tables.

  4. Organize your application using Modules

    master

    As applications grow, Vesper provides a module abstraction to separate different parts of your application into distinct directories. Instead of a single src directory, you can group related entities, modules, controllers, and schemas into dedicated folders.

    To implement a module:

    1. Create a dedicated directory for the feature (e.g., src/user).
    2. Place all related files (entities, controllers, schemas, etc.) inside that directory.
    3. Create a class representing the module (e.g., UserModule.ts) that defines the module's configuration in its constructor.
    export class UserModule {
        constructor() {
            this.schemas = [
                __dirname + "/schema/**/*.graphql"
            ];
            this.controllers = [
                { controller: UserController, action: "users", type: "query" },
                { controller: UserController, action: "user", type: "query" },
                { controller: UserController, action: "userSave", type: "mutation" },
                { controller: UserController, action: "userDelete", type: "mutation" },
            ];
            this.entities = [
                User
            ];
            this.resolvers = [
                // ...
            ];
        }
    }
  5. Optimize Resolvers using the 'many' flag (DataLoader pattern)

    master

    By default, a resolver method is called once for every item in a list, which can cause N+1 query problems. To optimize this, use the many: true flag in your resolver registration.

    When many: true is set, the resolver method receives an array of parent objects instead of a single object. This allows you to perform a single batch database query (similar to the DataLoader pattern) to fetch data for all items at once.

    // Resolver implementation receiving an array of posts
    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);
                });
            });
    }
    
    // Registration with 'many: true'
    bootstrap({
        resolvers: [
            { 
                resolver: PostResolver, 
                model: Post, 
                methods: [{ methodName: "categoryNames", many: true }] 
            },
        ],
    });
  6. Use Resolvers to handle complex model data

    master

    In Vesper, Resolver classes are used to resolve fields in a GraphQL model that require additional logic or database queries beyond what a standard controller provides. Instead of bloating your controller with logic for fields that might not even be requested, you should move that logic into a dedicated Resolver class.

    For example, if a Post model has a categoryNames field that isn't part of the base Post entity, you create a PostResolver to handle that specific field.

    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));
        }
    }
  7. How Vesper handles entity relations

    master

    Vesper uses TypeORM to manage entity relationships, including one-to-one, one-to-many, many-to-one, and many-to-many relations. Vesper features an automatic entity relation resolver, meaning that when you define relations in your TypeORM EntitySchema, Vesper will automatically resolve and return those related entities when they are requested in a GraphQL query. You do not need to manually implement the resolution logic for the requested fields.

    query {
        posts {
            id
            title
            text
            categories {
                id
                name
            }
        }
    }
  8. Follow Vesper naming conventions for Models, Controllers, and Schemas

    master

    To maintain consistency, follow these naming conventions for your core components:

    Models

    Models must reside in the model or entity directory. Use a single file per model with the filename matching the class name.

    • Example: Photo.ts, Album.ts

    Controllers

    Use one controller per model. The controller name should include the model name followed by Controller.

    • Example: PhotoController.ts, AlbumController.ts

    GraphQL Schemas

    Create separate .graphql files for each model and controller. Store them in the appropriate subdirectories within schema/.

    • Model Schemas: schema/model/Photo.graphql, schema/model/Album.graphql
    • Controller Schemas: schema/controller/PhotoController.graphql, schema/controller/AlbumController.graphql

    Args

    For argument interfaces, take the method name and append the Args postfix. Place these in the args/ directory.

    • Example: For a postSave mutation, create PostSaveArgs.ts in the args/ directory.
    // model/Photo.ts
    export class Photo {
        // ...
    }
    
    // controller/PhotoController.ts
    export class PhotoController {
        // ...
    }
    # schema/model/Photo.graphql
    type Photo {
        # ...
    }
    
    # schema/controller/PhotoController.graphql
    type PhotoController {
        # ...
    }
  9. Optimize Resolvers using batch loading

    master

    By default, a resolver method is called once for every instance of the model being resolved. If you are loading a list of items, this can lead to the 'N+1 query problem'.

    To optimize this, Vesper allows you to accept an array of models in your @Resolve() method. This enables you to perform a single batch query (e.g., using an IN clause) to fetch all required data for the entire collection at once.

    Instead of: categoryNames(post: Post)

    Use: categoryNames(posts: Post[])

    @Resolver(Post)
    export class PostResolver implements ResolverInterface<Post> {
    
        constructor(private entityManager: EntityManager) {}
    
        @Resolve()
        categoryNames(posts: Post[]) {
            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);
                    });
                });
        }
    }
  10. What are Services and how to use them

    master

    In Vesper, Services are classes used to encapsulate application logic that is not directly related to controllers or resolvers. This promotes the separation of concerns principle.

    Common types of services include:

    • Repositories: Services containing database queries.
    • Managers: Services managing specific business logic.
    • Utilities: Services containing utility functions.

    Services can be injected into other services, controllers, resolvers, or validators using the Vesper dependency injection container.

    export class PasswordEncryptor {
        encrypt(password) {
            // ... do password encryption ...
            return password;
        }
    }
  11. Optimize Resolvers using Batch Loading (DataLoader pattern)

    master

    To prevent the N+1 query problem, Vesper allows you to implement batch loading in your resolvers. Instead of receiving a single entity, your @Resolve() method can accept an array of the parent entities. This allows you to perform a single database query for all requested items in a single execution cycle.

    @Resolver(Post)
    export class PostResolver implements ResolverInterface<Post> {
        @Resolve()
        async categoryNames(posts: Post[]) {
            // 'posts' is an array of all parent entities in the current batch
            const postIds = posts.map(post => post.id);
            
            // Perform one query for all IDs
            const categories = await this.entityManager
                .createQueryBuilder(Category, "category")
                .innerJoinAndSelect("category.posts", "post", "post.id IN (:...postIds)", { postIds })
                .getMany();
    
            // Map results back to the original array order
            return posts.map(post => {
                return categories
                    .filter(category => category.posts.some(categoryPost => categoryPost.id === post.id))
                    .map(category => category.name);
            });
        }
    }
  12. Distinguish between Models and Entities

    master

    In Vesper, there is a distinction between how data is represented in your API versus how it is stored in your database:

    • Model: A GraphQL type definition (e.g., type Post { id: Int ... }). This defines the shape of the data exposed via your GraphQL schema.
    • Entity: The database representation of a model. An entity is defined using EntitySchema from TypeORM and maps the model's fields to database columns and tables.