Files
docs.nestjs.com/content/graphql/subscriptions.md
2020-01-13 12:11:57 -08:00

5.4 KiB

Subscriptions

Subscription is just another GraphQL operation type like Query and Mutation. It allows creating real-time subscriptions over a bidirectional transport layer, mainly over websockets. Read more about the subscriptions here. Below is a commentAdded subscription example, copied directly from the official Apollo documentation:

Subscription: {
  commentAdded: {
    subscribe: () => pubSub.asyncIterator('commentAdded');
  }
}

warning Notice The pubsub is an instance of PubSub class. Read more about it here.

Schema first

To create an equivalent subscription in Nest, we'll make use of the @Subscription() decorator.

const pubSub = new PubSub();

@Resolver('Author')
export class AuthorResolver {
  constructor(
    private readonly authorsService: AuthorsService,
    private readonly postsService: PostsService,
  ) {}

  @Query('author')
  async getAuthor(@Args('id') id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveProperty('posts')
  async getPosts(@Parent() author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }

  @Subscription()
  commentAdded() {
    return pubSub.asyncIterator('commentAdded');
  }
}

In order to filter out specific events based on context and arguments, we can set a filter property.

@Subscription('commentAdded', {
  filter: (payload, variables) =>
    payload.commentAdded.repositoryName === variables.repoFullName,
})
commentAdded() {
  return pubSub.asyncIterator('commentAdded');
}

To mutate the published payload, we can use a resolve function.

@Subscription('commentAdded', {
  resolve: value => value,
})
commentAdded() {
  return pubSub.asyncIterator('commentAdded');
}

If you need to access some of the injected providers (e.g. use external service to validate the data), you can use the following construction:

@Subscription('commentAdded', {
  resolve(this: AuthorResolver, value) {
    // "this" refers to an instance of "AuthorResolver" 
    return value;
  }
})
commentAdded() {
  return pubSub.asyncIterator('commentAdded');
}

Likewise with filters.

@Subscription('commentAdded', {
  filter(this: AuthorResolver, payload, variables) {
    // "this" refers to an instance of "AuthorResolver" 
    return payload.commentAdded.repositoryName === variables.repoFullName;
  }
})
commentAdded() {
  return pubSub.asyncIterator('commentAdded');
}

Type definitions

The last step is to update type definitions file.

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post]
}

type Post {
  id: Int!
  title: String
  votes: Int
}

type Query {
  author(id: Int!): Author
}

type Comment {
  id: String
  content: String
}

type Subscription {
  commentAdded(repoFullName: String!): Comment
}

Well done. We created a single commentAdded(repoFullName: String!): Comment subscription. You can find a full sample implementation here.

Code first

To create a subscription using the class-first approach, we'll make use of the @Subscription() decorator.

const pubSub = new PubSub();

@Resolver('Author')
export class AuthorResolver {
  constructor(
    private readonly authorsService: AuthorsService,
    private readonly postsService: PostsService,
  ) {}

  @Query(returns => Author, { name: 'author' })
  async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveProperty('posts')
  async getPosts(@Parent() author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }

  @Subscription(returns => Comment)
  commentAdded() {
    return pubSub.asyncIterator('commentAdded');
  }
}

In order to filter out specific events based on context and arguments, we can set a filter property.

@Subscription(returns => Comment, {
  filter: (payload, variables) =>
    payload.commentAdded.repositoryName === variables.repoFullName,
})
commentAdded(@Args({ name: 'repoFullName', type: () => String }) repoFullName: string ) {
  return pubSub.asyncIterator('commentAdded');
}

To mutate the published payload, we can use a resolve function.

@Subscription(returns => Comment, {
  resolve: value => value,
})
commentAdded() {
  return pubSub.asyncIterator('commentAdded');
}

PubSub

We used a local PubSub instance here. Instead, we should define PubSub as a provider, inject it through the constructor (using @Inject() decorator), and reuse it among the whole application. You can read more about Nest custom providers here.

{
  provide: 'PUB_SUB',
  useValue: new PubSub(),
}

Module

In order to enable subscriptions, we have to set installSubscriptionHandlers property to true.

GraphQLModule.forRoot({
  typePaths: ['./**/*.graphql'],
  installSubscriptionHandlers: true,
}),

To customize the subscriptions server (e.g. change port), you can use subscriptions property (read more).