0

I have 4 subgraph services namely products, couriers, cargo_service, tax. (Not a real-world example, just playing with graphql federation concepts).

Below is my code for those services.

products service

const { ApolloServer} = require("@apollo/server");
const{ startStandaloneServer } = require('@apollo/server/standalone');
const {gql} = require("graphql-tag");
const { buildSubgraphSchema } = require("@apollo/subgraph");

const PORT = 4001;

const typeDefs = gql`
    type Product @key(fields: "id") {
        id: ID!
        name: String
        price: Float
        courier: Courier @external
        estimatedShippingCharge: Float @requires(fields: "courier {shippingCharge tax} ")
    }
    
    extend type Courier @key(fields: "id") {
        id: ID! @external
        shippingCharge: Float @external
        tax: Float @external
    }
    
    extend type Query {
        product(id: ID!): Product
        products: [Product]
    }
`;

const resolvers = {
    Product: {
        __resolveReference(object) {
            return products.find(product => product.id === object.id);
        },
        estimatedShippingCharge(object) {
            console.log(object);
            return object.courier.shippingCharge + object.courier.tax;
        }
    },
    Query: {
        product(_, {id}) {
            return products.find(product => product.id === id);
        },
        products() {
            return products;
        }
    }
}

const server = new ApolloServer({
    schema: buildSubgraphSchema([{ typeDefs, resolvers }])
});

startStandaloneServer(server, {
    listen:{
        port: PORT
    }
}).then(({url}) => {
    console.log(`Products service ready at ${url}`);
});

const products = [
    {
        id: "1",
        name: "Headphones",
        price: 10.99
    },
    {
        id: "2",
        name: "Keyboard",
        price: 20.99
    },
    {
        id: "3",
        name: "Mouse",
        price: 15.99
    }
]

couriers service

const { ApolloServer} = require("@apollo/server");
const{ startStandaloneServer } = require('@apollo/server/standalone');
const {gql} = require("graphql-tag");
const { buildSubgraphSchema } = require("@apollo/subgraph");

const PORT = 4002;

const typeDefs = gql`
    type Courier @key(fields: "id") {
        id: ID!
        name: String
        shippingCharge: Float  @external
        tax: Float @external  
    }
    
    extend type Product @key(fields: "id") {
        id: ID!
        courier: Courier
    }
    
    extend type Query {
        courier(id: ID!): Courier
        couriers: [Courier]
    }
`;

const resolvers = {
    Courier: {
        __resolveReference(object) {
            return couriers.find(courier => courier.id === object.id);
        }
    },
    Product: {
        __resolveReference(object) {
            return products.find(product => product.id === object.id);
        }
    },
    Query: {
        courier(_, {id}) {
            return couriers.find(courier => courier.id === id);
        },
        couriers() {
            return couriers;
        }
    }
}

const server = new ApolloServer({
    schema: buildSubgraphSchema([{ typeDefs, resolvers }])
});

startStandaloneServer(server, {
    listen:{
        port: PORT
    }
}).then(({url}) => {
    console.log(`Products service ready at ${url}`);
});

const couriers = [
    {
        id: "1",
        name: "DHL"
    },
    {
        id: "2",
        name: "FedEx"
    }
];

const products = [
    {
        id: "1",
        courier: couriers[0]
    },
    {
        id: "2",
        courier: couriers[1]
    },
    {
        id: "3",
        courier: couriers[0]
    }
];

cargo_service

const { ApolloServer} = require("@apollo/server");
const{ startStandaloneServer } = require('@apollo/server/standalone');
const {gql} = require("graphql-tag");
const { buildSubgraphSchema } = require("@apollo/subgraph");

const PORT = 4003;

const typeDefs = gql`
    extend type Courier @key(fields: "id") {
        id: ID!
        shippingCharge: Float
    }
`;

const resolvers = {
    Courier: {
        __resolveReference(object) {
            return shippingCharges.find(shippingCharge => shippingCharge.id === object.id);
        }
    }
}

const server = new ApolloServer({
    schema: buildSubgraphSchema([{ typeDefs, resolvers }])
});

startStandaloneServer(server, {
    listen:{
        port: PORT
    }
}).then(({url}) => {
    console.log(`Products service ready at ${url}`);
});

const shippingCharges = [
    {
        id: "1",
        shippingCharge: 100.99
    },
    {
        id: "2",
        shippingCharge: 200.99
    }
]

tax service

const { ApolloServer} = require("@apollo/server");
const{ startStandaloneServer } = require('@apollo/server/standalone');
const {gql} = require("graphql-tag");
const { buildSubgraphSchema } = require("@apollo/subgraph");

const PORT = 4004;

const typeDefs = gql`
    extend type Courier @key(fields: "id") {
        id: ID!
        tax: Float
    }
`;

const resolvers = {
    Courier: {
        __resolveReference(object) {
            return taxes.find(tax => tax.id === object.id);
        }
    }
}

const server = new ApolloServer({
    schema: buildSubgraphSchema([{ typeDefs, resolvers }])
});

startStandaloneServer(server, {
    listen:{
        port: PORT
    }
}).then(({url}) => {
    console.log(`Products service ready at ${url}`);
});

const taxes = [
    {
        id: "1",
        tax: 100.99
    },
    {
        id: "2",
        tax: 200.99
    }
];

And following is my gateway code.

const { ApolloServer } = require("@apollo/server");
const { ApolloGateway, IntrospectAndCompose } = require("@apollo/gateway");
const { startStandaloneServer } = require("@apollo/server/standalone");
const {serializeQueryPlan} = require('@apollo/query-planner');

const PORT = 4000;

const supergraphSdl = new IntrospectAndCompose({
    subgraphs: [
        { name: "products", url: "http://localhost:4001" },
        { name: "couriers", url: "http://localhost:4002" },
        { name: "cargo_service", url: "http://localhost:4003" },
        { name: "tax", url: "http://localhost:4004" },
    ],
});

const gateway = new ApolloGateway({
    supergraphSdl,
    experimental_didResolveQueryPlan: function(options) {
        if (options.requestContext.operationName !== 'IntrospectionQuery') {
            console.log(serializeQueryPlan(options.queryPlan));
        }
    }
});

const server = new ApolloServer({gateway});
startStandaloneServer(server, {
    listen:{
        port: PORT
    }
}).then(({url}) => {
    console.log(`Products service ready at ${url}`);
});

When I queried the following document I get an error.

query {
  product(id: "1") {
    id
    name
    price
    courier {
      id
      name
      shippingCharge
      tax
    }
    estimatedShippingCharge
  }
}

The error is as below.

{
  "data": {
    "product": {
      "id": "1",
      "name": "Headphones",
      "price": 10.99,
      "courier": {
        "id": "1",
        "name": "DHL",
        "shippingCharge": 100.99,
        "tax": 100.99
      },
      "estimatedShippingCharge": null
    }
  },
  "errors": [
    {
      "message": "Cannot read properties of undefined (reading 'shippingCharge')",
      "path": [
        "product",
        "estimatedShippingCharge"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "GraphQLError: Cannot read properties of undefined (reading 'shippingCharge')",
          "    at downstreamServiceError (D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:514:16)",
          "    at D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:334:59",
          "    at Array.map (<anonymous>)",
          "    at sendOperation (D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:334:44)",
          "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
          "    at async D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:279:49",
          "    at async executeNode (D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:199:17)",
          "    at async executeNode (D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:190:27)",
          "    at async executeNode (D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:173:40)",
          "    at async D:\\poc_ballerina_gateway_requires\\javascript_services\\node_modules\\@apollo\\gateway\\dist\\executeQueryPlan.js:96:35"
        ],
        "serviceName": "products"
      }
    }
  ]
}

Since courier -> shippingCharge resolved correctly in the query, Why does it complain as undefined when trying to resolve shippingCharge as undefined?. Is there any mistake in my code?. According to the documentation of @requires, they should support this kind of operation. https://www.apollographql.com/docs/federation/entities-advanced/#using-requires-with-object-subfields

PS: Used the Apollo router (https://www.apollographql.com/docs/router/quickstart/) as well. Got same kind of error

Ishad
  • 121
  • 1
  • 11

2 Answers2

0

What is the object that is logged right before return object.courier.shippingCharge + object.courier.tax;? Is it actually a courier? Given your schema I suspect it is just the parent product object with no courier field. Your estimatedShippingCharge resolver needs to first find the courier for this product before it can get its shippingCharge property.

Michel Floyd
  • 18,793
  • 4
  • 24
  • 39
  • Isn't that courier field supposed to be added when resolving it by the gateway. Is there any limitation on `@requires` specifications for field argument?. If so can you please provide a reference for it – Ishad May 23 '23 at 00:18
  • Yes, the courier field will be added by the gateway but not necessarily *before* the `estimatedShippingCharge` resolver is called as resolvers are fired asynchronously. – Michel Floyd May 23 '23 at 15:56
  • Yes. I understand. But how can I handle it to get this resolved in proper order? Isn't this need to be handled by the gateway? – Ishad May 24 '23 at 00:13
0

Opened an issue on GitHub (https://github.com/apollographql/federation/issues/2591) and got the answer there.

Ishad
  • 121
  • 1
  • 11