Egg: [RFC] egg-graphql支持apollo-server-koa

Created on 30 Apr 2019  ·  8Comments  ·  Source: eggjs/egg

背景

egg-graphql目前存在以下问题

  1. 不支持formatError ,无法捕获异常
  2. 不支持文件上传
  3. graphiql调试界面没有graphql-playground好看
  4. 不支持apollo-server的选项

思路

修改lib/load_schema.js

//增加
app.schemaConfig={
    typeDefs:schemas,
    resolvers:resolverMap,
    directiveResolvers:directiveMap
  }

修改app/middleware/graphql.js

const { ApolloServer } = require('apollo-server-koa')
const compose = require('koa-compose')
/**
 * @param {Object} options The `options` of apollo-server.
 * @return {Promise} The compose of middleware in apollo-server-koa.
 */

module.exports = (_, app) => {
  let graphqlConfig=app.config.graphql
  if(typeof graphqlConfig==='function'){
    graphqlConfig=graphqlConfig(app)
  }
  const options = {...app.schemaConfig,...graphqlConfig}
  const {graphiql=true,router,...ApolloServerConfig}=options
  const server = new ApolloServer({
    context: options=>options.ctx,
    //不设置request.credentials 会导致请求不带cookie
    playground:graphiql&&{
      settings:{
        "request.credentials": "include"
      }
    },
    ...ApolloServerConfig,
  })

  const middlewares = []
  const proxyApp = {
    use: m => {
      middlewares.push(m)
    },
  }
  server.applyMiddleware({
    app: proxyApp,
    path:router
  })
  return compose(middlewares)
}

修改后的优点

  1. 支持apollo-server的options
  2. 支持文件上传.
  3. 更漂亮的调试界面.

    修改后的问题

  4. 不支持onPreGraphiQL,不过onPreGraphiQL只在开发阶段用
    如果要支持onPreGraphQL的话,可以在app/middleware/graphql.js里添加

    跟进

proposals

Most helpful comment

之前实现了一个兼容现有 egg-graphql 功能,支持 ts,且基本完全支持 apollo-server-koa 现有功能的版本,思路如下:

// app/middleware/graphql.js
'use strict';

const { ApolloServer } = require('apollo-server-koa');
const { get } = require('lodash');

module.exports = (_, app) => {
  const options = app.config.graphql;
  const graphQLRouter = options.router || '/graphql';
  let apolloServer;

  return async (ctx, next) => {
    const { onPreGraphQL, onPrePlayground, playground } = options;
    if (ctx.path === graphQLRouter) {
      if (ctx.request.accepts([ 'json', 'html' ]) === 'html') {
        playground && onPrePlayground && await onPrePlayground(ctx);
      } else {
        onPreGraphQL && await onPreGraphQL(ctx);
      }
    }

    // init apollo server
    if (!apolloServer) {
      const { getApolloServerOptions } = options;
      const apolloServerOptions = Object.assign(
        {
          // log the error stack by default
          formatError: error => {
            const stacktrace = (get(error, 'extensions.exception.stacktrace') || []).join('\n');
            ctx.logger.error('egg-graphql', stacktrace);
            return error;
          },
        },
        options.apolloServerOptions,
        // pass ctx to getApolloServerOptions
        getApolloServerOptions && getApolloServerOptions(ctx),
        // pass schema and context to apollo server
        {
          schema: app.schema,
          context: ({ ctx }) => ctx,
        }
      );
      apolloServer = new ApolloServer(apolloServerOptions);
      apolloServer.applyMiddleware({ app, path: graphQLRouter });
    }

    await next();
  };
};

感兴趣的小伙儿伴可以一起试用维护:

https://github.com/Carrotzpc/egg-graphql/tree/next#readme

All 8 comments

赞,这类插件,交给社区来维护吧,我这边接触不多。

@supperchong Thank you very much.
The existing official egg-graphql's GraphQL version is too fucking old.

之前实现了一个兼容现有 egg-graphql 功能,支持 ts,且基本完全支持 apollo-server-koa 现有功能的版本,思路如下:

// app/middleware/graphql.js
'use strict';

const { ApolloServer } = require('apollo-server-koa');
const { get } = require('lodash');

module.exports = (_, app) => {
  const options = app.config.graphql;
  const graphQLRouter = options.router || '/graphql';
  let apolloServer;

  return async (ctx, next) => {
    const { onPreGraphQL, onPrePlayground, playground } = options;
    if (ctx.path === graphQLRouter) {
      if (ctx.request.accepts([ 'json', 'html' ]) === 'html') {
        playground && onPrePlayground && await onPrePlayground(ctx);
      } else {
        onPreGraphQL && await onPreGraphQL(ctx);
      }
    }

    // init apollo server
    if (!apolloServer) {
      const { getApolloServerOptions } = options;
      const apolloServerOptions = Object.assign(
        {
          // log the error stack by default
          formatError: error => {
            const stacktrace = (get(error, 'extensions.exception.stacktrace') || []).join('\n');
            ctx.logger.error('egg-graphql', stacktrace);
            return error;
          },
        },
        options.apolloServerOptions,
        // pass ctx to getApolloServerOptions
        getApolloServerOptions && getApolloServerOptions(ctx),
        // pass schema and context to apollo server
        {
          schema: app.schema,
          context: ({ ctx }) => ctx,
        }
      );
      apolloServer = new ApolloServer(apolloServerOptions);
      apolloServer.applyMiddleware({ app, path: graphQLRouter });
    }

    await next();
  };
};

感兴趣的小伙儿伴可以一起试用维护:

https://github.com/Carrotzpc/egg-graphql/tree/next#readme

Is this possible to add an authorization layer/middleware before accessing Graphql paths? I need this so much!

@taviroquai
One soluton

  1. You may use isLogin middleware before graphql.
//config.default.js
config.middleware=['isLogin','graphql']
config.isLogin = {
ignore:/^\/api\/v1\/login$/
}
  1. Use restful api login and then set session.
post /api/v1/login
    3.
//middleware/isLogin.js
module.exports = options => {
  return async function(ctx, next) {
    if (!ctx.session.user) {
      return (ctx.body = {
        ...commonCode.unLogin,
      })
    }
await next()
}

two solution
reference apollo-server

@supperchong thanks. I need to access the Graphql operation info (for example: https://github.com/graphql/express-graphql/blob/master/src/index.js#L460)
Maybe I have to parse the graphql request by my own... but it would be nice to have this before... ah I see, I can create another middleware to parse the graphql request.

@taviroquai Oh I know, It will be a feature latter.I have a idea ,

  1. define a authorization function named auth
//auth.js
let auth=resolver=>{
   return (_, args, ctx) {
      const token = req.headers.authentication || ''

      // try to retrieve a user with the token
      const user = getUser(token)

      // optionally block the user
      // we could also check user roles/permissions here
      if (!user) throw new AuthenticationError('you must be logged in to query this schema')
      return resolver(_, args, ctx)
   }
}
  1. wrap resolver with auth if need authorization
    ```js
    const auth = require('../util/auth.js')
    module.exports = {
    Query: {
    companies: auth((root, args, ctx) => {

    return ctx.service.company.getCompanies(args)
    }),
    },
    }

or use directive?

Cool! I would even extract to configuration...

authorization.json

{
  "anonymous": [
    "Query.companies"
  ],
  "admin": [
    "Query.companies"
  ]
}

auth.js

// Deny or allow using currentPath
const authConfig = require('./authorization.json');
let auth = (currentPath, resolver) => { ... }
module.exports = auth;

resolvers.js

const resolvers = {
  Query: {
    companies: (root, args, ctx) => {
      return ctx.service.company.getCompanies(args)
    },
  },
}

Add authorization

const resolvers = require('./resolvers');
const auth = require('./auth');
const wrapAuth = (resolver, path = '') => {
  Object.keys(resolver).forEach(key => {
    const currentPath = [path, key].join('.');
    if (typeof resolver[key] === 'object') wrapAuth(resolver[key], currentPath);
    else resolver[key] = auth.bind(null, currentPath, resolver);
  }
}
wrapAuth(resolvers);
return resolvers;
Was this page helpful?
0 / 5 - 0 ratings

Related issues

popomore picture popomore  ·  38Comments

Yao-JSON picture Yao-JSON  ·  34Comments

itsky365 picture itsky365  ·  62Comments

zrxisme picture zrxisme  ·  44Comments

popomore picture popomore  ·  47Comments