apollographql / federation

🌐  Build and scale a single data graph across multiple services with Apollo's federation gateway.
https://apollographql.com/docs/federation/
Other
667 stars 253 forks source link

Apollo Gateway Managed await gateway.load() without manual configuration raises UnhandledPromiseRejectionWarning #831

Open sbilello opened 3 years ago

sbilello commented 3 years ago

Expected Behavior

Be able to run await gateway.load() for a managed apollo gateway without getting an exception and extracting schema, executor, typeDefs

Actual Behavior

(node:15023) UnhandledPromiseRejectionWarning: Error: When a manual configuration is not provided, gateway requires an Apollo configuration. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ for more information. Manual configuration options include: `serviceList`, `supergraphSdl`, and `experimental_updateServiceDefinitions`.

Code Snippet index.js

const { ApolloServer } = require('apollo-server');
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const depthLimit = require('graphql-depth-limit');

const graphVariant = process.env.GRAPH_VARIANT || 'current';
// 10 SECONDS TIMEOUT
const timeout = process.env.TIMEOUT || 10000;

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    request.http.headers.set('Authorization', context.token);
    request.http.timeout = timeout;
  }
}
(async () => {
  const gateway = new ApolloGateway({
    debug: true,
    buildService({ name, url }) {
      return new AuthenticatedDataSource({ url });
    },
  });

  await gateway.load();
// It is not possible to wait for loading of the configuration without serviceList.... ?!?!?

// Reference: https://moonhighway.com/securing-your-graphql-server
  const server = new ApolloServer({
    validationRules: [
      depthLimit(5),
      createComplexityLimitRule(1000, {
        onCost: cost => console.log('query cost: ', cost)
      })
    ],
    gateway,
    engine: {
      key: process.env.APOLLO_KEY,
      graphVariant: graphVariant
    },
    // Disable subscriptions (not currently supported with ApolloGateway)
    subscriptions: false,
    context: ({ req }) => {
      // Get the user token from the headers
      const token = req.headers.authorization || '';
      // Forward the token
      return { token };
    }
    ,
    plugins: [
      require('apollo-server-plugin-operation-registry')({
        // De-structure the object to get the HTTP `headers` and the GraphQL
        // request `context`.  Additional validation is possible, but this
        // function must be synchronous.  For more details, see the note below.
        forbidUnregisteredOperations({
                                       context, // Destructure the shared request `context`.
                                       request: {
                                         http: { headers }, // Destructure the `headers` class.
                                       },
                                     }) {
          // Just for demo purpose
          // This can be an authentication token or an environment variable
          if (headers.get('Let-me-pass') === 'Pretty please?') {
            return false;
          }
          // Enforce operation safelisting on all other users.
          return true;
        },
      }),
    ],
    tracing: true
  });

  server.listen(
      {
        port: 4000,
        hostname: '0.0.0.0',
      }
  ).then(({ url }) => {
    console.log(`🚀 Server ready at ${url} with graphVariant ${graphVariant}`);
  });
})();

package.json

{
  "name": "apollo-gateway",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": " node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@apollo/gateway": "^0.28.2",
    "apollo-server": "^2.25.0",
    "apollo-server-plugin-operation-registry": "^0.11.0",
    "graphql": "^15.5.0",
    "graphql-depth-limit": "^1.1.0",
    "graphql-validation-complexity": "^0.4.2"
  }
}

The final goal is to integrate a rate-limiter library in a managed federation graph: rate-limiter

I also found some interesting comments over here: https://github.com/apollographql/apollo-feature-requests/issues/145#issuecomment-696141713 https://github.com/apollographql/apollo-feature-requests/issues/145#issuecomment-608551476

These comments look to be old. Are these still valid today?

Do you recommend graphql-shield with graphql-middleware or another approach to make it work?

This is the comment where I took inspiration to try to make it work https://github.com/maticzav/graphql-shield/issues/456#issuecomment-533229287

trevor-scheer commented 3 years ago

Hey @sbilello, thanks for providing the details, this gives us a pretty good jumping off point.

Is calling gateway.load() manually a requirement? When the gateway is passed into Apollo Server, it will handle calling load with the correct parameters based on config/env variables, etc. For reference. If you aren't providing those values to load then the gateway will not be able to run in managed mode, which I think is the main point of this issue.

You mention

extracting schema, executor, typeDefs ...so I suspect you actually might want to do something with those before handing them off to Apollo Server.

Your next questions seem to be about directive support in federation but I don't quite know what you're asking. Maybe open a separate issue with some more details and/or post your questions in the forum? Not quite sure how to help without some more context.

sbilello commented 3 years ago

thanks @trevor-scheer Yes, I was trying to follow this example from here

const gateway = new ApolloGateway({
      serviceList: [
//there are 2 sub-services, i use the first service as the auth service, handle user login staffs
        { name: "accounts", url: "http://localhost:4001/graphql" },
        { name: "schools", url: "http://localhost:4002/graphql" },
      ],
      buildService({ name, url }) {
        return new RemoteGraphQLDataSource({
          url,
          willSendRequest({ request, context }) {
            //sub-service can read the share context with gateway here
            if(context.user)
            {
              //i record the user model into header as a json string
              //cause so far i can not find another way to translate the data out expect via http headers
              request.http.headers.set('user',JSON.stringify(context.user));
            }            
          },
        });
      },
    });

    (async () => {
      const { schema, executor } = await gateway.load();

      const server = new ApolloServer({ schema, executor,
        subscriptions: false,

        context: async ({req,res}) => {
          // get the user token from the headers
         const token = req.headers.authorization || '';

          if(token && req.body.operationName==null)
          {
// this is for testing, in practically way, there should add some JWT verify code or call some mutation method like loginWithToken(token)

          var a_login_request=`mutation {
            login(username:\"my_username\",password:\"my_password\")
              {
                status,
                user{username,id,name}
              }  
          }`;

          try{
            let authReq={query:a_login_request,variables:req.body.variables}
            let result:any=await (gateway['serviceMap']['accounts']).process({request:authReq,context:{}})
            //direct call the process function over the account service
            //to invoke a login request and get the user data back
            return {user:result.data.login.user}
          }catch(error){
            console.error(error);
          }
        }
        return {};
        },
      });

The main goal for me is integrating the rate-limiter to be able to rate-limit the query by some metadata that can be the userId that it is performing such request. I tried to read the github threads relative to integrate directive in a federation because this is related to how I can @ratelimit a query for object or field.

I was trying to get the schema to perform


SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives);

const server = new ApolloServer({
  schema: schema,
})

but I don't have all the typeDefs and resolvers so I was wondering if it is possible to get all these things from the schema registry and if this is the way to go or not.

I ended up finding all these different github threads:

There are actually 2 rate-limiter that I found:

https://github.com/teamplanes/graphql-rate-limit/ and https://github.com/ravangen/graphql-rate-limit/

Do you know which one works better? I can post this question to the community forum if you recommend it

liranms commented 3 years ago

I'm also interested in the exact scenario here of integrating a rate limiter to the apollo gateway, would love to hear if you got this to work and how, thanks!