apollographql / apollographql/graphql-subscriptions

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

Open
#259 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1.6k
Forks
129
PR merge metrics
No merged PRs in 30d

Description

# 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:
```typescript
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:
- `createTask` => creates a task in db
- `updateTask` => update a task in db
- `subscribeTasks` => subscribe to tasks given a filter parameter, and the tasks subscribed need to update whenever a `Task` is created or updated

## ⚠️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:
```graphql
subscription {
subscribeTasks (
where: {
completed: {
equals: false
},
createdAt: {
equals: "2022-06-25"
}
}
) {
id
title
completed
completedAt
createdAt
}
}
```

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

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

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

@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:
```typescript
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:
```typescript
@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string) {
return pubSub.asyncIterator('commentAdded');
}
```

- [ ] has-reproduction
- [ ] feature
- [x] blocking
- [ ] good first issue

Contributor guide

Open the contributing guide

Research direction

Start by reading the TaskResolver subscription, createTask and updateTask methods, then trace TaskService.tasks and the graphql-subscriptions PubSub usage. Compare the requested filtering and database-change behavior with the NestJS subscription filter example; done means the subscription applies the requested TaskWhereInput filters and updates appropriately for created or updated tasks.

Written by the indexing model from the issue text.

Assessment

Tech stack
graphql, postgresql, typescript
Domain
api, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.