Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Cannot return null for non-nullable field" when subscribing on NestJS with Graphql

Tags:

I have a nodejs backend done with Nestjs and I'm using Graphql. My frontend is Ionic/Angular using Apollo-angular for graphql stuff. I'm having a problem subscribing data additions / changes. Playground (provided by Nestjs) works just fine, which gives me a hint that the problem is in frontend.

I have game and scores in my data model, each score belonging to a game. In frontend I'm trying to listen to the new scores added to a specific game.

Backend

Here's a snippet from my resolver:

@Mutation(returns => Score)
async addScore(@Args('data') data: ScoreInput): Promise<IScore> {
  return await this.scoresService.createScore(data);
}

@Subscription(returns => Score, {
  filter: (payload, variables) => payload.scoreAdded.game + '' === variables.gameId + '',
})
scoreAdded(@Args('gameId') gameId: string) {
  return this.pubSub.asyncIterator('scoreAdded');
}

Here's the service method:

async createScore(data: any): Promise<IScore> {
  const score = await this.scoreModel.create(data);
  this.pubSub.publish('scoreAdded', { scoreAdded: score });
}

These are in my schema.gql:

type Score {
  id: String
  game: String
  result: Int
}

type Subscription {
  scoreAdded(gameId: String!): Score!
}

Frontend

Based on Apollo-angular's documentation, in my frontend I have this kind of service:

import { Injectable } from '@angular/core';
import { Subscription } from 'apollo-angular';
import { SCORE_ADDED } from './graphql.queries';

@Injectable({
  providedIn: 'root',
})
export class ScoreListenerService extends Subscription {
  document = SCORE_ADDED;
}

This is in the frontend's graphql.queries:

export const SCORE_ADDED = gql`
  subscription scoreAdded($gameId: String!) {
    scoreAdded(gameId: $gameId) {
      id
      game
      result
    }
  }
`;

and I'm using this service like this in my component:

this.scoreListener.subscribe({ gameId: this.gameId }).subscribe(({ data }) => {
  const score = data.scoreAdded;
  console.log(score);
});

The problem

With all this, my frontend gives me an error ERROR Error: GraphQL error: Cannot return null for non-nullable field Subscription.scoreAdded.

Doing the subscription like this in Playground works, no problem at all.

subscription {
  scoreAdded(gameId: "5d24ad2c4cf6d3151ad31e3d") {
    id
    game
    result
  }
}

Different problem

I noticed that if I use resolve in my backend's resolver like this:

  @Subscription(returns => Score, {
    resolve: value => value,
    filter: (payload, variables) => payload.scoreAdded.game + '' === variables.gameId + '',
  })
  scoreAdded(@Args('gameId') gameId: string) {
    return this.pubSub.asyncIterator('scoreAdded');
  }

the error in frontend goes away, BUT it screws up the data in subscription, playground getting the added score with null in each attribute and the subscribe in frontend is NOT triggered at all.

Any help, what am I doing wrong here? It looks to me that my frontend is not correct but I'm not sure is it my bad or possibly a bug in Apollo-angular...

like image 906
Dvlpr Avatar asked Aug 03 '19 07:08

Dvlpr


1 Answers

Ok, got my problem solved. As I suspected, the problem was in the frontend side code. So nothing wrong with the way I implemented the nestjs stuff on the backend side. Turned out to be a stupid mistake by me, not initializing the WS for subscriptions, which was clearly explained here https://www.apollographql.com/docs/angular/features/subscriptions/.

So, I changed this

const graphqlUri = 'http://localhost:3000/graphql';

export function createApollo(httpLink: HttpLink) {
  return {
    link: httpLink.create({ graphqlUri }),
    cache: new InMemoryCache(),
    defaultOptions: {
      query: {
        fetchPolicy: 'network-only',
        errorPolicy: 'all',
      },
    },
  };
}

to this

const graphqlUri = 'http://localhost:3000/graphql';
const wsUrl = 'ws://localhost:3000/graphql';

export function createApollo(httpLink: HttpLink) {
  const link = split(
    // split based on operation type
    ({ query }) => {
      const { kind, operation } = getMainDefinition(query);
      return kind === 'OperationDefinition' && operation === 'subscription';
    },
    new WebSocketLink({
      uri: wsUrl,
      options: {
        reconnect: true,
      },
    }),
    httpLink.create({
      uri: graphqlUri,
    })
  );
  return {
    link,
    cache: new InMemoryCache(),
    defaultOptions: {
      query: {
        fetchPolicy: 'network-only',
        errorPolicy: 'all',
      },
    },
  };
}
like image 69
Dvlpr Avatar answered Oct 08 '22 21:10

Dvlpr