diff --git a/README.md b/README.md index 72aef91..100e421 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,24 @@ exports.graphql = { onPreGraphQL: function* (ctx) {}, // 开发工具 graphiQL 路由前的拦截器,建议用于做权限操作(如只提供开发者使用) onPreGraphiQL: function* (ctx) {}, + // apollo server的透传参数,参考[文档](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#parameters) + apolloServerOptions: { + rootValue, + formatError, + formatResponse, + mocks, + schemaDirectives, + introspection, + playground, + debug, + validationRules, + tracing, + cacheControl, + subscriptions, + engine, + persistedQueries, + cors, + } }; // 添加中间件拦截请求 diff --git a/app/middleware/graphql.js b/app/middleware/graphql.js index f65bc65..763e753 100644 --- a/app/middleware/graphql.js +++ b/app/middleware/graphql.js @@ -45,21 +45,31 @@ module.exports = (_, app) => { return async (ctx, next) => { /* istanbul ignore else */ if (ctx.path === graphQLRouter) { + const { + onPreGraphiQL, + onPreGraphQL, + apolloServerOptions, + } = options; if (ctx.request.accepts([ 'json', 'html' ]) === 'html' && graphiql) { - if (options.onPreGraphiQL) { - await options.onPreGraphiQL(ctx); + if (onPreGraphiQL) { + await onPreGraphiQL(ctx); } return graphiqlKoa({ endpointURL: graphQLRouter, })(ctx); } - if (options.onPreGraphQL) { - await options.onPreGraphQL(ctx); + if (onPreGraphQL) { + await onPreGraphQL(ctx); } - return graphqlKoa({ - schema: app.schema, - context: ctx, - })(ctx); + const serverOptions = Object.assign( + {}, + apolloServerOptions, + { + schema: app.schema, + context: ctx, + } + ); + return graphqlKoa(serverOptions)(ctx); } await next(); }; diff --git a/test/app/graphql-options/graphql.test.js b/test/app/graphql-options/graphql.test.js new file mode 100644 index 0000000..4c02e56 --- /dev/null +++ b/test/app/graphql-options/graphql.test.js @@ -0,0 +1,35 @@ +'use strict'; + +const assert = require('assert'); +const mm = require('egg-mock'); + +describe('test/app/graphql-options.test.js', () => { + let app; + + before(() => { + app = mm.app({ + baseDir: 'apps/graphql-options-app', + }); + return app.ready(); + }); + + after(mm.restore); + + it('should return custom error, use formatError', async () => { + const resp = await app.httpRequest() + .get('/graphql?query=query+getUser($id:Int){user(id:$id){name}}&variables={"id":1}') + .expect(200); + assert.equal(resp.body.errors[0].code, 100001); + }); + + it('should return frameworks, user formatResponse', async () => { + const resp = await app.httpRequest() + .get('/graphql?query=query+getFramework($id:Int){framework(id:$id){name}}&variables={"id":1}') + .expect(200); + assert.deepEqual(resp.body.data, { + frameworks: { + name: 'framework1', + }, + }); + }); +}); diff --git a/test/fixtures/apps/graphql-options-app/app/extend/application.js b/test/fixtures/apps/graphql-options-app/app/extend/application.js new file mode 100644 index 0000000..b500efe --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/extend/application.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + changedName: 'frameworks', +}; diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/directives/directive.js b/test/fixtures/apps/graphql-options-app/app/graphql/directives/directive.js new file mode 100644 index 0000000..2b62a94 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/directives/directive.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + upper(next) { + return next().then(str => { + if (typeof str === 'string') { + return str.toUpperCase(); + } + return str; + }); + }, +}; diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/directives/schema.graphql b/test/fixtures/apps/graphql-options-app/app/graphql/directives/schema.graphql new file mode 100644 index 0000000..c3cbc1a --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/directives/schema.graphql @@ -0,0 +1 @@ +directive @upper on FIELD_DEFINITION diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/connector.js b/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/connector.js new file mode 100644 index 0000000..9dbbd78 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/connector.js @@ -0,0 +1,30 @@ +'use strict'; + +const DataLoader = require('dataloader'); + +class FrameworkConnector { + constructor(ctx) { + this.ctx = ctx; + this.loader = new DataLoader(this.fetch.bind(this)); + } + + fetch(ids) { + return Promise.resolve(ids.map(id => ({ + id, + name: `framework${id}`, + projects: [], + }))); + } + + fetchByIds(ids) { + return this.loader.loadMany(ids); + } + + fetchById(id) { + return this.loader.load(id); + } + +} + +module.exports = FrameworkConnector; + diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/resolver.js b/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/resolver.js new file mode 100644 index 0000000..5a20a34 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/resolver.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + Query: { + framework(root, { id }, ctx) { + return ctx.connector.framework.fetchById(id); + }, + }, +}; diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/schema.graphql b/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/schema.graphql new file mode 100644 index 0000000..f186ad9 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/group/framework/schema.graphql @@ -0,0 +1,6 @@ + +type Framework { + id: Int! + name: String! + projects: [Project] +} diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/project/resolver.js b/test/fixtures/apps/graphql-options-app/app/graphql/project/resolver.js new file mode 100644 index 0000000..e089622 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/project/resolver.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = app => { + return { + Query: { + projects() { + console.log(app); + return []; + }, + }, + }; +}; diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/project/schema.graphql b/test/fixtures/apps/graphql-options-app/app/graphql/project/schema.graphql new file mode 100644 index 0000000..2249d1c --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/project/schema.graphql @@ -0,0 +1,4 @@ + +type Project { + name: String! +} diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/schemaDirectives/schema.graphql b/test/fixtures/apps/graphql-options-app/app/graphql/schemaDirectives/schema.graphql new file mode 100644 index 0000000..1b53968 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/schemaDirectives/schema.graphql @@ -0,0 +1 @@ +directive @lowerCase on FIELD_DEFINITION diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/schemaDirectives/schemaDirective.js b/test/fixtures/apps/graphql-options-app/app/graphql/schemaDirectives/schemaDirective.js new file mode 100644 index 0000000..48f3926 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/schemaDirectives/schemaDirective.js @@ -0,0 +1,21 @@ +'use strict'; + +const { SchemaDirectiveVisitor } = require('graphql-tools'); +const { defaultFieldResolver } = require('graphql'); + +class LowerCaseDirective extends SchemaDirectiveVisitor { + visitFieldDefinition(field) { + const { resolve = defaultFieldResolver } = field; + field.resolve = async function(...args) { + let result = await resolve.apply(this, args); + if (typeof result === 'string') { + result = result.toLowerCase(); + } + return result; + }; + } +} + +module.exports = { + lowerCase: LowerCaseDirective, +}; diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/user/connector.js b/test/fixtures/apps/graphql-options-app/app/graphql/user/connector.js new file mode 100644 index 0000000..c204605 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/user/connector.js @@ -0,0 +1,38 @@ +'use strict'; + +const DataLoader = require('dataloader'); + +class UserConnector { + constructor(ctx) { + this.ctx = ctx; + this.loader = new DataLoader(this.fetch.bind(this)); + } + + fetch(ids) { + // this.ctx.model.user.find(ids); + return Promise.resolve(ids.map(id => ({ + id, + name: `name${id}`, + upperName: `name${id}`, + lowerName: `name${id}`, + password: `password${id}`, + projects: [], + }))); + } + + fetchByIds(ids) { + return this.loader.loadMany(ids); + } + + // eslint-disable-next-line no-unused-vars + fetchById(id) { + const err = new Error(); + err.code = 100001; + throw err; + // return this.loader.load(id); + } + +} + +module.exports = UserConnector; + diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/user/resolver.js b/test/fixtures/apps/graphql-options-app/app/graphql/user/resolver.js new file mode 100644 index 0000000..6a794a6 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/user/resolver.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + Query: { + user(root, { id }, ctx) { + return ctx.connector.user.fetchById(id); + }, + }, +}; diff --git a/test/fixtures/apps/graphql-options-app/app/graphql/user/schema.graphql b/test/fixtures/apps/graphql-options-app/app/graphql/user/schema.graphql new file mode 100644 index 0000000..3edd1b2 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/graphql/user/schema.graphql @@ -0,0 +1,14 @@ +type Query { + user(id: Int): User + projects: [Project!] + framework(id: Int): Framework +} + +type User { + id: String! + password: String! + name: String! + upperName: String @upper + lowerName: String @lowerCase + projects: [Project!] +} diff --git a/test/fixtures/apps/graphql-options-app/app/model/framework.js b/test/fixtures/apps/graphql-options-app/app/model/framework.js new file mode 100644 index 0000000..66cb4ec --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/model/framework.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = () => { + class Framework { + find(ids) { + return ids.map(id => ({ + id, + name: `name${id}`, + })); + } + } + + return Framework; +}; diff --git a/test/fixtures/apps/graphql-options-app/app/model/user.js b/test/fixtures/apps/graphql-options-app/app/model/user.js new file mode 100644 index 0000000..aad645f --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/model/user.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = () => { + class User { + find(ids) { + return ids.map(id => ({ + id, + name: `name${id}`, + password: `password${id}`, + })); + } + } + + return User; +}; diff --git a/test/fixtures/apps/graphql-options-app/app/router.js b/test/fixtures/apps/graphql-options-app/app/router.js new file mode 100644 index 0000000..2bd8a4c --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/router.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = function(app) { + app.get('/user', async ctx => { + const req = { + query: `{ + user(id: 2) { + name + } + }`, + }; + ctx.body = await ctx.graphql.query(JSON.stringify(req)); + }); + + app.get('/framework', async ctx => { + const req = { + query: `{ + framework(id: 2) { + name + } + }`, + }; + ctx.body = await ctx.graphql.query(JSON.stringify(req)); + }); +}; diff --git a/test/fixtures/apps/graphql-options-app/app/service/framework.js b/test/fixtures/apps/graphql-options-app/app/service/framework.js new file mode 100644 index 0000000..02c13f5 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/service/framework.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = app => { + class FrameworkService extends app.Service { + async getFrameworkList() { + return [ + { id: 1, name: 'framework1' }, + { id: 2, name: 'framework2' }, + ]; + } + } + return FrameworkService; +}; diff --git a/test/fixtures/apps/graphql-options-app/app/service/user.js b/test/fixtures/apps/graphql-options-app/app/service/user.js new file mode 100644 index 0000000..e6b136d --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/app/service/user.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = app => { + class UserService extends app.Service { + async getUserList() { + return [ + { id: '1', name: 'user1' }, + { id: '2', name: 'user2' }, + ]; + } + } + return UserService; +}; diff --git a/test/fixtures/apps/graphql-options-app/config/config.unittest.js b/test/fixtures/apps/graphql-options-app/config/config.unittest.js new file mode 100644 index 0000000..be7c3d3 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/config/config.unittest.js @@ -0,0 +1,45 @@ +'use strict'; + +exports.keys = 'plugin-graphql'; +exports.middleware = [ 'graphql' ]; + +function checkNested(obj, level, ...rest) { + if (obj === undefined) return false; + if (rest.length === 0 && obj.hasOwnProperty(level)) return true; + return checkNested(obj[level], ...rest); +} + +exports.graphql = { + graphiql: true, + async onPreGraphiQL(ctx) { + await ctx.service.user.getUserList(); + await ctx.service.framework.getFrameworkList(); + return {}; + }, + apolloServerOptions: { + formatError(err) { + if (err.code === 100001) { + err.message = 'api error'; + } else { + err.message = 'unknown'; + } + return err; + }, + formatResponse(ctx, context) { + const name = context.context.app.changedName; // use egg context & app + const data = ctx.data; + if (data.framework !== undefined) { + data[name] = data.framework; + delete data.framework; + } + for (const i in ctx.errors) { + const error = ctx.errors[i]; + if (checkNested(error, 'extensions', 'exception', 'code')) { + error.code = error.extensions.exception.code; + delete error.extensions; + } + } + return ctx; + }, + }, +}; diff --git a/test/fixtures/apps/graphql-options-app/package.json b/test/fixtures/apps/graphql-options-app/package.json new file mode 100644 index 0000000..0826572 --- /dev/null +++ b/test/fixtures/apps/graphql-options-app/package.json @@ -0,0 +1,3 @@ +{ + "name": "grapgql-app" +} \ No newline at end of file