nitrogql logonitrogql

nitrogql:model plugin

nitrogql:model-plugin is a built-in plugin for enhancing generated resolver types. It helps you to define which fields use default resolvers. Without this plugin, you have to define all resolvers by yourself, which is not practically possible.

Usage

To use the plugin, you need to add it to the plugins array in the configuration file.

schema: ./schema/*.graphql
extensions:
  nitrogql:
    plugins:
      - "nitrogql:model-plugin"
    # ...

@model directive definition

The plugin adds a @model directive with the following definition.

directive @model(
  type: String
) on OBJECT | FIELD_DEFINITION

This directive is relevant when you are using generated resolver types.

Using @model directive on fields

An example usage of the @model directive is:

type Query {
  me: User!
}

type User {
  id: ID! @model
  name: String! @model
  email: String!
  posts: [Post!]!
}

Fields marked with @model directive are considered as part of the model, which means that they are included in data interchanged between resolvers.

Concretely, resolvers that return a User must return an object containing id and name fields. Also, resolvers for User's fields receive an object with those fields as the first argument.

Also as a consequence, resolvers for model fields need not be defined. The default resolver should be able to pick up the appropriate value from the model object.

With the above setting, the following resolvers implementation is considered valid in terms of type safety.

const queryResolvers: Resolvers<Context>["Query"] = {
  me: async () => {
    // returns logged in user
    return {
      id: "1234",
      name: "John Smith",
    };
  }
};

const userResolvers: Resolvers<Context>["User"] = {
  // no need to define resolvers for id and name
  email: async (user) => {
    const userFromDB = await db.findUserById(user.id);
    return userFromDB.email;
  },
  posts: async (user) => {
    const postsFromDB = await db.findPostsByUserId(user.id);
    return postsFromDB;
  }
};

Using @model directive on object types

Instead of marking fields with @model, you can apply the @model directive to the whole object type. Doing so will replace the model type with one you specify in the type argument. The type argument accepts any string that is a valid TypeScript code that represents a type.

If you use @model this way, you must define resolvers for all fields of the object type.

An example usage of the @model directive is:

type Query {
  me: User!
}

type User @model(type: "import('@/models/user').User") {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

With the above setting, you would implement resolvers like the following.

import { User } from "@/models/user";

const queryResolvers: Resolvers<Context>["Query"] = {
  me: async () => {
    // returns logged in user
    return new User("1234", "John doe");
  }
};

const userResolvers: Resolvers<Context>["User"] = {
  id: (user) => {
    return user.id;
  },
  name: (user) => {
    return user.name;
  },
  email: async (user) => {
    // user is an instance of the User class
    return await user.getEmail();
  },
  posts: async (user) => {
    return await user.getPosts();
  }
};

Effects on Runtime Schema

The @model directive is purely for type definition generation. Therefore, it is removed from the runtime schema (one generated by using the generate.serverGraphqlOutput option).

🧺 See Also: the blog post for the 1.1 release also explains the @model directive and its usage in detail.