Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

defaultMergedResolver unexpected behaviour for aliased and nullable fields #1171

Closed
carmelid opened this issue Jul 16, 2019 · 3 comments
Closed
Labels

Comments

@carmelid
Copy link

This is a somewhat odd one, but caused a lot of headache before I figured out where it went wrong. Essentially, I have a GraphQL server as an api-gateway, that links to other services through the magic of makeRemoteExecutableSchema. Here's a tiny working example of the setup:

import { execute, toPromise } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';
import { ApolloServer, gql } from 'apollo-server';
import {
  buildClientSchema,
  IntrospectionQuery,
  introspectionQuery,
  parse,
} from 'graphql';
import {
  makeExecutableSchema,
  makeRemoteExecutableSchema,
} from 'graphql-tools';
import fetch from 'node-fetch';

async function startAPI() {
  // Create the link to the backend service
  const link = createHttpLink({
    uri: 'http://localhost:5001/graphql',
    fetch: fetch as any,
  });
  // Fetch the schema from the backend service
  const introspectedSchema = await toPromise(
    execute(link, { query: parse(introspectionQuery) }),
  ).then(res => res.data as IntrospectionQuery);

  const schema = makeRemoteExecutableSchema({
    link,
    schema: buildClientSchema(introspectedSchema),
  });
  // Start the API-server on port 5000
  const server = new ApolloServer({ schema });
  server
    .listen(5000)
    .then(() => console.log('API gateway started'))
    .catch(console.log);
}

async function startBackendService() {
  // TypeDefs with a non-root type
  const typeDefs = gql`
    type Product {
      name: String
      price(asOf: String): Int
    }
    type Query {
      products: [Product]
    }
  `;

  const resolvers = {
    Query: {
      products: () => {
        return [
          {
            name: 'First product',
            price: 1234,
          },
          {
            name: 'Another Product',
            price: 18,
          },
        ];
      },
    },
    Product: {
      /* Dummy resolver that return null for certain arguments */
      price: (
        source: { name: string; price: number },
        args?: { asOf?: string },
      ) => {
        if (args && args.asOf) {
          const requestedDate = new Date(args.asOf);
          const today = new Date();
          if (requestedDate > today) {
            return null;
          }
        }
        return source.price;
      },
    },
  };
  const server = new ApolloServer({
    schema: makeExecutableSchema({ typeDefs, resolvers }),
  });
  // Start the server on port 5001
  await server
    .listen(5001)
    .then(() => console.log('Backend started'))
    .catch(console.log);
}

startBackendService().then(startAPI)

Compiling and running the above code works fine and serves the playground on http://localhost:5000/ (and on 5001).

Also

# Write your query or mutation here
query{
  products{
    name
    priceTomorrow: price(asOf: "2020-07-16")
  }
}

yields the expected

{
  "data": {
    "products": [
      {
        "name": "First product",
        "priceTomorrow": null
      },
      {
        "name": "Another Product",
        "priceTomorrow": null
      }
    ]
  }
}

However, if we run

# Write your query or mutation here
query{
  products{
    name
    price
    priceTomorrow: price(asOf: "2020-07-16")
  }
}

we get

{
  "data": {
    "products": [
      {
        "name": "First product",
        "price": 1234,
        "priceTomorrow": 1234
      },
      {
        "name": "Another Product",
        "price": 18,
        "priceTomorrow": 18
      }
    ]
  }
}

where I would expect

{
  "data": {
    "products": [
      {
        "name": "First product",
        "price": 1234,
        "priceTomorrow": null
      },
      {
        "name": "Another Product",
        "price": 18,
        "priceTomorrow": null
      }
    ]
  }
}

This is due to the api-gateway assigning the defaultMergedResolver to the fields in Product, and the defaultMergedResolver not expecting fields to be null.

Note however that this only applies when the aliased field is accompanied by a field with the original field name (aliased or not). For example this query:

# Write your query or mutation here
query{
  products{
    price: name
    priceTomorrow: price(asOf: "2020-07-16")
  }
}

throws an error, even though I would expect it to return

{
  "data": {
    "products": [
      {
        "price": "First product",
        "priceTomorrow": null
      },
      {
        "price": "Another Product",
        "priceTomorrow": null
      }
    ]
  }
}

but this

# Write your query or mutation here
query{
  products{
    anyOtherAlias: price
    priceTomorrow: price(asOf: "2020-07-16")
  }
}

works perfectly fine.

@yaacovCR
Copy link
Collaborator

This should just work correctly in graphql-tools-fork, as in the fork, the result is loaded from the correct responseKey with only a single attempt.

@kamilkisiela
Copy link
Collaborator

We recently released an alpha version of GraphQL Tools (#1308) that should fix the issue.

Please update graphql-tools to next or run:

npx match-version graphql-tools next

Let us know if it solves the problem, we're counting for your feedback! :)

@yaacovCR yaacovCR mentioned this issue Mar 27, 2020
22 tasks
@yaacovCR
Copy link
Collaborator

Rolled into #1306, closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants