apollographql / graphql-subscriptions

:newspaper: A small module that implements GraphQL subscriptions for Node.js
MIT License
1.58k stars 133 forks source link

Subscription: make `Subscription` resolver independent of resolvers for `Create` and `Update` records #259

Open Mingyang-Li opened 2 years ago

Mingyang-Li commented 2 years ago

How do you make a Subscription resolver independent of resolvers for Creating and Updating records

❌ Current behaviour: subscribeTasks resolver doesn't have proper filters, nor does it return me the latest Task items

✔️ Expected behaviour: subscribeTasks resolver returns me all Task items given a filter parameter (based on TaskWhereInput) in the query

I'm building a GraphQL API using NestJS.

The API is simple, it has to do CRUD operations on Task entity - I'm using PostgreSQL

Data model:

import { Field } from '@nestjs/graphql';
import { ObjectType } from '@nestjs/graphql';
import { ID } from '@nestjs/graphql';

@ObjectType()
export class Task {
  @Field(() => ID, { nullable: false })
  id!: string;

  @Field(() => String, { nullable: true })
  title!: string | null;

  @Field(() => Date, { nullable: true })
  createdAt!: Date | null;

  @Field(() => Boolean, { nullable: true, defaultValue: false })
  completed!: boolean | null;

  @Field(() => Date, { nullable: true })
  completedAt!: Date | null;
}

The API needs to have the following 3 core resolvers:

⚠️My biggest question:

How do I make sure subscribeTasks resolver only updates the tasks subscribed BASED ON CHANGES IN THE DB? (rather than triggering updates all the tasks subscribed using PubSub by publishing pubSubEvents in createTask and updateTask resolvers?)

I also need a proper filter to be applied on subscribeTasks resolver. So that I can write subscription queries like how I'd do with a normal gql query like this:

subscription {
  subscribeTasks (
    where: {
      completed: {
        equals: false
      },
      createdAt: {
        equals: "2022-06-25"
      }
    }
  ) {
    id
    title
    completed
    completedAt
    createdAt
  }
}

TaskWhereInput:

@InputType()
export class TaskWhereInput {
  @Field(() => [TaskWhereInput], { nullable: true })
  AND?: Array<TaskWhereInput>;

  @Field(() => [TaskWhereInput], { nullable: true })
  OR?: Array<TaskWhereInput>;

  @Field(() => [TaskWhereInput], { nullable: true })
  NOT?: Array<TaskWhereInput>;

  @Field(() => StringFilter, { nullable: true })
  id?: StringFilter;

  @Field(() => StringNullableFilter, { nullable: true })
  title?: StringNullableFilter;

  @Field(() => DateTimeNullableFilter, { nullable: true })
  createdAt?: DateTimeNullableFilter;

  @Field(() => BoolNullableFilter, { nullable: true })
  completed?: BoolNullableFilter;

  @Field(() => DateTimeNullableFilter, { nullable: true })
  completedAt?: DateTimeNullableFilter;
}

This is how my resolvers look like:

import { Resolver, Mutation, Subscription, Args, Query } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { TaskCreateInput } from 'src/generated/task/task-create.input';
import { TaskUpdateInput } from 'src/generated/task/task-update.input';
import { TaskWhereUniqueInput } from 'src/generated/task/task-where-unique.input';
import { TaskWhereInput } from 'src/generated/task/task-where.input';
import { Task } from 'src/generated/task/task.model';
import { TaskService } from './Task.service';

@Resolver(() => Task)
export class TaskResolver {
  constructor(
    private readonly taskService: TaskService,
    private pubsub: PubSub,
  ) {}

  @Mutation(() => [Task])
  public async createTask(@Args('input') input: TaskCreateInput) {
    try {
      const res = await this.taskService.createTask(input);
      this.pubsub.publish('taskUpdatedEvent', { taskUpdatedEvent: res });
      return res;
    } catch (error) {
      throw error;
    }
  }

  @Mutation(() => [Task])
  public async updateTask(
    @Args('where') where: TaskWhereUniqueInput,
    @Args('data') data: TaskUpdateInput,
  ) {
    try {
      const res = await this.taskService.updateTask(where, data);
      this.pubsub.publish('taskAddedEvent', { taskAddedEvent: res });
      return res;
    } catch (error) {
      throw error;
    }
  }

  @Subscription(() => [Task])
  public async subscribeTasks(
    @Args('where', { nullable: true }) where?: TaskWhereInput,
  ) {
    this.pubsub.asyncIterator('taskAddedEvent');
    this.pubsub.asyncIterator('taskUpdatedEvent');
    return await this.taskService.tasks(where);
  }

  @Query(() => [Task])
  async tasks(@Args('where', { nullable: true }) where: TaskWhereInput) {
    return await this.taskService.tasks(where);
  }
}

Speaking of subscription filters, I also have no idea how to apply what's on NestJS doc in my use case (MANY filters, not just one)

NestJS doc filter demo:

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