Model resolvers in Orionjs provide a way to define computed fields and relationships on your GraphQL types. Unlike regular resolvers which define root queries and mutations, model resolvers add fields to specific GraphQL types.

Creating Model Resolvers

A model resolver controller is a class decorated with @ModelResolvers() that specifies the GraphQL type to extend, and contains methods decorated with @ModelResolver() to define fields on that type:

import { ModelResolver, ModelResolvers, createModelResolver } from '@orion-js/graphql'
import { Inject } from '@orion-js/services'
import { schemaWithName, InferSchemaType } from '@orion-js/schema'
import { PostService } from '../services/PostService'

// Define schemas using schemaWithName
export const UserSchema = schemaWithName('UserSchema', {
  _id: { type: String },
  name: { type: String },
  email: { type: String }
})

export const PostSchema = schemaWithName('PostSchema', {
  _id: { type: String },
  title: { type: String },
  content: { type: String },
  authorId: { type: String }
})

// Infer types from schemas
export type UserType = InferSchemaType<typeof UserSchema>
export type PostType = InferSchemaType<typeof PostSchema>

@ModelResolvers(UserSchema)
export default class UserResolvers {
  @Inject(() => PostService)
  private postService: PostService

  @ModelResolver()
  posts = createModelResolver<UserType>({
    returns: [PostSchema],
    resolve: async (user) => {
      return await this.postService.getUserPosts(user._id)
    }
  })
}

The @ModelResolvers() decorator takes the schema that represents your GraphQL type. Each method decorated with @ModelResolver() adds a new field to that type.

Model Resolver Methods

Model resolver methods receive the parent object as their first parameter, followed by any parameters, the viewer, and GraphQL info:

@ModelResolver()
posts = createModelResolver<UserType>({
  params: PostFiltersParams,
  returns: [PostSchema],
  resolve: async (user, params, viewer, info) => {
    const { limit, sortBy } = params
    return await this.postService.getUserPosts(user._id, { limit, sortBy })
  }
})

Method Parameters

  1. Parent Object: The first parameter is always the parent object (the instance of the type you’re adding the field to)
  2. Params: If you specify params, this will be the parameters passed to the field
  3. Viewer: The viewer object (contains authentication context)
  4. Info: The GraphQL info object for query optimization

Model Resolver Options

The @ModelResolver() decorator accepts options to customize the field:

@ModelResolver({
  name: 'allPosts',  // Override the field name (defaults to method name)
  description: 'All posts created by this user',  // Field description in GraphQL schema
})

Available Options

OptionTypeDescription
namestringCustom name for the GraphQL field (defaults to method name)
descriptionstringDescription for the field in GraphQL schema documentation

Configuring Return Types

Specify the return type in the returns property of createModelResolver:

// Return a single item
@ModelResolver()
latestPost = createModelResolver<UserType>({
  returns: PostSchema,
  resolve: async (user) => {
    return await this.postService.getLatestUserPost(user._id)
  }
})

// Return an array
@ModelResolver()
posts = createModelResolver<UserType>({
  returns: [PostSchema],
  resolve: async (user) => {
    return await this.postService.getUserPosts(user._id)
  }
})

// Return scalar types
@ModelResolver()
postCount = createModelResolver<UserType>({
  returns: Number,
  resolve: async (user) => {
    return await this.postService.countUserPosts(user._id)
  }
})

Field Parameters

You can define parameters for your model resolver fields:

export const PostFiltersParams = schemaWithName('PostFiltersParams', {
  limit: { type: Number },
  offset: { type: Number },
  sortBy: { type: String }
})

export type PostFiltersParamsType = InferSchemaType<typeof PostFiltersParams>

@ModelResolver()
posts = createModelResolver<UserType>({
  params: PostFiltersParams,
  returns: [PostSchema],
  resolve: async (user, params) => {
    return await this.postService.getUserPosts(
      user._id,
      params.limit || 10,
      params.offset || 0,
      params.sortBy || 'createdAt'
    )
  }
})

Field Middleware

You can apply middleware to model resolvers using @UseMiddleware():

@ModelResolver()
@UseMiddleware(authMiddleware)
privatePosts = createModelResolver<UserType>({
  returns: [PrivatePostSchema],
  resolve: async (user, params, viewer) => {
    // Only accessible if middleware passes
    return await this.postService.getUserPrivatePosts(user._id)
  }
})

Custom Model Names

By default, the @ModelResolvers() decorator uses the class name of the provided schema. You can specify a custom name:

@ModelResolvers(UserSchema, { modelName: 'CustomUser' })
export default class UserResolvers {
  // Fields will be added to the CustomUser type
}

Handling Relationships

Model resolvers are ideal for handling relationships between your GraphQL types:

One-to-Many Relationships

// On the User type
@ModelResolver()
posts = createModelResolver<UserType>({
  returns: [PostSchema],
  resolve: async (user) => {
    return await this.postService.getUserPosts(user._id)
  }
})

// On the Post type
@ModelResolvers(PostSchema)
export default class PostResolvers {
  @Inject(() => UserService)
  private userService: UserService
  
  @ModelResolver()
  author = createModelResolver<PostType>({
    returns: UserSchema,
    resolve: async (post) => {
      return await this.userService.getUser(post.authorId)
    }
  })
}

Many-to-Many Relationships

@ModelResolvers(UserSchema)
export class UserResolvers {
  @Inject(() => GroupService)
  private groupService: GroupService
  
  @ModelResolver()
  groups = createModelResolver<UserType>({
    returns: [GroupSchema],
    resolve: async (user) => {
      return await this.groupService.getUserGroups(user._id)
    }
  })
}

@ModelResolvers(GroupSchema)
export class GroupResolvers {
  @Inject(() => UserService)
  private userService: UserService
  
  @ModelResolver()
  members = createModelResolver<GroupType>({
    returns: [UserSchema],
    resolve: async (group) => {
      return await this.userService.getGroupMembers(group._id)
    }
  })
}

DataLoader for Performance

For optimal performance, use DataLoader with your model resolvers to batch and cache database queries:

@ModelResolvers(PostSchema)
export default class PostResolvers {
  @Inject(() => UserService)
  private userService: UserService
  
  @ModelResolver()
  author = createModelResolver<PostType>({
    returns: UserSchema,
    resolve: async (post) => {
      // Using DataLoader, this will batch requests for multiple posts with the same author
      return await this.userService.loadUserById(post.authorId)
    }
  })
}

Best Practices

  1. Use for Computed Fields: Add fields that aren’t directly stored in your database.

  2. Handle Relationships: Define one-to-many and many-to-many relationships.

  3. Optimize with DataLoader: Use DataLoader to batch and cache similar requests.

  4. Keep it Simple: Each resolver should have a single responsibility.

  5. Apply Authorization: Use middleware to protect access to sensitive fields.

  6. Separate by Type: Create separate controller classes for each GraphQL type.

  7. Avoid Circular Dependencies: Be careful with bidirectional relationships to avoid circular dependencies.

  8. Consider Field Complexity: For computationally expensive fields, consider using query complexity analysis.

  9. Document Your Fields: Add descriptions to help API consumers understand your schema.

  10. Keep Consistent Naming: Follow a consistent naming convention for related fields.

Was this page helpful?