Urigo / SOFA

The best way to create REST APIs - Generate RESTful APIs from your GraphQL Server
https://www.the-guild.dev/graphql/sofa-api
MIT License
1.07k stars 86 forks source link

Integrating SOFA with Apollo Federation #214

Open John-Blaine-Jr opened 4 years ago

John-Blaine-Jr commented 4 years ago

I have a microservice API that is using SOFA and seems to be serving up data via REST just fine:

const { ApolloServer } = require('apollo-server-express');
const { buildFederatedSchema } = require('@apollo/federation');
const express = require('express');
const { useSofa } = require('sofa-api');

const { resolvers } = require('./schema/resolvers');
const { typeDefs } = require('./schema/typeDefs');

const graphQL = new ApolloServer({
  schema: buildFederatedSchema(
    {
      typeDefs,
      resolvers
    }
  )
});

const app = express();

app.use('/api', useSofa({
  schema: buildFederatedSchema({
    typeDefs,
    resolvers
  })
}));

graphQL.applyMiddleware({ app });

const port = process.env.PORT || 4001;

app.listen({ port }, () => {
  console.log('Server ready http://localhost: ' + port);
});

But then, when I try to federate that microservice, and any number of microservices, into a gateway API as instantiated below, I recieve a null value whenever I make a REST query.

const { useSofa, OpenAPI } = require('sofa-api');
const { ApolloServer } = require('apollo-server-express');
const { ApolloGateway } = require("@apollo/gateway");

const gateway = new ApolloGateway({
  serviceList: [
    { name: "salesLineItem", url: '${process.env.NUTRIEN_SALES_DOCUMENT_SERVICE_URL}/graphql' },
    { name: "salesDocument", url: '${process.env.NUTRIEN_SALES_LINE_ITEM_SERVICE_URL}/graphql' },
    { name: "productPrice", url: '${process.env.NUTRIEN_PRODUCT_PRICE_SERVICE_URL}/graphql' },
  ]
});

(async () => {
  const { schema, executor } = await gateway.load();
  console.log('Gateway schema', JSON.stringify(schema));
  const graphql = new ApolloServer({ schema, executor });

  const app = express();

  app.use(
    '/api',
    useSofa({
      schema
    }),
  );

  graphql.applyMiddleware({ app });

  app.listen({ port: process.env.PORT }, () => {
    console.log('Server ready http://localhost:' + ${process.env.PORT});
  });
})();

So, the same query succeeds when made against the microservice, but returns null when made against the federated gateway service. Is it not possible to use SOFA with a federated Apollo server?

ardatan commented 4 years ago

Could you try the following?

useSofa({
  schema,
  execute: ({ source, variableNames, operationName }) => apolloServer.executeOperation({ query: source, variables: variableNames, operationName })
})
John-Blaine-Jr commented 4 years ago

Thank you for the fast response!

I think you meant:

useSofa({
  schema,
  /* executor instead of execute */ 
  executor: ({ source, variableNames, operationName }) => apolloServer.executeOperation({ query: source, variables: variableNames, operationName })
})

I did try this, but I am getting the same result - null is returned at the gateway layer and the actual payload is returned at the microservice layer.

ardatan commented 4 years ago

@John-Blaine-Jr I meant execute not executor.

ardatan commented 4 years ago

@John-Blaine-Jr Any news?

John-Blaine-Jr commented 4 years ago

@ardatan No, I tried using execute instead of executor and was getting an error instead of null. I'll find the error again when I have some time later

John-Blaine-Jr commented 4 years ago

I am overwriting the original message because I have good news! It worked! But I had to make one alteration:

useSofa({
  schema,
  executor: ({ source, variableValues, operationName }) => apolloServer.executeOperation({ query: source, variables: variableValues, operationName })
})

Change variableNames to variableValues and it works! That's a win!

byteninja commented 4 years ago
useSofa({
  schema,
  execute: ({ source, variableValues, operationName }) => apolloServer.executeOperation({ query: source, variables: variableValues, operationName })
})

is the right code i think. this works fine for me on queries, but fails for mutations where the variables should be parsed from the body. The variableValues are always an empty Object ({}) in this case and the mutation fails

yuraxdrumz commented 1 year ago

Don't know if its too late @byteninja, but I stumbled on this issue and found the following:

  1. variableValues come from query params on GET requests and body on POST requests
  2. inner executeQueryPlan in apollo gateway looks like so
        try {
          let postProcessingErrors: GraphQLError[];
          const variables = requestContext.request.variables;
          ({ data, errors: postProcessingErrors } = computeResponse({
            operation,
            variables,
            input: unfilteredData,
            introspectionHandling: (f) => executeIntrospection(
              operationContext.schema,
              f.expandFragments().toSelectionNode(),
              operationContext.operation.variableDefinitions,
              variables,
            ),
          }));

Where it expects the variables from the context, but the issue is contextValue.request.variables is empty on POST

The solution I found that works is setting contextValue.request.variables = variableValues; inside the custom execute function

Example

      execute: ({ document, variableValues, operationName, contextValue }) => {
        contextValue.request.variables = variableValues;
        return executor({ queryHash: operationName, operationName: operationName, document: document, request: contextValue.request });
      },

Seems good so far